From 83ca18747bb60dc805ff1ebc50a6e69d69b83fdb Mon Sep 17 00:00:00 2001 From: Max Radermacher Date: Thu, 21 Mar 2024 13:10:38 -0500 Subject: [PATCH] Add parsing/encoding support for call links --- Signal.xcodeproj/project.pbxproj | 16 +++++ Signal/Signal-AppStore.entitlements | 1 + Signal/Signal.entitlements | 1 + Signal/URLs/UrlOpener.swift | 10 ++- Signal/src/Calls/CallLink.swift | 73 ++++++++++++++++++++ Signal/test/Calls/CallLinkTest.swift | 33 +++++++++ SignalServiceKit/Subscriptions/Stripe.swift | 2 +- SignalServiceKit/src/Util/FeatureFlags.swift | 2 + 8 files changed, 136 insertions(+), 2 deletions(-) create mode 100644 Signal/src/Calls/CallLink.swift create mode 100644 Signal/test/Calls/CallLinkTest.swift diff --git a/Signal.xcodeproj/project.pbxproj b/Signal.xcodeproj/project.pbxproj index 166f5255f3..fda0d0b3d6 100644 --- a/Signal.xcodeproj/project.pbxproj +++ b/Signal.xcodeproj/project.pbxproj @@ -633,6 +633,8 @@ 5050A8792B76E2E100E9BFA4 /* PreKeyId.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5050A8782B76E2E100E9BFA4 /* PreKeyId.swift */; }; 5050A87B2B76EEC500E9BFA4 /* PreKeyIdTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5050A87A2B76EEC500E9BFA4 /* PreKeyIdTest.swift */; }; 5052AF5E2ACB0E9700D7EE9F /* MergePair.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5052AF5D2ACB0E9700D7EE9F /* MergePair.swift */; }; + 50552C2E2BAC066A00815474 /* CallLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50552C2D2BAC066A00815474 /* CallLink.swift */; }; + 50552C312BAC079A00815474 /* CallLinkTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50552C302BAC079A00815474 /* CallLinkTest.swift */; }; 50597BBA2B97C38C004681E1 /* SignalAccountStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50597BB92B97C38C004681E1 /* SignalAccountStore.swift */; }; 50597BBC2B97C449004681E1 /* UsernameLookupRecordStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50597BBB2B97C449004681E1 /* UsernameLookupRecordStore.swift */; }; 50597BBF2B97D629004681E1 /* SearchableNameFinder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50597BBE2B97D629004681E1 /* SearchableNameFinder.swift */; }; @@ -3433,6 +3435,8 @@ 5050A8782B76E2E100E9BFA4 /* PreKeyId.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreKeyId.swift; sourceTree = ""; }; 5050A87A2B76EEC500E9BFA4 /* PreKeyIdTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreKeyIdTest.swift; sourceTree = ""; }; 5052AF5D2ACB0E9700D7EE9F /* MergePair.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MergePair.swift; sourceTree = ""; }; + 50552C2D2BAC066A00815474 /* CallLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallLink.swift; sourceTree = ""; }; + 50552C302BAC079A00815474 /* CallLinkTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallLinkTest.swift; sourceTree = ""; }; 50597BB92B97C38C004681E1 /* SignalAccountStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignalAccountStore.swift; sourceTree = ""; }; 50597BBB2B97C449004681E1 /* UsernameLookupRecordStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UsernameLookupRecordStore.swift; sourceTree = ""; }; 50597BBE2B97D629004681E1 /* SearchableNameFinder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchableNameFinder.swift; sourceTree = ""; }; @@ -6859,6 +6863,14 @@ path = Launch; sourceTree = ""; }; + 50552C2F2BAC079000815474 /* Calls */ = { + isa = PBXGroup; + children = ( + 50552C302BAC079A00815474 /* CallLinkTest.swift */, + ); + path = Calls; + sourceTree = ""; + }; 50597BBD2B97D624004681E1 /* Search */ = { isa = PBXGroup; children = ( @@ -8486,6 +8498,7 @@ 17ACF11D267D71E0009BE867 /* AudioSession+WebRTC.swift */, 88D23D2D23CEC1BE00B0E74B /* AudioSource.swift */, 88D23D2B23CEC17400B0E74B /* CallAudioService.swift */, + 50552C2D2BAC066A00815474 /* CallLink.swift */, 88588D25252E59CE00405414 /* CallService.swift */, B98D24692B8D2A2A00B1A8CC /* CallStarter.swift */, 8841584B252F9F1C0078903D /* SignalCall.swift */, @@ -8563,6 +8576,7 @@ 34C6B0A41FA0E46F00D35993 /* Assets */, 1704690725D4C2DA000793D8 /* attachments */, D979DA122B8D1B3E000EEAB8 /* Badge */, + 50552C2F2BAC079000815474 /* Calls */, D931080C2B338D00006A034E /* CallsTab */, B660F6751C29867F00687D6E /* contact */, 50B6BCB22AEC58190010FB3B /* Contacts */, @@ -12396,6 +12410,7 @@ 88ABB8B52534070400229EAA /* CallHeader.swift in Sources */, 88D23D2423CEC0C700B0E74B /* CallKitCallManager.swift in Sources */, 88D23D2523CEC0C700B0E74B /* CallKitCallUIAdaptee.swift in Sources */, + 50552C2E2BAC066A00815474 /* CallLink.swift in Sources */, E1E78CAF2B573BD100B6FC2D /* CallMemberCameraOffView.swift in Sources */, E1E78CAD2B573B5800B6FC2D /* CallMemberChromeOverlayView.swift in Sources */, E1E78CB42B575C2700B6FC2D /* CallMemberVideoView.swift in Sources */, @@ -12998,6 +13013,7 @@ D99ABC742A3D0BE10034CD3B /* BitmapsImageParsingTest.swift in Sources */, D9317FD32A4BAC8300075A92 /* BitmapsImagePixelMergingTest.swift in Sources */, D9317FD82A4BC4FC00075A92 /* BitmapsRectTest.swift in Sources */, + 50552C312BAC079A00815474 /* CallLinkTest.swift in Sources */, D93108152B34B6BE006A034E /* CallRecordLoaderTest.swift in Sources */, D9F02BE72B96556C00E872C2 /* CallsListViewController+ViewModelLoaderTest.swift in Sources */, 954AEE6A1DF33E01002E5410 /* ContactsPickerTest.swift in Sources */, diff --git a/Signal/Signal-AppStore.entitlements b/Signal/Signal-AppStore.entitlements index 97f3569d6a..d965bc165a 100644 --- a/Signal/Signal-AppStore.entitlements +++ b/Signal/Signal-AppStore.entitlements @@ -11,6 +11,7 @@ applinks:signal.group applinks:signal.me applinks:signaldonations.org + applinks:signal.link com.apple.developer.default-data-protection NSFileProtectionComplete diff --git a/Signal/Signal.entitlements b/Signal/Signal.entitlements index 1391ba0730..4077eb2df3 100644 --- a/Signal/Signal.entitlements +++ b/Signal/Signal.entitlements @@ -11,6 +11,7 @@ applinks:signal.group applinks:signal.me applinks:signaldonations.org + applinks:signal.link com.apple.developer.default-data-protection NSFileProtectionComplete diff --git a/Signal/URLs/UrlOpener.swift b/Signal/URLs/UrlOpener.swift index 119b395a33..997dc0ca80 100644 --- a/Signal/URLs/UrlOpener.swift +++ b/Signal/URLs/UrlOpener.swift @@ -15,6 +15,7 @@ private enum OpenableUrl { case signalProxy(URL) case linkDevice(DeviceProvisioningURL) case completeIDEALDonation(Stripe.IDEALCallbackType) + case callLink(CallLink) } class UrlOpener { @@ -73,6 +74,9 @@ class UrlOpener { if let donationType = Stripe.parseStripeIDEALCallback(url) { return .completeIDEALDonation(donationType) } + if let callLink = CallLink(url: url), FeatureFlags.callLinkJoin { + return .callLink(callLink) + } owsFailDebug("Couldn't parse URL") return nil } @@ -131,7 +135,7 @@ class UrlOpener { private func shouldDismiss(for url: OpenableUrl) -> Bool { switch url { case .completeIDEALDonation: return false - case .groupInvite, .linkDevice, .phoneNumberLink, .signalProxy, .stickerPack, .usernameLink: return true + case .groupInvite, .linkDevice, .phoneNumberLink, .signalProxy, .stickerPack, .usernameLink, .callLink: return true } } @@ -209,6 +213,10 @@ class UrlOpener { OWSLogger.warn("[Donations] Unexpected error encountered with iDEAL donation") } } + + case .callLink(let callLink): + // CallLink TODO: Join the call. + Logger.debug("Trying to open \(callLink)") } } } diff --git a/Signal/src/Calls/CallLink.swift b/Signal/src/Calls/CallLink.swift new file mode 100644 index 0000000000..1169a85440 --- /dev/null +++ b/Signal/src/Calls/CallLink.swift @@ -0,0 +1,73 @@ +// +// Copyright 2024 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only +// + +import Foundation +import SignalCoreKit +import SignalRingRTC + +struct CallLink { + // MARK: - + + private enum Constants { + static let scheme = "https" + static let host = "signal.link" + static let path = "/call/" + static let key = "key" + } + + // MARK: - + + let rootKey: CallLinkRootKey + + init(rootKey: CallLinkRootKey) { + self.rootKey = rootKey + } + + /// Parses a URL of the form: https://signal.link/call/#key=value + init?(url: URL) { + guard + var components = URLComponents(url: url, resolvingAgainstBaseURL: false), + components.scheme == Constants.scheme, + components.user == nil, + components.password == nil, + components.host == Constants.host, + components.port == nil, + components.path == Constants.path, + components.query == nil + else { + return nil + } + components.percentEncodedQuery = components.percentEncodedFragment + guard + let queryItems = components.queryItems, + queryItems.count == 1, + let keyItem = queryItems.first, + keyItem.name == Constants.key, + let keyValue = keyItem.value, + let rootKey = try? CallLinkRootKey(keyValue) + else { + return nil + } + self.init(rootKey: rootKey) + } + + static func generate() -> CallLink { + let rootKey = CallLinkRootKey.generate() + return CallLink(rootKey: rootKey) + } + + func url() -> URL { + var components = URLComponents() + components.scheme = Constants.scheme + components.host = Constants.host + components.path = Constants.path + components.queryItems = [ + URLQueryItem(name: Constants.key, value: rootKey.description), + ] + components.percentEncodedFragment = components.percentEncodedQuery + components.query = nil + return components.url! + } +} diff --git a/Signal/test/Calls/CallLinkTest.swift b/Signal/test/Calls/CallLinkTest.swift new file mode 100644 index 0000000000..374d84758b --- /dev/null +++ b/Signal/test/Calls/CallLinkTest.swift @@ -0,0 +1,33 @@ +// +// Copyright 2024 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only +// + +import XCTest + +@testable import Signal + +final class CallLinkTest: XCTestCase { + private func parse(_ urlString: String) -> CallLink? { + return CallLink(url: URL(string: urlString)!) + } + + func testUrlString() { + XCTAssertNil(parse("https://signal.link/call/#key=bcdf-ghkm-npqr-stxz-bcdf-ghkm-npqr-stx")) + XCTAssertNil(parse("http://signal.link/call/#key=bcdf-ghkm-npqr-stxz-bcdf-ghkm-npqr-stxz")) + XCTAssertNil(parse("https://signal.art/call/#key=bcdf-ghkm-npqr-stxz-bcdf-ghkm-npqr-stxz")) + XCTAssertNil(parse("https://signal.link/c/#key=bcdf-ghkm-npqr-stxz-bcdf-ghkm-npqr-stxz")) + } + + func testRoundtrip() throws { + let urlString = "https://signal.link/call/#key=bcdf-ghkm-npqr-stxz-bcdf-ghkm-npqr-stxz" + let callLink = try XCTUnwrap(parse(urlString)) + XCTAssertEqual(callLink.url().absoluteString, urlString) + } + + func testGenerate() { + let url1 = CallLink.generate().url() + let url2 = CallLink.generate().url() + XCTAssertNotEqual(url1, url2) + } +} diff --git a/SignalServiceKit/Subscriptions/Stripe.swift b/SignalServiceKit/Subscriptions/Stripe.swift index 6c5f978373..033dfb495a 100644 --- a/SignalServiceKit/Subscriptions/Stripe.swift +++ b/SignalServiceKit/Subscriptions/Stripe.swift @@ -5,6 +5,7 @@ import Foundation import PassKit +import SignalCoreKit /// Stripe donations /// @@ -509,7 +510,6 @@ public extension Stripe { let components = URLComponents(string: url.absoluteString), let queryItems = components.queryItems else { - owsFailDebug("Invalid URL.") return nil } diff --git a/SignalServiceKit/src/Util/FeatureFlags.swift b/SignalServiceKit/src/Util/FeatureFlags.swift index 03dd6d4354..1b9f622fce 100644 --- a/SignalServiceKit/src/Util/FeatureFlags.swift +++ b/SignalServiceKit/src/Util/FeatureFlags.swift @@ -93,6 +93,8 @@ public class FeatureFlags: NSObject { public static let readV2Attachments = false public static let newAttachmentsUseV2 = false + + public static let callLinkJoin = build.includes(.dev) } // MARK: -