diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index e60c736f..ba8ed069 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -12,13 +12,19 @@ needs to be listed here. - Craig Newell - Eduardo Perez - Florian Reinhart +- Geoff Verdouw - Jeffrey Macko +- Jonny - Kyle Browning - Laurent Gaches +- Lukáš Petr - Mads Odgaard +- Nikola Paunović - Roderic Campbell +- Simon Kempendorf - Tanner - Tanner Nelson +- Timothy Ellis <3098078+TimAEllis@users.noreply.github.com> - grosch - itcohorts - tanner0101 diff --git a/Package.swift b/Package.swift index 83b55257..fa977865 100644 --- a/Package.swift +++ b/Package.swift @@ -13,20 +13,26 @@ let package = Package( ], dependencies: [ .package(url: "https://github.com/apple/swift-log.git", from: "1.0.0"), - .package(url: "https://github.com/apple/swift-crypto.git", "1.0.0" ..< "3.0.0"), + .package(url: "https://github.com/apple/swift-crypto.git", "1.0.0"..<"3.0.0"), .package(url: "https://github.com/swift-server/async-http-client.git", from: "1.10.0"), ], targets: [ - .executableTarget(name: "APNSwiftExample", dependencies: [ - .target(name: "APNSwift"), - ]), - .testTarget(name: "APNSwiftTests", dependencies: [ - .target(name: "APNSwift"), - ]), - .target(name: "APNSwift", dependencies: [ - .product(name: "Logging", package: "swift-log"), - .product(name: "Crypto", package: "swift-crypto"), - .product(name: "AsyncHTTPClient", package: "async-http-client") - ]), + .executableTarget( + name: "APNSwiftExample", + dependencies: [ + .target(name: "APNSwift") + ]), + .testTarget( + name: "APNSwiftTests", + dependencies: [ + .target(name: "APNSwift") + ]), + .target( + name: "APNSwift", + dependencies: [ + .product(name: "Logging", package: "swift-log"), + .product(name: "Crypto", package: "swift-crypto"), + .product(name: "AsyncHTTPClient", package: "async-http-client"), + ]), ] ) diff --git a/README.md b/README.md index 8041c23c..2c969628 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,9 @@ [![sswg:incubating|94x20](https://img.shields.io/badge/sswg-incubating-yellow.svg)](https://github.com/swift-server/sswg/blob/master/process/incubation.md#sandbox-level) [![License](https://img.shields.io/badge/License-Apache%202.0-yellow.svg)](https://www.apache.org/licenses/LICENSE-2.0.html) [![Build](https://github.com/kylebrowning/APNSwift/workflows/test/badge.svg)](https://github.com/kylebrowning/APNSwift/actions) -[![Swift](https://img.shields.io/badge/Swift-5.2-brightgreen.svg?colorA=orange&colorB=4E4E4E)](https://swift.org) +[![Swift](https://img.shields.io/badge/Swift-5.6-brightgreen.svg?colorA=orange&colorB=4E4E4E)](https://swift.org) +[![Documentation](https://img.shields.io/badge/documentation-blueviolet.svg)](https://swiftpackageindex.com/swift-server-community/APNSwift/master/documentation/apnswift) + # APNSwift @@ -20,49 +22,45 @@ dependencies: [ ## Getting Started ```swift -struct BasicNotification: APNSwiftNotification { - let aps: APNSwiftPayload +struct BasicNotification: APNSNotification { + let aps: APNSPayload } -let group = MultiThreadedEventLoopGroup(numberOfThreads: 1) var logger = Logger(label: "com.apnswift") logger.logLevel = .debug -/// Create your HTTPClient (or pass in one you already have) -let httpClient = HTTPClient(eventLoopGroupProvider: .shared(group)) +/// Create your `APNSConfiguration.Authentication` -/// Create your `APNSwiftConfiguration.Authentication` -let authenticationConfig: APNSwiftConfiguration.Authentication = .init( +let authenticationConfig: APNSConfiguration.Authentication = .init( privateKey: try .loadFrom(filePath: "/Users/kylebrowning/Documents/AuthKey_9UC9ZLQ8YW.p8"), teamIdentifier: "ABBM6U9RM5", keyIdentifier: "9UC9ZLQ8YW" ) -/// If you need to use an a secrets manager instead of read from disk, use +/// If you need to use a secrets manager instead of reading from the disk, use /// `loadfrom(string:)` -let apnsConfig = try APNSwiftConfiguration( +let apnsConfig = try APNSConfiguration( authenticationConfig: authenticationConfig, topic: "com.grasscove.Fern", environment: .sandbox, logger: logger ) -let apns = APNSwiftConnection(configuration: apnsConfig, logger: logger) +let apns = APNSClient(configuration: apnsConfig) -let aps = APNSwiftPayload(alert: .init(title: "Hey There", subtitle: "Subtitle", body: "Body"), hasContentAvailable: true) +let aps = APNSPayload(alert: .init(title: "Hey There", subtitle: "Subtitle", body: "Body"), hasContentAvailable: true) let deviceToken = "myDeviceToken" try await apns.send(notification, pushType: .alert, to: deviceToken) try await httpClient.shutdown() -try! group.syncShutdownGracefully() exit(0) ``` -### APNSwiftConfiguration +### APNSConfiguration -[`APNSwiftConfiguration`](https://github.com/kylebrowning/swift-nio-http2-apns/blob/master/Sources/APNSwift/APNSwiftConfiguration.swift) is a structure that provides the system with common configuration. +[`APNSConfiguration`](https://github.com/kylebrowning/swift-nio-http2-apns/blob/master/Sources/APNSwift/APNSConfiguration.swift) is a structure that provides the system with common configuration. ```swift -let apnsConfig = try APNSwiftConfiguration( +let apnsConfig = try APNSConfiguration( authenticationConfig: authenticationConfig, topic: "com.grasscove.Fern", environment: .sandbox, @@ -70,67 +68,67 @@ let apnsConfig = try APNSwiftConfiguration( ) ``` -#### APNSwiftConfiguration.Authentication -[`APNSwiftConfiguration.Authentication`](https://github.com/swift-server-community/APNSwift/blob/master/Sources/APNSwift/APNSwiftConfiguration.swift#L26) is a struct that provides authentication keys and metadata to the signer. +#### APNSConfiguration.Authentication +[`APNSConfiguration.Authentication`](https://github.com/swift-server-community/APNSwift/blob/master/Sources/APNSwift/APNSConfiguration.swift#L26) is a struct that provides authentication keys and metadata to the signer. ```swift -let authenticationConfig: APNSwiftConfiguration.Authentication = .init( +let authenticationConfig: APNSConfiguration.Authentication = .init( privateKey: try .loadFrom(filePath: "/Users/kylebrowning/Documents/AuthKey_9UC9ZLQ8YW.p8"), teamIdentifier: "ABBM6U9RM5", keyIdentifier: "9UC9ZLQ8YW" ) ``` -### APNSwiftConnection +### APNSClient -[`APNSwiftConnection`](https://github.com/kylebrowning/swift-nio-http2-apns/blob/master/Sources/APNSwift/APNSwiftConnection.swift) provides functions to send a notification to a specific device token string. +[`APNSClient`](https://github.com/kylebrowning/swift-nio-http2-apns/blob/master/Sources/APNSwift/APNSClient.swift) provides functions to send a notification to a specific device token string. -#### Example `APNSwiftConnection` +#### Example `APNSClient` ```swift -let apns = APNSwiftConnection(configuration: apnsConfig, logger: logger) +let apns = APNSClient(configuration: apnsConfig) ``` -### APNSwiftAlert +### APNSAlert -[`APNSwiftAlert`](https://github.com/kylebrowning/APNSwift/blob/tn-concise-naming/Sources/APNSwift/APNSwiftAlert.swift) is the actual meta data of the push notification alert someone wishes to send. More details on the specifics of each property are provided [here](https://developer.apple.com/library/archive/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/PayloadKeyReference.html). They follow a 1-1 naming scheme listed in Apple's documentation +[`APNSAlert`](https://github.com/kylebrowning/APNSwift/blob/tn-concise-naming/Sources/APNSwift/APNSAlert.swift) is the actual meta data of the push notification alert someone wishes to send. More details on the specifics of each property are provided [here](https://developer.apple.com/library/archive/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/PayloadKeyReference.html). They follow a 1-1 naming scheme listed in Apple's documentation -#### Example `APNSwiftAlert` +#### Example `APNSAlert` ```swift -let alert = APNSwiftAlert(title: "Hey There", subtitle: "Full moon sighting", body: "There was a full moon last night did you see it") +let alert = APNSAlert(title: "Hey There", subtitle: "Full moon sighting", body: "There was a full moon last night did you see it") ``` -### APNSwiftPayload +### APNSPayload -[`APNSwiftPayload`](https://github.com/kylebrowning/APNSwift/blob/tn-concise-naming/Sources/APNSwift/APNSwiftPayload.swift) is the meta data of the push notification. Things like the alert, badge count. More details on the specifics of each property are provided [here](https://developer.apple.com/library/archive/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/PayloadKeyReference.html). They follow a 1-1 naming scheme listed in Apple's documentation +[`APNSPayload`](https://github.com/kylebrowning/APNSwift/blob/tn-concise-naming/Sources/APNSwift/APNSPayload.swift) is the meta data of the push notification. Things like the alert, badge count. More details on the specifics of each property are provided [here](https://developer.apple.com/library/archive/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/PayloadKeyReference.html). They follow a 1-1 naming scheme listed in Apple's documentation -#### Example `APNSwiftPayload` +#### Example `APNSPayload` ```swift let alert = ... -let aps = APNSwiftPayload(alert: alert, badge: 1, sound: .normal("cow.wav")) +let aps = APNSPayload(alert: alert, badge: 1, sound: .normal("cow.wav")) ``` ### Custom Notification Data -Apple provides engineers with the ability to add custom payload data to each notification. In order to facilitate this we have the `APNSwiftNotification`. +Apple provides engineers with the ability to add custom payload data to each notification. In order to facilitate this we have the `APNSNotification`. #### Example ```swift struct AcmeNotification: APNSwiftNotification { let acme2: [String] - let aps: APNSwiftPayload + let aps: APNSPayload - init(acme2: [String], aps: APNSwiftPayload) { + init(acme2: [String], aps: APNSPayload) { self.acme2 = acme2 self.aps = aps } } -let apns: APNSwiftConnection: = ... -let aps: APNSwiftPayload = ... +let apns: APNSClient: = ... +let aps: APNSPayload = ... let notification = AcmeNotification(acme2: ["bang", "whiz"], aps: aps) let res = try apns.send(notification, to: "de1d666223de85db0186f654852cc960551125ee841ca044fdf5ef6a4756a77e") ``` diff --git a/Sources/APNSwift/APNSwiftAlert.swift b/Sources/APNSwift/APNSAlert.swift similarity index 89% rename from Sources/APNSwift/APNSwiftAlert.swift rename to Sources/APNSwift/APNSAlert.swift index 521d7c5d..88f76ba5 100644 --- a/Sources/APNSwift/APNSwiftAlert.swift +++ b/Sources/APNSwift/APNSAlert.swift @@ -13,7 +13,7 @@ //===----------------------------------------------------------------------===// /// This structure provides the data structure for an APNS Alert -public struct APNSwiftAlert: Codable { +public struct APNSAlert: Codable { public let title: String? public let subtitle: String? public let body: String? @@ -49,9 +49,15 @@ public struct APNSwiftAlert: Codable { ```` */ public init( - title: String? = nil, subtitle: String? = nil, body: String? = nil, - titleLocKey: String? = nil, titleLocArgs: [String]? = nil, actionLocKey: String? = nil, - locKey: String? = nil, locArgs: [String]? = nil, launchImage: String? = nil + title: String? = nil, + subtitle: String? = nil, + body: String? = nil, + titleLocKey: String? = nil, + titleLocArgs: [String]? = nil, + actionLocKey: String? = nil, + locKey: String? = nil, + locArgs: [String]? = nil, + launchImage: String? = nil ) { self.title = title self.subtitle = subtitle diff --git a/Sources/APNSwift/APNSwiftBearerTokenFactory.swift b/Sources/APNSwift/APNSBearerTokenFactory.swift similarity index 57% rename from Sources/APNSwift/APNSwiftBearerTokenFactory.swift rename to Sources/APNSwift/APNSBearerTokenFactory.swift index f07688a4..7a84852b 100644 --- a/Sources/APNSwift/APNSwiftBearerTokenFactory.swift +++ b/Sources/APNSwift/APNSBearerTokenFactory.swift @@ -2,7 +2,7 @@ // // This source file is part of the APNSwift open source project // -// Copyright (c) 2019 the APNSwift project authors +// Copyright (c) 2022 the APNSwift project authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information @@ -16,25 +16,20 @@ import Crypto import Logging import NIOCore -internal final actor APNSwiftBearerTokenFactory { +internal final actor APNSBearerTokenFactory { private var cachedBearerToken: String? - internal var currentBearerToken: String? { - guard !isTokenStale, let cachedBearerToken = cachedBearerToken else { - do { - tokenCreated = NIODeadline.now() - let newToken = try makeNewBearerToken() - cachedBearerToken = newToken - return newToken - } catch { + internal func getCurrentBearerToken() async throws -> String { - logger?.error("Failed to generate token: \(error)") - return nil - } + guard !isTokenStale, let cachedBearerToken = cachedBearerToken else { + tokenCreated = NIODeadline.now() + let newToken = try await makeNewBearerToken() + cachedBearerToken = newToken + return newToken } - logger?.debug("returning cached token \(cachedBearerToken.prefix(8))...") + logger?.debug("APNS cached token \(cachedBearerToken.prefix(8))...") return cachedBearerToken } @@ -42,15 +37,15 @@ internal final actor APNSwiftBearerTokenFactory { NIODeadline.now() - tokenCreated > TimeAmount.minutes(55) } - private let signer: APNSwiftSigner + private let signer: APNSSigner private let logger: Logger? private var tokenCreated: NIODeadline = NIODeadline.now() internal init( - authenticationConfig: APNSwiftConfiguration.Authentication, + authenticationConfig: APNSConfiguration.Authentication, logger: Logger? = nil ) { - self.signer = APNSwiftSigner( + self.signer = APNSSigner( privateKey: authenticationConfig.privateKey, teamIdentifier: authenticationConfig.teamIdentifier, keyIdentifier: authenticationConfig.keyIdentifier @@ -58,9 +53,9 @@ internal final actor APNSwiftBearerTokenFactory { self.logger = logger } - private func makeNewBearerToken() throws -> String { - let newToken = try signer.sign() - logger?.debug("Creating a new APNS token \(newToken.prefix(8))...") + private func makeNewBearerToken() async throws -> String { + let newToken = try await signer.sign() + logger?.debug("APNS new token \(newToken.prefix(8))...") return newToken } diff --git a/Sources/APNSwift/APNSClient+LoggerConfig.swift b/Sources/APNSwift/APNSClient+LoggerConfig.swift deleted file mode 100644 index 1d81f6a1..00000000 --- a/Sources/APNSwift/APNSClient+LoggerConfig.swift +++ /dev/null @@ -1,34 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the APNSwift open source project -// -// Copyright (c) 2019-2020 the APNSwift project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of APNSwift project authors -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -import Logging - -extension APNSwiftClient { - internal func logger(from loggerConfig: LoggerConfig) -> Logger? { - switch loggerConfig { - case .none: - return nil - case .clientLogger: - return self.logger - case .custom(let customLogger): - return customLogger - } - } -} - -public enum LoggerConfig { - case none - case clientLogger - case custom(Logger) -} diff --git a/Sources/APNSwift/APNSwiftClient.swift b/Sources/APNSwift/APNSClient.swift similarity index 53% rename from Sources/APNSwift/APNSwiftClient.swift rename to Sources/APNSwift/APNSClient.swift index 306b012e..7ba097fc 100644 --- a/Sources/APNSwift/APNSwiftClient.swift +++ b/Sources/APNSwift/APNSClient.swift @@ -2,7 +2,7 @@ // // This source file is part of the APNSwift open source project // -// Copyright (c) 2019-2020 the APNSwift project authors +// Copyright (c) 2022 the APNSwift project authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information @@ -12,31 +12,138 @@ // //===----------------------------------------------------------------------===// +import AsyncHTTPClient import Foundation import Logging -import NIO +import NIOCore +import NIOFoundationCompat -public protocol APNSwiftClient { - var logger: Logger? { get } +public final class APNSClient { - func send( + private let configuration: APNSConfiguration + private let bearerTokenFactory: APNSBearerTokenFactory + private let httpClient: HTTPClient + + internal let jsonEncoder = JSONEncoder() + internal let jsonDecoder = JSONDecoder() + + private var logger: Logger? { + configuration.logger + } + + /// APNSClient manages the connection and sending of push notifications to Apple's servers + /// + /// - Parameter configuration: `APNSConfiguration` contains various values the client will need. + public init( + configuration: APNSConfiguration + ) { + self.configuration = configuration + self.bearerTokenFactory = APNSBearerTokenFactory( + authenticationConfig: configuration.authenticationConfig, + logger: configuration.logger + ) + self.httpClient = HTTPClient( + eventLoopGroupProvider: configuration.eventLoopGroupProvider.httpClientValue + ) + } + + /// Shuts down the connections + public func shutdown() async throws { + try await httpClient.shutdown() + } + + /// This method sends a raw payload to Apple, since it is raw, use this with caution as requests may fail + /// - Parameters: + /// - payload: The APS payload in ByteBuffer form + /// - pushType: The push type, ie, alert, mdm, voip, etc + /// - deviceToken: A device token which will receive the push + /// - environment: An optional environment to override for this push + /// - expiration: The date at which the notification is no longer valid + /// - priority: The priority of the notification. If you omit this header, APNs sets the notification priority to 10 + /// - collapseIdentifier: An identifier you use to coalesce multiple notifications into a single notification for the user + /// - topic: An optional topic to override for this push + /// - apnsID: A canonical UUID that is the unique ID for the notification + /// + /// For more information see: [Sending Notification Requests To APNs](https://developer.apple.com/documentation/usernotifications/setting_up_a_remote_notification_server/sending_notification_requests_to_apns) + public func send( rawBytes payload: ByteBuffer, - pushType: APNSwiftConnection.PushType, + pushType: APNSClient.PushType, to deviceToken: String, - on environment: APNSwiftConfiguration.Environment?, + on environment: APNSConfiguration.Environment?, expiration: Date?, priority: Int?, collapseIdentifier: String?, topic: String?, - logger: Logger?, apnsID: UUID? - ) async throws + ) async throws { + + let topic = topic ?? configuration.topic + let urlBase: String = + environment?.url.absoluteString ?? configuration.environment.url.absoluteString + + var request = HTTPClientRequest(url: "\(urlBase)/3/device/\(deviceToken)") + request.method = .POST + request.headers.add(name: "content-type", value: "application/json") + request.headers.add(name: "user-agent", value: "APNS/swift-nio") + request.headers.add(name: "content-length", value: "\(payload.readableBytes)") + request.headers.add(name: "apns-topic", value: topic) + request.headers.add(name: "apns-push-type", value: pushType.rawValue) + request.headers.add(name: "host", value: urlBase) + + if let priority = priority { + request.headers.add(name: "apns-priority", value: String(priority)) + } + + if let epochTime = expiration?.timeIntervalSince1970 { + request.headers.add(name: "apns-expiration", value: String(Int(epochTime))) + } + + if let collapseId = collapseIdentifier { + request.headers.add(name: "apns-collapse-id", value: collapseId) + } + + let bearerToken = try await bearerTokenFactory.getCurrentBearerToken() + + request.headers.add(name: "authorization", value: "bearer \(bearerToken)") + + if let apnsID = apnsID { + request.headers.add(name: "apns-id", value: apnsID.uuidString.lowercased()) + } + + request.body = .bytes(payload) + + logger?.debug("APNS request - executing") + + let response = try await httpClient.execute( + request, + timeout: configuration.timeout ?? .seconds(30) + ) + logger?.debug("APNS request - finished - \(response.status)") + if response.status != .ok { + let body = try await response.body.collect(upTo: 1024 * 1024) + + let error = try jsonDecoder.decode(APNSError.ResponseStruct.self, from: body) + logger?.warning("APNS request - failed - \(error.reason)") + throw APNSError.ResponseError.badRequest(error.reason) + } + } } -extension APNSwiftClient { +extension APNSClient { + public enum PushType: String { + case alert + case background + case mdm + case voip + case fileprovider + case complication + } +} + +extension APNSClient { /** - APNSwiftConnection send method. Sends a notification to the desired deviceToken. + APNSClient send method. Sends a notification to the desired deviceToken. - Parameter payload: the alert to send. - Parameter pushType: push type of the notification. - Parameter deviceToken: device token to send alert to. @@ -50,26 +157,25 @@ extension APNSwiftClient { [Retrieve Your App's Device Token](https://developer.apple.com/documentation/usernotifications/registering_your_app_with_apns#2942135) ### Usage Example: ### ``` - let apns = APNSwiftConnection.connect() + let apns = APNSClient() let expiry = Date().addingTimeInterval(5) try apns.send(notification, pushType: .alert, to: "b27a07be2092c7fbb02ab5f62f3135c615e18acc0ddf39a30ffde34d41665276", with: JSONEncoder(), expiration: expiry, priority: 10, collapseIdentifier: "huro2").wait() ``` */ public func send( - _ alert: APNSwiftAlert, - pushType: APNSwiftConnection.PushType = .alert, + _ alert: APNSAlert, + pushType: APNSClient.PushType = .alert, to deviceToken: String, - on environment: APNSwiftConfiguration.Environment? = nil, + on environment: APNSConfiguration.Environment? = nil, with encoder: JSONEncoder = JSONEncoder(), expiration: Date? = nil, priority: Int? = nil, collapseIdentifier: String? = nil, topic: String? = nil, - loggerConfig: LoggerConfig = .clientLogger, apnsID: UUID? = nil ) async throws { try await self.send( - APNSwiftPayload(alert: alert), + APNSPayload(alert: alert), pushType: pushType, to: deviceToken, on: environment, @@ -78,13 +184,12 @@ extension APNSwiftClient { priority: priority, collapseIdentifier: collapseIdentifier, topic: topic, - loggerConfig: loggerConfig, apnsID: apnsID ) } /** - APNSwiftConnection send method. Sends a notification to the desired deviceToken. + APNSClient send method. Sends a notification to the desired deviceToken. - Parameter payload: the payload to send. - Parameter pushType: push type of the notification. - Parameter deviceToken: device token to send alert to. @@ -98,24 +203,26 @@ extension APNSwiftClient { [Retrieve Your App's Device Token](https://developer.apple.com/documentation/usernotifications/registering_your_app_with_apns#2942135) ### Usage Example: ### ``` - let apns = APNSwiftConnection.connect() + let apns = APNSClient() let expiry = Date().addingTimeInterval(5) try apns.send(notification, pushType: .alert, to: "b27a07be2092c7fbb02ab5f62f3135c615e18acc0ddf39a30ffde34d41665276", with: JSONEncoder(), expiration: expiry, priority: 10, collapseIdentifier: "huro2").wait() ``` */ public func send( - _ payload: APNSwiftPayload, - pushType: APNSwiftConnection.PushType = .alert, + _ payload: APNSPayload, + pushType: APNSClient.PushType = .alert, to deviceToken: String, - on environment: APNSwiftConfiguration.Environment? = nil, + on environment: APNSConfiguration.Environment? = nil, with encoder: JSONEncoder = JSONEncoder(), expiration: Date? = nil, priority: Int? = nil, collapseIdentifier: String? = nil, topic: String? = nil, - loggerConfig: LoggerConfig = .clientLogger, apnsID: UUID? = nil ) async throws { + struct BasicNotification: APNSNotification { + let aps: APNSPayload + } try await self.send( BasicNotification(aps: payload), pushType: pushType, @@ -126,13 +233,12 @@ extension APNSwiftClient { priority: priority, collapseIdentifier: collapseIdentifier, topic: topic, - loggerConfig: loggerConfig, apnsID: apnsID ) } /** - APNSwiftConnection send method. Sends a notification to the desired deviceToken. + APNSClient send method. Sends a notification to the desired deviceToken. - Parameter notification: the notification meta data and alert to send. - Parameter pushType: push type of the notification. - Parameter deviceToken: device token to send alert to. @@ -146,25 +252,29 @@ extension APNSwiftClient { [Retrieve Your App's Device Token](https://developer.apple.com/documentation/usernotifications/registering_your_app_with_apns#2942135) ### Usage Example: ### ``` - let apns = APNSwiftConnection.connect() + let apns = APNSClient() let expiry = Date().addingTimeInterval(5) try apns.send(notification, pushType: .alert, to: "b27a07be2092c7fbb02ab5f62f3135c615e18acc0ddf39a30ffde34d41665276", with: JSONEncoder(), expiration: expiry, priority: 10, collapseIdentifier: "huro2").wait() ``` */ public func send( _ notification: Notification, - pushType: APNSwiftConnection.PushType = .alert, + pushType: APNSClient.PushType = .alert, to deviceToken: String, - on environment: APNSwiftConfiguration.Environment? = nil, - with encoder: JSONEncoder = JSONEncoder(), + on environment: APNSConfiguration.Environment? = nil, + with encoder: JSONEncoder? = nil, expiration: Date? = nil, priority: Int? = nil, collapseIdentifier: String? = nil, topic: String? = nil, - loggerConfig: LoggerConfig = .clientLogger, apnsID: UUID? = nil - ) async throws where Notification: APNSwiftNotification { - let data: Data = try encoder.encode(notification) + ) async throws where Notification: APNSNotification { + let data: Data + if let encoder = encoder { + data = try encoder.encode(notification) + } else { + data = try jsonEncoder.encode(notification) + } try await self.send( raw: data, pushType: pushType, @@ -174,7 +284,6 @@ extension APNSwiftClient { priority: priority, collapseIdentifier: collapseIdentifier, topic: topic, - loggerConfig: loggerConfig, apnsID: apnsID ) } @@ -183,14 +292,13 @@ extension APNSwiftClient { /// For more information see: [Creating APN Payload](https://developer.apple.com/library/archive/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/CreatingtheNotificationPayload.html) public func send( raw payload: Bytes, - pushType: APNSwiftConnection.PushType = .alert, + pushType: APNSClient.PushType = .alert, to deviceToken: String, - on environment: APNSwiftConfiguration.Environment? = nil, + on environment: APNSConfiguration.Environment? = nil, expiration: Date?, priority: Int?, collapseIdentifier: String?, topic: String?, - loggerConfig: LoggerConfig = .clientLogger, apnsID: UUID? = nil ) async throws where Bytes: Collection, Bytes.Element == UInt8 { @@ -205,38 +313,7 @@ extension APNSwiftClient { priority: priority, collapseIdentifier: collapseIdentifier, topic: topic, - logger: logger(from: loggerConfig), apnsID: apnsID ) } - - public func send( - rawBytes payload: ByteBuffer, - pushType: APNSwiftConnection.PushType = .alert, - to deviceToken: String, - on environment: APNSwiftConfiguration.Environment? = nil, - expiration: Date? = nil, - priority: Int? = nil, - collapseIdentifier: String? = nil, - topic: String? = nil, - loggerConfig: LoggerConfig = .clientLogger, - apnsID: UUID? = nil - ) async throws { - try await self.send( - rawBytes: payload, - pushType: pushType, - to: deviceToken, - on: environment, - expiration: expiration, - priority: priority, - collapseIdentifier: collapseIdentifier, - topic: topic, - logger: logger(from: loggerConfig), - apnsID: apnsID - ) - } -} - -private struct BasicNotification: APNSwiftNotification { - let aps: APNSwiftPayload } diff --git a/Sources/APNSwift/APNSConfiguration.swift b/Sources/APNSwift/APNSConfiguration.swift new file mode 100644 index 00000000..3f030fca --- /dev/null +++ b/Sources/APNSwift/APNSConfiguration.swift @@ -0,0 +1,112 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the APNSwift open source project +// +// Copyright (c) 2022 the APNSwift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of APNSwift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import AsyncHTTPClient +import Crypto +import Foundation +import Logging +import NIOCore + +/// This is structure that provides the system with common configuration. +public struct APNSConfiguration { + public typealias APNSPrivateKey = P256.Signing.PrivateKey + internal var authenticationConfig: Authentication + + public struct Authentication { + + /// Configurtion for handling bearer tokens + /// - Parameters: + /// - privateKey: A string of the private key used to sign requests + /// - teamIdentifier: An Apple developer team identifier + /// - keyIdentifier: A key identifier provided by Apple + public init( + privateKey: APNSConfiguration.APNSPrivateKey, + teamIdentifier: String, + keyIdentifier: String + ) { + self.privateKey = privateKey + self.teamIdentifier = teamIdentifier + self.keyIdentifier = keyIdentifier + } + + internal let privateKey: APNSPrivateKey + internal let teamIdentifier: String + internal let keyIdentifier: String + } + + internal let topic: String + internal let environment: Environment + internal let logger: Logger? + /// Optional timeout time if the connection does not receive a response. + internal let timeout: TimeAmount? + internal let eventLoopGroupProvider: EventLoopGroupProvider + + /// `APNSConfiguration` provides the values for APNSClient to use when sending pushes + /// - Parameters: + /// - authenticationConfig: A configuration type to handle bearer tokens + /// - topic: A string for which the push is sent to e.g `com.grasscove.Fern` + /// - environment: The environment which APNSClient will connect to + /// - eventLoopGroupProvider: The event loop provider for APNSwift + /// - logger: An optional logger + /// - timeout: An optional timeout for requests + public init( + authenticationConfig: APNSConfiguration.Authentication, + topic: String, + environment: APNSConfiguration.Environment, + eventLoopGroupProvider: EventLoopGroupProvider, + logger: Logger? = nil, + timeout: TimeAmount? = nil + ) { + self.topic = topic + self.authenticationConfig = authenticationConfig + self.environment = environment + self.eventLoopGroupProvider = eventLoopGroupProvider + self.timeout = timeout + self.logger = logger + } +} + +extension APNSConfiguration { + /// Provides an enum to manage the URL at which the push is sent. + public enum Environment { + case production + case sandbox + + public var url: URL { + switch self { + case .production: + return URL(string: "https://api.push.apple.com")! + case .sandbox: + return URL(string: "https://api.development.push.apple.com")! + } + } + } + + /// Specifies how `EventLoopGroup` will be created and establishes lifecycle ownership. + public enum EventLoopGroupProvider { + /// `EventLoopGroup` will be provided by the user. Owner of this group is responsible for its lifecycle. + case shared(EventLoopGroup) + /// `EventLoopGroup` will be created by the client. When `syncShutdown` is called, created `EventLoopGroup` will be shut down as well. + case createNew + + internal var httpClientValue: HTTPClient.EventLoopGroupProvider { + switch self { + case .createNew: + return .createNew + case .shared(let group): + return .shared(group) + } + } + } +} diff --git a/Sources/APNSwift/APNSwiftErrors.swift b/Sources/APNSwift/APNSErrors.swift similarity index 97% rename from Sources/APNSwift/APNSwiftErrors.swift rename to Sources/APNSwift/APNSErrors.swift index f23153fe..67d7d403 100644 --- a/Sources/APNSwift/APNSwiftErrors.swift +++ b/Sources/APNSwift/APNSErrors.swift @@ -2,7 +2,7 @@ // // This source file is part of the APNSwift open source project // -// Copyright (c) 2019 the APNSwift project authors +// Copyright (c) 2022 the APNSwift project authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information @@ -14,7 +14,7 @@ import Foundation -public struct APNSwiftError: Equatable { +public struct APNSError: Equatable { public enum ResponseError: Error, Equatable { case badRequest(ResponseErrorMessage) } diff --git a/Sources/APNSwift/APNSwiftNotification.swift b/Sources/APNSwift/APNSNotification.swift similarity index 87% rename from Sources/APNSwift/APNSwiftNotification.swift rename to Sources/APNSwift/APNSNotification.swift index bcb87147..fbcd0340 100644 --- a/Sources/APNSwift/APNSwiftNotification.swift +++ b/Sources/APNSwift/APNSNotification.swift @@ -13,6 +13,6 @@ //===----------------------------------------------------------------------===// /// This is a protocol which allows developers to construct their own Notification payload -public protocol APNSwiftNotification: Codable { - var aps: APNSwiftPayload { get } +public protocol APNSNotification: Codable { + var aps: APNSPayload { get } } diff --git a/Sources/APNSwift/APNSPayload.swift b/Sources/APNSwift/APNSPayload.swift new file mode 100644 index 00000000..37d12d5a --- /dev/null +++ b/Sources/APNSwift/APNSPayload.swift @@ -0,0 +1,98 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the APNSwift open source project +// +// Copyright (c) 2022-2020 the APNSwift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of APNSwift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +/// This structure provides the data structure for an APNS Payload +public struct APNSPayload: Codable { + public let alert: APNSAlert? + public let badge: Int? + public let sound: APNSSoundType? + public let contentAvailable: Int? + public let mutableContent: Int? + public let category: String? + public let threadID: String? + public let targetContentId: String? + public let interruptionLevel: InterruptionLevel? + public let relevanceScore: Float? + public let filterCriteria: String? + + /// An APNs Push payload provides the properties to send along for a push notification + /// + /// For more information see: [Generating a remote notification](https://developer.apple.com/documentation/usernotifications/setting_up_a_remote_notification_server/generating_a_remote_notification) + /// - Parameters: + /// - alert: The information for displaying an alert. + /// - badge: The number to display in a badge on your app’s icon. + /// - sound: The name of a sound file in your app’s main bundle or in the Library/Sounds folder of your app’s container directory. + /// - hasContentAvailable: The background notification flag. To perform a silent background update, specify the value 1 and don’t include the alert, badge, or sound keys in your payload. + /// - hasMutableContent: The notification service app extension flag. If the value is 1, the system passes the notification to your notification service app extension before delivery. + /// - category: The notification’s type. This string must correspond to the identifier of one of the UNNotificationCategory objects you register at launch time. + /// - threadID: An app-specific identifier for grouping related notifications. + /// - targetContentId: The identifier of the window brought forward. + /// - interruptionLevel: The importance and delivery timing of a notification. The string values “passive”, “active”, “time-sensitive”, or “critical” correspond to the UNNotificationInterruptionLevel enumeration cases. + /// - relevanceScore: The relevance score, a number between 0 and 1, that the system uses to sort the notifications from your app. + public init( + alert: APNSAlert? = nil, + badge: Int? = nil, + sound: APNSSoundType? = nil, + hasContentAvailable: Bool? = false, + hasMutableContent: Bool? = false, + category: String? = nil, + threadID: String? = nil, + targetContentId: String? = nil, + interruptionLevel: InterruptionLevel? = nil, + relevanceScore: Float? = nil, + filterCriteria: String? = nil + ) { + + self.alert = alert + self.badge = badge + self.sound = sound + if let hasContentAvailable = hasContentAvailable { + self.contentAvailable = hasContentAvailable ? 1 : 0 + } else { + self.contentAvailable = nil + } + if let hasMutableContent = hasMutableContent { + self.mutableContent = hasMutableContent ? 1 : 0 + } else { + self.mutableContent = nil + } + self.category = category + self.threadID = threadID + self.targetContentId = targetContentId + self.interruptionLevel = interruptionLevel + self.relevanceScore = relevanceScore + self.filterCriteria = filterCriteria + } + + enum CodingKeys: String, CodingKey { + case alert + case badge + case sound + case contentAvailable = "content-available" + case mutableContent = "mutable-content" + case category + case threadID = "thread-id" + case targetContentId = "target-content-id" + case interruptionLevel = "interruption-level" + case relevanceScore = "relevance-score" + case filterCriteria = "filter-criteria" + } + + public enum InterruptionLevel: String, Codable { + case passive + case active + case timeSensitive = "time-sensitive" + case critical + } +} diff --git a/Sources/APNSwift/APNSwiftSigner.swift b/Sources/APNSwift/APNSSigner.swift similarity index 94% rename from Sources/APNSwift/APNSwiftSigner.swift rename to Sources/APNSwift/APNSSigner.swift index f7c179be..7c759b7f 100644 --- a/Sources/APNSwift/APNSwiftSigner.swift +++ b/Sources/APNSwift/APNSSigner.swift @@ -2,7 +2,7 @@ // // This source file is part of the APNSwift open source project // -// Copyright (c) 2019-2020 the APNSwift project authors +// Copyright (c) 2022-2020 the APNSwift project authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information @@ -15,7 +15,7 @@ import Crypto import Foundation -internal struct APNSwiftSigner { +internal final actor APNSSigner { internal init( privateKey: P256.Signing.PrivateKey, teamIdentifier: String, keyIdentifier: String diff --git a/Sources/APNSwift/APNSwiftSound.swift b/Sources/APNSwift/APNSSound.swift similarity index 93% rename from Sources/APNSwift/APNSwiftSound.swift rename to Sources/APNSwift/APNSSound.swift index f40e1d58..142671a6 100644 --- a/Sources/APNSwift/APNSwiftSound.swift +++ b/Sources/APNSwift/APNSSound.swift @@ -28,7 +28,7 @@ public struct APNSSoundDictionary: Codable, Equatable { ### Usage Example: ### ```` let apsSound = APNSSoundDictionary(isCritical: true, name: "cow.wav", volume: 0.8) - let aps = APNSwiftPayload(alert: alert, badge: 1, sound: .dictionary(apsSound)) + let aps = APNSPayload(alert: alert, badge: 1, sound: .dictionary(apsSound)) ```` */ public init(isCritical: Bool, name: String, volume: Double) { @@ -40,12 +40,12 @@ public struct APNSSoundDictionary: Codable, Equatable { /// An enum to define how to use sound. /// - Parameter string: use this for a normal alert sound /// - Parameter critical: use for a critical alert type -public enum APNSwiftSoundType: Codable, Equatable { +public enum APNSSoundType: Codable, Equatable { case normal(String) case critical(APNSSoundDictionary) } -extension APNSwiftSoundType { +extension APNSSoundType { public func encode(to encoder: Encoder) throws { var container = encoder.singleValueContainer() switch self { diff --git a/Sources/APNSwift/APNSwiftConfiguration.swift b/Sources/APNSwift/APNSwiftConfiguration.swift deleted file mode 100644 index b49498cb..00000000 --- a/Sources/APNSwift/APNSwiftConfiguration.swift +++ /dev/null @@ -1,80 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the APNSwift open source project -// -// Copyright (c) 2019 the APNSwift project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of APNSwift project authors -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -import AsyncHTTPClient -import Crypto -import Foundation -import Logging -import NIOCore - -/// This is structure that provides the system with common configuration. -public struct APNSwiftConfiguration { - public typealias APNSPrivateKey = P256.Signing.PrivateKey - internal var authenticationConfig: Authentication - - public struct Authentication { - public init( - privateKey: APNSwiftConfiguration.APNSPrivateKey, - teamIdentifier: String, - keyIdentifier: String - ) { - self.privateKey = privateKey - self.teamIdentifier = teamIdentifier - self.keyIdentifier = keyIdentifier - } - - internal let privateKey: APNSPrivateKey - internal let teamIdentifier: String - internal let keyIdentifier: String - } - - internal let httpClient: HTTPClient - internal let topic: String - internal let environment: Environment - internal let logger: Logger? - /// Optional timeout time if the connection does not receive a response. - internal let timeout: TimeAmount? - - public init( - httpClient: HTTPClient, - authenticationConfig: APNSwiftConfiguration.Authentication, - topic: String, - environment: APNSwiftConfiguration.Environment, - logger: Logger? = nil, - timeout: TimeAmount? = nil - ) { - self.httpClient = httpClient - self.topic = topic - self.authenticationConfig = authenticationConfig - self.environment = environment - self.timeout = timeout - self.logger = logger - } -} - -extension APNSwiftConfiguration { - public enum Environment { - case production - case sandbox - - public var url: URL { - switch self { - case .production: - return URL(string: "https://api.push.apple.com")! - case .sandbox: - return URL(string: "https://api.development.push.apple.com")! - } - } - } -} diff --git a/Sources/APNSwift/APNSwiftConnection.swift b/Sources/APNSwift/APNSwiftConnection.swift deleted file mode 100644 index 96fc5358..00000000 --- a/Sources/APNSwift/APNSwiftConnection.swift +++ /dev/null @@ -1,120 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the APNSwift open source project -// -// Copyright (c) 2019 the APNSwift project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of APNSwift project authors -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -import AsyncHTTPClient -import Foundation -import Logging -import NIOCore -import NIOFoundationCompat - -public final class APNSwiftConnection: APNSwiftClient { - - private let configuration: APNSwiftConfiguration - private let bearerTokenFactory: APNSwiftBearerTokenFactory - public var logger: Logger? - - private let jsonDecoder = JSONDecoder() - - public init( - configuration: APNSwiftConfiguration, - logger: Logger? = nil - ) { - self.configuration = configuration - self.logger = logger - self.bearerTokenFactory = APNSwiftBearerTokenFactory( - authenticationConfig: configuration.authenticationConfig, - logger: logger - ) - } - - /// This is to be used with caution. APNSwift cannot gurantee delivery if you do not have the correct payload. - /// For more information see: [Creating APN Payload](https://developer.apple.com/library/archive/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/CreatingtheNotificationPayload.html) - public func send( - rawBytes payload: ByteBuffer, - pushType: APNSwiftConnection.PushType, - to deviceToken: String, - on environment: APNSwiftConfiguration.Environment?, - expiration: Date?, - priority: Int?, - collapseIdentifier: String?, - topic: String?, - logger: Logger?, - apnsID: UUID? - ) async throws { - let logger = logger ?? self.configuration.logger - logger?.debug( - "Sending \(pushType) to \(deviceToken.prefix(8))... at: \(topic ?? configuration.topic)" - ) - var urlBase: String - if let overriddenEnvironment = environment { - urlBase = overriddenEnvironment.url.absoluteString - } else { - urlBase = configuration.environment.url.absoluteString - } - var request = HTTPClientRequest(url: "\(urlBase)/3/device/\(deviceToken)") - request.method = .POST - request.headers.add(name: "content-type", value: "application/json") - request.headers.add(name: "user-agent", value: "APNS/swift-nio") - request.headers.add(name: "content-length", value: "\(payload.readableBytes)") - - if let notificationSpecificTopic = topic { - request.headers.add(name: "apns-topic", value: notificationSpecificTopic) - } else { - request.headers.add(name: "apns-topic", value: configuration.topic) - } - - if let priority = priority { - request.headers.add(name: "apns-priority", value: String(priority)) - } - if let epochTime = expiration?.timeIntervalSince1970 { - request.headers.add(name: "apns-expiration", value: String(Int(epochTime))) - } - if let collapseId = collapseIdentifier { - request.headers.add(name: "apns-collapse-id", value: collapseId) - } - request.headers.add(name: "apns-push-type", value: pushType.rawValue) - request.headers.add(name: "host", value: urlBase) - - // Only use token auth if bearer token is present. - if let bearerToken = await bearerTokenFactory.currentBearerToken { - request.headers.add(name: "authorization", value: "bearer \(bearerToken)") - } - if let apnsID = apnsID { - request.headers.add(name: "apns-id", value: apnsID.uuidString.lowercased()) - } - - request.body = .bytes(payload) - - let response = try await configuration.httpClient.execute( - request, timeout: configuration.timeout ?? .seconds(30)) - if response.status != .ok { - let body = try await response.body.collect(upTo: 1024 * 1024) - - let error = try jsonDecoder.decode(APNSwiftError.ResponseStruct.self, from: body) - logger?.warning("Response - bad request \(error.reason)") - throw APNSwiftError.ResponseError.badRequest(error.reason) - } - } -} - -extension APNSwiftConnection { - public enum PushType: String { - case alert - case background - case mdm - case voip - case fileprovider - case complication - } -} diff --git a/Sources/APNSwift/APNSwiftPayload.swift b/Sources/APNSwift/APNSwiftPayload.swift deleted file mode 100644 index 632a21e4..00000000 --- a/Sources/APNSwift/APNSwiftPayload.swift +++ /dev/null @@ -1,73 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the APNSwift open source project -// -// Copyright (c) 2019-2020 the APNSwift project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of APNSwift project authors -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -/// This structure provides the data structure for an APNS Payload -public struct APNSwiftPayload: Codable { - public let alert: APNSwift.APNSwiftAlert? - public let badge: Int? - public let sound: APNSwift.APNSwiftSoundType? - public let contentAvailable: Int? - public let mutableContent: Int? - public let category: String? - public let threadID: String? - public let targetContentId: String? - public let interruptionLevel: String? - public let relevanceScore: Float? - - public init( - alert: APNSwift.APNSwiftAlert? = nil, - badge: Int? = nil, - sound: APNSwift.APNSwiftSoundType? = nil, - hasContentAvailable: Bool? = false, - hasMutableContent: Bool? = false, - category: String? = nil, - threadID: String? = nil, - targetContentId: String? = nil, - interruptionLevel: String? = nil, - relevanceScore: Float? = nil - ) { - - self.alert = alert - self.badge = badge - self.sound = sound - if let hasContentAvailable = hasContentAvailable { - self.contentAvailable = hasContentAvailable ? 1 : 0 - } else { - self.contentAvailable = nil - } - if let hasMutableContent = hasMutableContent { - self.mutableContent = hasMutableContent ? 1 : 0 - } else { - self.mutableContent = nil - } - self.category = category - self.threadID = threadID - self.targetContentId = targetContentId - self.interruptionLevel = interruptionLevel - self.relevanceScore = relevanceScore - } - - enum CodingKeys: String, CodingKey { - case alert - case badge - case sound - case contentAvailable = "content-available" - case mutableContent = "mutable-content" - case category - case threadID = "thread-id" - case targetContentId = "target-content-id" - case interruptionLevel = "interruption-level" - case relevanceScore = "relevance-score" - } -} diff --git a/Sources/APNSwift/P256.Signing.PrivateKey+PrivateFilePath.swift b/Sources/APNSwift/P256.Signing.PrivateKey+PrivateFilePath.swift index a3eb7c73..c5fa3125 100644 --- a/Sources/APNSwift/P256.Signing.PrivateKey+PrivateFilePath.swift +++ b/Sources/APNSwift/P256.Signing.PrivateKey+PrivateFilePath.swift @@ -2,7 +2,7 @@ // // This source file is part of the APNSwift open source project // -// Copyright (c) 2019 the APNSwift project authors +// Copyright (c) 2022 the APNSwift project authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information @@ -21,7 +21,7 @@ extension P256.Signing.PrivateKey { guard let data = try? Data(contentsOf: URL(fileURLWithPath: filePath)), let pemString = String(data: data, encoding: .utf8) else { - throw APNSwiftError.SigningError.certificateFileDoesNotExist + throw APNSError.SigningError.certificateFileDoesNotExist } return try loadFrom(string: pemString) } diff --git a/Sources/APNSwiftExample/main.swift b/Sources/APNSwiftExample/main.swift index 08660c04..23cc99bf 100644 --- a/Sources/APNSwiftExample/main.swift +++ b/Sources/APNSwiftExample/main.swift @@ -2,7 +2,7 @@ // // This source file is part of the APNSwift open source project // -// Copyright (c) 2019 the APNSwift project authors +// Copyright (c) 2022 the APNSwift project authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information @@ -18,57 +18,53 @@ import Foundation import Logging import NIO -let group = MultiThreadedEventLoopGroup(numberOfThreads: 1) - /// optional var logger = Logger(label: "com.apnswift") logger.logLevel = .debug -let httpClient = HTTPClient(eventLoopGroupProvider: .shared(group)) - -let authenticationConfig: APNSwiftConfiguration.Authentication = .init( +let authenticationConfig: APNSConfiguration.Authentication = .init( privateKey: try .loadFrom(filePath: "/Users/kylebrowning/Documents/AuthKey_9UC9ZLQ8YW.p8"), teamIdentifier: "ABBM6U9RM5", keyIdentifier: "9UC9ZLQ8YW" ) -let apnsConfig = APNSwiftConfiguration( - httpClient: httpClient, +let apnsConfig = APNSConfiguration( authenticationConfig: authenticationConfig, topic: "com.grasscove.Fern", environment: .sandbox, + eventLoopGroupProvider: .createNew, logger: logger ) -let apnsProdConfig = APNSwiftConfiguration( - httpClient: httpClient, +let apnsProdConfig = APNSConfiguration( authenticationConfig: authenticationConfig, topic: "com.grasscove.Fern", environment: .production, + eventLoopGroupProvider: .createNew, logger: logger ) -struct AcmeNotification: APNSwiftNotification { +struct AcmeNotification: APNSNotification { let acme2: [String] - let aps: APNSwiftPayload + let aps: APNSPayload - init(acme2: [String], aps: APNSwiftPayload) { + init(acme2: [String], aps: APNSPayload) { self.acme2 = acme2 self.aps = aps } } -let alert = APNSwiftAlert(title: "Hey There", subtitle: "Subtitle", body: "Body") +let alert = APNSAlert(title: "Hey There", subtitle: "Subtitle", body: "Body") let apsSound = APNSSoundDictionary(isCritical: true, name: "cow.wav", volume: 0.8) -let aps = APNSwiftPayload( +let aps = APNSPayload( alert: alert, badge: 0, sound: .critical(apsSound), hasContentAvailable: true) let notification = AcmeNotification(acme2: ["bang", "whiz"], aps: aps) let dt = "80745890ac499fa0c61c2348b56cdf735343963e085dd2283fb48a9fa56b0527759ed783ae6278f4f09aa3c4cc9d5b9f5ac845c3648e655183e2318404bc254ffcd1eea427ad528c3d0b253770422a80" -let apns = APNSwiftConnection(configuration: apnsConfig, logger: logger) -let apnsProd = APNSwiftConnection(configuration: apnsProdConfig, logger: logger) +let apns = APNSClient(configuration: apnsConfig) +let apnsProd = APNSClient(configuration: apnsProdConfig) let expiry = Date().addingTimeInterval(5) let dispatchGroup = DispatchGroup() dispatchGroup.enter() @@ -81,8 +77,8 @@ Task { notification, pushType: .alert, to: dt, expiration: expiry, priority: 10) /// Overriden environment try await apnsProd.send(aps, to: dt, on: .sandbox) - try await httpClient.shutdown() - try! group.syncShutdownGracefully() + try await apns.shutdown() + try await apnsProd.shutdown() dispatchGroup.leave() } catch { diff --git a/Tests/APNSwiftTests/APNSwiftConfigurationTests.swift b/Tests/APNSwiftTests/APNSwiftConfigurationTests.swift index 1f483606..81d9783f 100644 --- a/Tests/APNSwiftTests/APNSwiftConfigurationTests.swift +++ b/Tests/APNSwiftTests/APNSwiftConfigurationTests.swift @@ -2,7 +2,7 @@ // // This source file is part of the APNSwift open source project // -// Copyright (c) 2019 the APNSwift project authors +// Copyright (c) 2022 the APNSwift project authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information @@ -12,41 +12,47 @@ // //===----------------------------------------------------------------------===// -import XCTest -@testable import APNSwift import AsyncHTTPClient import Logging import NIO import NIOSSL +import XCTest + +@testable import APNSwift -class APNSwiftConfigurationTests: XCTestCase { +class APNSConfigurationTests: XCTestCase { let httpClient = HTTPClient(eventLoopGroupProvider: .createNew) - func configuration(environment: APNSwiftConfiguration.Environment) throws { - let privateKey: APNSwiftConfiguration.APNSPrivateKey = try .loadFrom(string: appleECP8PrivateKey) - let authenticationConfig: APNSwiftConfiguration.Authentication = .init( + func configuration(environment: APNSConfiguration.Environment) throws { + let privateKey: APNSConfiguration.APNSPrivateKey = try .loadFrom( + string: appleECP8PrivateKey) + let authenticationConfig: APNSConfiguration.Authentication = .init( privateKey: privateKey, teamIdentifier: "MY_TEAM_ID", keyIdentifier: "MY_KEY_ID" ) - let apnsConfiguration = APNSwiftConfiguration( - httpClient: httpClient, + let apnsConfiguration = APNSConfiguration( authenticationConfig: authenticationConfig, topic: "MY_TOPIC", environment: environment, + eventLoopGroupProvider: .createNew, timeout: .seconds(5) ) switch environment { case .production: - XCTAssertEqual(apnsConfiguration.environment.url, URL(string: "https://api.push.apple.com")) + XCTAssertEqual( + apnsConfiguration.environment.url, URL(string: "https://api.push.apple.com")) case .sandbox: - XCTAssertEqual(apnsConfiguration.environment.url, URL(string: "https://api.development.push.apple.com")) + XCTAssertEqual( + apnsConfiguration.environment.url, + URL(string: "https://api.development.push.apple.com")) } - let loadedKey: APNSwiftConfiguration.APNSPrivateKey = try .loadFrom(string: appleECP8PrivateKey) - XCTAssertEqual(loadedKey.rawRepresentation, authenticationConfig.privateKey.rawRepresentation) + let loadedKey: APNSConfiguration.APNSPrivateKey = try .loadFrom(string: appleECP8PrivateKey) + XCTAssertEqual( + loadedKey.rawRepresentation, authenticationConfig.privateKey.rawRepresentation) XCTAssertEqual("MY_KEY_ID", authenticationConfig.keyIdentifier) XCTAssertEqual("MY_TEAM_ID", authenticationConfig.teamIdentifier) @@ -64,17 +70,17 @@ class APNSwiftConfigurationTests: XCTestCase { } let appleECP8PrivateKey = """ ------BEGIN PRIVATE KEY----- -MIGTAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBHkwdwIBAQQg2sD+kukkA8GZUpmm -jRa4fJ9Xa/JnIG4Hpi7tNO66+OGgCgYIKoZIzj0DAQehRANCAATZp0yt0btpR9kf -ntp4oUUzTV0+eTELXxJxFvhnqmgwGAm1iVW132XLrdRG/ntlbQ1yzUuJkHtYBNve -y+77Vzsd ------END PRIVATE KEY----- -""" + -----BEGIN PRIVATE KEY----- + MIGTAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBHkwdwIBAQQg2sD+kukkA8GZUpmm + jRa4fJ9Xa/JnIG4Hpi7tNO66+OGgCgYIKoZIzj0DAQehRANCAATZp0yt0btpR9kf + ntp4oUUzTV0+eTELXxJxFvhnqmgwGAm1iVW132XLrdRG/ntlbQ1yzUuJkHtYBNve + y+77Vzsd + -----END PRIVATE KEY----- + """ let appleECP8PublicKey = """ ------BEGIN PUBLIC KEY----- -MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE2adMrdG7aUfZH57aeKFFM01dPnkx -C18ScRb4Z6poMBgJtYlVtd9ly63URv57ZW0Ncs1LiZB7WATb3svu+1c7HQ== ------END PUBLIC KEY----- -""" + -----BEGIN PUBLIC KEY----- + MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE2adMrdG7aUfZH57aeKFFM01dPnkx + C18ScRb4Z6poMBgJtYlVtd9ly63URv57ZW0Ncs1LiZB7WATb3svu+1c7HQ== + -----END PUBLIC KEY----- + """ } diff --git a/Tests/APNSwiftTests/APNSwiftRequestTests.swift b/Tests/APNSwiftTests/APNSwiftRequestTests.swift index 0d622e29..9914dff7 100644 --- a/Tests/APNSwiftTests/APNSwiftRequestTests.swift +++ b/Tests/APNSwiftTests/APNSwiftRequestTests.swift @@ -2,7 +2,7 @@ // // This source file is part of the APNSwift open source project // -// Copyright (c) 2019 the APNSwift project authors +// Copyright (c) 2022 the APNSwift project authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information @@ -17,20 +17,22 @@ import Foundation import NIO import NIOHTTP1 import NIOHTTP2 - import XCTest -@testable import APNSwift +@testable import APNSwift -final class APNSwiftRequestTests: XCTestCase { +final class APNSRequestTests: XCTestCase { func testAlertEncoding() throws { - let alert = APNSwiftAlert(title: "title", subtitle: "subtitle", body: "body", titleLocKey: "titlelockey", - titleLocArgs: ["titlelocarg1"], actionLocKey: "actionkey", locKey: "lockey", locArgs: ["locarg1"], launchImage: "launchImage") + let alert = APNSAlert( + title: "title", subtitle: "subtitle", body: "body", titleLocKey: "titlelockey", + titleLocArgs: ["titlelocarg1"], actionLocKey: "actionkey", locKey: "lockey", + locArgs: ["locarg1"], launchImage: "launchImage") let jsonData = try JSONEncoder().encode(alert) - let jsonDic = try JSONSerialization.jsonObject(with: jsonData, options: []) as? [String: Any] + let jsonDic = + try JSONSerialization.jsonObject(with: jsonData, options: []) as? [String: Any] let keys = jsonDic?.keys @@ -46,7 +48,6 @@ final class APNSwiftRequestTests: XCTestCase { XCTAssertTrue(keys?.contains("title-loc-key") ?? false) XCTAssertTrue(jsonDic?["title-loc-key"] is String) - XCTAssertTrue(keys?.contains("title-loc-args") ?? false) XCTAssertTrue(jsonDic?["title-loc-args"] is [String]) @@ -64,11 +65,12 @@ final class APNSwiftRequestTests: XCTestCase { } func testMinimalAlertEncoding() throws { - let alert = APNSwiftAlert(title: "title", body: "body") + let alert = APNSAlert(title: "title", body: "body") let jsonData = try JSONEncoder().encode(alert) - let jsonDic = try JSONSerialization.jsonObject(with: jsonData, options: []) as? [String: Any] + let jsonDic = + try JSONSerialization.jsonObject(with: jsonData, options: []) as? [String: Any] let keys = jsonDic?.keys @@ -86,38 +88,40 @@ final class APNSwiftRequestTests: XCTestCase { XCTAssertFalse(keys?.contains("loc-args") ?? false) XCTAssertFalse(keys?.contains("launch-image") ?? false) } - func testMinimalSwiftPayloadEncoding() throws { - let payload = APNSwiftPayload(alert: nil, sound: .normal("pong.wav")) + func testMinimalSwiftPayloadEncoding() throws { + let payload = APNSPayload(alert: nil, sound: .normal("pong.wav")) - let jsonData = try JSONEncoder().encode(payload) + let jsonData = try JSONEncoder().encode(payload) - let jsonDic = try JSONSerialization.jsonObject(with: jsonData, options: []) as? [String: Any] + let jsonDic = + try JSONSerialization.jsonObject(with: jsonData, options: []) as? [String: Any] - let keys = jsonDic?.keys + let keys = jsonDic?.keys - XCTAssertTrue(keys?.contains("sound") ?? false) - XCTAssertTrue(jsonDic?["sound"] is String) - XCTAssertTrue(jsonDic?["sound"] as! String == "pong.wav") + XCTAssertTrue(keys?.contains("sound") ?? false) + XCTAssertTrue(jsonDic?["sound"] is String) + XCTAssertTrue(jsonDic?["sound"] as! String == "pong.wav") - XCTAssertFalse(keys?.contains("title") ?? false) - XCTAssertFalse(keys?.contains("body") ?? false) - XCTAssertFalse(keys?.contains("subtitle") ?? false) - XCTAssertFalse(keys?.contains("title-loc-key") ?? false) - XCTAssertFalse(keys?.contains("title-loc-args") ?? false) - XCTAssertFalse(keys?.contains("action-loc-key") ?? false) - XCTAssertFalse(keys?.contains("loc-key") ?? false) - XCTAssertFalse(keys?.contains("loc-args") ?? false) - XCTAssertFalse(keys?.contains("launch-image") ?? false) - } + XCTAssertFalse(keys?.contains("title") ?? false) + XCTAssertFalse(keys?.contains("body") ?? false) + XCTAssertFalse(keys?.contains("subtitle") ?? false) + XCTAssertFalse(keys?.contains("title-loc-key") ?? false) + XCTAssertFalse(keys?.contains("title-loc-args") ?? false) + XCTAssertFalse(keys?.contains("action-loc-key") ?? false) + XCTAssertFalse(keys?.contains("loc-key") ?? false) + XCTAssertFalse(keys?.contains("loc-args") ?? false) + XCTAssertFalse(keys?.contains("launch-image") ?? false) + } - func testMinimalSwiftPayloadDecoding() throws { - let payload = APNSwiftPayload(alert: APNSwiftAlert(title: "title", body: "body"), sound: .normal("pong.wav")) + func testMinimalSwiftPayloadDecoding() throws { + let payload = APNSPayload( + alert: APNSAlert(title: "title", body: "body"), sound: .normal("pong.wav")) - let jsonData = try JSONEncoder().encode(payload) - let decodedPayload = try JSONDecoder().decode(APNSwiftPayload.self, from: jsonData) + let jsonData = try JSONEncoder().encode(payload) + let decodedPayload = try JSONDecoder().decode(APNSPayload.self, from: jsonData) - XCTAssertEqual(payload.alert?.title, decodedPayload.alert?.title) - XCTAssertEqual(payload.alert?.body, decodedPayload.alert?.body) - XCTAssertEqual(payload.sound, decodedPayload.sound) - } + XCTAssertEqual(payload.alert?.title, decodedPayload.alert?.title) + XCTAssertEqual(payload.alert?.body, decodedPayload.alert?.body) + XCTAssertEqual(payload.sound, decodedPayload.sound) + } } diff --git a/scripts/generate_contributors_list.sh b/scripts/generate_contributors_list.sh index cab513ee..73b22abc 100755 --- a/scripts/generate_contributors_list.sh +++ b/scripts/generate_contributors_list.sh @@ -3,7 +3,7 @@ ## ## This source file is part of the APNSwift open source project ## -## Copyright (c) 2019 the APNSwift project authors +## Copyright (c) 2022 the APNSwift project authors ## Licensed under Apache License v2.0 ## ## See LICENSE.txt for license information