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

195 lines
6.4 KiB
Swift

//
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
import LibSignalClient
enum OutgoingCallEventSyncMessageEvent {
case callUpdated
case callDeleted
}
protocol OutgoingCallEventSyncMessageManager {
typealias CallEvent = OutgoingCallEventSyncMessageEvent
/// Send a sync message with the state on the given call record.
///
/// - Parameter callEventTimestamp
/// The time at which the event that triggered this sync message occurred.
func sendSyncMessage(
callRecord: CallRecord,
callEvent: CallEvent,
callEventTimestamp: UInt64,
tx: DBWriteTransaction
)
}
final class OutgoingCallEventSyncMessageManagerImpl: OutgoingCallEventSyncMessageManager {
private let appReadiness: AppReadiness
private let databaseStorage: SDSDatabaseStorage
private let messageSenderJobQueue: MessageSenderJobQueue
private let callRecordConversationIdAdapter: any CallRecordSyncMessageConversationIdAdapter
private var logger: CallRecordLogger { .shared }
init(
appReadiness: AppReadiness,
databaseStorage: SDSDatabaseStorage,
messageSenderJobQueue: MessageSenderJobQueue,
callRecordConversationIdAdapter: any CallRecordSyncMessageConversationIdAdapter
) {
self.appReadiness = appReadiness
self.databaseStorage = databaseStorage
self.messageSenderJobQueue = messageSenderJobQueue
self.callRecordConversationIdAdapter = callRecordConversationIdAdapter
}
func sendSyncMessage(
callRecord: CallRecord,
callEvent: CallEvent,
callEventTimestamp: UInt64,
tx: DBWriteTransaction
) {
let outgoingCallEventType: OutgoingCallEvent.EventType? = {
switch callEvent {
case .callDeleted:
return .deleted
case .callUpdated:
return OutgoingCallEvent.EventType(callRecord.callStatus)
}
}()
guard let outgoingCallEventType else {
return
}
let conversationId: Data
do {
conversationId = try callRecordConversationIdAdapter.getConversationId(callRecord: callRecord, tx: tx)
} catch {
owsFailDebug("\(error)")
return
}
let outgoingCallEvent = OutgoingCallEvent(
timestamp: callEventTimestamp,
conversationId: conversationId,
callId: callRecord.callId,
callType: OutgoingCallEvent.CallType(callRecord.callType),
eventDirection: OutgoingCallEvent.EventDirection(callRecord.callDirection),
eventType: outgoingCallEventType
)
sendSyncMessage(
outgoingCallEvent: outgoingCallEvent,
tx: tx
)
}
/// Enqueue the given call event to be sent to linked devices in a sync
/// message.
///
/// - Note
/// This can be called before the app is finished launching. For example,
/// IncompleteCallsJob runs on launch and can modify call state in the
/// database that triggers a sync message. In that scenario, though, we
/// won't have the required state set up yet to send a message.
///
/// Instead, we can construct the sync message payload now, but fire the
/// sync message send when we're ready. That's fine, since these messages
/// can be delayed, and remote devices should be robust to out-of-order
/// updates.
private func sendSyncMessage(
outgoingCallEvent callEvent: OutgoingCallEvent,
tx syncTx: DBWriteTransaction
) {
let syncTx = SDSDB.shimOnlyBridge(syncTx)
if appReadiness.isAppReady {
logger.info("Enqueuing call event sync message: \(callEvent.callType), \(callEvent.eventDirection), \(callEvent.eventType).")
_sendSyncMessage(outgoingCallEvent: callEvent, tx: syncTx)
} else {
logger.info("Delaying call event sync message because app isn't ready.")
appReadiness.runNowOrWhenAppDidBecomeReadyAsync {
self.databaseStorage.write { asyncTx in
self._sendSyncMessage(outgoingCallEvent: callEvent, tx: asyncTx)
}
}
}
}
private func _sendSyncMessage(
outgoingCallEvent: OutgoingCallEvent,
tx: SDSAnyWriteTransaction
) {
guard let localThread = TSContactThread.getOrCreateLocalThread(transaction: tx) else {
owsFailDebug("Missing local thread for sync message!")
return
}
let outgoingCallEventMessage = OutgoingCallEventSyncMessage(
thread: localThread,
event: outgoingCallEvent,
tx: tx
)
let preparedMessage = PreparedOutgoingMessage.preprepared(
transientMessageWithoutAttachments: outgoingCallEventMessage
)
messageSenderJobQueue.add(
message: preparedMessage, transaction: tx
)
}
}
// MARK: - Conversions for outgoing sync message types
private extension OutgoingCallEvent.CallType {
init(_ callType: CallRecord.CallType) {
switch callType {
case .audioCall: self = .audio
case .videoCall: self = .video
case .groupCall: self = .group
case .adHocCall: self = .adHocCall
}
}
}
private extension OutgoingCallEvent.EventDirection {
init(_ callDirection: CallRecord.CallDirection) {
switch callDirection {
case .incoming: self = .incoming
case .outgoing: self = .outgoing
}
}
}
private extension OutgoingCallEvent.EventType {
init?(_ callStatus: CallRecord.CallStatus) {
switch callStatus {
case
.individual(.pending),
.individual(.incomingMissed),
.group(.generic),
.group(.ringing),
.group(.ringingMissed),
.group(.ringingMissedNotificationProfile):
// Local-only statuses
return nil
case
.individual(.accepted),
.group(.joined),
.callLink(.joined),
.group(.ringingAccepted):
self = .accepted
case
.individual(.notAccepted),
.group(.ringingDeclined):
self = .notAccepted
case
.callLink(.generic):
// [CallLink] TODO: Verify the correct message is sent in this case.
self = .observed
}
}
}