From 08796542cc8aa1960823f17d1b57d10899acecc8 Mon Sep 17 00:00:00 2001 From: Graham Burgsma Date: Mon, 10 Aug 2020 15:18:17 -0400 Subject: [PATCH 1/3] Use Vapor client instead to get helpers and reduce lower level work --- Sources/FCM/FCM.swift | 4 +- Sources/FCM/Helpers/FCM+AccessToken.swift | 50 ++-- Sources/FCM/Helpers/FCM+BatchSend.swift | 128 ++++----- Sources/FCM/Helpers/FCM+CreateTopic.swift | 124 ++++----- Sources/FCM/Helpers/FCM+DeleteTopic.swift | 120 ++++----- Sources/FCM/Helpers/FCM+RegisterAPNS.swift | 300 ++++++++++----------- Sources/FCM/Helpers/FCM+SendMessage.swift | 54 ++-- 7 files changed, 387 insertions(+), 393 deletions(-) diff --git a/Sources/FCM/FCM.swift b/Sources/FCM/FCM.swift index 3867c4d..1ab576e 100644 --- a/Sources/FCM/FCM.swift +++ b/Sources/FCM/FCM.swift @@ -7,7 +7,7 @@ import JWT public struct FCM { let application: Application - let client: HTTPClient + let client: Client let scope = "https://www.googleapis.com/auth/cloud-platform" let audience = "https://www.googleapis.com/oauth2/v4/token" @@ -38,7 +38,7 @@ public struct FCM { if !application.http.client.configuration.ignoreUncleanSSLShutdown { application.http.client.configuration.ignoreUncleanSSLShutdown = true } - self.client = application.http.client.shared + self.client = application.client } } diff --git a/Sources/FCM/Helpers/FCM+AccessToken.swift b/Sources/FCM/Helpers/FCM+AccessToken.swift index 06fb2f0..40aaa43 100644 --- a/Sources/FCM/Helpers/FCM+AccessToken.swift +++ b/Sources/FCM/Helpers/FCM+AccessToken.swift @@ -7,36 +7,30 @@ extension FCM { fatalError("FCM gAuth can't be nil") } if !gAuth.hasExpired, let token = accessToken { - return client.eventLoopGroup.future(token) + return client.eventLoop.future(token) } - return application.eventLoopGroup.future(()).flatMapThrowing { _ throws -> Data in - var payload: [String: String] = [:] - payload["grant_type"] = "urn:ietf:params:oauth:grant-type:jwt-bearer" - payload["assertion"] = try self.getJWT() - return try JSONEncoder().encode(payload) - }.flatMapThrowing { data -> HTTPClient.Request in - var headers = HTTPHeaders() - headers.add(name: "Content-Type", value: "application/json") - return try HTTPClient.Request(url: self.audience, method: .POST, headers: headers, body: .data(data)) - }.flatMap { request in - return self.client.execute(request: request).flatMapThrowing { res throws -> String in - guard let body = res.body, let data = body.getData(at: body.readerIndex, length: body.readableBytes) else { - throw Abort(.notFound, reason: "Data not found") - } - if res.status.code != 200 { - let code = "Code: \(res.status.code)" - let message = "Message: \(String(data: data, encoding: .utf8) ?? "n/a"))" - let reason = "[FCM] Unable to refresh access token. \(code) \(message)" - throw Abort(.internalServerError, reason: reason) - } - struct Result: Codable { - var access_token: String - } - guard let result = try? JSONDecoder().decode(Result.self, from: data) else { - throw Abort(.notFound, reason: "Data not found") - } - return result.access_token + + return client.post(URI(string: audience)) { (req) in + try req.content.encode([ + "grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer", + "assertion": try self.getJWT(), + ]) + } + .flatMapThrowing { res throws -> String in + if res.status.code != 200 { + let code = "Code: \(res.status.code)" + let message = "Message: \(res.content))" + let reason = "[FCM] Unable to refresh access token. \(code) \(message)" + throw Abort(.internalServerError, reason: reason) + } + + struct Result: Codable { + var access_token: String + } + guard let result = try? res.content.decode(Result.self) else { + throw Abort(.notFound, reason: "Data not found") } + return result.access_token } } } diff --git a/Sources/FCM/Helpers/FCM+BatchSend.swift b/Sources/FCM/Helpers/FCM+BatchSend.swift index d366722..1144dd7 100644 --- a/Sources/FCM/Helpers/FCM+BatchSend.swift +++ b/Sources/FCM/Helpers/FCM+BatchSend.swift @@ -1,67 +1,67 @@ import Foundation import Vapor -extension FCM { - public func batchSend(_ message: FCMMessageDefault, tokens: String...) -> EventLoopFuture<[String]> { - _send(message, tokens: tokens) - } - - public func batchSend(_ message: FCMMessageDefault, tokens: String..., on eventLoop: EventLoop) -> EventLoopFuture<[String]> { - _send(message, tokens: tokens).hop(to: eventLoop) - } - - public func batchSend(_ message: FCMMessageDefault, tokens: [String]) -> EventLoopFuture<[String]> { - _send(message, tokens: tokens) - } - - public func batchSend(_ message: FCMMessageDefault, tokens: [String], on eventLoop: EventLoop) -> EventLoopFuture<[String]> { - _send(message, tokens: tokens).hop(to: eventLoop) - } - - private func _send(_ message: FCMMessageDefault, tokens: [String]) -> EventLoopFuture<[String]> { - if message.apns == nil, - let apnsDefaultConfig = apnsDefaultConfig { - message.apns = apnsDefaultConfig - } - if message.android == nil, - let androidDefaultConfig = androidDefaultConfig { - message.android = androidDefaultConfig - } - if message.webpush == nil, - let webpushDefaultConfig = webpushDefaultConfig { - message.webpush = webpushDefaultConfig - } - var preparedTokens: [[String]] = [] - tokens.enumerated().forEach { i, token in - if Double(i).truncatingRemainder(dividingBy: 20) == 0 { - preparedTokens.append([token]) - } else { - if var arr = preparedTokens.popLast() { - arr.append(token) - preparedTokens.append(arr) - } else { - preparedTokens.append([token]) - } - } - } - var deviceGroups: [String: [String]] = [:] - return preparedTokens.map { tokens in - createTopic(tokens: tokens).map { - deviceGroups[$0] = tokens - } - }.flatten(on: application.eventLoopGroup.next()).flatMap { - var results: [String] = [] - return deviceGroups.map { deviceGroup in - let message = message - message.token = nil - message.condition = nil - message.topic = deviceGroup.key - return self.send(message).map { - results.append($0) - }.flatMap { - self.deleteTopic(deviceGroup.key, tokens: deviceGroup.value) - } - }.flatten(on: self.application.eventLoopGroup.next()).transform(to: results) - } - } -} +//extension FCM { +// public func batchSend(_ message: FCMMessageDefault, tokens: String...) -> EventLoopFuture<[String]> { +// _send(message, tokens: tokens) +// } +// +// public func batchSend(_ message: FCMMessageDefault, tokens: String..., on eventLoop: EventLoop) -> EventLoopFuture<[String]> { +// _send(message, tokens: tokens).hop(to: eventLoop) +// } +// +// public func batchSend(_ message: FCMMessageDefault, tokens: [String]) -> EventLoopFuture<[String]> { +// _send(message, tokens: tokens) +// } +// +// public func batchSend(_ message: FCMMessageDefault, tokens: [String], on eventLoop: EventLoop) -> EventLoopFuture<[String]> { +// _send(message, tokens: tokens).hop(to: eventLoop) +// } +// +// private func _send(_ message: FCMMessageDefault, tokens: [String]) -> EventLoopFuture<[String]> { +// if message.apns == nil, +// let apnsDefaultConfig = apnsDefaultConfig { +// message.apns = apnsDefaultConfig +// } +// if message.android == nil, +// let androidDefaultConfig = androidDefaultConfig { +// message.android = androidDefaultConfig +// } +// if message.webpush == nil, +// let webpushDefaultConfig = webpushDefaultConfig { +// message.webpush = webpushDefaultConfig +// } +// var preparedTokens: [[String]] = [] +// tokens.enumerated().forEach { i, token in +// if Double(i).truncatingRemainder(dividingBy: 20) == 0 { +// preparedTokens.append([token]) +// } else { +// if var arr = preparedTokens.popLast() { +// arr.append(token) +// preparedTokens.append(arr) +// } else { +// preparedTokens.append([token]) +// } +// } +// } +// var deviceGroups: [String: [String]] = [:] +// return preparedTokens.map { tokens in +// createTopic(tokens: tokens).map { +// deviceGroups[$0] = tokens +// } +// }.flatten(on: application.eventLoopGroup.next()).flatMap { +// var results: [String] = [] +// return deviceGroups.map { deviceGroup in +// let message = message +// message.token = nil +// message.condition = nil +// message.topic = deviceGroup.key +// return self.send(message).map { +// results.append($0) +// }.flatMap { +// self.deleteTopic(deviceGroup.key, tokens: deviceGroup.value) +// } +// }.flatten(on: self.application.eventLoopGroup.next()).transform(to: results) +// } +// } +//} diff --git a/Sources/FCM/Helpers/FCM+CreateTopic.swift b/Sources/FCM/Helpers/FCM+CreateTopic.swift index 9015e02..cdc3361 100644 --- a/Sources/FCM/Helpers/FCM+CreateTopic.swift +++ b/Sources/FCM/Helpers/FCM+CreateTopic.swift @@ -1,65 +1,65 @@ import Foundation import Vapor -extension FCM { - public func createTopic(_ name: String? = nil, tokens: String...) -> EventLoopFuture { - createTopic(name, tokens: tokens) - } - - public func createTopic(_ name: String? = nil, tokens: String..., on eventLoop: EventLoop) -> EventLoopFuture { - createTopic(name, tokens: tokens).hop(to: eventLoop) - } - - public func createTopic(_ name: String? = nil, tokens: [String]) -> EventLoopFuture { - _createTopic(name, tokens: tokens) - } - - public func createTopic(_ name: String? = nil, tokens: [String], on eventLoop: EventLoop) -> EventLoopFuture { - _createTopic(name, tokens: tokens).hop(to: eventLoop) - } - - private func _createTopic(_ name: String? = nil, tokens: [String]) -> EventLoopFuture { - guard let configuration = self.configuration else { - fatalError("FCM not configured. Use app.fcm.configuration = ...") - } - guard let serverKey = configuration.serverKey else { - fatalError("FCM: CreateTopic: Server Key is missing.") - } - let url = self.iidURL + "batchAdd" - let name = name ?? UUID().uuidString - return getAccessToken().flatMapThrowing { accessToken throws -> HTTPClient.Request in - struct Payload: Codable { - let to: String - let registration_tokens: [String] - - init (to: String, registration_tokens: [String]) { - self.to = "/topics/\(to)" - self.registration_tokens = registration_tokens - } - } - let payload = Payload(to: name, registration_tokens: tokens) - let payloadData = try JSONEncoder().encode(payload) - var headers = HTTPHeaders() - headers.add(name: "Authorization", value: "key=\(serverKey)") - headers.add(name: "Content-Type", value: "application/json") - return try .init(url: url, method: .POST, headers: headers, body: .data(payloadData)) - }.flatMap { request in - return self.client.execute(request: request).flatMapThrowing { res in - guard 200 ..< 300 ~= res.status.code else { - if let body = res.body, let googleError = try? JSONDecoder().decode(GoogleError.self, from: body) { - throw googleError - } else { - guard - let bb = res.body, - let bytes = bb.getBytes(at: 0, length: bb.readableBytes), - let reason = String(bytes: bytes, encoding: .utf8) else { - throw Abort(.internalServerError, reason: "FCM: CreateTopic: unable to decode error response") - } - throw Abort(.internalServerError, reason: reason) - } - } - return name - } - } - } -} +//extension FCM { +// public func createTopic(_ name: String? = nil, tokens: String...) -> EventLoopFuture { +// createTopic(name, tokens: tokens) +// } +// +// public func createTopic(_ name: String? = nil, tokens: String..., on eventLoop: EventLoop) -> EventLoopFuture { +// createTopic(name, tokens: tokens).hop(to: eventLoop) +// } +// +// public func createTopic(_ name: String? = nil, tokens: [String]) -> EventLoopFuture { +// _createTopic(name, tokens: tokens) +// } +// +// public func createTopic(_ name: String? = nil, tokens: [String], on eventLoop: EventLoop) -> EventLoopFuture { +// _createTopic(name, tokens: tokens).hop(to: eventLoop) +// } +// +// private func _createTopic(_ name: String? = nil, tokens: [String]) -> EventLoopFuture { +// guard let configuration = self.configuration else { +// fatalError("FCM not configured. Use app.fcm.configuration = ...") +// } +// guard let serverKey = configuration.serverKey else { +// fatalError("FCM: CreateTopic: Server Key is missing.") +// } +// let url = self.iidURL + "batchAdd" +// let name = name ?? UUID().uuidString +// return getAccessToken().flatMapThrowing { accessToken throws -> HTTPClient.Request in +// struct Payload: Codable { +// let to: String +// let registration_tokens: [String] +// +// init (to: String, registration_tokens: [String]) { +// self.to = "/topics/\(to)" +// self.registration_tokens = registration_tokens +// } +// } +// let payload = Payload(to: name, registration_tokens: tokens) +// let payloadData = try JSONEncoder().encode(payload) +// var headers = HTTPHeaders() +// headers.add(name: "Authorization", value: "key=\(serverKey)") +// headers.add(name: "Content-Type", value: "application/json") +// return try .init(url: url, method: .POST, headers: headers, body: .data(payloadData)) +// }.flatMap { request in +// return self.client.execute(request: request).flatMapThrowing { res in +// guard 200 ..< 300 ~= res.status.code else { +// if let body = res.body, let googleError = try? JSONDecoder().decode(GoogleError.self, from: body) { +// throw googleError +// } else { +// guard +// let bb = res.body, +// let bytes = bb.getBytes(at: 0, length: bb.readableBytes), +// let reason = String(bytes: bytes, encoding: .utf8) else { +// throw Abort(.internalServerError, reason: "FCM: CreateTopic: unable to decode error response") +// } +// throw Abort(.internalServerError, reason: reason) +// } +// } +// return name +// } +// } +// } +//} diff --git a/Sources/FCM/Helpers/FCM+DeleteTopic.swift b/Sources/FCM/Helpers/FCM+DeleteTopic.swift index e3a95bd..6ad0435 100644 --- a/Sources/FCM/Helpers/FCM+DeleteTopic.swift +++ b/Sources/FCM/Helpers/FCM+DeleteTopic.swift @@ -1,63 +1,63 @@ import Foundation import Vapor -extension FCM { - public func deleteTopic(_ name: String, tokens: String...) -> EventLoopFuture { - deleteTopic(name, tokens: tokens) - } - - public func deleteTopic(_ name: String, tokens: String..., on eventLoop: EventLoop) -> EventLoopFuture { - deleteTopic(name, tokens: tokens).hop(to: eventLoop) - } - - public func deleteTopic(_ name: String, tokens: [String]) -> EventLoopFuture { - _deleteTopic(name, tokens: tokens) - } - - public func deleteTopic(_ name: String, tokens: [String], on eventLoop: EventLoop) -> EventLoopFuture { - _deleteTopic(name, tokens: tokens).hop(to: eventLoop) - } - - private func _deleteTopic(_ name: String, tokens: [String]) -> EventLoopFuture { - guard let configuration = self.configuration else { - fatalError("FCM not configured. Use app.fcm.configuration = ...") - } - guard let serverKey = configuration.serverKey else { - fatalError("FCM: DeleteTopic: Server Key is missing.") - } - let url = self.iidURL + "batchRemove" - return getAccessToken().flatMapThrowing { accessToken throws -> HTTPClient.Request in - struct Payload: Codable { - let to: String - let registration_tokens: [String] - - init (to: String, registration_tokens: [String]) { - self.to = "/topics/\(to)" - self.registration_tokens = registration_tokens - } - } - let payload = Payload(to: name, registration_tokens: tokens) - let payloadData = try JSONEncoder().encode(payload) - var headers = HTTPHeaders() - headers.add(name: "Authorization", value: "key=\(serverKey)") - headers.add(name: "Content-Type", value: "application/json") - return try .init(url: url, method: .POST, headers: headers, body: .data(payloadData)) - }.flatMap { request in - return self.client.execute(request: request).flatMapThrowing { res in - guard 200 ..< 300 ~= res.status.code else { - if let body = res.body, let googleError = try? JSONDecoder().decode(GoogleError.self, from: body) { - throw googleError - } else { - guard - let bb = res.body, - let bytes = bb.getBytes(at: 0, length: bb.readableBytes), - let reason = String(bytes: bytes, encoding: .utf8) else { - throw Abort(.internalServerError, reason: "FCM: DeleteTopic: unable to decode error response") - } - throw Abort(.internalServerError, reason: reason) - } - } - } - } - } -} +//extension FCM { +// public func deleteTopic(_ name: String, tokens: String...) -> EventLoopFuture { +// deleteTopic(name, tokens: tokens) +// } +// +// public func deleteTopic(_ name: String, tokens: String..., on eventLoop: EventLoop) -> EventLoopFuture { +// deleteTopic(name, tokens: tokens).hop(to: eventLoop) +// } +// +// public func deleteTopic(_ name: String, tokens: [String]) -> EventLoopFuture { +// _deleteTopic(name, tokens: tokens) +// } +// +// public func deleteTopic(_ name: String, tokens: [String], on eventLoop: EventLoop) -> EventLoopFuture { +// _deleteTopic(name, tokens: tokens).hop(to: eventLoop) +// } +// +// private func _deleteTopic(_ name: String, tokens: [String]) -> EventLoopFuture { +// guard let configuration = self.configuration else { +// fatalError("FCM not configured. Use app.fcm.configuration = ...") +// } +// guard let serverKey = configuration.serverKey else { +// fatalError("FCM: DeleteTopic: Server Key is missing.") +// } +// let url = self.iidURL + "batchRemove" +// return getAccessToken().flatMapThrowing { accessToken throws -> HTTPClient.Request in +// struct Payload: Codable { +// let to: String +// let registration_tokens: [String] +// +// init (to: String, registration_tokens: [String]) { +// self.to = "/topics/\(to)" +// self.registration_tokens = registration_tokens +// } +// } +// let payload = Payload(to: name, registration_tokens: tokens) +// let payloadData = try JSONEncoder().encode(payload) +// var headers = HTTPHeaders() +// headers.add(name: "Authorization", value: "key=\(serverKey)") +// headers.add(name: "Content-Type", value: "application/json") +// return try .init(url: url, method: .POST, headers: headers, body: .data(payloadData)) +// }.flatMap { request in +// return self.client.execute(request: request).flatMapThrowing { res in +// guard 200 ..< 300 ~= res.status.code else { +// if let body = res.body, let googleError = try? JSONDecoder().decode(GoogleError.self, from: body) { +// throw googleError +// } else { +// guard +// let bb = res.body, +// let bytes = bb.getBytes(at: 0, length: bb.readableBytes), +// let reason = String(bytes: bytes, encoding: .utf8) else { +// throw Abort(.internalServerError, reason: "FCM: DeleteTopic: unable to decode error response") +// } +// throw Abort(.internalServerError, reason: reason) +// } +// } +// } +// } +// } +//} diff --git a/Sources/FCM/Helpers/FCM+RegisterAPNS.swift b/Sources/FCM/Helpers/FCM+RegisterAPNS.swift index cfa2063..5b213e5 100644 --- a/Sources/FCM/Helpers/FCM+RegisterAPNS.swift +++ b/Sources/FCM/Helpers/FCM+RegisterAPNS.swift @@ -1,153 +1,153 @@ import Foundation import Vapor -public struct RegisterAPNSID { - let appBundleId: String - let serverKey: String? - let sandbox: Bool - - public init (appBundleId: String, serverKey: String? = nil, sandbox: Bool = false) { - self.appBundleId = appBundleId - self.serverKey = serverKey - self.sandbox = sandbox - } -} - -extension RegisterAPNSID { - public static var env: RegisterAPNSID { - guard let appBundleId = Environment.get("FCM_APP_BUNDLE_ID") else { - fatalError("FCM: Register APNS: missing FCM_APP_BUNDLE_ID environment variable") - } - return .init(appBundleId: appBundleId) - } -} - -extension RegisterAPNSID { - public static var envSandbox: RegisterAPNSID { - let id: RegisterAPNSID = .env - return .init(appBundleId: id.appBundleId, sandbox: true) - } -} - -public struct APNSToFirebaseToken { - public let registration_token, apns_token: String - public let isRegistered: Bool -} - -extension FCM { - /// Helper method which registers your pure APNS token in Firebase Cloud Messaging - /// and returns firebase tokens for each APNS token - /// - /// Convenient way - /// - /// Declare `RegisterAPNSID` via extension - /// ```swift - /// extension RegisterAPNSID { - /// static var myApp: RegisterAPNSID { .init(appBundleId: "com.myapp") } - /// } - /// ``` - /// - public func registerAPNS( - _ id: RegisterAPNSID, - tokens: String..., - on eventLoop: EventLoop? = nil) -> EventLoopFuture<[APNSToFirebaseToken]> { - registerAPNS(appBundleId: id.appBundleId, serverKey: id.serverKey, sandbox: id.sandbox, tokens: tokens, on: eventLoop) - } - - /// Helper method which registers your pure APNS token in Firebase Cloud Messaging - /// and returns firebase tokens for each APNS token - /// - /// Convenient way - /// - /// Declare `RegisterAPNSID` via extension - /// ```swift - /// extension RegisterAPNSID { - /// static var myApp: RegisterAPNSID { .init(appBundleId: "com.myapp") } - /// } - /// ``` - /// - public func registerAPNS( - _ id: RegisterAPNSID, - tokens: [String], - on eventLoop: EventLoop? = nil) -> EventLoopFuture<[APNSToFirebaseToken]> { - registerAPNS(appBundleId: id.appBundleId, serverKey: id.serverKey, sandbox: id.sandbox, tokens: tokens, on: eventLoop) - } - - /// Helper method which registers your pure APNS token in Firebase Cloud Messaging - /// and returns firebase tokens for each APNS token - public func registerAPNS( - appBundleId: String, - serverKey: String? = nil, - sandbox: Bool = false, - tokens: String..., - on eventLoop: EventLoop? = nil) -> EventLoopFuture<[APNSToFirebaseToken]> { - registerAPNS(appBundleId: appBundleId, serverKey: serverKey, sandbox: sandbox, tokens: tokens, on: eventLoop) - } - - /// Helper method which registers your pure APNS token in Firebase Cloud Messaging - /// and returns firebase tokens for each APNS token - public func registerAPNS( - appBundleId: String, - serverKey: String? = nil, - sandbox: Bool = false, - tokens: [String], - on eventLoop: EventLoop? = nil) -> EventLoopFuture<[APNSToFirebaseToken]> { - let eventLoop = eventLoop ?? application.eventLoopGroup.next() - guard tokens.count <= 100 else { - return eventLoop.makeFailedFuture(Abort(.internalServerError, reason: "FCM: Register APNS: tokens count should be less or equeal 100")) - } - guard tokens.count > 0 else { - return eventLoop.future([]) - } - guard let configuration = self.configuration else { - #if DEBUG - fatalError("FCM not configured. Use app.fcm.configuration = ...") - #else - return eventLoop.future([]) - #endif - } - guard let serverKey = serverKey ?? configuration.serverKey else { - fatalError("FCM: Register APNS: Server Key is missing.") - } - let url = iidURL + "batchImport" - return eventLoop.future().flatMapThrowing { accessToken throws -> HTTPClient.Request in - struct Payload: Codable { - let application: String - let sandbox: Bool - let apns_tokens: [String] - } - let payload = Payload(application: appBundleId, sandbox: sandbox, apns_tokens: tokens) - let payloadData = try JSONEncoder().encode(payload) - - var headers = HTTPHeaders() - headers.add(name: "Authorization", value: "key=\(serverKey)") - headers.add(name: "Content-Type", value: "application/json") - - return try .init(url: url, method: .POST, headers: headers, body: .data(payloadData)) - }.flatMap { request in - return self.client.execute(request: request).flatMapThrowing { res in - guard 200 ..< 300 ~= res.status.code else { - guard - let bb = res.body, - let bytes = bb.getBytes(at: 0, length: bb.readableBytes), - let reason = String(bytes: bytes, encoding: .utf8) else { - throw Abort(.internalServerError, reason: "FCM: Register APNS: unable to decode error response") - } - throw Abort(.internalServerError, reason: reason) - } - struct Result: Codable { - struct Result: Codable { - let registration_token, apns_token, status: String - } - var results: [Result] - } - guard let body = res.body, let result = try? JSONDecoder().decode(Result.self, from: body) else { - throw Abort(.notFound, reason: "FCM: Register APNS: empty response") - } - return result.results.map { - .init(registration_token: $0.registration_token, apns_token: $0.apns_token, isRegistered: $0.status == "OK") - } - } - } - } -} +//public struct RegisterAPNSID { +// let appBundleId: String +// let serverKey: String? +// let sandbox: Bool +// +// public init (appBundleId: String, serverKey: String? = nil, sandbox: Bool = false) { +// self.appBundleId = appBundleId +// self.serverKey = serverKey +// self.sandbox = sandbox +// } +//} +// +//extension RegisterAPNSID { +// public static var env: RegisterAPNSID { +// guard let appBundleId = Environment.get("FCM_APP_BUNDLE_ID") else { +// fatalError("FCM: Register APNS: missing FCM_APP_BUNDLE_ID environment variable") +// } +// return .init(appBundleId: appBundleId) +// } +//} +// +//extension RegisterAPNSID { +// public static var envSandbox: RegisterAPNSID { +// let id: RegisterAPNSID = .env +// return .init(appBundleId: id.appBundleId, sandbox: true) +// } +//} +// +//public struct APNSToFirebaseToken { +// public let registration_token, apns_token: String +// public let isRegistered: Bool +//} +// +//extension FCM { +// /// Helper method which registers your pure APNS token in Firebase Cloud Messaging +// /// and returns firebase tokens for each APNS token +// /// +// /// Convenient way +// /// +// /// Declare `RegisterAPNSID` via extension +// /// ```swift +// /// extension RegisterAPNSID { +// /// static var myApp: RegisterAPNSID { .init(appBundleId: "com.myapp") } +// /// } +// /// ``` +// /// +// public func registerAPNS( +// _ id: RegisterAPNSID, +// tokens: String..., +// on eventLoop: EventLoop? = nil) -> EventLoopFuture<[APNSToFirebaseToken]> { +// registerAPNS(appBundleId: id.appBundleId, serverKey: id.serverKey, sandbox: id.sandbox, tokens: tokens, on: eventLoop) +// } +// +// /// Helper method which registers your pure APNS token in Firebase Cloud Messaging +// /// and returns firebase tokens for each APNS token +// /// +// /// Convenient way +// /// +// /// Declare `RegisterAPNSID` via extension +// /// ```swift +// /// extension RegisterAPNSID { +// /// static var myApp: RegisterAPNSID { .init(appBundleId: "com.myapp") } +// /// } +// /// ``` +// /// +// public func registerAPNS( +// _ id: RegisterAPNSID, +// tokens: [String], +// on eventLoop: EventLoop? = nil) -> EventLoopFuture<[APNSToFirebaseToken]> { +// registerAPNS(appBundleId: id.appBundleId, serverKey: id.serverKey, sandbox: id.sandbox, tokens: tokens, on: eventLoop) +// } +// +// /// Helper method which registers your pure APNS token in Firebase Cloud Messaging +// /// and returns firebase tokens for each APNS token +// public func registerAPNS( +// appBundleId: String, +// serverKey: String? = nil, +// sandbox: Bool = false, +// tokens: String..., +// on eventLoop: EventLoop? = nil) -> EventLoopFuture<[APNSToFirebaseToken]> { +// registerAPNS(appBundleId: appBundleId, serverKey: serverKey, sandbox: sandbox, tokens: tokens, on: eventLoop) +// } +// +// /// Helper method which registers your pure APNS token in Firebase Cloud Messaging +// /// and returns firebase tokens for each APNS token +// public func registerAPNS( +// appBundleId: String, +// serverKey: String? = nil, +// sandbox: Bool = false, +// tokens: [String], +// on eventLoop: EventLoop? = nil) -> EventLoopFuture<[APNSToFirebaseToken]> { +// let eventLoop = eventLoop ?? application.eventLoopGroup.next() +// guard tokens.count <= 100 else { +// return eventLoop.makeFailedFuture(Abort(.internalServerError, reason: "FCM: Register APNS: tokens count should be less or equeal 100")) +// } +// guard tokens.count > 0 else { +// return eventLoop.future([]) +// } +// guard let configuration = self.configuration else { +// #if DEBUG +// fatalError("FCM not configured. Use app.fcm.configuration = ...") +// #else +// return eventLoop.future([]) +// #endif +// } +// guard let serverKey = serverKey ?? configuration.serverKey else { +// fatalError("FCM: Register APNS: Server Key is missing.") +// } +// let url = iidURL + "batchImport" +// return eventLoop.future().flatMapThrowing { accessToken throws -> HTTPClient.Request in +// struct Payload: Codable { +// let application: String +// let sandbox: Bool +// let apns_tokens: [String] +// } +// let payload = Payload(application: appBundleId, sandbox: sandbox, apns_tokens: tokens) +// let payloadData = try JSONEncoder().encode(payload) +// +// var headers = HTTPHeaders() +// headers.add(name: "Authorization", value: "key=\(serverKey)") +// headers.add(name: "Content-Type", value: "application/json") +// +// return try .init(url: url, method: .POST, headers: headers, body: .data(payloadData)) +// }.flatMap { request in +// return self.client.execute(request: request).flatMapThrowing { res in +// guard 200 ..< 300 ~= res.status.code else { +// guard +// let bb = res.body, +// let bytes = bb.getBytes(at: 0, length: bb.readableBytes), +// let reason = String(bytes: bytes, encoding: .utf8) else { +// throw Abort(.internalServerError, reason: "FCM: Register APNS: unable to decode error response") +// } +// throw Abort(.internalServerError, reason: reason) +// } +// struct Result: Codable { +// struct Result: Codable { +// let registration_token, apns_token, status: String +// } +// var results: [Result] +// } +// guard let body = res.body, let result = try? JSONDecoder().decode(Result.self, from: body) else { +// throw Abort(.notFound, reason: "FCM: Register APNS: empty response") +// } +// return result.results.map { +// .init(registration_token: $0.registration_token, apns_token: $0.apns_token, isRegistered: $0.status == "OK") +// } +// } +// } +// } +//} diff --git a/Sources/FCM/Helpers/FCM+SendMessage.swift b/Sources/FCM/Helpers/FCM+SendMessage.swift index c149dbc..c9ea8cd 100644 --- a/Sources/FCM/Helpers/FCM+SendMessage.swift +++ b/Sources/FCM/Helpers/FCM+SendMessage.swift @@ -27,37 +27,37 @@ extension FCM { message.webpush = webpushDefaultConfig } let url = actionsBaseURL + configuration.projectId + "/messages:send" - return getAccessToken().flatMapThrowing { accessToken throws -> HTTPClient.Request in - struct Payload: Codable { - let validate_only: Bool + return getAccessToken().flatMap { accessToken -> EventLoopFuture in + var headers = HTTPHeaders() + headers.bearerAuthorization = .init(token: accessToken) + + struct Payload: Content { + let validate_only: Bool = false let message: FCMMessageDefault } - let payload = Payload(validate_only: false, message: message) - let payloadData = try JSONEncoder().encode(payload) - - var headers = HTTPHeaders() - headers.add(name: "Authorization", value: "Bearer \(accessToken)") - headers.add(name: "Content-Type", value: "application/json") - - return try .init(url: url, method: .POST, headers: headers, body: .data(payloadData)) - }.flatMap { request in - return self.client.execute(request: request).flatMapThrowing { res in - guard 200 ..< 300 ~= res.status.code else { - if let body = res.body, let googleError = try? JSONDecoder().decode(GoogleError.self, from: body) { - throw googleError - } else { - let reason = res.body?.debugDescription ?? "Unable to decode Firebase response" - throw Abort(.internalServerError, reason: reason) - } - } - struct Result: Codable { - var name: String - } - guard let body = res.body, let result = try? JSONDecoder().decode(Result.self, from: body) else { - throw Abort(.notFound, reason: "Data not found") + let payload = Payload(message: message) + + return self.client.post(URI(string: url), headers: headers) { (req) in + try req.content.encode(payload) + } + } + .flatMapThrowing { res in + guard 200 ..< 300 ~= res.status.code else { + if let googleError = try? res.content.decode(GoogleError.self) { + throw googleError + } else { + let reason = res.body?.debugDescription ?? "Unable to decode Firebase response" + throw Abort(.internalServerError, reason: reason) } - return result.name } + + struct Result: Codable { + var name: String + } + guard let result = try? res.content.decode(Result.self) else { + throw Abort(.notFound, reason: "Data not found") + } + return result.name } } } From 3a8896f15ee203ddd3bdbcb8269523d9e6c55947 Mon Sep 17 00:00:00 2001 From: Graham Burgsma Date: Mon, 10 Aug 2020 17:38:42 -0400 Subject: [PATCH 2/3] Update all other uses of client --- Sources/FCM/Helpers/FCM+AccessToken.swift | 2 +- Sources/FCM/Helpers/FCM+BatchSend.swift | 128 ++++----- Sources/FCM/Helpers/FCM+CreateTopic.swift | 117 ++++---- Sources/FCM/Helpers/FCM+DeleteTopic.swift | 114 ++++---- Sources/FCM/Helpers/FCM+RegisterAPNS.swift | 293 ++++++++++----------- Sources/FCM/Helpers/FCM+SendMessage.swift | 4 +- 6 files changed, 319 insertions(+), 339 deletions(-) diff --git a/Sources/FCM/Helpers/FCM+AccessToken.swift b/Sources/FCM/Helpers/FCM+AccessToken.swift index 40aaa43..70e59f3 100644 --- a/Sources/FCM/Helpers/FCM+AccessToken.swift +++ b/Sources/FCM/Helpers/FCM+AccessToken.swift @@ -16,7 +16,7 @@ extension FCM { "assertion": try self.getJWT(), ]) } - .flatMapThrowing { res throws -> String in + .flatMapThrowing { res -> String in if res.status.code != 200 { let code = "Code: \(res.status.code)" let message = "Message: \(res.content))" diff --git a/Sources/FCM/Helpers/FCM+BatchSend.swift b/Sources/FCM/Helpers/FCM+BatchSend.swift index 1144dd7..7f5d6ee 100644 --- a/Sources/FCM/Helpers/FCM+BatchSend.swift +++ b/Sources/FCM/Helpers/FCM+BatchSend.swift @@ -1,67 +1,67 @@ import Foundation import Vapor -//extension FCM { -// public func batchSend(_ message: FCMMessageDefault, tokens: String...) -> EventLoopFuture<[String]> { -// _send(message, tokens: tokens) -// } -// -// public func batchSend(_ message: FCMMessageDefault, tokens: String..., on eventLoop: EventLoop) -> EventLoopFuture<[String]> { -// _send(message, tokens: tokens).hop(to: eventLoop) -// } -// -// public func batchSend(_ message: FCMMessageDefault, tokens: [String]) -> EventLoopFuture<[String]> { -// _send(message, tokens: tokens) -// } -// -// public func batchSend(_ message: FCMMessageDefault, tokens: [String], on eventLoop: EventLoop) -> EventLoopFuture<[String]> { -// _send(message, tokens: tokens).hop(to: eventLoop) -// } -// -// private func _send(_ message: FCMMessageDefault, tokens: [String]) -> EventLoopFuture<[String]> { -// if message.apns == nil, -// let apnsDefaultConfig = apnsDefaultConfig { -// message.apns = apnsDefaultConfig -// } -// if message.android == nil, -// let androidDefaultConfig = androidDefaultConfig { -// message.android = androidDefaultConfig -// } -// if message.webpush == nil, -// let webpushDefaultConfig = webpushDefaultConfig { -// message.webpush = webpushDefaultConfig -// } -// var preparedTokens: [[String]] = [] -// tokens.enumerated().forEach { i, token in -// if Double(i).truncatingRemainder(dividingBy: 20) == 0 { -// preparedTokens.append([token]) -// } else { -// if var arr = preparedTokens.popLast() { -// arr.append(token) -// preparedTokens.append(arr) -// } else { -// preparedTokens.append([token]) -// } -// } -// } -// var deviceGroups: [String: [String]] = [:] -// return preparedTokens.map { tokens in -// createTopic(tokens: tokens).map { -// deviceGroups[$0] = tokens -// } -// }.flatten(on: application.eventLoopGroup.next()).flatMap { -// var results: [String] = [] -// return deviceGroups.map { deviceGroup in -// let message = message -// message.token = nil -// message.condition = nil -// message.topic = deviceGroup.key -// return self.send(message).map { -// results.append($0) -// }.flatMap { -// self.deleteTopic(deviceGroup.key, tokens: deviceGroup.value) -// } -// }.flatten(on: self.application.eventLoopGroup.next()).transform(to: results) -// } -// } -//} +extension FCM { + public func batchSend(_ message: FCMMessageDefault, tokens: String...) -> EventLoopFuture<[String]> { + _send(message, tokens: tokens) + } + + public func batchSend(_ message: FCMMessageDefault, tokens: String..., on eventLoop: EventLoop) -> EventLoopFuture<[String]> { + _send(message, tokens: tokens).hop(to: eventLoop) + } + + public func batchSend(_ message: FCMMessageDefault, tokens: [String]) -> EventLoopFuture<[String]> { + _send(message, tokens: tokens) + } + + public func batchSend(_ message: FCMMessageDefault, tokens: [String], on eventLoop: EventLoop) -> EventLoopFuture<[String]> { + _send(message, tokens: tokens).hop(to: eventLoop) + } + + private func _send(_ message: FCMMessageDefault, tokens: [String]) -> EventLoopFuture<[String]> { + if message.apns == nil, + let apnsDefaultConfig = apnsDefaultConfig { + message.apns = apnsDefaultConfig + } + if message.android == nil, + let androidDefaultConfig = androidDefaultConfig { + message.android = androidDefaultConfig + } + if message.webpush == nil, + let webpushDefaultConfig = webpushDefaultConfig { + message.webpush = webpushDefaultConfig + } + var preparedTokens: [[String]] = [] + tokens.enumerated().forEach { i, token in + if Double(i).truncatingRemainder(dividingBy: 20) == 0 { + preparedTokens.append([token]) + } else { + if var arr = preparedTokens.popLast() { + arr.append(token) + preparedTokens.append(arr) + } else { + preparedTokens.append([token]) + } + } + } + var deviceGroups: [String: [String]] = [:] + return preparedTokens.map { tokens in + createTopic(tokens: tokens).map { + deviceGroups[$0] = tokens + } + }.flatten(on: application.eventLoopGroup.next()).flatMap { + var results: [String] = [] + return deviceGroups.map { deviceGroup in + let message = message + message.token = nil + message.condition = nil + message.topic = deviceGroup.key + return self.send(message).map { + results.append($0) + }.flatMap { + self.deleteTopic(deviceGroup.key, tokens: deviceGroup.value) + } + }.flatten(on: self.application.eventLoopGroup.next()).transform(to: results) + } + } +} diff --git a/Sources/FCM/Helpers/FCM+CreateTopic.swift b/Sources/FCM/Helpers/FCM+CreateTopic.swift index cdc3361..5e92c90 100644 --- a/Sources/FCM/Helpers/FCM+CreateTopic.swift +++ b/Sources/FCM/Helpers/FCM+CreateTopic.swift @@ -1,65 +1,58 @@ import Foundation import Vapor -//extension FCM { -// public func createTopic(_ name: String? = nil, tokens: String...) -> EventLoopFuture { -// createTopic(name, tokens: tokens) -// } -// -// public func createTopic(_ name: String? = nil, tokens: String..., on eventLoop: EventLoop) -> EventLoopFuture { -// createTopic(name, tokens: tokens).hop(to: eventLoop) -// } -// -// public func createTopic(_ name: String? = nil, tokens: [String]) -> EventLoopFuture { -// _createTopic(name, tokens: tokens) -// } -// -// public func createTopic(_ name: String? = nil, tokens: [String], on eventLoop: EventLoop) -> EventLoopFuture { -// _createTopic(name, tokens: tokens).hop(to: eventLoop) -// } -// -// private func _createTopic(_ name: String? = nil, tokens: [String]) -> EventLoopFuture { -// guard let configuration = self.configuration else { -// fatalError("FCM not configured. Use app.fcm.configuration = ...") -// } -// guard let serverKey = configuration.serverKey else { -// fatalError("FCM: CreateTopic: Server Key is missing.") -// } -// let url = self.iidURL + "batchAdd" -// let name = name ?? UUID().uuidString -// return getAccessToken().flatMapThrowing { accessToken throws -> HTTPClient.Request in -// struct Payload: Codable { -// let to: String -// let registration_tokens: [String] -// -// init (to: String, registration_tokens: [String]) { -// self.to = "/topics/\(to)" -// self.registration_tokens = registration_tokens -// } -// } -// let payload = Payload(to: name, registration_tokens: tokens) -// let payloadData = try JSONEncoder().encode(payload) -// var headers = HTTPHeaders() -// headers.add(name: "Authorization", value: "key=\(serverKey)") -// headers.add(name: "Content-Type", value: "application/json") -// return try .init(url: url, method: .POST, headers: headers, body: .data(payloadData)) -// }.flatMap { request in -// return self.client.execute(request: request).flatMapThrowing { res in -// guard 200 ..< 300 ~= res.status.code else { -// if let body = res.body, let googleError = try? JSONDecoder().decode(GoogleError.self, from: body) { -// throw googleError -// } else { -// guard -// let bb = res.body, -// let bytes = bb.getBytes(at: 0, length: bb.readableBytes), -// let reason = String(bytes: bytes, encoding: .utf8) else { -// throw Abort(.internalServerError, reason: "FCM: CreateTopic: unable to decode error response") -// } -// throw Abort(.internalServerError, reason: reason) -// } -// } -// return name -// } -// } -// } -//} +extension FCM { + public func createTopic(_ name: String? = nil, tokens: String...) -> EventLoopFuture { + createTopic(name, tokens: tokens) + } + + public func createTopic(_ name: String? = nil, tokens: String..., on eventLoop: EventLoop) -> EventLoopFuture { + createTopic(name, tokens: tokens).hop(to: eventLoop) + } + + public func createTopic(_ name: String? = nil, tokens: [String]) -> EventLoopFuture { + _createTopic(name, tokens: tokens) + } + + public func createTopic(_ name: String? = nil, tokens: [String], on eventLoop: EventLoop) -> EventLoopFuture { + _createTopic(name, tokens: tokens).hop(to: eventLoop) + } + + private func _createTopic(_ name: String? = nil, tokens: [String]) -> EventLoopFuture { + guard let configuration = self.configuration else { + fatalError("FCM not configured. Use app.fcm.configuration = ...") + } + guard let serverKey = configuration.serverKey else { + fatalError("FCM: CreateTopic: Server Key is missing.") + } + let url = self.iidURL + "batchAdd" + let name = name ?? UUID().uuidString + return getAccessToken().flatMap { accessToken -> EventLoopFuture in + struct Payload: Content { + let to: String + let registration_tokens: [String] + + init(to: String, registration_tokens: [String]) { + self.to = "/topics/\(to)" + self.registration_tokens = registration_tokens + } + } + var headers = HTTPHeaders() + headers.add(name: .authorization, value: "key=\(serverKey)") + + return self.client.post(URI(string: url), headers: headers) { (req) in + let payload = Payload(to: name, registration_tokens: tokens) + try req.content.encode(payload) + } + }.flatMapThrowing { res in + guard 200 ..< 300 ~= res.status.code else { + if let googleError = try? res.content.decode(GoogleError.self) { + throw googleError + } else { + throw Abort(.internalServerError, reason: res.body?.description) + } + } + return name + } + } +} diff --git a/Sources/FCM/Helpers/FCM+DeleteTopic.swift b/Sources/FCM/Helpers/FCM+DeleteTopic.swift index 6ad0435..8f40bb0 100644 --- a/Sources/FCM/Helpers/FCM+DeleteTopic.swift +++ b/Sources/FCM/Helpers/FCM+DeleteTopic.swift @@ -1,63 +1,57 @@ import Foundation import Vapor -//extension FCM { -// public func deleteTopic(_ name: String, tokens: String...) -> EventLoopFuture { -// deleteTopic(name, tokens: tokens) -// } -// -// public func deleteTopic(_ name: String, tokens: String..., on eventLoop: EventLoop) -> EventLoopFuture { -// deleteTopic(name, tokens: tokens).hop(to: eventLoop) -// } -// -// public func deleteTopic(_ name: String, tokens: [String]) -> EventLoopFuture { -// _deleteTopic(name, tokens: tokens) -// } -// -// public func deleteTopic(_ name: String, tokens: [String], on eventLoop: EventLoop) -> EventLoopFuture { -// _deleteTopic(name, tokens: tokens).hop(to: eventLoop) -// } -// -// private func _deleteTopic(_ name: String, tokens: [String]) -> EventLoopFuture { -// guard let configuration = self.configuration else { -// fatalError("FCM not configured. Use app.fcm.configuration = ...") -// } -// guard let serverKey = configuration.serverKey else { -// fatalError("FCM: DeleteTopic: Server Key is missing.") -// } -// let url = self.iidURL + "batchRemove" -// return getAccessToken().flatMapThrowing { accessToken throws -> HTTPClient.Request in -// struct Payload: Codable { -// let to: String -// let registration_tokens: [String] -// -// init (to: String, registration_tokens: [String]) { -// self.to = "/topics/\(to)" -// self.registration_tokens = registration_tokens -// } -// } -// let payload = Payload(to: name, registration_tokens: tokens) -// let payloadData = try JSONEncoder().encode(payload) -// var headers = HTTPHeaders() -// headers.add(name: "Authorization", value: "key=\(serverKey)") -// headers.add(name: "Content-Type", value: "application/json") -// return try .init(url: url, method: .POST, headers: headers, body: .data(payloadData)) -// }.flatMap { request in -// return self.client.execute(request: request).flatMapThrowing { res in -// guard 200 ..< 300 ~= res.status.code else { -// if let body = res.body, let googleError = try? JSONDecoder().decode(GoogleError.self, from: body) { -// throw googleError -// } else { -// guard -// let bb = res.body, -// let bytes = bb.getBytes(at: 0, length: bb.readableBytes), -// let reason = String(bytes: bytes, encoding: .utf8) else { -// throw Abort(.internalServerError, reason: "FCM: DeleteTopic: unable to decode error response") -// } -// throw Abort(.internalServerError, reason: reason) -// } -// } -// } -// } -// } -//} +extension FCM { + public func deleteTopic(_ name: String, tokens: String...) -> EventLoopFuture { + deleteTopic(name, tokens: tokens) + } + + public func deleteTopic(_ name: String, tokens: String..., on eventLoop: EventLoop) -> EventLoopFuture { + deleteTopic(name, tokens: tokens).hop(to: eventLoop) + } + + public func deleteTopic(_ name: String, tokens: [String]) -> EventLoopFuture { + _deleteTopic(name, tokens: tokens) + } + + public func deleteTopic(_ name: String, tokens: [String], on eventLoop: EventLoop) -> EventLoopFuture { + _deleteTopic(name, tokens: tokens).hop(to: eventLoop) + } + + private func _deleteTopic(_ name: String, tokens: [String]) -> EventLoopFuture { + guard let configuration = self.configuration else { + fatalError("FCM not configured. Use app.fcm.configuration = ...") + } + guard let serverKey = configuration.serverKey else { + fatalError("FCM: DeleteTopic: Server Key is missing.") + } + let url = self.iidURL + "batchRemove" + return getAccessToken().flatMap { accessToken -> EventLoopFuture in + struct Payload: Content { + let to: String + let registration_tokens: [String] + + init(to: String, registration_tokens: [String]) { + self.to = "/topics/\(to)" + self.registration_tokens = registration_tokens + } + } + + var headers = HTTPHeaders() + headers.add(name: .authorization, value: "key=\(serverKey)") + + return self.client.post(URI(string: url), headers: headers) { (req) in + let payload = Payload(to: name, registration_tokens: tokens) + try req.content.encode(payload) + } + }.flatMapThrowing { res in + guard 200 ..< 300 ~= res.status.code else { + if let googleError = try? res.content.decode(GoogleError.self) { + throw googleError + } else { + throw Abort(.internalServerError, reason: res.body?.description) + } + } + } + } +} diff --git a/Sources/FCM/Helpers/FCM+RegisterAPNS.swift b/Sources/FCM/Helpers/FCM+RegisterAPNS.swift index 5b213e5..fcc8394 100644 --- a/Sources/FCM/Helpers/FCM+RegisterAPNS.swift +++ b/Sources/FCM/Helpers/FCM+RegisterAPNS.swift @@ -1,153 +1,146 @@ import Foundation import Vapor -//public struct RegisterAPNSID { -// let appBundleId: String -// let serverKey: String? -// let sandbox: Bool -// -// public init (appBundleId: String, serverKey: String? = nil, sandbox: Bool = false) { -// self.appBundleId = appBundleId -// self.serverKey = serverKey -// self.sandbox = sandbox -// } -//} -// -//extension RegisterAPNSID { -// public static var env: RegisterAPNSID { -// guard let appBundleId = Environment.get("FCM_APP_BUNDLE_ID") else { -// fatalError("FCM: Register APNS: missing FCM_APP_BUNDLE_ID environment variable") -// } -// return .init(appBundleId: appBundleId) -// } -//} -// -//extension RegisterAPNSID { -// public static var envSandbox: RegisterAPNSID { -// let id: RegisterAPNSID = .env -// return .init(appBundleId: id.appBundleId, sandbox: true) -// } -//} -// -//public struct APNSToFirebaseToken { -// public let registration_token, apns_token: String -// public let isRegistered: Bool -//} -// -//extension FCM { -// /// Helper method which registers your pure APNS token in Firebase Cloud Messaging -// /// and returns firebase tokens for each APNS token -// /// -// /// Convenient way -// /// -// /// Declare `RegisterAPNSID` via extension -// /// ```swift -// /// extension RegisterAPNSID { -// /// static var myApp: RegisterAPNSID { .init(appBundleId: "com.myapp") } -// /// } -// /// ``` -// /// -// public func registerAPNS( -// _ id: RegisterAPNSID, -// tokens: String..., -// on eventLoop: EventLoop? = nil) -> EventLoopFuture<[APNSToFirebaseToken]> { -// registerAPNS(appBundleId: id.appBundleId, serverKey: id.serverKey, sandbox: id.sandbox, tokens: tokens, on: eventLoop) -// } -// -// /// Helper method which registers your pure APNS token in Firebase Cloud Messaging -// /// and returns firebase tokens for each APNS token -// /// -// /// Convenient way -// /// -// /// Declare `RegisterAPNSID` via extension -// /// ```swift -// /// extension RegisterAPNSID { -// /// static var myApp: RegisterAPNSID { .init(appBundleId: "com.myapp") } -// /// } -// /// ``` -// /// -// public func registerAPNS( -// _ id: RegisterAPNSID, -// tokens: [String], -// on eventLoop: EventLoop? = nil) -> EventLoopFuture<[APNSToFirebaseToken]> { -// registerAPNS(appBundleId: id.appBundleId, serverKey: id.serverKey, sandbox: id.sandbox, tokens: tokens, on: eventLoop) -// } -// -// /// Helper method which registers your pure APNS token in Firebase Cloud Messaging -// /// and returns firebase tokens for each APNS token -// public func registerAPNS( -// appBundleId: String, -// serverKey: String? = nil, -// sandbox: Bool = false, -// tokens: String..., -// on eventLoop: EventLoop? = nil) -> EventLoopFuture<[APNSToFirebaseToken]> { -// registerAPNS(appBundleId: appBundleId, serverKey: serverKey, sandbox: sandbox, tokens: tokens, on: eventLoop) -// } -// -// /// Helper method which registers your pure APNS token in Firebase Cloud Messaging -// /// and returns firebase tokens for each APNS token -// public func registerAPNS( -// appBundleId: String, -// serverKey: String? = nil, -// sandbox: Bool = false, -// tokens: [String], -// on eventLoop: EventLoop? = nil) -> EventLoopFuture<[APNSToFirebaseToken]> { -// let eventLoop = eventLoop ?? application.eventLoopGroup.next() -// guard tokens.count <= 100 else { -// return eventLoop.makeFailedFuture(Abort(.internalServerError, reason: "FCM: Register APNS: tokens count should be less or equeal 100")) -// } -// guard tokens.count > 0 else { -// return eventLoop.future([]) -// } -// guard let configuration = self.configuration else { -// #if DEBUG -// fatalError("FCM not configured. Use app.fcm.configuration = ...") -// #else -// return eventLoop.future([]) -// #endif -// } -// guard let serverKey = serverKey ?? configuration.serverKey else { -// fatalError("FCM: Register APNS: Server Key is missing.") -// } -// let url = iidURL + "batchImport" -// return eventLoop.future().flatMapThrowing { accessToken throws -> HTTPClient.Request in -// struct Payload: Codable { -// let application: String -// let sandbox: Bool -// let apns_tokens: [String] -// } -// let payload = Payload(application: appBundleId, sandbox: sandbox, apns_tokens: tokens) -// let payloadData = try JSONEncoder().encode(payload) -// -// var headers = HTTPHeaders() -// headers.add(name: "Authorization", value: "key=\(serverKey)") -// headers.add(name: "Content-Type", value: "application/json") -// -// return try .init(url: url, method: .POST, headers: headers, body: .data(payloadData)) -// }.flatMap { request in -// return self.client.execute(request: request).flatMapThrowing { res in -// guard 200 ..< 300 ~= res.status.code else { -// guard -// let bb = res.body, -// let bytes = bb.getBytes(at: 0, length: bb.readableBytes), -// let reason = String(bytes: bytes, encoding: .utf8) else { -// throw Abort(.internalServerError, reason: "FCM: Register APNS: unable to decode error response") -// } -// throw Abort(.internalServerError, reason: reason) -// } -// struct Result: Codable { -// struct Result: Codable { -// let registration_token, apns_token, status: String -// } -// var results: [Result] -// } -// guard let body = res.body, let result = try? JSONDecoder().decode(Result.self, from: body) else { -// throw Abort(.notFound, reason: "FCM: Register APNS: empty response") -// } -// return result.results.map { -// .init(registration_token: $0.registration_token, apns_token: $0.apns_token, isRegistered: $0.status == "OK") -// } -// } -// } -// } -//} +public struct RegisterAPNSID { + let appBundleId: String + let serverKey: String? + let sandbox: Bool + + public init (appBundleId: String, serverKey: String? = nil, sandbox: Bool = false) { + self.appBundleId = appBundleId + self.serverKey = serverKey + self.sandbox = sandbox + } +} + +extension RegisterAPNSID { + public static var env: RegisterAPNSID { + guard let appBundleId = Environment.get("FCM_APP_BUNDLE_ID") else { + fatalError("FCM: Register APNS: missing FCM_APP_BUNDLE_ID environment variable") + } + return .init(appBundleId: appBundleId) + } +} + +extension RegisterAPNSID { + public static var envSandbox: RegisterAPNSID { + let id: RegisterAPNSID = .env + return .init(appBundleId: id.appBundleId, sandbox: true) + } +} + +public struct APNSToFirebaseToken { + public let registration_token, apns_token: String + public let isRegistered: Bool +} + +extension FCM { + /// Helper method which registers your pure APNS token in Firebase Cloud Messaging + /// and returns firebase tokens for each APNS token + /// + /// Convenient way + /// + /// Declare `RegisterAPNSID` via extension + /// ```swift + /// extension RegisterAPNSID { + /// static var myApp: RegisterAPNSID { .init(appBundleId: "com.myapp") } + /// } + /// ``` + /// + public func registerAPNS( + _ id: RegisterAPNSID, + tokens: String..., + on eventLoop: EventLoop? = nil) -> EventLoopFuture<[APNSToFirebaseToken]> { + registerAPNS(appBundleId: id.appBundleId, serverKey: id.serverKey, sandbox: id.sandbox, tokens: tokens, on: eventLoop) + } + + /// Helper method which registers your pure APNS token in Firebase Cloud Messaging + /// and returns firebase tokens for each APNS token + /// + /// Convenient way + /// + /// Declare `RegisterAPNSID` via extension + /// ```swift + /// extension RegisterAPNSID { + /// static var myApp: RegisterAPNSID { .init(appBundleId: "com.myapp") } + /// } + /// ``` + /// + public func registerAPNS( + _ id: RegisterAPNSID, + tokens: [String], + on eventLoop: EventLoop? = nil) -> EventLoopFuture<[APNSToFirebaseToken]> { + registerAPNS(appBundleId: id.appBundleId, serverKey: id.serverKey, sandbox: id.sandbox, tokens: tokens, on: eventLoop) + } + + /// Helper method which registers your pure APNS token in Firebase Cloud Messaging + /// and returns firebase tokens for each APNS token + public func registerAPNS( + appBundleId: String, + serverKey: String? = nil, + sandbox: Bool = false, + tokens: String..., + on eventLoop: EventLoop? = nil) -> EventLoopFuture<[APNSToFirebaseToken]> { + registerAPNS(appBundleId: appBundleId, serverKey: serverKey, sandbox: sandbox, tokens: tokens, on: eventLoop) + } + + /// Helper method which registers your pure APNS token in Firebase Cloud Messaging + /// and returns firebase tokens for each APNS token + public func registerAPNS( + appBundleId: String, + serverKey: String? = nil, + sandbox: Bool = false, + tokens: [String], + on eventLoop: EventLoop? = nil) -> EventLoopFuture<[APNSToFirebaseToken]> { + let eventLoop = eventLoop ?? application.eventLoopGroup.next() + guard tokens.count <= 100 else { + return eventLoop.makeFailedFuture(Abort(.internalServerError, reason: "FCM: Register APNS: tokens count should be less or equeal 100")) + } + guard tokens.count > 0 else { + return eventLoop.future([]) + } + guard let configuration = self.configuration else { + #if DEBUG + fatalError("FCM not configured. Use app.fcm.configuration = ...") + #else + return eventLoop.future([]) + #endif + } + guard let serverKey = serverKey ?? configuration.serverKey else { + fatalError("FCM: Register APNS: Server Key is missing.") + } + let url = iidURL + "batchImport" + + var headers = HTTPHeaders() + headers.add(name: .authorization, value: "key=\(serverKey)") + + return self.client.post(URI(string: url), headers: headers) { (req) in + struct Payload: Content { + let application: String + let sandbox: Bool + let apns_tokens: [String] + } + let payload = Payload(application: appBundleId, sandbox: sandbox, apns_tokens: tokens) + try req.content.encode(payload) + } + .flatMapThrowing { res in + guard 200 ..< 300 ~= res.status.code else { + let reason = res.body?.debugDescription ?? "FCM: Register APNS: unable to decode error response" + throw Abort(.internalServerError, reason: reason) + } + + struct Result: Codable { + struct Result: Codable { + let registration_token, apns_token, status: String + } + let results: [Result] + } + guard let result = try? res.content.decode(Result.self) else { + throw Abort(.notFound, reason: "FCM: Register APNS: empty response") + } + return result.results.map { + .init(registration_token: $0.registration_token, apns_token: $0.apns_token, isRegistered: $0.status == "OK") + } + } + } +} diff --git a/Sources/FCM/Helpers/FCM+SendMessage.swift b/Sources/FCM/Helpers/FCM+SendMessage.swift index c9ea8cd..e8ff44e 100644 --- a/Sources/FCM/Helpers/FCM+SendMessage.swift +++ b/Sources/FCM/Helpers/FCM+SendMessage.swift @@ -35,9 +35,9 @@ extension FCM { let validate_only: Bool = false let message: FCMMessageDefault } - let payload = Payload(message: message) return self.client.post(URI(string: url), headers: headers) { (req) in + let payload = Payload(message: message) try req.content.encode(payload) } } @@ -52,7 +52,7 @@ extension FCM { } struct Result: Codable { - var name: String + let name: String } guard let result = try? res.content.decode(Result.self) else { throw Abort(.notFound, reason: "Data not found") From 75cfff66f04201afce1f400319234d198aef231d Mon Sep 17 00:00:00 2001 From: Graham Burgsma Date: Mon, 10 Aug 2020 20:00:36 -0400 Subject: [PATCH 3/3] Generalize response error handling --- Sources/FCM/FCMError.swift | 18 ++++++++++++++ Sources/FCM/Helpers/FCM+AccessToken.swift | 12 ++------- Sources/FCM/Helpers/FCM+CreateTopic.swift | 29 +++++++++------------- Sources/FCM/Helpers/FCM+DeleteTopic.swift | 29 ++++++++-------------- Sources/FCM/Helpers/FCM+RegisterAPNS.swift | 10 ++------ Sources/FCM/Helpers/FCM+SendMessage.swift | 24 ++++++------------ 6 files changed, 52 insertions(+), 70 deletions(-) diff --git a/Sources/FCM/FCMError.swift b/Sources/FCM/FCMError.swift index d02f5e7..3601568 100644 --- a/Sources/FCM/FCMError.swift +++ b/Sources/FCM/FCMError.swift @@ -1,3 +1,5 @@ +import Vapor + public struct GoogleError: Error, Decodable { public let code: Int public let message: String @@ -39,3 +41,19 @@ public struct FCMError: Error, Decodable { case `internal` = "INTERNAL" } } + +extension EventLoopFuture where Value == ClientResponse { + func validate() -> EventLoopFuture { + return flatMapThrowing { (response) in + guard 200 ..< 300 ~= response.status.code else { + if let error = try? response.content.decode(GoogleError.self) { + throw error + } + let body = response.body.map(String.init) ?? "" + throw Abort(.internalServerError, reason: "FCM: Unexpected error '\(body)'") + } + + return response + } + } +} diff --git a/Sources/FCM/Helpers/FCM+AccessToken.swift b/Sources/FCM/Helpers/FCM+AccessToken.swift index 70e59f3..bb4cc69 100644 --- a/Sources/FCM/Helpers/FCM+AccessToken.swift +++ b/Sources/FCM/Helpers/FCM+AccessToken.swift @@ -16,20 +16,12 @@ extension FCM { "assertion": try self.getJWT(), ]) } + .validate() .flatMapThrowing { res -> String in - if res.status.code != 200 { - let code = "Code: \(res.status.code)" - let message = "Message: \(res.content))" - let reason = "[FCM] Unable to refresh access token. \(code) \(message)" - throw Abort(.internalServerError, reason: reason) - } - struct Result: Codable { var access_token: String } - guard let result = try? res.content.decode(Result.self) else { - throw Abort(.notFound, reason: "Data not found") - } + let result = try res.content.decode(Result.self) return result.access_token } } diff --git a/Sources/FCM/Helpers/FCM+CreateTopic.swift b/Sources/FCM/Helpers/FCM+CreateTopic.swift index 5e92c90..3346c9c 100644 --- a/Sources/FCM/Helpers/FCM+CreateTopic.swift +++ b/Sources/FCM/Helpers/FCM+CreateTopic.swift @@ -28,30 +28,25 @@ extension FCM { let url = self.iidURL + "batchAdd" let name = name ?? UUID().uuidString return getAccessToken().flatMap { accessToken -> EventLoopFuture in - struct Payload: Content { - let to: String - let registration_tokens: [String] - - init(to: String, registration_tokens: [String]) { - self.to = "/topics/\(to)" - self.registration_tokens = registration_tokens - } - } var headers = HTTPHeaders() headers.add(name: .authorization, value: "key=\(serverKey)") return self.client.post(URI(string: url), headers: headers) { (req) in + struct Payload: Content { + let to: String + let registration_tokens: [String] + + init(to: String, registration_tokens: [String]) { + self.to = "/topics/\(to)" + self.registration_tokens = registration_tokens + } + } let payload = Payload(to: name, registration_tokens: tokens) try req.content.encode(payload) } - }.flatMapThrowing { res in - guard 200 ..< 300 ~= res.status.code else { - if let googleError = try? res.content.decode(GoogleError.self) { - throw googleError - } else { - throw Abort(.internalServerError, reason: res.body?.description) - } - } + } + .validate() + .map { _ in return name } } diff --git a/Sources/FCM/Helpers/FCM+DeleteTopic.swift b/Sources/FCM/Helpers/FCM+DeleteTopic.swift index 8f40bb0..af49c6d 100644 --- a/Sources/FCM/Helpers/FCM+DeleteTopic.swift +++ b/Sources/FCM/Helpers/FCM+DeleteTopic.swift @@ -27,31 +27,24 @@ extension FCM { } let url = self.iidURL + "batchRemove" return getAccessToken().flatMap { accessToken -> EventLoopFuture in - struct Payload: Content { - let to: String - let registration_tokens: [String] - - init(to: String, registration_tokens: [String]) { - self.to = "/topics/\(to)" - self.registration_tokens = registration_tokens - } - } - var headers = HTTPHeaders() headers.add(name: .authorization, value: "key=\(serverKey)") return self.client.post(URI(string: url), headers: headers) { (req) in + struct Payload: Content { + let to: String + let registration_tokens: [String] + + init(to: String, registration_tokens: [String]) { + self.to = "/topics/\(to)" + self.registration_tokens = registration_tokens + } + } let payload = Payload(to: name, registration_tokens: tokens) try req.content.encode(payload) } - }.flatMapThrowing { res in - guard 200 ..< 300 ~= res.status.code else { - if let googleError = try? res.content.decode(GoogleError.self) { - throw googleError - } else { - throw Abort(.internalServerError, reason: res.body?.description) - } - } } + .validate() + .map { _ in () } } } diff --git a/Sources/FCM/Helpers/FCM+RegisterAPNS.swift b/Sources/FCM/Helpers/FCM+RegisterAPNS.swift index fcc8394..c990576 100644 --- a/Sources/FCM/Helpers/FCM+RegisterAPNS.swift +++ b/Sources/FCM/Helpers/FCM+RegisterAPNS.swift @@ -123,21 +123,15 @@ extension FCM { let payload = Payload(application: appBundleId, sandbox: sandbox, apns_tokens: tokens) try req.content.encode(payload) } + .validate() .flatMapThrowing { res in - guard 200 ..< 300 ~= res.status.code else { - let reason = res.body?.debugDescription ?? "FCM: Register APNS: unable to decode error response" - throw Abort(.internalServerError, reason: reason) - } - struct Result: Codable { struct Result: Codable { let registration_token, apns_token, status: String } let results: [Result] } - guard let result = try? res.content.decode(Result.self) else { - throw Abort(.notFound, reason: "FCM: Register APNS: empty response") - } + let result = try res.content.decode(Result.self) return result.results.map { .init(registration_token: $0.registration_token, apns_token: $0.apns_token, isRegistered: $0.status == "OK") } diff --git a/Sources/FCM/Helpers/FCM+SendMessage.swift b/Sources/FCM/Helpers/FCM+SendMessage.swift index e8ff44e..d4dc594 100644 --- a/Sources/FCM/Helpers/FCM+SendMessage.swift +++ b/Sources/FCM/Helpers/FCM+SendMessage.swift @@ -26,37 +26,27 @@ extension FCM { let webpushDefaultConfig = webpushDefaultConfig { message.webpush = webpushDefaultConfig } + let url = actionsBaseURL + configuration.projectId + "/messages:send" return getAccessToken().flatMap { accessToken -> EventLoopFuture in var headers = HTTPHeaders() headers.bearerAuthorization = .init(token: accessToken) - struct Payload: Content { - let validate_only: Bool = false - let message: FCMMessageDefault - } - return self.client.post(URI(string: url), headers: headers) { (req) in + struct Payload: Content { + let validate_only: Bool = false + let message: FCMMessageDefault + } let payload = Payload(message: message) try req.content.encode(payload) } } + .validate() .flatMapThrowing { res in - guard 200 ..< 300 ~= res.status.code else { - if let googleError = try? res.content.decode(GoogleError.self) { - throw googleError - } else { - let reason = res.body?.debugDescription ?? "Unable to decode Firebase response" - throw Abort(.internalServerError, reason: reason) - } - } - struct Result: Codable { let name: String } - guard let result = try? res.content.decode(Result.self) else { - throw Abort(.notFound, reason: "Data not found") - } + let result = try res.content.decode(Result.self) return result.name } }