diff --git a/NotificationExtension/Info.plist b/NotificationExtension/Info.plist
new file mode 100644
index 00000000..31726763
--- /dev/null
+++ b/NotificationExtension/Info.plist
@@ -0,0 +1,22 @@
+
+
+
+
+ TuskerInfo
+
+ PushProxyHost
+ $(TUSKER_PUSH_PROXY_HOST)
+ PushProxyScheme
+ $(TUSKER_PUSH_PROXY_SCHEME)
+ SentryDSN
+ $(SENTRY_DSN)
+
+ NSExtension
+
+ NSExtensionPointIdentifier
+ com.apple.usernotifications.service
+ NSExtensionPrincipalClass
+ $(PRODUCT_MODULE_NAME).NotificationService
+
+
+
diff --git a/NotificationExtension/NotificationExtension.entitlements b/NotificationExtension/NotificationExtension.entitlements
new file mode 100644
index 00000000..75da0284
--- /dev/null
+++ b/NotificationExtension/NotificationExtension.entitlements
@@ -0,0 +1,14 @@
+
+
+
+
+ com.apple.security.app-sandbox
+
+ com.apple.security.application-groups
+
+ group.$(BUNDLE_ID_PREFIX).Tusker
+
+ com.apple.security.network.client
+
+
+
diff --git a/NotificationExtension/NotificationService.swift b/NotificationExtension/NotificationService.swift
new file mode 100644
index 00000000..2657316c
--- /dev/null
+++ b/NotificationExtension/NotificationService.swift
@@ -0,0 +1,150 @@
+//
+// NotificationService.swift
+// NotificationExtension
+//
+// Created by Shadowfacts on 4/9/24.
+// Copyright © 2024 Shadowfacts. All rights reserved.
+//
+
+import UserNotifications
+import UserAccounts
+import PushNotifications
+import CryptoKit
+import OSLog
+import Pachyderm
+
+private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "NotificationService")
+
+class NotificationService: UNNotificationServiceExtension {
+
+ override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {
+ guard let mutableContent = request.content.mutableCopy() as? UNMutableNotificationContent else {
+ contentHandler(request.content)
+ return
+ }
+
+ guard request.content.userInfo["v"] as? Int == 1,
+ let accountID = request.content.userInfo["ctx"] as? String,
+ let account = UserAccountsManager.shared.getAccount(id: accountID),
+ let subscription = getSubscription(account: account),
+ let encryptedBody = (request.content.userInfo["data"] as? String).flatMap({ Data(base64Encoded: $0) }),
+ let salt = (request.content.userInfo["salt"] as? String).flatMap(decodeBase64URL(_:)),
+ let serverPublicKeyData = (request.content.userInfo["pk"] as? String).flatMap(decodeBase64URL(_:)) else {
+ logger.error("Missing info from push notification")
+ contentHandler(request.content)
+ return
+ }
+
+ guard let body = decryptNotification(subscription: subscription, serverPublicKeyData: serverPublicKeyData, salt: salt, encryptedBody: encryptedBody) else {
+ contentHandler(request.content)
+ return
+ }
+
+ let withoutPadding = body.dropFirst(2)
+
+ let notification: PushNotification
+ do {
+ notification = try JSONDecoder().decode(PushNotification.self, from: withoutPadding)
+ } catch {
+ logger.error("Unable to decode push payload: \(String(describing: error))")
+ contentHandler(request.content)
+ return
+ }
+
+ mutableContent.title = notification.title
+ mutableContent.body = notification.body
+
+ contentHandler(mutableContent)
+ }
+
+ override func serviceExtensionTimeWillExpire() {
+ }
+
+ private func getSubscription(account: UserAccountInfo) -> PushNotifications.PushSubscription? {
+ DispatchQueue.main.sync {
+ // this is necessary because of a swift bug: https://github.com/apple/swift/pull/72507
+ MainActor.runUnsafely {
+ PushManager.shared.pushSubscription(account: account)
+ }
+ }
+ }
+
+ private func decryptNotification(subscription: PushNotifications.PushSubscription, serverPublicKeyData: Data, salt: Data, encryptedBody: Data) -> Data? {
+ // See https://github.com/ClearlyClaire/webpush/blob/f14a4d52e201128b1b00245d11b6de80d6cfdcd9/lib/webpush/encryption.rb
+
+ var context = Data()
+ context.append(0)
+ let clientPublicKey = subscription.secretKey.publicKey.x963Representation
+ let clientPublicKeyLength = UInt16(clientPublicKey.count)
+ context.append(UInt8((clientPublicKeyLength >> 8) & 0xFF))
+ context.append(UInt8(clientPublicKeyLength & 0xFF))
+ context.append(clientPublicKey)
+ let serverPublicKeyLength = UInt16(serverPublicKeyData.count)
+ context.append(UInt8((serverPublicKeyLength >> 8) & 0xFF))
+ context.append(UInt8(serverPublicKeyLength & 0xFF))
+ context.append(serverPublicKeyData)
+
+ func info(encoding: String) -> Data {
+ var info = Data("Content-Encoding: \(encoding)\0P-256".utf8)
+ info.append(context)
+ return info
+ }
+
+ let sharedSecret: SharedSecret
+ do {
+ let serverPublicKey = try P256.KeyAgreement.PublicKey(x963Representation: serverPublicKeyData)
+ sharedSecret = try subscription.secretKey.sharedSecretFromKeyAgreement(with: serverPublicKey)
+ } catch {
+ logger.error("Error getting shared secret: \(String(describing: error))")
+ return nil
+ }
+
+ let sharedInfo = Data("Content-Encoding: auth\0".utf8)
+ let pseudoRandomKey = sharedSecret.hkdfDerivedSymmetricKey(using: SHA256.self, salt: subscription.authSecret, sharedInfo: sharedInfo, outputByteCount: 32)
+ let contentEncryptionKeyInfo = info(encoding: "aesgcm")
+ let contentEncryptionKey = HKDF.deriveKey(inputKeyMaterial: pseudoRandomKey, salt: salt, info: contentEncryptionKeyInfo, outputByteCount: 16)
+ let nonceInfo = info(encoding: "nonce")
+ let nonce = HKDF.deriveKey(inputKeyMaterial: pseudoRandomKey, salt: salt, info: nonceInfo, outputByteCount: 12)
+
+ let nonceAndEncryptedBody = nonce.withUnsafeBytes { noncePtr in
+ var data = Data(buffer: noncePtr.bindMemory(to: UInt8.self))
+ data.append(encryptedBody)
+ return data
+ }
+ do {
+ let sealedBox = try AES.GCM.SealedBox(combined: nonceAndEncryptedBody)
+ let decrypted = try AES.GCM.open(sealedBox, using: contentEncryptionKey)
+ return decrypted
+ } catch {
+ logger.error("Error decrypting push: \(String(describing: error))")
+ return nil
+ }
+ }
+
+}
+
+extension MainActor {
+ @_unavailableFromAsync
+ @available(macOS, obsoleted: 14.0)
+ @available(iOS, obsoleted: 17.0)
+ @available(watchOS, obsoleted: 10.0)
+ @available(tvOS, obsoleted: 17.0)
+ static func runUnsafely(_ body: @MainActor () throws -> T) rethrows -> T {
+ if #available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) {
+ return try MainActor.assumeIsolated(body)
+ }
+
+ dispatchPrecondition(condition: .onQueue(.main))
+ return try withoutActuallyEscaping(body) { fn in
+ try unsafeBitCast(fn, to: (() throws -> T).self)()
+ }
+ }
+}
+
+private func decodeBase64URL(_ s: String) -> Data? {
+ var str = s.replacingOccurrences(of: "-", with: "+").replacingOccurrences(of: "_", with: "/")
+ if str.count % 4 != 0 {
+ str.append(String(repeating: "=", count: 4 - str.count % 4))
+ }
+ return Data(base64Encoded: str)
+}
diff --git a/Packages/Pachyderm/Sources/Pachyderm/Model/PushNotification.swift b/Packages/Pachyderm/Sources/Pachyderm/Model/PushNotification.swift
new file mode 100644
index 00000000..178cab03
--- /dev/null
+++ b/Packages/Pachyderm/Sources/Pachyderm/Model/PushNotification.swift
@@ -0,0 +1,46 @@
+//
+// PushNotification.swift
+// Pachyderm
+//
+// Created by Shadowfacts on 4/9/24.
+//
+
+import Foundation
+import WebURL
+
+public struct PushNotification: Decodable {
+ public let accessToken: String
+ public let preferredLocale: String
+ public let notificationID: String
+ public let notificationType: Notification.Kind
+ public let icon: WebURL
+ public let title: String
+ public let body: String
+
+ public init(from decoder: any Decoder) throws {
+ let container = try decoder.container(keyedBy: CodingKeys.self)
+ self.accessToken = try container.decode(String.self, forKey: .accessToken)
+ self.preferredLocale = try container.decode(String.self, forKey: .preferredLocale)
+ // this should be a string, but mastodon encodes it as a json number
+ if let s = try? container.decode(String.self, forKey: .notificationID) {
+ self.notificationID = s
+ } else {
+ let i = try container.decode(Int.self, forKey: .notificationID)
+ self.notificationID = i.description
+ }
+ self.notificationType = try container.decode(Notification.Kind.self, forKey: .notificationType)
+ self.icon = try container.decode(WebURL.self, forKey: .icon)
+ self.title = try container.decode(String.self, forKey: .title)
+ self.body = try container.decode(String.self, forKey: .body)
+ }
+
+ private enum CodingKeys: String, CodingKey {
+ case accessToken = "access_token"
+ case preferredLocale = "preferred_locale"
+ case notificationID = "notification_id"
+ case notificationType = "notification_type"
+ case icon
+ case title
+ case body
+ }
+}
diff --git a/ShareExtension/ShareExtension.entitlements b/ShareExtension/ShareExtension.entitlements
index d5e9f9a9..75da0284 100644
--- a/ShareExtension/ShareExtension.entitlements
+++ b/ShareExtension/ShareExtension.entitlements
@@ -6,7 +6,7 @@
com.apple.security.application-groups
- group.space.vaccor.Tusker
+ group.$(BUNDLE_ID_PREFIX).Tusker
com.apple.security.network.client
diff --git a/Tusker.xcodeproj/project.pbxproj b/Tusker.xcodeproj/project.pbxproj
index d260e3ec..829c528e 100644
--- a/Tusker.xcodeproj/project.pbxproj
+++ b/Tusker.xcodeproj/project.pbxproj
@@ -95,6 +95,12 @@
D630C3C82BC43AFD00208903 /* PushNotifications in Frameworks */ = {isa = PBXBuildFile; productRef = D630C3C72BC43AFD00208903 /* PushNotifications */; };
D630C3CA2BC59FF500208903 /* MastodonController+Push.swift in Sources */ = {isa = PBXBuildFile; fileRef = D630C3C92BC59FF500208903 /* MastodonController+Push.swift */; };
D630C3CC2BC5FD4600208903 /* GetAuthorizationTokenService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D630C3CB2BC5FD4600208903 /* GetAuthorizationTokenService.swift */; };
+ D630C3D42BC61B6100208903 /* NotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D630C3D32BC61B6100208903 /* NotificationService.swift */; };
+ D630C3D82BC61B6100208903 /* NotificationExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = D630C3D12BC61B6000208903 /* NotificationExtension.appex */; platformFilters = (ios, maccatalyst, ); settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
+ D630C3DF2BC61C4900208903 /* PushNotifications in Frameworks */ = {isa = PBXBuildFile; productRef = D630C3DE2BC61C4900208903 /* PushNotifications */; };
+ D630C3E12BC61C6700208903 /* UserAccounts in Frameworks */ = {isa = PBXBuildFile; productRef = D630C3E02BC61C6700208903 /* UserAccounts */; };
+ D630C3E52BC6313400208903 /* Pachyderm in Frameworks */ = {isa = PBXBuildFile; productRef = D630C3E42BC6313400208903 /* Pachyderm */; };
+ D630C3E72BC6313F00208903 /* WebURLFoundationExtras in Frameworks */ = {isa = PBXBuildFile; productRef = D630C3E62BC6313F00208903 /* WebURLFoundationExtras */; };
D6311C5025B3765B00B27539 /* ImageDataCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6311C4F25B3765B00B27539 /* ImageDataCache.swift */; };
D6333B372137838300CE884A /* AttributedString+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6333B362137838300CE884A /* AttributedString+Helpers.swift */; };
D6333B792139AEFD00CE884A /* Date+TimeAgo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6333B782139AEFD00CE884A /* Date+TimeAgo.swift */; };
@@ -359,6 +365,13 @@
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
+ D630C3D62BC61B6100208903 /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = D6D4DDC4212518A000E1C4BB /* Project object */;
+ proxyType = 1;
+ remoteGlobalIDString = D630C3D02BC61B6000208903;
+ remoteInfo = NotificationExtension;
+ };
D6A4531B29EF64BA00032932 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = D6D4DDC4212518A000E1C4BB /* Project object */;
@@ -397,6 +410,7 @@
dstSubfolderSpec = 13;
files = (
D6A4531D29EF64BA00032932 /* ShareExtension.appex in Embed Foundation Extensions */,
+ D630C3D82BC61B6100208903 /* NotificationExtension.appex in Embed Foundation Extensions */,
D6E343B4265AAD6B00C4AA01 /* OpenInTusker.appex in Embed Foundation Extensions */,
);
name = "Embed Foundation Extensions";
@@ -501,6 +515,10 @@
D62FF04723D7CDD700909D6E /* AttributedStringHelperTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttributedStringHelperTests.swift; sourceTree = ""; };
D630C3C92BC59FF500208903 /* MastodonController+Push.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MastodonController+Push.swift"; sourceTree = ""; };
D630C3CB2BC5FD4600208903 /* GetAuthorizationTokenService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetAuthorizationTokenService.swift; sourceTree = ""; };
+ D630C3D12BC61B6000208903 /* NotificationExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = NotificationExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
+ D630C3D32BC61B6100208903 /* NotificationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationService.swift; sourceTree = ""; };
+ D630C3D52BC61B6100208903 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
+ D630C3D92BC61B6100208903 /* NotificationExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = NotificationExtension.entitlements; sourceTree = ""; };
D6311C4F25B3765B00B27539 /* ImageDataCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageDataCache.swift; sourceTree = ""; };
D6333B362137838300CE884A /* AttributedString+Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AttributedString+Helpers.swift"; sourceTree = ""; };
D6333B782139AEFD00CE884A /* Date+TimeAgo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Date+TimeAgo.swift"; sourceTree = ""; };
@@ -778,6 +796,17 @@
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
+ D630C3CE2BC61B6000208903 /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ D630C3E52BC6313400208903 /* Pachyderm in Frameworks */,
+ D630C3E72BC6313F00208903 /* WebURLFoundationExtras in Frameworks */,
+ D630C3DF2BC61C4900208903 /* PushNotifications in Frameworks */,
+ D630C3E12BC61C6700208903 /* UserAccounts in Frameworks */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
D6A4531029EF64BA00032932 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
@@ -970,6 +999,16 @@
path = Shortcuts;
sourceTree = "";
};
+ D630C3D22BC61B6100208903 /* NotificationExtension */ = {
+ isa = PBXGroup;
+ children = (
+ D630C3D92BC61B6100208903 /* NotificationExtension.entitlements */,
+ D630C3D32BC61B6100208903 /* NotificationService.swift */,
+ D630C3D52BC61B6100208903 /* Info.plist */,
+ );
+ path = NotificationExtension;
+ sourceTree = "";
+ };
D6370B9924421FE00092A7FF /* CoreData */ = {
isa = PBXGroup;
children = (
@@ -1489,6 +1528,7 @@
D6D4DDEE212518A200E1C4BB /* TuskerUITests */,
D6E343A9265AAD6B00C4AA01 /* OpenInTusker */,
D6A4531429EF64BA00032932 /* ShareExtension */,
+ D630C3D22BC61B6100208903 /* NotificationExtension */,
D6D4DDCD212518A000E1C4BB /* Products */,
D65A37F221472F300087646E /* Frameworks */,
);
@@ -1502,6 +1542,7 @@
D6D4DDEB212518A200E1C4BB /* TuskerUITests.xctest */,
D6E343A8265AAD6B00C4AA01 /* OpenInTusker.appex */,
D6A4531329EF64BA00032932 /* ShareExtension.appex */,
+ D630C3D12BC61B6000208903 /* NotificationExtension.appex */,
);
name = Products;
sourceTree = "";
@@ -1678,6 +1719,29 @@
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
+ D630C3D02BC61B6000208903 /* NotificationExtension */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = D630C3DA2BC61B6100208903 /* Build configuration list for PBXNativeTarget "NotificationExtension" */;
+ buildPhases = (
+ D630C3CD2BC61B6000208903 /* Sources */,
+ D630C3CE2BC61B6000208903 /* Frameworks */,
+ D630C3CF2BC61B6000208903 /* Resources */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ );
+ name = NotificationExtension;
+ packageProductDependencies = (
+ D630C3DE2BC61C4900208903 /* PushNotifications */,
+ D630C3E02BC61C6700208903 /* UserAccounts */,
+ D630C3E42BC6313400208903 /* Pachyderm */,
+ D630C3E62BC6313F00208903 /* WebURLFoundationExtras */,
+ );
+ productName = NotificationExtension;
+ productReference = D630C3D12BC61B6000208903 /* NotificationExtension.appex */;
+ productType = "com.apple.product-type.app-extension";
+ };
D6A4531229EF64BA00032932 /* ShareExtension */ = {
isa = PBXNativeTarget;
buildConfigurationList = D6A4532229EF64BA00032932 /* Build configuration list for PBXNativeTarget "ShareExtension" */;
@@ -1719,6 +1783,7 @@
dependencies = (
D6E343B3265AAD6B00C4AA01 /* PBXTargetDependency */,
D6A4531C29EF64BA00032932 /* PBXTargetDependency */,
+ D630C3D72BC61B6100208903 /* PBXTargetDependency */,
);
name = Tusker;
packageProductDependencies = (
@@ -1804,10 +1869,13 @@
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = YES;
- LastSwiftUpdateCheck = 1430;
+ LastSwiftUpdateCheck = 1530;
LastUpgradeCheck = 1500;
ORGANIZATIONNAME = Shadowfacts;
TargetAttributes = {
+ D630C3D02BC61B6000208903 = {
+ CreatedOnToolsVersion = 15.3;
+ };
D6A4531229EF64BA00032932 = {
CreatedOnToolsVersion = 14.3;
};
@@ -1860,11 +1928,19 @@
D6D4DDEA212518A200E1C4BB /* TuskerUITests */,
D6E343A7265AAD6B00C4AA01 /* OpenInTusker */,
D6A4531229EF64BA00032932 /* ShareExtension */,
+ D630C3D02BC61B6000208903 /* NotificationExtension */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
+ D630C3CF2BC61B6000208903 /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
D6A4531129EF64BA00032932 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
@@ -1965,6 +2041,14 @@
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
+ D630C3CD2BC61B6000208903 /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ D630C3D42BC61B6100208903 /* NotificationService.swift in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
D6A4530F29EF64BA00032932 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
@@ -2312,6 +2396,11 @@
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
+ D630C3D72BC61B6100208903 /* PBXTargetDependency */ = {
+ isa = PBXTargetDependency;
+ target = D630C3D02BC61B6000208903 /* NotificationExtension */;
+ targetProxy = D630C3D62BC61B6100208903 /* PBXContainerItemProxy */;
+ };
D6A4531C29EF64BA00032932 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = D6A4531229EF64BA00032932 /* ShareExtension */;
@@ -2370,6 +2459,100 @@
/* End PBXVariantGroup section */
/* Begin XCBuildConfiguration section */
+ D630C3DB2BC61B6100208903 /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
+ CODE_SIGN_ENTITLEMENTS = NotificationExtension/NotificationExtension.entitlements;
+ CODE_SIGN_IDENTITY = "iPhone Developer";
+ CODE_SIGN_STYLE = Automatic;
+ ENABLE_USER_SCRIPT_SANDBOXING = YES;
+ GCC_C_LANGUAGE_STANDARD = gnu17;
+ GENERATE_INFOPLIST_FILE = YES;
+ INFOPLIST_FILE = NotificationExtension/Info.plist;
+ INFOPLIST_KEY_CFBundleDisplayName = NotificationExtension;
+ INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2024 Shadowfacts. All rights reserved.";
+ IPHONEOS_DEPLOYMENT_TARGET = 15.0;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ "@executable_path/../../Frameworks",
+ );
+ LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
+ PRODUCT_BUNDLE_IDENTIFIER = space.vaccor.Tusker.NotificationExtension;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SKIP_INSTALL = YES;
+ SUPPORTS_MACCATALYST = YES;
+ SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
+ SWIFT_EMIT_LOC_STRINGS = YES;
+ SWIFT_VERSION = 5.0;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ };
+ name = Debug;
+ };
+ D630C3DC2BC61B6100208903 /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
+ CODE_SIGN_ENTITLEMENTS = NotificationExtension/NotificationExtension.entitlements;
+ CODE_SIGN_IDENTITY = "iPhone Developer";
+ CODE_SIGN_STYLE = Automatic;
+ ENABLE_USER_SCRIPT_SANDBOXING = YES;
+ GCC_C_LANGUAGE_STANDARD = gnu17;
+ GENERATE_INFOPLIST_FILE = YES;
+ INFOPLIST_FILE = NotificationExtension/Info.plist;
+ INFOPLIST_KEY_CFBundleDisplayName = NotificationExtension;
+ INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2024 Shadowfacts. All rights reserved.";
+ IPHONEOS_DEPLOYMENT_TARGET = 15.0;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ "@executable_path/../../Frameworks",
+ );
+ LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
+ PRODUCT_BUNDLE_IDENTIFIER = space.vaccor.Tusker.NotificationExtension;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SKIP_INSTALL = YES;
+ SUPPORTS_MACCATALYST = YES;
+ SWIFT_EMIT_LOC_STRINGS = YES;
+ SWIFT_VERSION = 5.0;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ };
+ name = Release;
+ };
+ D630C3DD2BC61B6100208903 /* Dist */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
+ CODE_SIGN_ENTITLEMENTS = NotificationExtension/NotificationExtension.entitlements;
+ CODE_SIGN_IDENTITY = "iPhone Developer";
+ CODE_SIGN_STYLE = Automatic;
+ ENABLE_USER_SCRIPT_SANDBOXING = YES;
+ GCC_C_LANGUAGE_STANDARD = gnu17;
+ GENERATE_INFOPLIST_FILE = YES;
+ INFOPLIST_FILE = NotificationExtension/Info.plist;
+ INFOPLIST_KEY_CFBundleDisplayName = NotificationExtension;
+ INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2024 Shadowfacts. All rights reserved.";
+ IPHONEOS_DEPLOYMENT_TARGET = 15.0;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ "@executable_path/../../Frameworks",
+ );
+ LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
+ PRODUCT_BUNDLE_IDENTIFIER = space.vaccor.Tusker.NotificationExtension;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SKIP_INSTALL = YES;
+ SUPPORTS_MACCATALYST = YES;
+ SWIFT_EMIT_LOC_STRINGS = YES;
+ SWIFT_VERSION = 5.0;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ };
+ name = Dist;
+ };
D63CC705290ECE77000E19DE /* Dist */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = D63CC703290EC472000E19DE /* Dist.xcconfig */;
@@ -2934,6 +3117,16 @@
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
+ D630C3DA2BC61B6100208903 /* Build configuration list for PBXNativeTarget "NotificationExtension" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ D630C3DB2BC61B6100208903 /* Debug */,
+ D630C3DC2BC61B6100208903 /* Release */,
+ D630C3DD2BC61B6100208903 /* Dist */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
D6A4532229EF64BA00032932 /* Build configuration list for PBXNativeTarget "ShareExtension" */ = {
isa = XCConfigurationList;
buildConfigurations = (
@@ -3045,6 +3238,23 @@
isa = XCSwiftPackageProductDependency;
productName = PushNotifications;
};
+ D630C3DE2BC61C4900208903 /* PushNotifications */ = {
+ isa = XCSwiftPackageProductDependency;
+ productName = PushNotifications;
+ };
+ D630C3E02BC61C6700208903 /* UserAccounts */ = {
+ isa = XCSwiftPackageProductDependency;
+ productName = UserAccounts;
+ };
+ D630C3E42BC6313400208903 /* Pachyderm */ = {
+ isa = XCSwiftPackageProductDependency;
+ productName = Pachyderm;
+ };
+ D630C3E62BC6313F00208903 /* WebURLFoundationExtras */ = {
+ isa = XCSwiftPackageProductDependency;
+ package = D6676CA127A8D0020052936B /* XCRemoteSwiftPackageReference "swift-url" */;
+ productName = WebURLFoundationExtras;
+ };
D635237029B78A7D009ED5E7 /* TuskerComponents */ = {
isa = XCSwiftPackageProductDependency;
productName = TuskerComponents;
diff --git a/Tusker/Extensions/MainActor+Unsafe.swift b/Tusker/Extensions/MainActor+Unsafe.swift
index ee3639b6..d4e4ee5f 100644
--- a/Tusker/Extensions/MainActor+Unsafe.swift
+++ b/Tusker/Extensions/MainActor+Unsafe.swift
@@ -47,6 +47,10 @@ public extension MainActor {
///
/// It will crash if run on any non-main thread.
@_unavailableFromAsync
+ @available(macOS, obsoleted: 14.0)
+ @available(iOS, obsoleted: 17.0)
+ @available(watchOS, obsoleted: 10.0)
+ @available(tvOS, obsoleted: 17.0)
static func runUnsafely(_ body: @MainActor () throws -> T) rethrows -> T {
if #available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) {
return try MainActor.assumeIsolated(body)