Skip to content

Commit

Permalink
☄️ Implement batch sending
Browse files Browse the repository at this point in the history
  • Loading branch information
MihaelIsaev committed May 16, 2020
1 parent 96738d0 commit d1bb4a5
Show file tree
Hide file tree
Showing 4 changed files with 196 additions and 1 deletion.
2 changes: 1 addition & 1 deletion Sources/FCM/FCMMessage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ public class FCMMessage<APNSPayload>: Codable where APNSPayload: FCMApnsPayloadP
public var condition: String?

/// Initialization with device token
public init(token: String,
public init(token: String? = nil,
notification: FCMNotification?,
data: [String: String]? = nil,
name: String? = nil,
Expand Down
67 changes: 67 additions & 0 deletions Sources/FCM/Helpers/FCM+BatchSend.swift
Original file line number Diff line number Diff line change
@@ -0,0 +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: 1) == 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)
}
}
}
65 changes: 65 additions & 0 deletions Sources/FCM/Helpers/FCM+CreateTopic.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import Foundation
import Vapor

extension FCM {
public func createTopic(_ name: String? = nil, tokens: String...) -> EventLoopFuture<String> {
createTopic(name, tokens: tokens)
}

public func createTopic(_ name: String? = nil, tokens: String..., on eventLoop: EventLoop) -> EventLoopFuture<String> {
createTopic(name, tokens: tokens).hop(to: eventLoop)
}

public func createTopic(_ name: String? = nil, tokens: [String]) -> EventLoopFuture<String> {
_createTopic(name, tokens: tokens)
}

public func createTopic(_ name: String? = nil, tokens: [String], on eventLoop: EventLoop) -> EventLoopFuture<String> {
_createTopic(name, tokens: tokens).hop(to: eventLoop)
}

private func _createTopic(_ name: String? = nil, tokens: [String]) -> EventLoopFuture<String> {
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
}
}
}
}
63 changes: 63 additions & 0 deletions Sources/FCM/Helpers/FCM+DeleteTopic.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import Foundation
import Vapor

extension FCM {
public func deleteTopic(_ name: String, tokens: String...) -> EventLoopFuture<Void> {
deleteTopic(name, tokens: tokens)
}

public func deleteTopic(_ name: String, tokens: String..., on eventLoop: EventLoop) -> EventLoopFuture<Void> {
deleteTopic(name, tokens: tokens).hop(to: eventLoop)
}

public func deleteTopic(_ name: String, tokens: [String]) -> EventLoopFuture<Void> {
_deleteTopic(name, tokens: tokens)
}

public func deleteTopic(_ name: String, tokens: [String], on eventLoop: EventLoop) -> EventLoopFuture<Void> {
_deleteTopic(name, tokens: tokens).hop(to: eventLoop)
}

private func _deleteTopic(_ name: String, tokens: [String]) -> EventLoopFuture<Void> {
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)
}
}
}
}
}
}

0 comments on commit d1bb4a5

Please sign in to comment.