Signal-iOS/SignalServiceKit/Calls/CallRecord/IncomingCallEventSyncMessageManager.swift
2024-06-04 14:58:58 -07:00

594 lines
23 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
import LibSignalClient
/// Handles incoming `CallEvent` sync messages.
///
/// - SeeAlso ``IncomingCallEventSyncMessageParams``
protocol IncomingCallEventSyncMessageManager {
func createOrUpdateRecordForIncomingSyncMessage(
incomingSyncMessage: IncomingCallEventSyncMessageParams,
syncMessageTimestamp: UInt64,
tx: DBWriteTransaction
)
}
final class IncomingCallEventSyncMessageManagerImpl: IncomingCallEventSyncMessageManager {
private let callRecordStore: CallRecordStore
private let callRecordDeleteManager: CallRecordDeleteManager
private let groupCallRecordManager: GroupCallRecordManager
private let individualCallRecordManager: IndividualCallRecordManager
private let interactionDeleteManager: InteractionDeleteManager
private let interactionStore: InteractionStore
private let markAsReadShims: Shims.MarkAsRead
private let recipientDatabaseTable: RecipientDatabaseTable
private let threadStore: ThreadStore
init(
callRecordStore: CallRecordStore,
callRecordDeleteManager: CallRecordDeleteManager,
groupCallRecordManager: GroupCallRecordManager,
individualCallRecordManager: IndividualCallRecordManager,
interactionDeleteManager: InteractionDeleteManager,
interactionStore: InteractionStore,
markAsReadShims: Shims.MarkAsRead,
recipientDatabaseTable: RecipientDatabaseTable,
threadStore: ThreadStore
) {
self.callRecordStore = callRecordStore
self.callRecordDeleteManager = callRecordDeleteManager
self.groupCallRecordManager = groupCallRecordManager
self.individualCallRecordManager = individualCallRecordManager
self.interactionDeleteManager = interactionDeleteManager
self.interactionStore = interactionStore
self.markAsReadShims = markAsReadShims
self.recipientDatabaseTable = recipientDatabaseTable
self.threadStore = threadStore
}
public func createOrUpdateRecordForIncomingSyncMessage(
incomingSyncMessage syncMessage: IncomingCallEventSyncMessageParams,
syncMessageTimestamp: UInt64,
tx: DBWriteTransaction
) {
let callId: UInt64 = syncMessage.callId
let callDirection: CallRecord.CallDirection = syncMessage.callDirection
let callType: CallRecord.CallType = syncMessage.callType
let callTimestamp: UInt64 = syncMessage.callTimestamp
let syncMessageConversationId = syncMessage.conversationId
let syncMessageEvent = syncMessage.callEvent
let logger = CallRecordLogger.shared.suffixed(with: "\(callDirection), \(syncMessageEvent)")
switch syncMessageConversationId {
case let .individual(contactServiceId):
guard
let contactThread = fetchThread(contactServiceId: contactServiceId, tx: tx),
let contactThreadRowId = contactThread.sqliteRowId
else {
logger.error("Missing contact thread for incoming call event sync message!")
return
}
if case .deleted = syncMessageEvent {
deleteCallRecordForIncomingSyncMessage(
callId: callId,
threadRowId: contactThreadRowId,
logger: logger,
tx: tx
)
return
}
let individualCallStatus: CallRecord.CallStatus.IndividualCallStatus = {
switch syncMessageEvent {
case .deleted: owsFail("Checked above how did we get here?")
case .accepted: return .accepted
case .notAccepted: return .notAccepted
}
}()
let individualCallInteractionType: RPRecentCallType = {
switch (callDirection, syncMessageEvent) {
case (_, .deleted): owsFail("Checked above how did we get here?")
case (.incoming, .accepted): return .incomingAnsweredElsewhere
case (.incoming, .notAccepted): return .incomingDeclinedElsewhere
case (.outgoing, .accepted): return .outgoing
case (.outgoing, .notAccepted): return .outgoingMissed
}
}()
switch callRecordStore.fetch(
callId: callId,
threadRowId: contactThreadRowId,
tx: tx
) {
case .matchDeleted:
logger.warn(
"Ignoring incoming individual call sync message: existing record was deleted!"
)
return
case .matchFound(let existingCallRecord):
guard let existingCallInteraction: TSCall = interactionStore
.fetchAssociatedInteraction(callRecord: existingCallRecord, tx: tx)
else { return }
logger.info("Updating existing record for individual call sync message.")
updateIndividualCallRecordForIncomingSyncMessage(
existingCallRecord: existingCallRecord,
existingCallInteraction: existingCallInteraction,
existingCallThread: contactThread,
newIndividualCallStatus: individualCallStatus,
newIndividualCallInteractionType: individualCallInteractionType,
syncMessageTimestamp: syncMessageTimestamp,
tx: tx
)
case .matchNotFound:
logger.info("Creating record for individual call sync message.")
createIndividualCallRecordForIncomingSyncMessage(
contactThread: contactThread,
contactThreadRowId: contactThreadRowId,
callId: callId,
callType: callType,
callDirection: callDirection,
individualCallStatus: individualCallStatus,
individualCallInteractionType: individualCallInteractionType,
callTimestamp: callTimestamp,
syncMessageTimestamp: syncMessageTimestamp,
tx: tx
)
}
case let .group(groupId):
guard
let groupThread = fetchThread(groupId: groupId, tx: tx),
let groupThreadRowId = groupThread.sqliteRowId
else {
logger.error("Missing group thread for incoming call event sync message!")
return
}
if case .deleted = syncMessageEvent {
deleteCallRecordForIncomingSyncMessage(
callId: callId,
threadRowId: groupThreadRowId,
logger: logger,
tx: tx
)
return
}
switch callRecordStore.fetch(
callId: callId,
threadRowId: groupThreadRowId,
tx: tx
) {
case .matchDeleted:
logger.warn(
"Ignoring incoming group call sync message: existing record was deleted!"
)
return
case .matchFound(let existingCallRecord):
guard let existingCallInteraction: OWSGroupCallMessage = interactionStore
.fetchAssociatedInteraction(callRecord: existingCallRecord, tx: tx)
else { return }
guard case let .group(existingCallStatus) = existingCallRecord.callStatus else {
logger.error("Missing group call status for group call record!")
return
}
var newCallDirection = existingCallRecord.callDirection
let newGroupCallStatus: CallRecord.CallStatus.GroupCallStatus
switch syncMessageEvent {
case .deleted:
owsFail("Checked above how did we get here?")
case .accepted:
switch callDirection {
case .incoming:
// We joined on another device. If we knew about ringing
// on this device we know the ringing was accepted, and
// otherwise it was a non-ringing join.
switch existingCallStatus {
case .generic, .joined:
newGroupCallStatus = .joined
case .ringing, .ringingAccepted, .ringingDeclined, .ringingMissed:
newGroupCallStatus = .ringingAccepted
}
case .outgoing:
if
case .outgoing = existingCallRecord.callDirection,
case .ringingAccepted = existingCallStatus
{
logger.warn("How did we already know about this call?")
return
}
// We rang a group from another device. It's possible we
// opportunistically learned about that call on this
// device via peek (and maybe joined), but this should
// be the first time we're learning about the ring.
switch existingCallStatus {
case .generic, .joined:
newCallDirection = .outgoing
newGroupCallStatus = .ringingAccepted
case .ringing, .ringingAccepted, .ringingDeclined, .ringingMissed:
logger.warn("How did we have a ringing call event for a call we started on another device?")
newGroupCallStatus = .ringingAccepted
}
}
case .notAccepted:
switch callDirection {
case .incoming:
// We declined on another device. If we joined the call
// on this device, we'll prefer the join.
switch existingCallStatus {
case .generic, .ringing, .ringingMissed, .ringingDeclined:
newGroupCallStatus = .ringingDeclined
case .joined, .ringingAccepted:
newGroupCallStatus = .ringingAccepted
}
case .outgoing:
logger.error("How did we decline our own outgoing call?")
return
}
}
logger.info("Updating existing record for group call sync message.")
updateGroupCallRecordForIncomingSyncMessage(
existingCallRecord: existingCallRecord,
existingCallInteraction: existingCallInteraction,
existingGroupThread: groupThread,
newCallDirection: newCallDirection,
newGroupCallStatus: newGroupCallStatus,
callEventTimestamp: callTimestamp,
syncMessageTimestamp: syncMessageTimestamp,
tx: tx
)
case .matchNotFound:
let groupCallStatus: CallRecord.CallStatus.GroupCallStatus
switch (callDirection, syncMessageEvent) {
case (_, .deleted):
owsFail("Checked above how did we get here?")
case (.outgoing, .notAccepted):
logger.error("How did we decline a call we started?")
return
case (.outgoing, .accepted):
// We don't track the status of outgoing group rings, so
// we'll assume the ringing was accepted.
groupCallStatus = .ringingAccepted
case (.incoming, .accepted):
// Unclear if there was ringing involved. If so, we'll
// update the call record to reflect that if we get the ring
// update.
groupCallStatus = .joined
case (.incoming, .notAccepted):
// We only send this combination for ring declines, so we
// can assume that's what this was.
groupCallStatus = .ringingDeclined
}
logger.info("Creating new record for group call sync message.")
createGroupCallRecordForIncomingSyncMessage(
callId: callId,
groupThread: groupThread,
groupThreadRowId: groupThreadRowId,
callDirection: callDirection,
groupCallStatus: groupCallStatus,
callEventTimestamp: callTimestamp,
syncMessageTimestamp: syncMessageTimestamp,
tx: tx
)
}
}
}
}
// MARK: - Deleting calls
private extension IncomingCallEventSyncMessageManagerImpl {
func deleteCallRecordForIncomingSyncMessage(
callId: UInt64,
threadRowId: Int64,
logger: PrefixedLogger,
tx: DBWriteTransaction
) {
switch callRecordStore.fetch(
callId: callId,
threadRowId: threadRowId,
tx: tx
) {
case .matchDeleted:
logger.warn(
"Ignoring incoming delete call sync message: existing record was already deleted!"
)
case .matchFound(let existingCallRecord):
// Don't send a sync message for the call delete: we're already
// reacting to one!
interactionDeleteManager.delete(
alongsideAssociatedCallRecords: [existingCallRecord],
sideEffects: .custom(associatedCallDelete: .localDeleteOnly),
tx: tx
)
case .matchNotFound:
callRecordDeleteManager.markCallAsDeleted(
callId: callId,
threadRowId: threadRowId,
tx: tx
)
}
}
}
// MARK: - Individual call
private extension IncomingCallEventSyncMessageManagerImpl {
func updateIndividualCallRecordForIncomingSyncMessage(
existingCallRecord: CallRecord,
existingCallInteraction: TSCall,
existingCallThread: TSContactThread,
newIndividualCallStatus: CallRecord.CallStatus.IndividualCallStatus,
newIndividualCallInteractionType: RPRecentCallType,
syncMessageTimestamp: UInt64,
tx: DBWriteTransaction
) {
interactionStore.updateIndividualCallInteractionType(
individualCallInteraction: existingCallInteraction,
newCallInteractionType: newIndividualCallInteractionType,
tx: tx
)
individualCallRecordManager.updateRecord(
contactThread: existingCallThread,
existingCallRecord: existingCallRecord,
newIndividualCallStatus: newIndividualCallStatus,
shouldSendSyncMessage: false,
tx: tx
)
markThingsAsReadForIncomingSyncMessage(
callInteraction: existingCallInteraction,
thread: existingCallThread,
syncMessageTimestamp: syncMessageTimestamp,
tx: tx
)
}
func createIndividualCallRecordForIncomingSyncMessage(
contactThread: TSContactThread,
contactThreadRowId: Int64,
callId: UInt64,
callType: CallRecord.CallType,
callDirection: CallRecord.CallDirection,
individualCallStatus: CallRecord.CallStatus.IndividualCallStatus,
individualCallInteractionType: RPRecentCallType,
callTimestamp: UInt64,
syncMessageTimestamp: UInt64,
tx: DBWriteTransaction
) {
let newIndividualCallInteraction = TSCall(
callType: individualCallInteractionType,
offerType: callType.individualCallOfferType,
thread: contactThread,
sentAtTimestamp: callTimestamp
)
interactionStore.insertInteraction(newIndividualCallInteraction, tx: tx)
guard let interactionRowId = newIndividualCallInteraction.sqliteRowId else {
owsFail("Missing SQLite row ID for just-inserted interaction!")
}
individualCallRecordManager.createRecordForInteraction(
individualCallInteraction: newIndividualCallInteraction,
individualCallInteractionRowId: interactionRowId,
contactThread: contactThread,
contactThreadRowId: contactThreadRowId,
callId: callId,
callType: callType,
callDirection: callDirection,
individualCallStatus: individualCallStatus,
shouldSendSyncMessage: false,
tx: tx
)
markThingsAsReadForIncomingSyncMessage(
callInteraction: newIndividualCallInteraction,
thread: contactThread,
syncMessageTimestamp: syncMessageTimestamp,
tx: tx
)
}
}
// MARK: - Group calls
private extension IncomingCallEventSyncMessageManagerImpl {
func updateGroupCallRecordForIncomingSyncMessage(
existingCallRecord: CallRecord,
existingCallInteraction: OWSGroupCallMessage,
existingGroupThread: TSGroupThread,
newCallDirection: CallRecord.CallDirection,
newGroupCallStatus: CallRecord.CallStatus.GroupCallStatus,
callEventTimestamp: UInt64,
syncMessageTimestamp: UInt64,
tx: DBWriteTransaction
) {
groupCallRecordManager.updateGroupCallRecord(
groupThread: existingGroupThread,
existingCallRecord: existingCallRecord,
newCallDirection: newCallDirection,
newGroupCallStatus: newGroupCallStatus,
newGroupCallRingerAci: nil,
callEventTimestamp: callEventTimestamp,
shouldSendSyncMessage: false,
tx: tx
)
markThingsAsReadForIncomingSyncMessage(
callInteraction: existingCallInteraction,
thread: existingGroupThread,
syncMessageTimestamp: syncMessageTimestamp,
tx: tx
)
}
func createGroupCallRecordForIncomingSyncMessage(
callId: UInt64,
groupThread: TSGroupThread,
groupThreadRowId: Int64,
callDirection: CallRecord.CallDirection,
groupCallStatus: CallRecord.CallStatus.GroupCallStatus,
callEventTimestamp: UInt64,
syncMessageTimestamp: UInt64,
tx: DBWriteTransaction
) {
let (newGroupCallInteraction, interactionRowId) = interactionStore.insertGroupCallInteraction(
groupThread: groupThread,
callEventTimestamp: callEventTimestamp,
tx: tx
)
_ = groupCallRecordManager.createGroupCallRecord(
callId: callId,
groupCallInteraction: newGroupCallInteraction,
groupCallInteractionRowId: interactionRowId,
groupThread: groupThread,
groupThreadRowId: groupThreadRowId,
callDirection: callDirection,
groupCallStatus: groupCallStatus,
groupCallRingerAci: nil,
callEventTimestamp: callEventTimestamp,
shouldSendSyncMessage: false,
tx: tx
)
markThingsAsReadForIncomingSyncMessage(
callInteraction: newGroupCallInteraction,
thread: groupThread,
syncMessageTimestamp: syncMessageTimestamp,
tx: tx
)
}
}
// MARK: -
private extension IncomingCallEventSyncMessageManagerImpl {
func markThingsAsReadForIncomingSyncMessage(
callInteraction: TSInteraction & OWSReadTracking,
thread: TSThread,
syncMessageTimestamp: UInt64,
tx: DBWriteTransaction
) {
owsAssert(callInteraction.uniqueThreadId == thread.uniqueId)
markAsReadShims.markThingsAsReadForIncomingSyncMessage(
callInteraction: callInteraction,
thread: thread,
syncMessageTimestamp: syncMessageTimestamp,
tx: tx
)
}
func fetchThread(
contactServiceId: ServiceId,
tx: DBReadTransaction
) -> TSContactThread? {
guard
let contactRecipient = recipientDatabaseTable.fetchRecipient(
serviceId: contactServiceId, transaction: tx
),
let contactThread = threadStore.fetchContactThread(
recipient: contactRecipient, tx: tx
)
else { return nil }
return contactThread
}
func fetchThread(groupId: Data, tx: DBReadTransaction) -> TSGroupThread? {
return threadStore.fetchGroupThread(groupId: groupId, tx: tx)
}
}
// MARK: -
private extension CallRecord.CallType {
var individualCallOfferType: TSRecentCallOfferType {
switch self {
case .audioCall: return .audio
case .videoCall: return .video
case .groupCall:
owsFailDebug("Should never ask for an individual call type for a group call!")
return .video
}
}
}
// MARK: - Shims
extension IncomingCallEventSyncMessageManagerImpl {
enum Shims {
typealias MarkAsRead = _IncomingCallEventSyncMessageManagerImpl_MarkAsRead
}
enum ShimsImpl {
typealias MarkAsRead = _IncomingCallEventSyncMessageManagerImpl_MarkAsReadImpl
}
}
protocol _IncomingCallEventSyncMessageManagerImpl_MarkAsRead {
/// Mark a grab-bag of things as read for the given interaction, in response
/// to an incoming call event sync message.
func markThingsAsReadForIncomingSyncMessage(
callInteraction: TSInteraction & OWSReadTracking,
thread: TSThread,
syncMessageTimestamp: UInt64,
tx: DBWriteTransaction
)
}
final class _IncomingCallEventSyncMessageManagerImpl_MarkAsReadImpl: _IncomingCallEventSyncMessageManagerImpl_MarkAsRead {
private let notificationPresenter: any NotificationPresenter
init(notificationPresenter: any NotificationPresenter) {
self.notificationPresenter = notificationPresenter
}
func markThingsAsReadForIncomingSyncMessage(
callInteraction: TSInteraction & OWSReadTracking,
thread: TSThread,
syncMessageTimestamp: UInt64,
tx: DBWriteTransaction
) {
let tx = SDSDB.shimOnlyBridge(tx)
if !callInteraction.wasRead {
callInteraction.markAsRead(
atTimestamp: syncMessageTimestamp,
thread: thread,
circumstance: .onLinkedDevice,
shouldClearNotifications: true,
transaction: tx
)
}
OWSReceiptManager.markAllCallInteractionsAsReadLocally(
beforeSQLId: callInteraction.grdbId,
thread: thread,
transaction: tx
)
tx.addAsyncCompletionOnMain {
self.notificationPresenter.cancelNotificationsForMissedCalls(
threadUniqueId: thread.uniqueId
)
}
}
}