Path: blob/main/SignalServiceKit/Calls/CallRecord/GroupCallRecordRingUpdateDelegate.swift
1 views
//
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
public import LibSignalClient
public import SignalRingRTC
/// Responsible for updating ``CallRecord``s in response to ring updates.
@available(iOSApplicationExtension, unavailable)
public protocol GroupCallRecordRingUpdateDelegate: AnyObject {
/// Informs the delegate that a ring update was received for the given group
/// and ring.
///
/// - Parameter ringUpdateSender
/// The user who sent the ring update. Note that interpreting this requires
/// inspecting `ringUpdate`. For example, if the ring is "requested" this
/// will be the person who initiated the ring. Alternatively, if the ring is
/// "canceled" this will be ourselves, as the cancellation will have come
/// from another of our own devices.
func didReceiveRingUpdate(
groupId: Data,
ringId: Int64,
ringUpdate: RingUpdate,
ringUpdateSender: Aci,
tx: DBWriteTransaction,
)
}
@available(iOSApplicationExtension, unavailable)
public final class GroupCallRecordRingUpdateHandler: GroupCallRecordRingUpdateDelegate {
private let callRecordStore: CallRecordStore
private let groupCallRecordManager: GroupCallRecordManager
private let interactionStore: InteractionStore
private let threadStore: ThreadStore
private let logger: PrefixedLogger = CallRecordLogger.shared
public init(
callRecordStore: CallRecordStore,
groupCallRecordManager: GroupCallRecordManager,
interactionStore: InteractionStore,
threadStore: ThreadStore,
) {
self.callRecordStore = callRecordStore
self.groupCallRecordManager = groupCallRecordManager
self.interactionStore = interactionStore
self.threadStore = threadStore
}
public func didReceiveRingUpdate(
groupId: Data,
ringId: Int64,
ringUpdate: RingUpdate,
ringUpdateSender: Aci,
tx: DBWriteTransaction,
) {
let ringUpdateLogger = logger.suffixed(with: "\(ringUpdate)")
let callId = callIdFromRingId(ringId)
let callEventTimestamp = Date().ows_millisecondsSince1970
guard
let groupThread = threadStore.fetchGroupThread(groupId: groupId, tx: tx),
let groupThreadRowId = groupThread.sqliteRowId
else {
logger.error("Received ring update, but missing group thread!")
return
}
let ringerAci: Aci? = {
switch ringUpdate {
case .requested, .expiredRing, .busyLocally, .cancelledByRinger:
// The "ring update sender" is the person who rang the group.
return ringUpdateSender
case .acceptedOnAnotherDevice, .declinedOnAnotherDevice, .busyOnAnotherDevice:
// The "ring update sender" is ourself for these updates.
return nil
}
}()
switch callRecordStore.fetch(
callId: callId,
conversationId: .thread(threadRowId: groupThreadRowId),
tx: tx,
) {
case .matchDeleted:
ringUpdateLogger.warn("Ignoring ring update: existing record was deleted!")
case .matchFound(let existingCallRecord):
guard case let .group(existingGroupCallStatus) = existingCallRecord.callStatus else {
logger.error("Received ring update, but existing record is not a group call!")
return
}
guard case .incoming = existingCallRecord.callDirection else {
logger.error("Received ring update for a call we started!")
return
}
/// Depending on the ring update, we may want to update the existing
/// record's status – or do nothing.
let newGroupCallStatus: CallRecord.CallStatus.GroupCallStatus
switch ringUpdate {
case .requested:
switch existingGroupCallStatus {
case .generic:
newGroupCallStatus = .ringing
case .joined:
// We had already joined, and learned late about the ring.
newGroupCallStatus = .ringingAccepted
case .ringing, .ringingAccepted, .ringingDeclined, .ringingMissed, .ringingMissedNotificationProfile:
logger.warn("Received ring request, but we already knew about the ringing!")
return
}
case .expiredRing, .cancelledByRinger:
switch existingGroupCallStatus {
case .generic, .ringing:
newGroupCallStatus = .ringingMissed
case .joined:
// We're learning about ringing via the ring expiration,
// rather than the ring request. Weird, but not a problem.
newGroupCallStatus = .ringingAccepted
case .ringingAccepted, .ringingDeclined, .ringingMissed, .ringingMissedNotificationProfile:
return
}
case .busyLocally, .busyOnAnotherDevice:
switch existingGroupCallStatus {
case .generic, .ringing:
newGroupCallStatus = .ringingMissed
case .joined:
// We're learning about ringing via this busy message,
// rather than the ring request. Weird, but not a problem.
newGroupCallStatus = .ringingAccepted
case .ringingAccepted, .ringingDeclined, .ringingMissed, .ringingMissedNotificationProfile:
logger.warn("Ring canceled due to busy, but we're preferring preexisting state.")
return
}
case .acceptedOnAnotherDevice:
switch existingGroupCallStatus {
case .generic, .joined, .ringing, .ringingDeclined, .ringingMissed, .ringingMissedNotificationProfile:
newGroupCallStatus = .ringingAccepted
case .ringingAccepted:
return
}
case .declinedOnAnotherDevice:
// We don't have the ringer's ACI in these states, so we'll end
// up with group call records in "ringing" states that don't
// have the ringer's ACI. That's ok – we'd prefer to track the
// ringing state.
//
// Note that this case implies we've missed ring messages,
// because otherwise we'd have marked this record as ringing
// already.
switch existingGroupCallStatus {
case .ringing, .ringingMissed, .ringingMissedNotificationProfile, .generic:
newGroupCallStatus = .ringingDeclined
case .joined:
newGroupCallStatus = .ringingAccepted
case .ringingAccepted:
if case .outgoing = existingCallRecord.callDirection {
logger.warn("How did we have a declined ring for a call we started?")
}
fallthrough
case .ringingDeclined:
return
}
}
ringUpdateLogger.info("Updating group call record for ring update.")
groupCallRecordManager.updateGroupCallRecord(
existingCallRecord: existingCallRecord,
newCallDirection: existingCallRecord.callDirection,
newGroupCallStatus: newGroupCallStatus,
newGroupCallRingerAci: ringerAci,
callEventTimestamp: callEventTimestamp,
shouldSendSyncMessage: false,
tx: tx,
)
case .matchNotFound:
let groupCallStatus: CallRecord.CallStatus.GroupCallStatus = {
switch ringUpdate {
case .requested:
return .ringing
case .expiredRing:
return .ringingMissed
case .cancelledByRinger, .busyLocally, .busyOnAnotherDevice:
logger.warn("Ring canceled, but we never learned of ring in the first place!")
return .ringingMissed
case .acceptedOnAnotherDevice:
logger.warn("Ring accepted on another device, but we never learned of ring in the first place!")
return .ringingAccepted
case .declinedOnAnotherDevice:
logger.warn("Ring declined on another device, but we never learned of ring in the first place!")
return .ringingDeclined
}
}()
let (newGroupCallInteraction, interactionRowId) = interactionStore.insertGroupCallInteraction(
groupThread: groupThread,
callEventTimestamp: callEventTimestamp,
tx: tx,
)
ringUpdateLogger.info("Creating group call record for ring update.")
do {
_ = try groupCallRecordManager.createGroupCallRecord(
callId: callId,
groupCallInteraction: newGroupCallInteraction,
groupCallInteractionRowId: interactionRowId,
groupThreadRowId: groupThreadRowId,
callDirection: .incoming,
groupCallStatus: groupCallStatus,
groupCallRingerAci: ringerAci,
callEventTimestamp: callEventTimestamp,
shouldSendSyncMessage: false,
tx: tx,
)
} catch let error {
owsFailBeta("Failed to insert call record: \(error)")
}
}
}
}