diff --git a/.gitignore b/.gitignore index 3b29812..5922fda 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ DerivedData/ .swiftpm/config/registries.json .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata .netrc +Package.resolved diff --git a/Package.swift b/Package.swift index e286861..c427ce8 100644 --- a/Package.swift +++ b/Package.swift @@ -1,28 +1,28 @@ // swift-tools-version: 5.6 -// The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription let package = Package( name: "Push-iOS", + platforms: [ + .macOS(.v12), + .iOS(.v15), + ], products: [ - // Products define the executables and libraries a package produces, and make them visible to other packages. .library( - name: "Push-iOS", - targets: ["Push-iOS"]), + name: "Push", + targets: ["Push"]), ], dependencies: [ - // Dependencies declare other packages that this package depends on. - // .package(url: /* package url */, from: "1.0.0"), + .package(url: "https://christophhagen.de/git/ch/Push-API.git", branch: "main"), + .package(url: "https://github.com/apple/swift-crypto.git", "1.0.0" ..< "3.0.0") ], targets: [ - // Targets are the basic building blocks of a package. A target can define a module or a test suite. - // Targets can depend on other targets in this package, and on products in packages this package depends on. .target( - name: "Push-iOS", - dependencies: []), - .testTarget( - name: "Push-iOSTests", - dependencies: ["Push-iOS"]), + name: "Push", + dependencies: [ + .product(name: "PushAPI", package: "Push-API"), + .product(name: "Crypto", package: "swift-crypto") + ]), ] ) diff --git a/Sources/Push-iOS/Push_iOS.swift b/Sources/Push-iOS/Push_iOS.swift deleted file mode 100644 index a532e71..0000000 --- a/Sources/Push-iOS/Push_iOS.swift +++ /dev/null @@ -1,6 +0,0 @@ -public struct Push_iOS { - public private(set) var text = "Hello, World!" - - public init() { - } -} diff --git a/Sources/Push/PushClient.swift b/Sources/Push/PushClient.swift new file mode 100644 index 0000000..dd3ca2c --- /dev/null +++ b/Sources/Push/PushClient.swift @@ -0,0 +1,162 @@ +import Foundation +import PushAPI +import SwiftUI + +#if canImport(CryptoKit) +import CryptoKit +#else +import Crypto +#endif + +/** + A client to interact with a push server. + */ +public final class PushClient { + + /** + The bas url to reach the push server + */ + public let server: URL + + /** + The application id of the service for which this client is used. + */ + public let application: ApplicationId + + private static let encoder = JSONEncoder() + private static let decoder = JSONDecoder() + + /** + Create a new client. + - Parameter server: The base url of the push server. + - Parameter application: The id of the application + */ + public init(server: URL, application: ApplicationId) { + self.server = server + self.application = application + } + + /** + Register the device with the push server. + - Parameter token: The APNs token of the device + - Parameter name: The optional device name for easier identification + - Returns: The new authentication token associated with the device + - Note: Depending on the application, the device must be approved by an administrator. + */ + public func register(token: PushToken, name: String? = nil) async -> AuthenticationToken? { + let device = DeviceRegistration( + pushToken: token, + application: application, + name: name ?? "") + return await post(.registerNewDevice, body: device) + } + + /** + Get the list of all devices in the same application. + - Parameter pushToken: The APNs token of the registered device. + - Parameter authToken: The authentication token of the device. + - Returns: The list of registered and approved devices in the application + */ + public func getDeviceList(pushToken: PushToken, authToken: AuthenticationToken) async -> [DeviceRegistration]? { + let device = DeviceAuthentication(pushToken: pushToken, authentication: authToken) + guard let data = await post(.listDevicesInApplication, body: device) else { + return nil + } + do { + return try PushClient.decoder.decode([DeviceRegistration].self, from: data) + } catch { + return nil + } + } + + /** + Get a list of unapproved devices for an administrator key. + - Parameter masterKey: The master key of the administrator + - Returns: A list of registered devices which need to be approved or rejected. + */ + public func getUnapprovedDevices(masterKey: String) async -> [DeviceRegistration]? { + let hash = hash(masterKey) + guard let data = await post(.listUnapprovedDevices, bodyData: hash) else { + return nil + } + do { + return try PushClient.decoder.decode([DeviceRegistration].self, from: data) + } catch { + return nil + } + } + + /** + Approve a device using the administrator key. + - Parameter pushToken: The push token of the approved device. + - Parameter masterKey: The administrator master key. + - Returns: `true`, if the device has been approved + */ + public func approve(pushToken: PushToken, with masterKey: String) async -> Bool { + let hash = hash(masterKey) + let device = DeviceDecision(pushToken: pushToken, masterKeyHash: hash) + return await post(.approveDevice, body: device) != nil + } + + /** + Reject a device using the administrator key. + - Parameter pushToken: The push token of the rejected device. + - Parameter masterKey: The administrator master key. + - Returns: `true`, if the device has been rejected + */ + public func reject(pushToken: PushToken, with masterKey: String) async -> Bool { + let hash = hash(masterKey) + let device = DeviceDecision(pushToken: pushToken, masterKeyHash: hash) + return await post(.rejectDevice, body: device) != nil + } + + /** + Check if the device has been approved. + - Parameter token: The APNs token of the device. + - Parameter authToken: The authentication token of the device. + - Returns: `true`, if the device is approved, `false` on error + */ + public func isConfirmed(token: PushToken, authentication: AuthenticationToken) async -> Bool { + let device = DeviceAuthentication(pushToken: token, authentication: authentication) + return await post(.isDeviceApproved, body: device) != nil + } + + /** + Send a push notification. + - Parameter message: The push message to send + - Returns: `true` if the message was sent + */ + public func send(push message: AuthenticatedPushMessage) async -> Bool { + await post(.sendPushNotification, body: message) != nil + } + + private func post(_ route: Route, body: T) async -> Data? where T: Encodable { + let bodyData = try! PushClient.encoder.encode(body) + return await post(route, bodyData: bodyData) + } + + private func post(_ route: Route, bodyData: Data) async -> Data? { + var request = URLRequest(url: server.appendingPathComponent(route.rawValue)) + request.httpBody = bodyData + request.httpMethod = "POST" + do { + let (data, response) = try await URLSession.shared.data(for: request) + guard let httpResponse = response as? HTTPURLResponse else { + return nil + } + guard httpResponse.statusCode == 200 else { + print("Failed with code: \(httpResponse.statusCode)") + return nil + } + return data + } catch { + print("Failed with error: \(error)") + return nil + } + } + + private func hash(_ masterKey: String) -> Data { + Data(SHA256.hash(data: masterKey.data(using: .utf8)!)) + } + +}