Compare commits
9 Commits
f454cc80e4
...
master
Author | SHA1 | Date | |
---|---|---|---|
|
ec309e77db | ||
|
c19bc3c299 | ||
|
5cc7782d4e | ||
|
2f0827a430 | ||
|
34edc88611 | ||
|
f2f802a24f | ||
|
151c6a432f | ||
|
baf3a1c06d | ||
|
ec0b94ee39 |
@ -29,7 +29,7 @@ let package = Package(
|
|||||||
.unsafeFlags(["-cross-module-optimization"], .when(configuration: .release))
|
.unsafeFlags(["-cross-module-optimization"], .when(configuration: .release))
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
.executableTarget(name: "Run", dependencies: [.target(name: "App")]),
|
.executableTarget(name: "FlurSchnaps-Server", dependencies: [.target(name: "App")]),
|
||||||
.testTarget(name: "AppTests", dependencies: [
|
.testTarget(name: "AppTests", dependencies: [
|
||||||
.target(name: "App"),
|
.target(name: "App"),
|
||||||
.product(name: "XCTVapor", package: "vapor"),
|
.product(name: "XCTVapor", package: "vapor"),
|
||||||
|
89
Sources/App/APNSInterface.swift
Normal file
89
Sources/App/APNSInterface.swift
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
import Foundation
|
||||||
|
import APNSwift
|
||||||
|
import NIO
|
||||||
|
import Crypto
|
||||||
|
|
||||||
|
struct APNSInterface {
|
||||||
|
|
||||||
|
private struct Payload: Codable {}
|
||||||
|
|
||||||
|
private var apnsConfiguration: APNSClientConfiguration!
|
||||||
|
private let title: String
|
||||||
|
private let messages: [String]
|
||||||
|
private let topic: String
|
||||||
|
private let apnsEventGroup = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount)
|
||||||
|
private let apnsRequestEncoder = JSONEncoder()
|
||||||
|
private let apnsResponseDecoder = JSONDecoder()
|
||||||
|
|
||||||
|
init?(_ config: ServerConfiguration) {
|
||||||
|
guard let key = load(keyAtPath: config.privateKeyFilePath) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
let authentication = APNSClientConfiguration.AuthenticationMethod.jwt(
|
||||||
|
privateKey: key,
|
||||||
|
keyIdentifier: config.keyIdentifier,
|
||||||
|
teamIdentifier: config.teamIdentifier)
|
||||||
|
|
||||||
|
|
||||||
|
apnsConfiguration = APNSClientConfiguration(
|
||||||
|
authenticationMethod: authentication,
|
||||||
|
environment: .sandbox)
|
||||||
|
title = config.messageTitle
|
||||||
|
messages = config.messages
|
||||||
|
topic = config.topic
|
||||||
|
}
|
||||||
|
|
||||||
|
func sendPush(to tokens: Set<String>) async {
|
||||||
|
let client = APNSClient(
|
||||||
|
configuration: apnsConfiguration,
|
||||||
|
eventLoopGroupProvider: .shared(apnsEventGroup),
|
||||||
|
responseDecoder: apnsResponseDecoder,
|
||||||
|
requestEncoder: apnsRequestEncoder)
|
||||||
|
log(info: "Client created")
|
||||||
|
let alert = APNSAlertNotificationContent(
|
||||||
|
title: .raw(title),
|
||||||
|
body: .raw(messages.randomElement()!))
|
||||||
|
|
||||||
|
let apnsNotification = APNSAlertNotification(
|
||||||
|
alert: alert,
|
||||||
|
expiration: .none,
|
||||||
|
priority: .immediately,
|
||||||
|
topic: topic,
|
||||||
|
payload: Payload(),
|
||||||
|
sound: .default)
|
||||||
|
do {
|
||||||
|
for token in tokenStorage.tokens {
|
||||||
|
log(info: "Sending push to \(token.prefix(6))...")
|
||||||
|
try await client.sendAlertNotification(
|
||||||
|
apnsNotification,
|
||||||
|
deviceToken: token,
|
||||||
|
deadline: .now() + .seconds(10))
|
||||||
|
log(info: "Sent push to \(token.prefix(6))...")
|
||||||
|
}
|
||||||
|
} catch let error as APNSError {
|
||||||
|
print(error)
|
||||||
|
} catch {
|
||||||
|
log(error: error.localizedDescription)
|
||||||
|
}
|
||||||
|
do {
|
||||||
|
log("Closing client")
|
||||||
|
try client.syncShutdown()
|
||||||
|
} catch {
|
||||||
|
log(error: error.localizedDescription)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func load(keyAtPath path: String) -> P256.Signing.PrivateKey? {
|
||||||
|
do {
|
||||||
|
guard let key = try P256.Signing.PrivateKey.loadFrom(filePath: path) else {
|
||||||
|
print("Failed to read key \(path)")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return key
|
||||||
|
} catch {
|
||||||
|
print("Failed to load key \(error)")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -16,11 +16,9 @@ struct ServerConfiguration: Codable {
|
|||||||
|
|
||||||
let messageTitle: String
|
let messageTitle: String
|
||||||
|
|
||||||
let messageBody: String
|
let messages: [String]
|
||||||
|
|
||||||
let buttonPin: Int
|
let buttonPin: Int
|
||||||
|
|
||||||
let bounceTime: Double
|
let bounceTime: Double
|
||||||
|
|
||||||
let tokens: [String]
|
|
||||||
}
|
}
|
||||||
|
@ -9,4 +9,6 @@ enum ServerStatus: Int, Codable {
|
|||||||
case pushFailed = 2
|
case pushFailed = 2
|
||||||
|
|
||||||
case starting = 3
|
case starting = 3
|
||||||
|
|
||||||
|
case failedToStart = 4
|
||||||
}
|
}
|
||||||
|
47
Sources/App/TokenStorage.swift
Normal file
47
Sources/App/TokenStorage.swift
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import Foundation
|
||||||
|
import BinaryCodable
|
||||||
|
|
||||||
|
struct TokenStorage {
|
||||||
|
|
||||||
|
private(set) var tokens: Set<String>
|
||||||
|
|
||||||
|
private let fileUrl: URL
|
||||||
|
|
||||||
|
private let encoder = BinaryEncoder()
|
||||||
|
|
||||||
|
init(in folder: URL) {
|
||||||
|
tokens = []
|
||||||
|
fileUrl = folder.appendingPathComponent("tokens")
|
||||||
|
|
||||||
|
loadTokensFromDisk()
|
||||||
|
}
|
||||||
|
|
||||||
|
private mutating func loadTokensFromDisk() {
|
||||||
|
guard FileManager.default.fileExists(atPath: fileUrl.path) else {
|
||||||
|
log(info: "No tokens loaded")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
do {
|
||||||
|
let data = try Data(contentsOf: fileUrl)
|
||||||
|
tokens = try BinaryDecoder().decode(from: data)
|
||||||
|
log(info: "\(tokens.count) tokens loaded")
|
||||||
|
} catch {
|
||||||
|
log(error: "Failed to read token file: \(error)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mutating func add(_ tokenUpload: TokenUpload) {
|
||||||
|
if let oldToken = tokenUpload.previousToken {
|
||||||
|
tokens.remove(oldToken.hex)
|
||||||
|
}
|
||||||
|
tokens.insert(tokenUpload.currentToken.hex)
|
||||||
|
do {
|
||||||
|
let data = try encoder.encode(tokens)
|
||||||
|
try data.write(to: fileUrl)
|
||||||
|
} catch {
|
||||||
|
log(error: "Failed to write token file: \(error)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -1,16 +1,8 @@
|
|||||||
import Vapor
|
import Vapor
|
||||||
import SwiftyGPIO
|
import SwiftyGPIO
|
||||||
import APNSwift
|
|
||||||
import NIO
|
|
||||||
import Crypto
|
|
||||||
|
|
||||||
private struct Payload: Codable {}
|
var apns: APNSInterface?
|
||||||
|
var tokenStorage: TokenStorage!
|
||||||
private var apnsConfiguration: APNSClientConfiguration!
|
|
||||||
private var apnsNotification: APNSAlertNotification<Payload>!
|
|
||||||
private let apnsEventGroup = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount)
|
|
||||||
private let apnsRequestEncoder = JSONEncoder()
|
|
||||||
private let apnsResponseDecoder = JSONDecoder()
|
|
||||||
|
|
||||||
// configures your application
|
// configures your application
|
||||||
public func configure(_ app: Application) throws {
|
public func configure(_ app: Application) throws {
|
||||||
@ -23,13 +15,18 @@ public func configure(_ app: Application) throws {
|
|||||||
let data = try Data(contentsOf: configUrl)
|
let data = try Data(contentsOf: configUrl)
|
||||||
config = try JSONDecoder().decode(from: data)
|
config = try JSONDecoder().decode(from: data)
|
||||||
} catch {
|
} catch {
|
||||||
print("Failed to load config: \(error)")
|
print("Failed to load config from \(configUrl.path): \(error)")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
Log.set(logFile: URL(fileURLWithPath: config.logPath))
|
||||||
|
|
||||||
|
tokenStorage = .init(in: resourcesFolderUrl)
|
||||||
|
|
||||||
app.http.server.configuration.port = config.port
|
app.http.server.configuration.port = config.port
|
||||||
|
|
||||||
guard configureAPNS(config) else {
|
apns = .init(config)
|
||||||
|
guard apns != nil else {
|
||||||
|
serverStatus = .failedToStart
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -39,6 +36,7 @@ public func configure(_ app: Application) throws {
|
|||||||
try routes(app)
|
try routes(app)
|
||||||
|
|
||||||
serverStatus = .running
|
serverStatus = .running
|
||||||
|
log(info: "Server is running")
|
||||||
}
|
}
|
||||||
|
|
||||||
private extension JSONDecoder {
|
private extension JSONDecoder {
|
||||||
@ -48,47 +46,6 @@ private extension JSONDecoder {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func load(keyAtPath path: String) -> P256.Signing.PrivateKey? {
|
|
||||||
do {
|
|
||||||
guard let key = try P256.Signing.PrivateKey.loadFrom(filePath: path) else {
|
|
||||||
print("Failed to read key \(path)")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return key
|
|
||||||
} catch {
|
|
||||||
print("Failed to load key \(error)")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func configureAPNS(_ config: ServerConfiguration) -> Bool {
|
|
||||||
guard let key = load(keyAtPath: config.privateKeyFilePath) else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
let authentication = APNSClientConfiguration.AuthenticationMethod.jwt(
|
|
||||||
privateKey: key,
|
|
||||||
keyIdentifier: config.keyIdentifier,
|
|
||||||
teamIdentifier: config.teamIdentifier)
|
|
||||||
|
|
||||||
|
|
||||||
apnsConfiguration = APNSClientConfiguration(
|
|
||||||
authenticationMethod: authentication,
|
|
||||||
environment: .sandbox)
|
|
||||||
|
|
||||||
let alert = APNSAlertNotificationContent(
|
|
||||||
title: .raw(config.messageTitle),
|
|
||||||
body: .raw(config.messageBody))
|
|
||||||
|
|
||||||
apnsNotification = APNSAlertNotification(
|
|
||||||
alert: alert,
|
|
||||||
expiration: .none,
|
|
||||||
priority: .immediately,
|
|
||||||
topic: config.topic,
|
|
||||||
payload: Payload(),
|
|
||||||
sound: .default)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
private func configureGPIO(_ config: ServerConfiguration) {
|
private func configureGPIO(_ config: ServerConfiguration) {
|
||||||
let gpio = RaspberryGPIO(
|
let gpio = RaspberryGPIO(
|
||||||
name: "GPIO\(config.buttonPin)",
|
name: "GPIO\(config.buttonPin)",
|
||||||
@ -96,7 +53,7 @@ private func configureGPIO(_ config: ServerConfiguration) {
|
|||||||
baseAddr: 0x7E000000)
|
baseAddr: 0x7E000000)
|
||||||
|
|
||||||
gpio.direction = .IN
|
gpio.direction = .IN
|
||||||
gpio.pull = .down
|
gpio.pull = .neither
|
||||||
gpio.bounceTime = config.bounceTime
|
gpio.bounceTime = config.bounceTime
|
||||||
gpio.onChange { _ in
|
gpio.onChange { _ in
|
||||||
log(info: "Push detected")
|
log(info: "Push detected")
|
||||||
@ -106,32 +63,11 @@ private func configureGPIO(_ config: ServerConfiguration) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func sendPush() {
|
private func sendPush() {
|
||||||
|
guard !tokenStorage.tokens.isEmpty else {
|
||||||
|
log(info: "No tokens registered to send push")
|
||||||
|
return
|
||||||
|
}
|
||||||
Task(priority: .userInitiated) {
|
Task(priority: .userInitiated) {
|
||||||
let client = APNSClient(
|
await apns?.sendPush(to: tokenStorage.tokens)
|
||||||
configuration: apnsConfiguration,
|
|
||||||
eventLoopGroupProvider: .shared(apnsEventGroup),
|
|
||||||
responseDecoder: apnsResponseDecoder,
|
|
||||||
requestEncoder: apnsRequestEncoder)
|
|
||||||
log(info: "Client created")
|
|
||||||
do {
|
|
||||||
for token in knownTokens {
|
|
||||||
log(info: "Sending push to \(token.prefix(6))...")
|
|
||||||
try await client.sendAlertNotification(
|
|
||||||
apnsNotification,
|
|
||||||
deviceToken: token,
|
|
||||||
deadline: .now() + .seconds(10))
|
|
||||||
log(info: "Sent push to \(token.prefix(6))...")
|
|
||||||
}
|
|
||||||
} catch let error as APNSError {
|
|
||||||
print(error)
|
|
||||||
} catch {
|
|
||||||
log(error: error.localizedDescription)
|
|
||||||
}
|
|
||||||
do {
|
|
||||||
log("Closing client")
|
|
||||||
try client.syncShutdown()
|
|
||||||
} catch {
|
|
||||||
log(error: error.localizedDescription)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,8 +6,6 @@ private let requestDecoder = BinaryDecoder()
|
|||||||
|
|
||||||
var serverStatus: ServerStatus = .starting
|
var serverStatus: ServerStatus = .starting
|
||||||
|
|
||||||
var knownTokens = Set<String>()
|
|
||||||
|
|
||||||
func routes(_ app: Application) throws {
|
func routes(_ app: Application) throws {
|
||||||
|
|
||||||
app.post("token") { req async throws -> HTTPResponseStatus in
|
app.post("token") { req async throws -> HTTPResponseStatus in
|
||||||
@ -15,10 +13,7 @@ func routes(_ app: Application) throws {
|
|||||||
return .badRequest
|
return .badRequest
|
||||||
}
|
}
|
||||||
let tokenUpload: TokenUpload = try requestDecoder.decode(from: data)
|
let tokenUpload: TokenUpload = try requestDecoder.decode(from: data)
|
||||||
if let oldToken = tokenUpload.previousToken {
|
tokenStorage.add(tokenUpload)
|
||||||
knownTokens.remove(oldToken.hex)
|
|
||||||
}
|
|
||||||
knownTokens.insert(tokenUpload.currentToken.hex)
|
|
||||||
return .ok
|
return .ok
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user