TM-SGNL-iOS/SignalNSE/NSECallMessageHandler.swift
TeleMessage developers dde0620daf initial commit
2025-05-03 12:28:28 -07:00

207 lines
8.3 KiB
Swift

//
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
import CallKit
import Foundation
import LibSignalClient
import SignalRingRTC
import SignalServiceKit
class NSECallMessageHandler: CallMessageHandler {
// MARK: Initializers
init() {
SwiftSingletons.register(self)
}
private var databaseStorage: SDSDatabaseStorage { SSKEnvironment.shared.databaseStorageRef }
private var groupCallManager: GroupCallManager { SSKEnvironment.shared.groupCallManagerRef }
private var identityManager: any OWSIdentityManager { DependenciesBridge.shared.identityManager }
private var messagePipelineSupervisor: MessagePipelineSupervisor { SSKEnvironment.shared.messagePipelineSupervisorRef }
private var notificationPresenter: NotificationPresenterImpl { SSKEnvironment.shared.notificationPresenterRef as! NotificationPresenterImpl }
private var profileManager: any ProfileManager { SSKEnvironment.shared.profileManagerRef }
private var tsAccountManager: any TSAccountManager { DependenciesBridge.shared.tsAccountManager }
// MARK: - Call Handlers
func receivedEnvelope(
_ envelope: SSKProtoEnvelope,
callEnvelope: CallEnvelopeType,
from caller: (aci: Aci, deviceId: UInt32),
toLocalIdentity localIdentity: OWSIdentity,
plaintextData: Data,
wasReceivedByUD: Bool,
sentAtTimestamp: UInt64,
serverReceivedTimestamp: UInt64,
serverDeliveryTimestamp: UInt64,
tx: SDSAnyWriteTransaction
) {
let bufferSecondsForMainAppToAnswerRing: UInt64 = 10
let serverReceivedTimestamp = serverReceivedTimestamp > 0 ? serverReceivedTimestamp : sentAtTimestamp
let approxMessageAge = (serverDeliveryTimestamp - serverReceivedTimestamp)
let messageAgeForRingRtc = approxMessageAge / kSecondInMs + bufferSecondsForMainAppToAnswerRing
switch callEnvelope {
case .offer(let offer):
guard let opaque = offer.opaque else {
return
}
let callOfferHandler = CallOfferHandlerImpl(
identityManager: identityManager,
notificationPresenter: notificationPresenter,
profileManager: profileManager,
tsAccountManager: tsAccountManager
)
let partialResult = callOfferHandler.startHandlingOffer(
caller: caller.aci,
sourceDevice: caller.deviceId,
localIdentity: localIdentity,
callId: offer.id,
callType: offer.type ?? .offerAudioCall,
sentAtTimestamp: sentAtTimestamp,
tx: tx
)
guard let partialResult else {
return
}
let callType: CallMediaType
switch offer.type ?? .offerAudioCall {
case .offerAudioCall: callType = .audioCall
case .offerVideoCall: callType = .videoCall
}
let isValid = isValidOfferMessage(
opaque: opaque,
messageAgeSec: messageAgeForRingRtc,
callMediaType: callType
)
guard isValid else {
NSELogger.uncorrelated.warn("missed a call because it's not valid (according to RingRTC)")
callOfferHandler.insertMissedCallInteraction(
for: offer.id,
in: partialResult.thread,
outcome: .incomingMissed,
callType: partialResult.offerMediaType,
sentAtTimestamp: sentAtTimestamp,
tx: tx
)
return
}
case .opaque(let opaque):
func validateGroupRing(groupId: Data, ringId: Int64) -> Bool {
databaseStorage.read { transaction in
if SignalServiceAddress(caller.aci).isLocalAddress {
// Always trust our other devices (important for cancellations).
return true
}
guard let thread = TSGroupThread.fetch(groupId: groupId, transaction: transaction) else {
owsFailDebug("discarding group ring \(ringId) from \(caller.aci) for unknown group")
return false
}
guard GroupsV2MessageProcessor.discardMode(
forMessageFrom: caller.aci,
groupId: groupId,
tx: transaction
) == .doNotDiscard else {
NSELogger.uncorrelated.warn("discarding group ring \(ringId) from \(caller.aci)")
return false
}
guard thread.groupMembership.fullMembers.count <= RemoteConfig.current.maxGroupCallRingSize else {
NSELogger.uncorrelated.warn("discarding group ring \(ringId) from \(caller.aci) for too-large group")
return false
}
return true
}
}
let shouldHandleExternally = { () -> Bool in
guard opaque.urgency == .handleImmediately else {
return false
}
guard let opaqueData = opaque.data else {
return false
}
return isValidOpaqueRing(
opaqueCallMessage: opaqueData,
messageAgeSec: messageAgeForRingRtc,
validateGroupRing: validateGroupRing
)
}()
guard shouldHandleExternally else {
NSELogger.uncorrelated.info("Ignoring opaque message; not a valid ring according to RingRTC.")
return
}
case .answer, .iceUpdate, .hangup, .busy:
NSELogger.uncorrelated.warn("Dropping call message; the main app should be connected")
return
}
externallyHandleCallMessage(
envelope: envelope,
plaintextData: plaintextData,
wasReceivedByUD: wasReceivedByUD,
serverDeliveryTimestamp: serverDeliveryTimestamp,
tx: tx
)
}
private func externallyHandleCallMessage(
envelope: SSKProtoEnvelope,
plaintextData: Data,
wasReceivedByUD: Bool,
serverDeliveryTimestamp: UInt64,
tx: SDSAnyWriteTransaction
) {
do {
let payload = try CallMessageRelay.enqueueCallMessageForMainApp(
envelope: envelope,
plaintextData: plaintextData,
wasReceivedByUD: wasReceivedByUD,
serverDeliveryTimestamp: serverDeliveryTimestamp,
transaction: tx
)
// We don't want to risk consuming any call messages that the main app needs to perform the call
// We suspend message processing in our process to give the main app a chance to wake and take over
let suspension = messagePipelineSupervisor.suspendMessageProcessing(for: .nseWakingUpApp(suspensionId: UUID(), payloadString: "\(payload)"))
DispatchQueue.sharedUtility.asyncAfter(deadline: .now() + .seconds(10)) {
suspension.invalidate()
}
NSELogger.uncorrelated.info("Notifying primary app of incoming call with push payload: \(payload)")
CXProvider.reportNewIncomingVoIPPushPayload(payload.payloadDict) { error in
if let error = error {
owsFailDebug("Failed to notify main app of call message: \(error)")
} else {
NSELogger.uncorrelated.info("Successfully notified main app of call message.")
}
}
} catch {
owsFailDebug("Failed to create relay voip payload for call message \(error)")
}
}
func receivedGroupCallUpdateMessage(
_ updateMessage: SSKProtoDataMessageGroupCallUpdate,
forGroupId groupId: GroupIdentifier,
serverReceivedTimestamp: UInt64
) async {
await groupCallManager.peekGroupCallAndUpdateThread(
forGroupId: groupId,
peekTrigger: .receivedGroupUpdateMessage(
eraId: updateMessage.eraID,
messageTimestamp: serverReceivedTimestamp
)
)
}
}