Skip to content

Commit

Permalink
Support for Swift6 and async/await
Browse files Browse the repository at this point in the history
  • Loading branch information
gennaro-safehill committed Sep 30, 2024
1 parent e80bb25 commit d7b1c64
Show file tree
Hide file tree
Showing 14 changed files with 215 additions and 231 deletions.
6 changes: 3 additions & 3 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
// swift-tools-version:5.2
// swift-tools-version:6.0

import PackageDescription

let package = Package(
name: "FCM",
platforms: [
.macOS(.v10_15)
.macOS(.v13)
],
products: [
//Vapor client for Firebase Cloud Messaging
Expand All @@ -14,7 +14,7 @@ let package = Package(
dependencies: [
// 💧 A server-side Swift web framework.
.package(url: "https://github.com/vapor/vapor.git", from: "4.0.0"),
.package(url: "https://github.com/vapor/jwt.git", from: "4.0.0"),
.package(url: "https://github.com/vapor/jwt.git", exact: "5.0.0-rc.1"),
],
targets: [
.target(name: "FCM", dependencies: [
Expand Down
10 changes: 6 additions & 4 deletions Sources/FCM/FCM.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import JWT

// MARK: Engine

public struct FCM {
public struct FCM: Sendable {
let application: Application

let client: Client
Expand Down Expand Up @@ -62,18 +62,20 @@ extension FCM {
nonmutating set {
application.storage[ConfigurationKey.self] = newValue
if let newValue = newValue {
warmUpCache(with: newValue.email)
Task {
await warmUpCache(with: newValue.email)
}
}
}
}

private func warmUpCache(with email: String) {
private func warmUpCache(with email: String) async {
if gAuth == nil {
gAuth = GAuthPayload(iss: email, sub: email, scope: scope, aud: audience)
}
if jwt == nil {
do {
jwt = try generateJWT()
jwt = try await generateJWT()
} catch {
fatalError("FCM Unable to generate JWT: \(error)")
}
Expand Down
2 changes: 1 addition & 1 deletion Sources/FCM/FCMConfiguration.swift
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import Foundation
import Vapor

public struct FCMConfiguration {
public struct FCMConfiguration : @unchecked Sendable {
let email, projectId, key: String
let serverKey, senderId: String?

Expand Down
23 changes: 14 additions & 9 deletions Sources/FCM/FCMError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ public struct GoogleError: Error, Decodable {
public struct FCMError: Error, Decodable {
public let errorCode: ErrorCode

public enum ErrorCode: String, Decodable {
public enum ErrorCode: String, Decodable, Sendable {
case unspecified = "UNSPECIFIED_ERROR"
case invalid = "INVALID_ARGUMENT"
case unregistered = "UNREGISTERED"
Expand All @@ -45,15 +45,20 @@ public struct FCMError: Error, Decodable {
extension EventLoopFuture where Value == ClientResponse {
func validate() -> EventLoopFuture<ClientResponse> {
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)'")
}

try response.validate()
return response
}
}
}

extension ClientResponse {
func validate() throws {
guard 200 ..< 300 ~= self.status.code else {
if let error = try? self.content.decode(GoogleError.self) {
throw error
}
let body = self.body.map(String.init) ?? ""
throw Abort(.internalServerError, reason: "FCM: Unexpected error '\(body)'")
}
}
}
2 changes: 1 addition & 1 deletion Sources/FCM/FCMMessage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import Foundation

public typealias FCMMessageDefault = FCMMessage<FCMApnsPayload>

public class FCMMessage<APNSPayload>: Codable where APNSPayload: FCMApnsPayloadProtocol {
public class FCMMessage<APNSPayload>: @unchecked Sendable, Codable where APNSPayload: FCMApnsPayloadProtocol {
/// Output Only.
/// The identifier of the message sent,
/// in the format of projects/*/messages/{message_id}.
Expand Down
2 changes: 1 addition & 1 deletion Sources/FCM/GAuthPayload.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ struct GAuthPayload: JWTPayload {
self.aud = aud
}

func verify(using signer: JWTSigner) throws {
func verify(using algorithm: some JWTAlgorithm) throws {
// not used
}

Expand Down
26 changes: 14 additions & 12 deletions Sources/FCM/Helpers/FCM+AccessToken.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,29 @@ import Foundation
import Vapor

extension FCM {
func getAccessToken() -> EventLoopFuture<String> {
func getAccessToken() async throws -> String {
guard let gAuth = gAuth else {
fatalError("FCM gAuth can't be nil")
}
if !gAuth.hasExpired, let token = accessToken {
return client.eventLoop.future(token)
return token
}

return client.post(URI(string: audience)) { (req) in

let jwt = try await self.getJWT()

let response = try await 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(),
"assertion": jwt,
])
}
.validate()
.flatMapThrowing { res -> String in
struct Result: Codable {
let access_token: String
}

return try res.content.decode(Result.self, using: JSONDecoder()).access_token

try response.validate()

struct Result: Codable {
let access_token: String
}

return try response.content.decode(Result.self, using: JSONDecoder()).access_token
}
}
157 changes: 68 additions & 89 deletions Sources/FCM/Helpers/FCM+BatchSend.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,135 +2,114 @@ import Foundation
import Vapor

extension FCM {
@available(*, deprecated, message: "🧨 Requests to the endpoint will start failing after 6/21/2024. Migrate to the standard send method.")
public func batchSend(_ message: FCMMessageDefault, tokens: String...) -> EventLoopFuture<[String]> {
_send(message, tokens: tokens)
}

@available(*, deprecated, message: "🧨 Requests to the endpoint will start failing after 6/21/2024. Migrate to the standard send method.")
public func batchSend(_ message: FCMMessageDefault, tokens: String..., on eventLoop: EventLoop) -> EventLoopFuture<[String]> {
_send(message, tokens: tokens).hop(to: eventLoop)
}

@available(*, deprecated, message: "🧨 Requests to the endpoint will start failing after 6/21/2024. Migrate to the standard send method.")
public func batchSend(_ message: FCMMessageDefault, tokens: [String]) -> EventLoopFuture<[String]> {
_send(message, tokens: tokens)
}

@available(*, deprecated, message: "🧨 Requests to the endpoint will start failing after 6/21/2024. Migrate to the standard send method.")
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]> {
private func _send(_ message: FCMMessageDefault, tokens: [String]) async throws -> [String] {
guard let configuration = self.configuration else {
fatalError("FCM not configured. Use app.fcm.configuration = ...")
}

let urlPath = URI(string: actionsBaseURL + configuration.projectId + "/messages:send").path

return getAccessToken().flatMap { accessToken in
tokens.chunked(into: 500).map { chunk in
self._sendChunk(
message,
tokens: chunk,
urlPath: urlPath,
accessToken: accessToken
)
}
.flatten(on: self.client.eventLoop)
.map { $0.flatMap { $0 } }
let accessToken = try await getAccessToken()

var result = [String]()
for chunk in tokens.chunked(into: 500) {
let partial = try await self._sendChunk(
message,
tokens: chunk,
urlPath: urlPath,
accessToken: accessToken
)
result.append(contentsOf: partial)
}

return result
}

private func _sendChunk(
_ message: FCMMessageDefault,
tokens: [String],
urlPath: String,
accessToken: String
) -> EventLoopFuture<[String]> {
) async throws -> [String] {
var body = ByteBufferAllocator().buffer(capacity: 0)
let boundary = "subrequest_boundary"

struct Payload: Encodable {
let message: FCMMessageDefault
}

do {
let parts: [MultipartPart] = try tokens.map { token in
var partBody = ByteBufferAllocator().buffer(capacity: 0)
let parts: [MultipartPart] = try tokens.map { token in
var partBody = ByteBufferAllocator().buffer(capacity: 0)

partBody.writeString("""
POST \(urlPath)\r
Content-Type: application/json\r
accept: application/json\r
\r
partBody.writeString("""
POST \(urlPath)\r
Content-Type: application/json\r
accept: application/json\r
\r
""")
""")

let message = FCMMessageDefault(
token: token,
notification: message.notification,
data: message.data,
name: message.name,
android: message.android ?? androidDefaultConfig,
webpush: message.webpush ?? webpushDefaultConfig,
apns: message.apns ?? apnsDefaultConfig
)
let message = FCMMessageDefault(
token: token,
notification: message.notification,
data: message.data,
name: message.name,
android: message.android ?? androidDefaultConfig,
webpush: message.webpush ?? webpushDefaultConfig,
apns: message.apns ?? apnsDefaultConfig
)

try partBody.writeJSONEncodable(Payload(message: message))
try partBody.writeJSONEncodable(Payload(message: message))

return MultipartPart(headers: ["Content-Type": "application/http"], body: partBody)
}

try MultipartSerializer().serialize(parts: parts, boundary: boundary, into: &body)
} catch {
return client.eventLoop.makeFailedFuture(error)
return MultipartPart(headers: ["Content-Type": "application/http"], body: partBody)
}

try MultipartSerializer().serialize(parts: parts, boundary: boundary, into: &body)

var headers = HTTPHeaders()
headers.contentType = .init(type: "multipart", subType: "mixed", parameters: ["boundary": boundary])
headers.bearerAuthorization = .init(token: accessToken)

return self.client
let response = try await self.client
.post(URI(string: batchURL), headers: headers) { req in
req.body = body
}
.validate()
.flatMapThrowing { (res: ClientResponse) in
guard
let boundary = res.headers.contentType?.parameters["boundary"]
else {
throw Abort(.internalServerError, reason: "FCM: Missing \"boundary\" in batch response headers")
}
guard
let body = res.body
else {
throw Abort(.internalServerError, reason: "FCM: Missing response body from batch operation")
}

try response.validate()

struct Result: Decodable {
let name: String
}
guard
let boundary = response.headers.contentType?.parameters["boundary"]
else {
throw Abort(.internalServerError, reason: "FCM: Missing \"boundary\" in batch response headers")
}
guard
let body = response.body
else {
throw Abort(.internalServerError, reason: "FCM: Missing response body from batch operation")
}

let jsonDecoder = JSONDecoder()
var result: [String] = []

let parser = MultipartParser(boundary: boundary)
parser.onBody = { body in
let bytes = body.readableBytesView
if let indexOfBodyStart = bytes.firstIndex(of: 0x7B) /* '{' */ {
body.moveReaderIndex(to: indexOfBodyStart)
if let name = try? jsonDecoder.decode(Result.self, from: body).name {
result.append(name)
}
}
}
struct Result: Decodable {
let name: String
}

try parser.execute(body)
let jsonDecoder = JSONDecoder()
var result: [String] = []

return result
let parser = MultipartParser(boundary: boundary)
parser.onBody = { body in
let bytes = body.readableBytesView
if let indexOfBodyStart = bytes.firstIndex(of: 0x7B) /* '{' */ {
body.moveReaderIndex(to: indexOfBodyStart)
if let name = try? jsonDecoder.decode(Result.self, from: body).name {
result.append(name)
}
}
}

try parser.execute(body)

return result
}
}

Expand Down
Loading

0 comments on commit d7b1c64

Please sign in to comment.