Convert functions to async/await

This commit is contained in:
Christoph Hagen 2022-10-12 19:28:28 +02:00
parent 7ef5da08d0
commit 26adcc2868
5 changed files with 124 additions and 109 deletions

View File

@ -0,0 +1,18 @@
//
// File.swift
//
//
// Created by CH on 12.10.22.
//
import Foundation
extension Optional {
func unwrap(or error: @autoclosure () -> Error) throws -> Wrapped {
guard let unwrapped = self else {
throw error()
}
return unwrapped
}
}

View File

@ -87,17 +87,14 @@ final class SQLiteDatabase {
} }
} }
func passwordHashForExistingPlayer(named name: PlayerName, in database: Database) -> EventLoopFuture<PasswordHash> { /**
User.query(on: database).filter(\.$name == name).first() Change the password of a user with a recovery token
.unwrap(or: Abort(.forbidden)).map { $0.passwordHash }
}
func deletePlayer(named name: PlayerName, in database: Database) -> EventLoopFuture<Void> {
user(named: name, in: database).flatMap { user in
self.tables.leaveTable(player: user, in: database)
}.flatMap {
User.query(on: database).filter(\.$name == name).delete()
} }
func deletePlayer(named name: PlayerName, in database: Database) async throws {
let user = try await user(named: name, in: database)
try await tables.leaveTable(player: user, in: database)
try await User.query(on: database).filter(\.$name == name).delete()
} }
func isValid(sessionToken token: SessionToken) -> Bool { func isValid(sessionToken token: SessionToken) -> Bool {
@ -147,29 +144,25 @@ final class SQLiteDatabase {
tables.tableInfo(player: player) tables.tableInfo(player: player)
} }
private func points(for player: PlayerName, in database: Database) -> EventLoopFuture<Int> { private func points(for player: PlayerName, in database: Database) async throws -> Int {
User.query(on: database) try await User.query(on: database)
.filter(\.$name == player) .filter(\.$name == player)
.first() .first()
.unwrap(or: Abort(.notFound)) .unwrap(or: Abort(.notFound))
.map { $0.points } .points
} }
private func user(named name: PlayerName, in database: Database) -> EventLoopFuture<User> { private func user(named name: PlayerName, in database: Database) async throws -> User {
User.query(on: database) try await User
.query(on: database)
.filter(\.$name == name) .filter(\.$name == name)
.first() .first()
.unwrap(or: Abort(.notFound)) .unwrap(or: Abort(.notFound))
} }
private func user(withToken token: SessionToken, in database: Database) -> EventLoopFuture<User> { private func user(withToken token: SessionToken, in database: Database) async throws -> User {
database.eventLoop let name = try playerNameForToken[token].unwrap(or: Abort(.unauthorized))
.future() return try await user(named: name, in: database)
.map { self.playerNameForToken[token] }
.unwrap(or: Abort(.unauthorized))
.flatMap { name in
self.user(named: name, in: database)
}
} }
// MARK: Tables // MARK: Tables
@ -181,26 +174,23 @@ final class SQLiteDatabase {
- Parameter isPublic: Indicates that this is a game joinable by everyone - Parameter isPublic: Indicates that this is a game joinable by everyone
- Returns: The table id - Returns: The table id
*/ */
func createTable(named name: TableName, player: PlayerName, isPublic: Bool, in database: Database) -> EventLoopFuture<TableInfo> { func createTable(named name: TableName, player: PlayerName, isPublic: Bool, in database: Database) async throws -> TableInfo {
user(named: player, in: database).flatMap { player in let user = try await user(named: player, in: database)
self.tables.createTable(named: name, player: player, isPublic: isPublic, in: database) return try await tables.createTable(named: name, player: user, isPublic: isPublic, in: database)
}
} }
func getPublicTableInfos() -> [PublicTableInfo] { func getPublicTableInfos() -> [PublicTableInfo] {
tables.publicTableList tables.publicTableList
} }
func join(tableId: UUID, playerToken: SessionToken, in database: Database) -> EventLoopFuture<TableInfo> { func join(tableId: UUID, playerToken: SessionToken, in database: Database) async throws -> TableInfo {
user(withToken: playerToken, in: database).flatMap { player in let user = try await user(withToken: playerToken, in: database)
self.tables.join(tableId: tableId, player: player, in: database) return try await tables.join(tableId: tableId, player: user, in: database)
}
} }
func leaveTable(playerToken: SessionToken, in database: Database) -> EventLoopFuture<Void> { func leaveTable(playerToken: SessionToken, in database: Database) async throws {
user(withToken: playerToken, in: database).flatMap { player in let user = try await user(withToken: playerToken, in: database)
self.tables.leaveTable(player: player, in: database) try await tables.leaveTable(player: user, in: database)
}
} }
func performAction(playerToken: SessionToken, action: PlayerAction) -> PlayerActionResult { func performAction(playerToken: SessionToken, action: PlayerAction) -> PlayerActionResult {

View File

@ -38,19 +38,14 @@ final class TableManagement {
- Parameter isPublic: Indicates that this is a game joinable by everyone - Parameter isPublic: Indicates that this is a game joinable by everyone
- Returns: The table id - Returns: The table id
*/ */
func createTable(named name: TableName, player: User, isPublic: Bool, in database: Database) -> EventLoopFuture<TableInfo> { func createTable(named name: TableName, player: User, isPublic: Bool, in database: Database) async throws -> TableInfo {
let table = Table(name: name, isPublic: isPublic) let table = Table(name: name, isPublic: isPublic)
return table.create(on: database).flatMap { try await table.create(on: database)
player.$table.id = table.id player.$table.id = table.id
return player.update(on: database) try await player.update(on: database)
}.flatMap { let waitingTable = WaitingTable(newTable: table)
Table.query(on: database).with(\.$players).filter(\.$id == table.id!).first() self.tables[waitingTable.id] = waitingTable
}.unwrap(or: Abort(.notFound)) return waitingTable.tableInfo(forPlayer: player.name)
.map { storedTable in
let table = WaitingTable(newTable: storedTable)
self.tables[table.id] = table
return table.tableInfo(forPlayer: player.name)
}
} }
/// A list of all public tables /// A list of all public tables
@ -77,51 +72,51 @@ final class TableManagement {
- Parameter player: The name of the player who wants to join. - Parameter player: The name of the player who wants to join.
- Returns: The result of the join operation - Returns: The result of the join operation
*/ */
func join(tableId: UUID, player: User, in database: Database) -> EventLoopFuture<TableInfo> { func join(tableId: UUID, player: User, in database: Database) async throws -> TableInfo {
return database.eventLoop.future().flatMapThrowing { _ -> ManageableTable in let table = try joinableTable(for: player, id: tableId)
if let existing = self.currentTable(for: player.name) { player.$table.id = table.id
guard existing.id == tableId else { try await player.update(on: database)
throw Abort(.forbidden) // 403 table.sendUpdateToAllPlayers()
} return table.tableInfo(forPlayer: player.name)
return existing }
private func joinableTable(for player: User, id tableId: UUID) throws -> ManageableTable {
if let existing = self.currentTable(for: player.name) {
guard existing.id == tableId else {
throw Abort(.forbidden) // 403
} }
guard let table = self.tables[tableId] else { return existing
throw Abort(.gone) // 410
}
guard let joinableTable = table as? WaitingTable,
joinableTable.add(player: player.name, points: player.points) else {
throw Abort(.expectationFailed) // 417
}
return joinableTable
}.flatMap { table -> EventLoopFuture<ManageableTable> in
player.$table.id = table.id
return player.update(on: database).map { table }
}.map { table in
table.sendUpdateToAllPlayers()
return table.tableInfo(forPlayer: player.name)
} }
guard let table = self.tables[tableId] else {
throw Abort(.gone) // 410
}
guard let joinableTable = table as? WaitingTable,
joinableTable.add(player: player.name, points: player.points) else {
throw Abort(.expectationFailed) // 417
}
return joinableTable
} }
/** /**
A player leaves the table it previously joined A player leaves the table it previously joined
- Parameter player: The player leaving the table - Parameter player: The player leaving the table
*/ */
func leaveTable(player: User, in database: Database) -> EventLoopFuture<Void> { func leaveTable(player: User, in database: Database) async throws {
guard let oldTable = currentTable(for: player.name) else { guard let oldTable = currentTable(for: player.name) else {
return database.eventLoop.makeSucceededVoidFuture() return
} }
player.$table.id = nil player.$table.id = nil
guard let table = WaitingTable(oldTable: oldTable, removing: player.name) else { guard let table = WaitingTable(oldTable: oldTable, removing: player.name) else {
tables[oldTable.id] = nil tables[oldTable.id] = nil
return player.update(on: database).flatMap { try await player.update(on: database)
Table.query(on: database).filter(\.$id == oldTable.id).delete() try await Table.query(on: database).filter(\.$id == oldTable.id).delete()
} return
} }
/// `player.canStartGame` is automatically set to false, because table is not full /// `player.canStartGame` is automatically set to false, because table is not full
tables[table.id] = table tables[table.id] = table
table.sendUpdateToAllPlayers() table.sendUpdateToAllPlayers()
// TODO: Update points for all players // TODO: Update points for all players
return player.update(on: database) try await player.update(on: database)
} }
func connect(player: PlayerName, using socket: WebSocket) -> Bool { func connect(player: PlayerName, using socket: WebSocket) -> Bool {

View File

@ -48,13 +48,25 @@ final class PasswordReset: Model {
@Field(.expiry) @Field(.expiry)
var expiryDate: Date var expiryDate: Date
init() { } init() {
self.resetToken = .newToken()
self.expiryDate = Self.currentExpiryDate()
}
func renew() {
self.resetToken = .newToken()
self.expiryDate = Self.currentExpiryDate()
}
/// Creates a new password reset. /// Creates a new password reset.
init(id: UUID? = nil, user: User) { init(id: UUID? = nil, user: User) {
self.id = id self.id = id
self.user = user self.$user.id = user.id!
self.resetToken = .newToken() self.resetToken = .newToken()
self.expiryDate = Date().addingTimeInterval(15*60) self.expiryDate = Self.currentExpiryDate()
}
private static func currentExpiryDate() -> Date {
Date().addingTimeInterval(15*60)
} }
} }

View File

@ -76,17 +76,17 @@ func registerPlayer(_ app: Application) {
- Returns: Nothing - Returns: Nothing
*/ */
func deletePlayer(_ app: Application) { func deletePlayer(_ app: Application) {
app.post("player", "delete", ":name") { req -> EventLoopFuture<String> in app.post("player", "delete", ":name") { request async throws -> HTTPResponseStatus in
guard let name = req.parameters.get("name"), guard let name = request.parameters.get("name"),
let password = req.body.string else { let password = request.body.string else {
throw Abort(.badRequest) // 400 return .badRequest // 400
} }
return server.passwordHashForExistingPlayer(named: name, in: req.db) let hash = try await server.passwordHashForExistingPlayer(named: name, in: request.db)
.guard({ hash in guard try request.password.verify(password, created: hash) else {
(try? req.password.verify(password, created: hash)) ?? false return .forbidden // 403
}, else: Abort(.forbidden)).flatMap { _ in }
server.deletePlayer(named: name, in: req.db) try await server.deletePlayer(named: name, in: request.db)
}.map { "" } return .ok
} }
} }
@ -101,17 +101,16 @@ func deletePlayer(_ app: Application) {
- Returns: The session token for the user - Returns: The session token for the user
*/ */
func loginPlayer(_ app: Application) { func loginPlayer(_ app: Application) {
app.post("player", "login", ":name") { req -> EventLoopFuture<String> in app.post("player", "login", ":name") { request async throws -> String in
guard let name = req.parameters.get("name"), guard let name = request.parameters.get("name"),
let password = req.body.string else { let password = request.body.string else {
throw Abort(.badRequest) // 400 throw Abort(.badRequest) // 400
} }
return server.passwordHashForExistingPlayer(named: name, in: req.db) let hash = try await server.passwordHashForExistingPlayer(named: name, in: request.db)
.guard({ hash in guard try request.password.verify(password, created: hash) else {
(try? req.password.verify(password, created: hash)) ?? false throw Abort(.forbidden) // 403
}, else: Abort(.forbidden)).map { _ in }
server.startNewSessionForRegisteredPlayer(named: name) return server.startNewSessionForRegisteredPlayer(named: name)
}
} }
} }
@ -205,10 +204,10 @@ func openWebsocket(_ app: Application) {
- 401: The session token is invalid - 401: The session token is invalid
*/ */
func createTable(_ app: Application) { func createTable(_ app: Application) {
app.post("table", "create", ":visibility", ":name") { req -> EventLoopFuture<String> in app.post("table", "create", ":visibility", ":name") { request -> String in
guard let visibility = req.parameters.get("visibility"), guard let visibility = request.parameters.get("visibility"),
let tableName = req.parameters.get("name"), let tableName = request.parameters.get("name"),
let token = req.body.string else { let token = request.body.string else {
throw Abort(.badRequest) // 400 throw Abort(.badRequest) // 400
} }
let isPublic: Bool let isPublic: Bool
@ -223,8 +222,8 @@ func createTable(_ app: Application) {
guard let player = server.registeredPlayerExists(withSessionToken: token) else { guard let player = server.registeredPlayerExists(withSessionToken: token) else {
throw Abort(.unauthorized) // 401 throw Abort(.unauthorized) // 401
} }
return server.createTable(named: tableName, player: player, isPublic: isPublic, in: req.db) let result = try await server.createTable(named: tableName, player: player, isPublic: isPublic, in: request.db)
.flatMapThrowing(encodeJSON) return try encodeJSON(result)
} }
} }
@ -263,14 +262,14 @@ func getPublicTables(_ app: Application) {
*/ */
func joinTable(_ app: Application) { func joinTable(_ app: Application) {
app.post("table", "join", ":table") { req -> EventLoopFuture<String> in app.post("table", "join", ":table") { request -> String in
guard let string = req.parameters.get("table"), guard let string = request.parameters.get("table"),
let table = UUID(uuidString: string), let table = UUID(uuidString: string),
let token = req.body.string else { let token = request.body.string else {
throw Abort(.badRequest) throw Abort(.badRequest)
} }
return server.join(tableId: table, playerToken: token, in: req.db) let result = try await server.join(tableId: table, playerToken: token, in: request.db)
.flatMapThrowing(encodeJSON) return try encodeJSON(result)
} }
} }
@ -283,11 +282,12 @@ func joinTable(_ app: Application) {
- Returns: Nothing - Returns: Nothing
*/ */
func leaveTable(_ app: Application) { func leaveTable(_ app: Application) {
app.post("table", "leave") { req -> EventLoopFuture<String> in app.post("table", "leave") { request -> HTTPResponseStatus in
guard let token = req.body.string else { guard let token = request.body.string else {
throw Abort(.badRequest) throw Abort(.badRequest)
} }
return server.leaveTable(playerToken: token, in: req.db).map { "" } try await server.leaveTable(playerToken: token, in: request.db)
return .ok
} }
} }