TM-SGNL-iOS/SignalServiceKit/Calls/CallRecord/IndividualCallRecordManager.swift
TeleMessage developers dde0620daf initial commit
2025-05-03 12:28:28 -07:00

363 lines
13 KiB
Swift

//
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
public protocol IndividualCallRecordManager {
/// Updates the call interaction type for the given interaction, and
/// correspondingly updates the call record for this interaction if one
/// exists.
func updateInteractionTypeAndRecordIfExists(
individualCallInteraction: TSCall,
individualCallInteractionRowId: Int64,
contactThread: TSContactThread,
newCallInteractionType: RPRecentCallType,
tx: DBWriteTransaction
)
/// Update the call record for the given call interaction's current state,
/// or create one if none exists.
func createOrUpdateRecordForInteraction(
individualCallInteraction: TSCall,
individualCallInteractionRowId: Int64,
contactThread: TSContactThread,
contactThreadRowId: Int64,
callId: UInt64,
tx: DBWriteTransaction
) throws
/// Create a call record for the given interaction's current state.
func createRecordForInteraction(
individualCallInteraction: TSCall,
individualCallInteractionRowId: Int64,
contactThread: TSContactThread,
contactThreadRowId: Int64,
callId: UInt64,
callType: CallRecord.CallType,
callDirection: CallRecord.CallDirection,
individualCallStatus: CallRecord.CallStatus.IndividualCallStatus,
callEventTimestamp: UInt64,
shouldSendSyncMessage: Bool,
tx: DBWriteTransaction
) throws -> CallRecord
/// Update the given call record.
func updateRecord(
contactThread: TSContactThread,
existingCallRecord: CallRecord,
newIndividualCallStatus: CallRecord.CallStatus.IndividualCallStatus,
shouldSendSyncMessage: Bool,
tx: DBWriteTransaction
)
}
public class IndividualCallRecordManagerImpl: IndividualCallRecordManager {
private let callRecordStore: CallRecordStore
private let interactionStore: InteractionStore
private let outgoingSyncMessageManager: OutgoingCallEventSyncMessageManager
private let statusTransitionManager: IndividualCallRecordStatusTransitionManager
private var logger: PrefixedLogger { CallRecordLogger.shared }
init(
callRecordStore: CallRecordStore,
interactionStore: InteractionStore,
outgoingSyncMessageManager: OutgoingCallEventSyncMessageManager
) {
self.callRecordStore = callRecordStore
self.interactionStore = interactionStore
self.outgoingSyncMessageManager = outgoingSyncMessageManager
self.statusTransitionManager = IndividualCallRecordStatusTransitionManager()
}
public func updateInteractionTypeAndRecordIfExists(
individualCallInteraction: TSCall,
individualCallInteractionRowId: Int64,
contactThread: TSContactThread,
newCallInteractionType: RPRecentCallType,
tx: DBWriteTransaction
) {
guard
let newIndividualCallStatus = CallRecord.CallStatus.IndividualCallStatus(
individualCallInteractionType: newCallInteractionType
)
else {
logger.error("Cannot update interaction or call record, missing or invalid parameters!")
return
}
interactionStore.updateIndividualCallInteractionType(
individualCallInteraction: individualCallInteraction,
newCallInteractionType: newCallInteractionType,
tx: tx
)
guard let existingCallRecord = callRecordStore.fetch(
interactionRowId: individualCallInteractionRowId, tx: tx
) else {
logger.info("No existing call record found!")
return
}
updateRecord(
contactThread: contactThread,
existingCallRecord: existingCallRecord,
newIndividualCallStatus: newIndividualCallStatus,
shouldSendSyncMessage: true,
tx: tx
)
}
/// Create or update the record for the given interaction, using the latest
/// state of the interaction.
///
/// Sends a sync message with the latest call record.
public func createOrUpdateRecordForInteraction(
individualCallInteraction: TSCall,
individualCallInteractionRowId: Int64,
contactThread: TSContactThread,
contactThreadRowId: Int64,
callId: UInt64,
tx: DBWriteTransaction
) throws {
guard
let callDirection = CallRecord.CallDirection(
individualCallInteractionType: individualCallInteraction.callType
),
let individualCallStatus = CallRecord.CallStatus.IndividualCallStatus(
individualCallInteractionType: individualCallInteraction.callType
)
else { return }
switch callRecordStore.fetch(
callId: callId,
conversationId: .thread(threadRowId: contactThreadRowId),
tx: tx
) {
case .matchDeleted:
logger.warn("Ignoring: existing record for call was deleted!")
case .matchFound(let existingCallRecord):
updateRecord(
contactThread: contactThread,
existingCallRecord: existingCallRecord,
newIndividualCallStatus: individualCallStatus,
shouldSendSyncMessage: true,
tx: tx
)
case .matchNotFound:
_ = try createRecordForInteraction(
individualCallInteraction: individualCallInteraction,
individualCallInteractionRowId: individualCallInteractionRowId,
contactThread: contactThread,
contactThreadRowId: contactThreadRowId,
callId: callId,
callType: CallRecord.CallType(individualCallOfferTypeType: individualCallInteraction.offerType),
callDirection: callDirection,
individualCallStatus: individualCallStatus,
callEventTimestamp: individualCallInteraction.timestamp,
shouldSendSyncMessage: true,
tx: tx
)
}
}
public func createRecordForInteraction(
individualCallInteraction: TSCall,
individualCallInteractionRowId: Int64,
contactThread: TSContactThread,
contactThreadRowId: Int64,
callId: UInt64,
callType: CallRecord.CallType,
callDirection: CallRecord.CallDirection,
individualCallStatus: CallRecord.CallStatus.IndividualCallStatus,
callEventTimestamp: UInt64,
shouldSendSyncMessage: Bool,
tx: DBWriteTransaction
) throws -> CallRecord {
logger.info("Creating new 1:1 call record from interaction.")
let callRecord = CallRecord(
callId: callId,
interactionRowId: individualCallInteractionRowId,
threadRowId: contactThreadRowId,
callType: callType,
callDirection: callDirection,
callStatus: .individual(individualCallStatus),
callBeganTimestamp: callEventTimestamp
)
let insertResult = Result(catching: { try callRecordStore.insert(callRecord: callRecord, tx: tx) })
if shouldSendSyncMessage {
outgoingSyncMessageManager.sendSyncMessage(
callRecord: callRecord,
callEvent: .callUpdated,
callEventTimestamp: callRecord.callBeganTimestamp,
tx: tx
)
}
try insertResult.get()
return callRecord
}
public func updateRecord(
contactThread: TSContactThread,
existingCallRecord: CallRecord,
newIndividualCallStatus: CallRecord.CallStatus.IndividualCallStatus,
shouldSendSyncMessage: Bool,
tx: DBWriteTransaction
) {
guard case let .individual(individualCallStatus) = existingCallRecord.callStatus else {
logger.error("Missing individual call status while trying to update record!")
return
}
guard statusTransitionManager.isStatusTransitionAllowed(
fromIndividualCallStatus: individualCallStatus,
toIndividualCallStatus: newIndividualCallStatus
) else {
logger.warn("Status transition \(individualCallStatus) -> \(newIndividualCallStatus) not allowed. Skipping record update.")
return
}
callRecordStore.updateCallAndUnreadStatus(
callRecord: existingCallRecord,
newCallStatus: .individual(newIndividualCallStatus),
tx: tx
)
if shouldSendSyncMessage {
outgoingSyncMessageManager.sendSyncMessage(
callRecord: existingCallRecord,
callEvent: .callUpdated,
callEventTimestamp: existingCallRecord.callBeganTimestamp,
tx: tx
)
}
}
}
private extension CallRecord.CallType {
init(individualCallOfferTypeType: TSRecentCallOfferType) {
switch individualCallOfferTypeType {
case .audio: self = .audioCall
case .video: self = .videoCall
}
}
}
private extension CallRecord.CallDirection {
init?(
individualCallInteractionType: RPRecentCallType
) {
switch individualCallInteractionType {
case
.incoming,
.incomingMissed,
.incomingDeclined,
.incomingIncomplete,
.incomingBusyElsewhere,
.incomingDeclinedElsewhere,
.incomingAnsweredElsewhere,
.incomingMissedBecauseOfDoNotDisturb,
.incomingMissedBecauseOfChangedIdentity,
.incomingMissedBecauseBlockedSystemContact:
self = .incoming
case
.outgoing,
.outgoingIncomplete,
.outgoingMissed:
self = .outgoing
@unknown default:
CallRecordLogger.shared.warn("Unknown call type!")
return nil
}
}
}
extension CallRecord.CallStatus.IndividualCallStatus {
public init?(
individualCallInteractionType: RPRecentCallType
) {
switch individualCallInteractionType {
case
.incomingIncomplete,
.outgoingIncomplete:
self = .pending
case
.incoming,
.outgoing,
.incomingAnsweredElsewhere:
// The "elsewhere" is a linked device that should be sending us a
// sync message.
self = .accepted
case
.incomingDeclined,
.outgoingMissed,
.incomingDeclinedElsewhere:
// The "elsewhere" is a linked device that should be sending us a
// sync message.
self = .notAccepted
case
.incomingMissed,
.incomingMissedBecauseOfChangedIdentity,
.incomingMissedBecauseOfDoNotDisturb,
.incomingMissedBecauseBlockedSystemContact,
.incomingBusyElsewhere:
// Note that "busy elsewhere" means we should display the call
// as missed, but the busy linked device won't send a sync
// message.
self = .incomingMissed
@unknown default:
CallRecordLogger.shared.warn("Unknown call type!")
return nil
}
}
}
// MARK: -
public class IndividualCallRecordStatusTransitionManager {
public init() {}
public func isStatusTransitionAllowed(
fromIndividualCallStatus: CallRecord.CallStatus.IndividualCallStatus,
toIndividualCallStatus: CallRecord.CallStatus.IndividualCallStatus
) -> Bool {
switch fromIndividualCallStatus {
case .pending:
switch toIndividualCallStatus {
case .pending: return false
case .accepted, .notAccepted, .incomingMissed:
// Pending can transition to anything.
return true
}
case .accepted:
switch toIndividualCallStatus {
case .accepted, .pending: return false
case .notAccepted, .incomingMissed:
// Accepted trumps declined or missed.
return false
}
case .notAccepted:
switch toIndividualCallStatus {
case .notAccepted, .pending: return false
case .accepted:
// Accepted trumps declined...
return true
case .incomingMissed:
// ...but declined trumps missed.
return false
}
case .incomingMissed:
switch toIndividualCallStatus {
case .incomingMissed, .pending: return false
case .accepted, .notAccepted:
// Accepted or declined trumps missed.
return true
}
}
}
}