diff --git a/Signal/Calls/CallLinkStateUpdater.swift b/Signal/Calls/CallLinkStateUpdater.swift index c08a597cac..aa1eefeedf 100644 --- a/Signal/Calls/CallLinkStateUpdater.swift +++ b/Signal/Calls/CallLinkStateUpdater.swift @@ -57,13 +57,19 @@ actor CallLinkStateUpdater { rootKey: CallLinkRootKey, updateAndFetch: (CallLinkAuthCredential) async throws -> SignalServiceKit.CallLinkState ) async throws -> SignalServiceKit.CallLinkState { - return try await _updateExclusively(rootKey: rootKey, updateAndFetch: updateAndFetch)! + return try await _updateExclusively(rootKey: rootKey, updateAndFetch: updateAndFetch)!.get() + } + + private enum UpdateAction { + case update(SignalServiceKit.CallLinkState) + case notFound + case delete } private func _updateExclusively( rootKey: CallLinkRootKey, updateAndFetch: (CallLinkAuthCredential) async throws -> SignalServiceKit.CallLinkState? - ) async throws -> SignalServiceKit.CallLinkState? { + ) async throws -> Result? { let roomId = rootKey.deriveRoomId() await withCheckedContinuation { continuation in @@ -90,13 +96,34 @@ actor CallLinkStateUpdater { return try callLinkStore.fetch(roomId: roomId, tx: tx) } let authCredential = try await authCredentialManager.fetchCallLinkAuthCredential(localIdentifiers: localIdentifiers) - let newState = try await updateAndFetch(authCredential) + let updateResult = await Result { try await updateAndFetch(authCredential) } + + let updateAction: UpdateAction + let returnResult: Result? + + switch updateResult { + case .success(let callLinkState?): + updateAction = .update(callLinkState) + returnResult = .success(callLinkState) + case .success(nil): + updateAction = .delete + returnResult = nil + case .failure(let error as CallLinkNotFoundError): + updateAction = .notFound + returnResult = .failure(error) + case .failure(let error): + throw error + } + try await db.awaitableWrite { tx in if var newRecord = try self.callLinkStore.fetch(roomId: roomId, tx: tx) { if !newRecord.isDeleted { - if let newState { + switch updateAction { + case .update(let newState): newRecord.updateState(newState) - } else { + case .notFound: + break + case .delete: newRecord.markDeleted(atTimestampMs: Date.ows_millisecondTimestamp()) try self.callRecordDeleteManager.deleteCallRecords( self.callRecordStore.fetchExisting(conversationId: .callLink(callLinkRowId: newRecord.id), limit: nil, tx: tx), @@ -111,13 +138,27 @@ actor CallLinkStateUpdater { try self.callLinkStore.update(newRecord, tx: tx) } } - return newState + + return returnResult } - func readCallLink(rootKey: CallLinkRootKey) async throws -> SignalServiceKit.CallLinkState { - return try await updateExclusively(rootKey: rootKey, updateAndFetch: { authCredential in + /// Reads a call link from the server. + /// + /// There are two layers of errors interesting to callers: the method itself + /// and the `Result` that's returned. + /// + /// This is a "state updater" object, so if the "state update" operation is + /// successful, no error is thrown. The "state update" is successful when + /// we're able to call `clearNeedsFetch` on the underlying CallLinkRecord. + /// (For example, no error is thrown when the call link can't be found, but + /// an error *is* thrown when there's no network.) + /// + /// Many callers will want access to the `CallLinkState`, and they can use + /// `try readCallLink(...).get()` to gloss over this distinction. + func readCallLink(rootKey: CallLinkRootKey) async throws -> Result { + return try await _updateExclusively(rootKey: rootKey, updateAndFetch: { authCredential in return try await callLinkFetcher.readCallLink(rootKey, authCredential: authCredential) - }) + })! } func deleteCallLink(rootKey: CallLinkRootKey, adminPasskey: Data) async throws { diff --git a/Signal/Calls/CallService.swift b/Signal/Calls/CallService.swift index fb15892514..eaf7300151 100644 --- a/Signal/Calls/CallService.swift +++ b/Signal/Calls/CallService.swift @@ -569,7 +569,7 @@ final class CallService: CallServiceStateObserver, CallServiceStateDelegate { case .reuse(let callLinkState): state = callLinkState case .fetch: - state = try await callLinkStateUpdater.readCallLink(rootKey: callLink.rootKey) + state = try await callLinkStateUpdater.readCallLink(rootKey: callLink.rootKey).get() } let localIdentifiers = DependenciesBridge.shared.tsAccountManager.localIdentifiersWithMaybeSneakyTransaction! let authCredential = try await authCredentialManager.fetchCallLinkAuthCredential(localIdentifiers: localIdentifiers) diff --git a/SignalUI/Calls/CallLinkFetcher.swift b/SignalUI/Calls/CallLinkFetcher.swift index d9c747aa8f..101fce562c 100644 --- a/SignalUI/Calls/CallLinkFetcher.swift +++ b/SignalUI/Calls/CallLinkFetcher.swift @@ -8,6 +8,8 @@ import LibSignalClient public import SignalRingRTC public import SignalServiceKit +public struct CallLinkNotFoundError: Error {} + public class CallLinkFetcherImpl { private let sfuClient: SFUClient // Even though we never use this, we need to retain it to ensure @@ -27,20 +29,24 @@ public class CallLinkFetcherImpl { let sfuUrl = DebugFlags.callingUseTestSFU.get() ? TSConstants.sfuTestURL : TSConstants.sfuURL let secretParams = CallLinkSecretParams.deriveFromRootKey(rootKey.bytes) let authCredentialPresentation = authCredential.present(callLinkParams: secretParams) - return SignalServiceKit.CallLinkState(try await self.sfuClient.readCallLink( - sfuUrl: sfuUrl, - authCredentialPresentation: authCredentialPresentation.serialize(), - linkRootKey: rootKey - ).unwrap()) + do { + return try await SignalServiceKit.CallLinkState(self.sfuClient.readCallLink( + sfuUrl: sfuUrl, + authCredentialPresentation: authCredentialPresentation.serialize(), + linkRootKey: rootKey + ).unwrap()) + } catch where error.rawValue == 404 { + throw CallLinkNotFoundError() + } } } -private struct SFUError: Error { +public struct SFUError: Error { let rawValue: UInt16 } extension SFUResult { - public func unwrap() throws -> Value { + public func unwrap() throws(SFUError) -> Value { switch self { case .success(let value): return value