add support of party alias

This commit is contained in:
Evgenii Alekseev 2020-03-09 01:48:24 +03:00
parent 1e6064e081
commit 16ce0bf61c
36 changed files with 393 additions and 77 deletions

View File

@ -0,0 +1,5 @@
create table parties (
player_id bigserial unique,
party_name text not null,
party_alias text);
create unique index parties_party_name_idx on parties(party_name);

View File

@ -0,0 +1,5 @@
create table parties (
player_id integer primary key autoincrement,
party_name text not null,
party_alias text);
create unique index parties_party_name_idx on parties(party_name);

View File

@ -16,7 +16,9 @@ import me.arcanis.ffxivbis.service.Ariyala
import scala.concurrent.{ExecutionContext, Future}
class AriyalaHelper(ariyala: ActorRef) {
trait AriyalaHelper {
def ariyala: ActorRef
def downloadBiS(link: String, job: Job.Job)
(implicit executionContext: ExecutionContext, timeout: Timeout): Future[BiS] =

View File

@ -17,7 +17,9 @@ import me.arcanis.ffxivbis.service.impl.DatabaseBiSHandler
import scala.concurrent.{ExecutionContext, Future}
class BiSHelper(storage: ActorRef, ariyala: ActorRef) extends AriyalaHelper(ariyala) {
trait BiSHelper extends AriyalaHelper {
def storage: ActorRef
def addPieceBiS(playerId: PlayerId, piece: Piece)
(implicit executionContext: ExecutionContext, timeout: Timeout): Future[Int] =

View File

@ -18,7 +18,9 @@ import me.arcanis.ffxivbis.service.impl.DatabaseLootHandler
import scala.concurrent.{ExecutionContext, Future}
class LootHelper(storage: ActorRef) {
trait LootHelper {
def storage: ActorRef
def addPieceLoot(playerId: PlayerId, piece: Piece)
(implicit executionContext: ExecutionContext, timeout: Timeout): Future[Int] =

View File

@ -12,12 +12,14 @@ import akka.actor.ActorRef
import akka.pattern.ask
import akka.util.Timeout
import me.arcanis.ffxivbis.http.api.v1.json.ApiAction
import me.arcanis.ffxivbis.models.{Party, Player, PlayerId}
import me.arcanis.ffxivbis.models.{Party, PartyDescription, Player, PlayerId}
import me.arcanis.ffxivbis.service.impl.{DatabaseBiSHandler, DatabasePartyHandler}
import scala.concurrent.{ExecutionContext, Future}
class PlayerHelper(storage: ActorRef, ariyala: ActorRef) extends AriyalaHelper(ariyala) {
trait PlayerHelper extends AriyalaHelper {
def storage: ActorRef
def addPlayer(player: Player)
(implicit executionContext: ExecutionContext, timeout: Timeout): Future[Int] =
@ -38,6 +40,10 @@ class PlayerHelper(storage: ActorRef, ariyala: ActorRef) extends AriyalaHelper(a
case ApiAction.remove => removePlayer(player.playerId)
}
def getPartyDescription(partyId: String)
(implicit executionContext: ExecutionContext, timeout: Timeout): Future[PartyDescription] =
(storage ? DatabasePartyHandler.GetPartyDescription(partyId)).mapTo[PartyDescription]
def getPlayers(partyId: String, maybePlayerId: Option[PlayerId])
(implicit executionContext: ExecutionContext, timeout: Timeout): Future[Seq[Player]] =
maybePlayerId match {
@ -50,4 +56,8 @@ class PlayerHelper(storage: ActorRef, ariyala: ActorRef) extends AriyalaHelper(a
def removePlayer(playerId: PlayerId)
(implicit executionContext: ExecutionContext, timeout: Timeout): Future[Int] =
(storage ? DatabasePartyHandler.RemovePlayer(playerId)).mapTo[Int]
def updateDescription(partyDescription: PartyDescription)
(implicit executionContext: ExecutionContext, timeout: Timeout): Future[Int] =
(storage ? DatabasePartyHandler.UpdateParty(partyDescription)).mapTo[Int]
}

View File

@ -18,8 +18,8 @@ import scala.io.Source
class Swagger(config: Config) extends SwaggerHttpService {
override val apiClasses: Set[Class[_]] = Set(
classOf[api.v1.BiSEndpoint], classOf[api.v1.LootEndpoint],
classOf[api.v1.PlayerEndpoint], classOf[api.v1.TypesEndpoint],
classOf[api.v1.UserEndpoint]
classOf[api.v1.PartyEndpoint], classOf[api.v1.PlayerEndpoint],
classOf[api.v1.TypesEndpoint], classOf[api.v1.UserEndpoint]
)
override val info: Info = Info(

View File

@ -17,7 +17,9 @@ import me.arcanis.ffxivbis.service.impl.DatabaseUserHandler
import scala.concurrent.{ExecutionContext, Future}
class UserHelper(storage: ActorRef) {
trait UserHelper {
def storage: ActorRef
def addUser(user: User, isHashedPassword: Boolean)
(implicit executionContext: ExecutionContext, timeout: Timeout): Future[Int] =

View File

@ -27,8 +27,8 @@ import me.arcanis.ffxivbis.models.PlayerId
import scala.util.{Failure, Success}
@Path("api/v1")
class BiSEndpoint(override val storage: ActorRef, ariyala: ActorRef)(implicit timeout: Timeout)
extends BiSHelper(storage, ariyala) with Authorization with JsonSupport {
class BiSEndpoint(override val storage: ActorRef, override val ariyala: ActorRef)(implicit timeout: Timeout)
extends BiSHelper with Authorization with JsonSupport {
def route: Route = createBiS ~ getBiS ~ modifyBiS

View File

@ -28,7 +28,7 @@ import scala.util.{Failure, Success}
@Path("api/v1")
class LootEndpoint(override val storage: ActorRef)(implicit timeout: Timeout)
extends LootHelper(storage) with Authorization with JsonSupport with HttpHandler {
extends LootHelper with Authorization with JsonSupport with HttpHandler {
def route: Route = getLoot ~ modifyLoot

View File

@ -0,0 +1,107 @@
/*
* Copyright (c) 2020 Evgeniy Alekseev.
*
* This file is part of ffxivbis
* (see https://github.com/arcan1s/ffxivbis).
*
* License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause
*/
package me.arcanis.ffxivbis.http.api.v1
import akka.actor.ActorRef
import akka.http.scaladsl.model.{HttpEntity, StatusCodes}
import akka.http.scaladsl.server.Directives._
import akka.http.scaladsl.server._
import akka.util.Timeout
import io.swagger.v3.oas.annotations.enums.ParameterIn
import io.swagger.v3.oas.annotations.media.{Content, Schema}
import io.swagger.v3.oas.annotations.parameters.RequestBody
import io.swagger.v3.oas.annotations.responses.ApiResponse
import io.swagger.v3.oas.annotations.security.SecurityRequirement
import io.swagger.v3.oas.annotations.{Operation, Parameter}
import javax.ws.rs._
import me.arcanis.ffxivbis.http.api.v1.json._
import me.arcanis.ffxivbis.http.{Authorization, PlayerHelper}
import scala.util.{Failure, Success}
@Path("api/v1")
class PartyEndpoint(override val storage: ActorRef, override val ariyala: ActorRef)(implicit timeout: Timeout)
extends PlayerHelper with Authorization with JsonSupport with HttpHandler {
def route: Route = getPartyDescription ~ modifyPartyDescription
@GET
@Path("party/{partyId}/description")
@Produces(value = Array("application/json"))
@Operation(summary = "get party description", description = "Return the party description",
parameters = Array(
new Parameter(name = "partyId", in = ParameterIn.PATH, description = "unique party ID", example = "abcdefgh"),
),
responses = Array(
new ApiResponse(responseCode = "200", description = "Party description",
content = Array(new Content(schema = new Schema(implementation = classOf[PartyDescriptionResponse])))),
new ApiResponse(responseCode = "401", description = "Supplied authorization is invalid",
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))),
new ApiResponse(responseCode = "403", description = "Access is forbidden",
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))),
new ApiResponse(responseCode = "500", description = "Internal server error",
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))),
),
security = Array(new SecurityRequirement(name = "basic auth", scopes = Array("get"))),
tags = Array("party"),
)
def getPartyDescription: Route =
path("party" / Segment / "description") { partyId =>
extractExecutionContext { implicit executionContext =>
authenticateBasicBCrypt(s"party $partyId", authGet(partyId)) { _ =>
get {
onComplete(getPartyDescription(partyId)) {
case Success(response) => complete(PartyDescriptionResponse.fromDescription(response))
case Failure(exception) => throw exception
}
}
}
}
}
@POST
@Consumes(value = Array("application/json"))
@Path("party/{partyId}/description")
@Operation(summary = "modify party description", description = "Edit party description",
parameters = Array(
new Parameter(name = "partyId", in = ParameterIn.PATH, description = "unique party ID", example = "abcdefgh"),
),
requestBody = new RequestBody(description = "new party description", required = true,
content = Array(new Content(schema = new Schema(implementation = classOf[PartyDescriptionResponse])))),
responses = Array(
new ApiResponse(responseCode = "202", description = "Party description has been modified"),
new ApiResponse(responseCode = "400", description = "Invalid parameters were supplied",
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))),
new ApiResponse(responseCode = "401", description = "Supplied authorization is invalid",
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))),
new ApiResponse(responseCode = "403", description = "Access is forbidden",
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))),
new ApiResponse(responseCode = "500", description = "Internal server error",
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))),
),
security = Array(new SecurityRequirement(name = "basic auth", scopes = Array("post"))),
tags = Array("party"),
)
def modifyPartyDescription: Route =
path("party" / Segment / "description") { partyId =>
extractExecutionContext { implicit executionContext =>
authenticateBasicBCrypt(s"party $partyId", authPost(partyId)) { _ =>
post {
entity(as[PartyDescriptionResponse]) { partyDescription =>
val description = partyDescription.copy(partyId = partyId)
onComplete(updateDescription(description.toDescription)) {
case Success(_) => complete(StatusCodes.Accepted, HttpEntity.Empty)
case Failure(exception) => throw exception
}
}
}
}
}
}
}

View File

@ -27,8 +27,8 @@ import me.arcanis.ffxivbis.models.PlayerId
import scala.util.{Failure, Success}
@Path("api/v1")
class PlayerEndpoint(override val storage: ActorRef, ariyala: ActorRef)(implicit timeout: Timeout)
extends PlayerHelper(storage, ariyala) with Authorization with JsonSupport with HttpHandler {
class PlayerEndpoint(override val storage: ActorRef, override val ariyala: ActorRef)(implicit timeout: Timeout)
extends PlayerHelper with Authorization with JsonSupport with HttpHandler {
def route: Route = getParty ~ modifyParty

View File

@ -21,6 +21,7 @@ class RootApiV1Endpoint(storage: ActorRef, ariyala: ActorRef, config: Config)
private val biSEndpoint = new BiSEndpoint(storage, ariyala)
private val lootEndpoint = new LootEndpoint(storage)
private val partyEndpoint = new PartyEndpoint(storage, ariyala)
private val playerEndpoint = new PlayerEndpoint(storage, ariyala)
private val typesEndpoint = new TypesEndpoint(config)
private val userEndpoint = new UserEndpoint(storage)
@ -28,8 +29,8 @@ class RootApiV1Endpoint(storage: ActorRef, ariyala: ActorRef, config: Config)
def route: Route =
handleExceptions(exceptionHandler) {
handleRejections(rejectionHandler) {
biSEndpoint.route ~ lootEndpoint.route ~ playerEndpoint.route ~
typesEndpoint.route ~ userEndpoint.route
biSEndpoint.route ~ lootEndpoint.route ~ partyEndpoint.route ~
playerEndpoint.route ~ typesEndpoint.route ~ userEndpoint.route
}
}
}

View File

@ -28,7 +28,7 @@ import scala.util.{Failure, Success}
@Path("api/v1")
class UserEndpoint(override val storage: ActorRef)(implicit timeout: Timeout)
extends UserHelper(storage) with Authorization with JsonSupport {
extends UserHelper with Authorization with JsonSupport {
def route: Route = createParty ~ createUser ~ deleteUser ~ getUsers

View File

@ -42,6 +42,7 @@ trait JsonSupport extends SprayJsonSupport with DefaultJsonProtocol {
implicit val partyIdFormat: RootJsonFormat[PartyIdResponse] = jsonFormat1(PartyIdResponse.apply)
implicit val pieceFormat: RootJsonFormat[PieceResponse] = jsonFormat3(PieceResponse.apply)
implicit val lootFormat: RootJsonFormat[LootResponse] = jsonFormat2(LootResponse.apply)
implicit val partyDescriptionFormat: RootJsonFormat[PartyDescriptionResponse] = jsonFormat2(PartyDescriptionResponse.apply)
implicit val playerFormat: RootJsonFormat[PlayerResponse] = jsonFormat7(PlayerResponse.apply)
implicit val playerActionFormat: RootJsonFormat[PlayerActionResponse] = jsonFormat2(PlayerActionResponse.apply)
implicit val playerIdFormat: RootJsonFormat[PlayerIdResponse] = jsonFormat3(PlayerIdResponse.apply)

View File

@ -0,0 +1,23 @@
/*
* Copyright (c) 2020 Evgeniy Alekseev.
*
* This file is part of ffxivbis
* (see https://github.com/arcan1s/ffxivbis).
*
* License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause
*/
package me.arcanis.ffxivbis.http.api.v1.json
import io.swagger.v3.oas.annotations.media.Schema
import me.arcanis.ffxivbis.models.PartyDescription
case class PartyDescriptionResponse(
@Schema(description = "party id", required = true) partyId: String,
@Schema(description = "party name") partyAlias: Option[String]) {
def toDescription: PartyDescription = PartyDescription(partyId, partyAlias)
}
object PartyDescriptionResponse {
def fromDescription(description: PartyDescription): PartyDescriptionResponse =
PartyDescriptionResponse(description.partyId, description.partyAlias)
}

View File

@ -13,10 +13,12 @@ import akka.http.scaladsl.model.StatusCodes
import akka.http.scaladsl.server.Directives._
import akka.http.scaladsl.server._
import akka.util.Timeout
import me.arcanis.ffxivbis.http.Authorization
import me.arcanis.ffxivbis.http.{Authorization, PlayerHelper}
class BasePartyView(override val storage: ActorRef)(implicit timeout: Timeout)
extends Authorization {
import scala.util.{Failure, Success}
class BasePartyView(override val storage: ActorRef, override val ariyala: ActorRef)(implicit timeout: Timeout)
extends PlayerHelper with Authorization {
def route: Route = getIndex
@ -25,8 +27,10 @@ class BasePartyView(override val storage: ActorRef)(implicit timeout: Timeout)
extractExecutionContext { implicit executionContext =>
authenticateBasicBCrypt(s"party $partyId", authGet(partyId)) { _ =>
get {
complete {
(StatusCodes.OK, RootView.toHtml(BasePartyView.template(partyId)))
onComplete(getPartyDescription(partyId)) {
case Success(description) =>
complete(StatusCodes.OK, RootView.toHtml(BasePartyView.template(partyId, description.alias)))
case Failure(exception) => throw exception
}
}
}
@ -42,16 +46,16 @@ object BasePartyView {
def root(partyId: String): Text.TypedTag[String] =
a(href:=s"/party/$partyId", title:="root")("root")
def template(partyId: String): String =
def template(partyId: String, alias: String): String =
"<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01//EN\" \"http://www.w3.org/TR/html4/strict.dtd\">" +
html(lang:="en",
head(
titleTag(s"Party $partyId"),
titleTag(s"Party $alias"),
link(rel:="stylesheet", `type`:="text/css", href:="/static/styles.css")
),
body(
h2(s"Party $partyId"),
h2(s"Party $alias"),
br,
h2(a(href:=s"/party/$partyId/players", title:="party")("party")),
h2(a(href:=s"/party/$partyId/bis", title:="bis management")("best in slot")),

View File

@ -19,8 +19,8 @@ import me.arcanis.ffxivbis.models.{Piece, Player, PlayerId}
import scala.concurrent.{ExecutionContext, Future}
import scala.util.Try
class BiSView(override val storage: ActorRef, ariyala: ActorRef)(implicit timeout: Timeout)
extends BiSHelper(storage, ariyala) with Authorization {
class BiSView(override val storage: ActorRef, override val ariyala: ActorRef)(implicit timeout: Timeout)
extends BiSHelper with Authorization {
def route: Route = getBiS ~ modifyBiS

View File

@ -13,13 +13,14 @@ import akka.http.scaladsl.model.StatusCodes
import akka.http.scaladsl.server.Directives._
import akka.http.scaladsl.server._
import akka.util.Timeout
import me.arcanis.ffxivbis.http.UserHelper
import me.arcanis.ffxivbis.models.{Permission, User}
import me.arcanis.ffxivbis.http.{PlayerHelper, UserHelper}
import me.arcanis.ffxivbis.models.{PartyDescription, Permission, User}
import scala.concurrent.Future
import scala.util.{Failure, Success}
class IndexView(storage: ActorRef)(implicit timeout: Timeout)
extends UserHelper(storage) {
class IndexView(override val storage: ActorRef, override val ariyala: ActorRef)(implicit timeout: Timeout)
extends PlayerHelper with UserHelper {
def route: Route = createParty ~ getIndex
@ -27,13 +28,17 @@ class IndexView(storage: ActorRef)(implicit timeout: Timeout)
path("party") {
extractExecutionContext { implicit executionContext =>
post {
formFields("username".as[String], "password".as[String]) { (username, password) =>
onComplete(newPartyId) {
case Success(partyId) =>
formFields("username".as[String], "password".as[String], "alias".as[String].?) { (username, password, maybeAlias) =>
onComplete {
newPartyId.flatMap { partyId =>
val user = User(partyId, username, password, Permission.admin)
onComplete(addUser(user, isHashedPassword = false)) {
case _ => redirect(s"/party/$partyId", StatusCodes.Found)
addUser(user, isHashedPassword = false).flatMap { _ =>
if (maybeAlias.getOrElse("").isEmpty) Future.successful(partyId)
else updateDescription(PartyDescription(partyId, maybeAlias)).map(_ => partyId)
}
}
} {
case Success(partyId) => redirect(s"/party/$partyId", StatusCodes.Found)
case Failure(exception) => throw exception
}
}
@ -67,6 +72,7 @@ object IndexView {
body(
form(action:=s"party", method:="post")(
label("create a new party"),
input(name:="alias", id:="alias", placeholder:="party alias", title:="alias", `type`:="text"),
input(name:="username", id:="username", placeholder:="username", title:="username", `type`:="text"),
input(name:="password", id:="password", placeholder:="password", title:="password", `type`:="password"),
input(name:="add", id:="add", `type`:="submit", value:="add")

View File

@ -20,7 +20,7 @@ import scala.concurrent.{ExecutionContext, Future}
import scala.util.{Failure, Success, Try}
class LootSuggestView(override val storage: ActorRef)(implicit timeout: Timeout)
extends LootHelper(storage) with Authorization {
extends LootHelper with Authorization {
def route: Route = getIndex ~ suggestLoot

View File

@ -20,7 +20,7 @@ import scala.concurrent.{ExecutionContext, Future}
import scala.util.Try
class LootView (override val storage: ActorRef)(implicit timeout: Timeout)
extends LootHelper(storage) with Authorization {
extends LootHelper with Authorization {
def route: Route = getLoot ~ modifyLoot

View File

@ -18,8 +18,8 @@ import me.arcanis.ffxivbis.models._
import scala.concurrent.{ExecutionContext, Future}
class PlayerView(override val storage: ActorRef, ariyala: ActorRef)(implicit timeout: Timeout)
extends PlayerHelper(storage, ariyala) with Authorization {
class PlayerView(override val storage: ActorRef, override val ariyala: ActorRef)(implicit timeout: Timeout)
extends PlayerHelper with Authorization {
def route: Route = getParty ~ modifyParty

View File

@ -16,8 +16,8 @@ import akka.util.Timeout
class RootView(storage: ActorRef, ariyala: ActorRef)(implicit timeout: Timeout) {
private val basePartyView = new BasePartyView(storage)
private val indexView = new IndexView(storage)
private val basePartyView = new BasePartyView(storage, ariyala)
private val indexView = new IndexView(storage, ariyala)
private val biSView = new BiSView(storage, ariyala)
private val lootView = new LootView(storage)

View File

@ -20,7 +20,7 @@ import scala.concurrent.{ExecutionContext, Future}
import scala.util.Try
class UserView(override val storage: ActorRef)(implicit timeout: Timeout)
extends UserHelper(storage) with Authorization {
extends UserHelper with Authorization {
def route: Route = getUsers ~ modifyUsers

View File

@ -15,15 +15,15 @@ import me.arcanis.ffxivbis.service.LootSelector
import scala.jdk.CollectionConverters._
import scala.util.Random
case class Party(partyId: String, rules: Seq[String], players: Map[PlayerId, Player])
case class Party(partyDescription: PartyDescription, rules: Seq[String], players: Map[PlayerId, Player])
extends StrictLogging {
require(players.keys.forall(_.partyId == partyId), "party id must be same")
require(players.keys.forall(_.partyId == partyDescription.partyId), "party id must be same")
def getPlayers: Seq[Player] = players.values.toSeq
def player(playerId: PlayerId): Option[Player] = players.get(playerId)
def withPlayer(player: Player): Party =
try {
require(player.partyId == partyId, "player must belong to this party")
require(player.partyId == partyDescription.partyId, "player must belong to this party")
copy(players = players + (player.playerId -> player))
} catch {
case exception: Exception =>
@ -36,10 +36,7 @@ case class Party(partyId: String, rules: Seq[String], players: Map[PlayerId, Pla
}
object Party {
def apply(partyId: Option[String], config: Config): Party =
new Party(partyId.getOrElse(randomPartyId), getRules(config), Map.empty)
def apply(partyId: String, config: Config,
def apply(party: PartyDescription, config: Config,
players: Map[Long, Player], bis: Seq[Loot], loot: Seq[Loot]): Party = {
val bisByPlayer = bis.groupBy(_.playerId).view.mapValues(piece => BiS(piece.map(_.piece)))
val lootByPlayer = loot.groupBy(_.playerId).view
@ -49,7 +46,7 @@ object Party {
.withBiS(bisByPlayer.get(playerId))
.withLoot(lootByPlayer.getOrElse(playerId, Seq.empty)))
}
Party(partyId, getRules(config), playersWithItems)
Party(party, getRules(config), playersWithItems)
}
def getRules(config: Config): Seq[String] =

View File

@ -0,0 +1,17 @@
/*
* Copyright (c) 2020 Evgeniy Alekseev.
*
* This file is part of ffxivbis
* (see https://github.com/arcan1s/ffxivbis).
*
* License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause
*/
package me.arcanis.ffxivbis.models
case class PartyDescription(partyId: String, partyAlias: Option[String]) {
def alias: String = partyAlias.getOrElse(partyId)
}
object PartyDescription {
def empty(partyId: String): PartyDescription = PartyDescription(partyId, None)
}

View File

@ -32,10 +32,11 @@ trait Database extends Actor with StrictLogging {
def getParty(partyId: String, withBiS: Boolean, withLoot: Boolean): Future[Party] =
for {
partyDescription <- profile.getPartyDescription(partyId)
players <- profile.getParty(partyId)
bis <- if (withBiS) profile.getPiecesBiS(partyId) else Future(Seq.empty)
loot <- if (withLoot) profile.getPieces(partyId) else Future(Seq.empty)
} yield Party(partyId, context.system.settings.config, players, bis, loot)
} yield Party(partyDescription, context.system.settings.config, players, bis, loot)
}
object Database {

View File

@ -9,7 +9,7 @@
package me.arcanis.ffxivbis.service.impl
import akka.pattern.pipe
import me.arcanis.ffxivbis.models.{BiS, Player, PlayerId}
import me.arcanis.ffxivbis.models.{BiS, PartyDescription, Player, PlayerId}
import me.arcanis.ffxivbis.service.Database
import scala.concurrent.Future
@ -26,6 +26,10 @@ trait DatabasePartyHandler { this: Database =>
val client = sender()
getParty(partyId, withBiS = true, withLoot = true).pipeTo(client)
case GetPartyDescription(partyId) =>
val client = sender()
profile.getPartyDescription(partyId).pipeTo(client)
case GetPlayer(playerId) =>
val client = sender()
val player = profile.getPlayerFull(playerId).flatMap { maybePlayerData =>
@ -43,6 +47,10 @@ trait DatabasePartyHandler { this: Database =>
case RemovePlayer(playerId) =>
val client = sender()
profile.deletePlayer(playerId).pipeTo(client)
case UpdateParty(description) =>
val client = sender()
profile.insertPartyDescription(description).pipeTo(client)
}
}
@ -51,10 +59,14 @@ object DatabasePartyHandler {
override def partyId: String = player.partyId
}
case class GetParty(partyId: String) extends Database.DatabaseRequest
case class GetPartyDescription(partyId: String) extends Database.DatabaseRequest
case class GetPlayer(playerId: PlayerId) extends Database.DatabaseRequest {
override def partyId: String = playerId.partyId
}
case class RemovePlayer(playerId: PlayerId) extends Database.DatabaseRequest {
override def partyId: String = playerId.partyId
}
case class UpdateParty(partyDescription: PartyDescription) extends Database.DatabaseRequest {
override def partyId: String = partyDescription.partyId
}
}

View File

@ -18,7 +18,7 @@ import slick.jdbc.JdbcProfile
import scala.concurrent.{ExecutionContext, Future}
class DatabaseProfile(context: ExecutionContext, config: Config)
extends BiSProfile with LootProfile with PlayersProfile with UsersProfile {
extends BiSProfile with LootProfile with PartyProfile with PlayersProfile with UsersProfile {
implicit val executionContext: ExecutionContext = context
@ -29,6 +29,7 @@ class DatabaseProfile(context: ExecutionContext, config: Config)
val bisTable: TableQuery[BiSPieces] = TableQuery[BiSPieces]
val lootTable: TableQuery[LootPieces] = TableQuery[LootPieces]
val partiesTable: TableQuery[Parties] = TableQuery[Parties]
val playersTable: TableQuery[Players] = TableQuery[Players]
val usersTable: TableQuery[Users] = TableQuery[Users]
@ -55,10 +56,10 @@ class DatabaseProfile(context: ExecutionContext, config: Config)
private def byPartyId[T](partyId: String, callback: Seq[Long] => Future[T]): Future[T] =
getPlayers(partyId).map(callback).flatten
private def byPlayerId[T](playerId: PlayerId, callback: Long => Future[T]): Future[T] =
getPlayer(playerId).map {
getPlayer(playerId).flatMap {
case Some(id) => callback(id)
case None => Future.failed(new Error(s"Could not find player $playerId"))
}.flatten
}
}
object DatabaseProfile {

View File

@ -0,0 +1,48 @@
/*
* Copyright (c) 2020 Evgeniy Alekseev.
*
* This file is part of ffxivbis
* (see https://github.com/arcan1s/ffxivbis).
*
* License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause
*/
package me.arcanis.ffxivbis.storage
import me.arcanis.ffxivbis.models.PartyDescription
import scala.concurrent.Future
trait PartyProfile { this: DatabaseProfile =>
import dbConfig.profile.api._
case class PartyRep(partyId: Option[Long], partyName: String, partyAlias: Option[String]) {
def toDescription: PartyDescription = PartyDescription(partyName, partyAlias)
}
object PartyRep {
def fromDescription(party: PartyDescription, id: Option[Long]): PartyRep =
PartyRep(id, party.partyId, party.partyAlias)
}
class Parties(tag: Tag) extends Table[PartyRep](tag, "parties") {
def partyId: Rep[Long] = column[Long]("party_id", O.AutoInc, O.PrimaryKey)
def partyName: Rep[String] = column[String]("party_name")
def partyAlias: Rep[Option[String]] = column[Option[String]]("party_alias")
def * =
(partyId.?, partyName, partyAlias) <> ((PartyRep.apply _).tupled, PartyRep.unapply)
}
def getPartyDescription(partyId: String): Future[PartyDescription] =
db.run(partyDescription(partyId).result.headOption.map(_.map(_.toDescription).getOrElse(PartyDescription.empty(partyId))))
def getUniquePartyId(partyId: String): Future[Option[Long]] =
db.run(partyDescription(partyId).map(_.partyId).result.headOption)
def insertPartyDescription(partyDescription: PartyDescription): Future[Int] =
getUniquePartyId(partyDescription.partyId).flatMap {
case Some(id) => db.run(partiesTable.update(PartyRep.fromDescription(partyDescription, Some(id))))
case _ => db.run(partiesTable.insertOrUpdate(PartyRep.fromDescription(partyDescription, None)))
}
private def partyDescription(partyId: String) =
partiesTable.filter(_.partyName === partyId)
}

View File

@ -39,7 +39,6 @@ trait PlayersProfile { this: DatabaseProfile =>
(partyId, playerId.?, created, nick, job, bisLink, priority) <> ((PlayerRep.apply _).tupled, PlayerRep.unapply)
}
def deletePlayer(playerId: PlayerId): Future[Int] = db.run(player(playerId).delete)
def getParty(partyId: String): Future[Map[Long, Player]] =
db.run(players(partyId).result).map(_.foldLeft(Map.empty[Long, Player]) {
@ -53,10 +52,10 @@ trait PlayersProfile { this: DatabaseProfile =>
def getPlayers(partyId: String): Future[Seq[Long]] =
db.run(players(partyId).map(_.playerId).result)
def insertPlayer(playerObj: Player): Future[Int] =
getPlayer(playerObj.playerId).map {
getPlayer(playerObj.playerId).flatMap {
case Some(id) => db.run(playersTable.update(PlayerRep.fromPlayer(playerObj, Some(id))))
case _ => db.run(playersTable.insertOrUpdate(PlayerRep.fromPlayer(playerObj, None)))
}.flatten
}
private def player(playerId: PlayerId) =
playersTable

View File

@ -49,10 +49,10 @@ trait UsersProfile { this: DatabaseProfile =>
def getUsers(partyId: String): Future[Seq[User]] =
db.run(user(partyId, None).result).map(_.map(_.toUser))
def insertUser(userObj: User): Future[Int] =
db.run(user(userObj.partyId, Some(userObj.username)).map(_.userId).result.headOption).map {
db.run(user(userObj.partyId, Some(userObj.username)).map(_.userId).result.headOption).flatMap {
case Some(id) => db.run(usersTable.insertOrUpdate(UserRep.fromUser(userObj, Some(id))))
case _ => db.run(usersTable.insertOrUpdate(UserRep.fromUser(userObj, None)))
}.flatten
}
private def user(partyId: String, username: Option[String]) =
usersTable

View File

@ -5,12 +5,13 @@ import akka.http.scaladsl.model.{StatusCodes, Uri}
import akka.http.scaladsl.model.headers.{Authorization, BasicHttpCredentials}
import akka.http.scaladsl.testkit.{RouteTestTimeout, ScalatestRouteTest}
import akka.http.scaladsl.server._
import akka.pattern.ask
import akka.testkit.TestKit
import akka.pattern.ask
import com.typesafe.config.Config
import me.arcanis.ffxivbis.{Fixtures, Settings}
import me.arcanis.ffxivbis.http.api.v1.json._
import me.arcanis.ffxivbis.service.{Ariyala, PartyService, impl}
import me.arcanis.ffxivbis.models.PartyDescription
import me.arcanis.ffxivbis.service.{Ariyala, impl}
import me.arcanis.ffxivbis.storage.Migration
import org.scalatest.{Matchers, WordSpec}
@ -23,22 +24,19 @@ class PartyEndpointTest extends WordSpec
private val auth: Authorization =
Authorization(BasicHttpCredentials(Fixtures.userAdmin.username, Fixtures.userPassword))
private val endpoint: Uri = Uri(s"/party/${Fixtures.partyId}")
private val playerId = PlayerIdResponse.fromPlayerId(Fixtures.playerEmpty.playerId)
private val endpoint: Uri = Uri(s"/party/${Fixtures.partyId}/description")
private val timeout: FiniteDuration = 60 seconds
implicit private val routeTimeout: RouteTestTimeout = RouteTestTimeout(timeout)
private val storage: ActorRef = system.actorOf(impl.DatabaseImpl.props)
private val ariyala: ActorRef = system.actorOf(Ariyala.props)
private val party: ActorRef = system.actorOf(PartyService.props(storage))
private val route: Route = new PlayerEndpoint(party, ariyala)(timeout).route
private val route: Route = new PartyEndpoint(storage, ariyala)(timeout).route
override def testConfig: Config = Settings.withRandomDatabase
override def beforeAll: Unit = {
Await.result(Migration(system.settings.config), timeout)
Await.result((storage ? impl.DatabaseUserHandler.AddUser(Fixtures.userAdmin, isHashedPassword = true))(timeout).mapTo[Int], timeout)
Await.result((storage ? impl.DatabasePartyHandler.AddPlayer(Fixtures.playerEmpty))(timeout).mapTo[Int], timeout)
}
override def afterAll: Unit = {
@ -48,13 +46,23 @@ class PartyEndpointTest extends WordSpec
"api v1 party endpoint" must {
"get users" in {
val uri = endpoint.withQuery(Uri.Query(Map("nick" -> playerId.nick, "job" -> playerId.job)))
val response = Seq(PlayerResponse.fromPlayer(Fixtures.playerEmpty))
"get empty party description" in {
Get(endpoint).withHeaders(auth) ~> route ~> check {
status shouldEqual StatusCodes.OK
responseAs[PartyDescriptionResponse].toDescription shouldEqual PartyDescription.empty(Fixtures.partyId)
}
}
"update party description" in {
val entity = PartyDescriptionResponse(Fixtures.partyId, Some("random party name"))
Post(endpoint, entity).withHeaders(auth) ~> route ~> check {
status shouldEqual StatusCodes.Accepted
}
Get(endpoint).withHeaders(auth) ~> route ~> check {
status shouldEqual StatusCodes.OK
responseAs[Seq[PlayerResponse]] shouldEqual response
responseAs[PartyDescriptionResponse].toDescription shouldEqual entity.toDescription
}
}

View File

@ -0,0 +1,62 @@
package me.arcanis.ffxivbis.http.api.v1
import akka.actor.ActorRef
import akka.http.scaladsl.model.{StatusCodes, Uri}
import akka.http.scaladsl.model.headers.{Authorization, BasicHttpCredentials}
import akka.http.scaladsl.testkit.{RouteTestTimeout, ScalatestRouteTest}
import akka.http.scaladsl.server._
import akka.pattern.ask
import akka.testkit.TestKit
import com.typesafe.config.Config
import me.arcanis.ffxivbis.{Fixtures, Settings}
import me.arcanis.ffxivbis.http.api.v1.json._
import me.arcanis.ffxivbis.service.{Ariyala, PartyService, impl}
import me.arcanis.ffxivbis.storage.Migration
import org.scalatest.{Matchers, WordSpec}
import scala.concurrent.Await
import scala.concurrent.duration._
import scala.language.postfixOps
class PlayerEndpointTest extends WordSpec
with Matchers with ScalatestRouteTest with JsonSupport {
private val auth: Authorization =
Authorization(BasicHttpCredentials(Fixtures.userAdmin.username, Fixtures.userPassword))
private val endpoint: Uri = Uri(s"/party/${Fixtures.partyId}")
private val playerId = PlayerIdResponse.fromPlayerId(Fixtures.playerEmpty.playerId)
private val timeout: FiniteDuration = 60 seconds
implicit private val routeTimeout: RouteTestTimeout = RouteTestTimeout(timeout)
private val storage: ActorRef = system.actorOf(impl.DatabaseImpl.props)
private val ariyala: ActorRef = system.actorOf(Ariyala.props)
private val party: ActorRef = system.actorOf(PartyService.props(storage))
private val route: Route = new PlayerEndpoint(party, ariyala)(timeout).route
override def testConfig: Config = Settings.withRandomDatabase
override def beforeAll: Unit = {
Await.result(Migration(system.settings.config), timeout)
Await.result((storage ? impl.DatabaseUserHandler.AddUser(Fixtures.userAdmin, isHashedPassword = true))(timeout).mapTo[Int], timeout)
Await.result((storage ? impl.DatabasePartyHandler.AddPlayer(Fixtures.playerEmpty))(timeout).mapTo[Int], timeout)
}
override def afterAll: Unit = {
TestKit.shutdownActorSystem(system)
Settings.clearDatabase(system.settings.config)
}
"api v1 player endpoint" must {
"get users" in {
val uri = endpoint.withQuery(Uri.Query(Map("nick" -> playerId.nick, "job" -> playerId.job)))
val response = Seq(PlayerResponse.fromPlayer(Fixtures.playerEmpty))
Get(endpoint).withHeaders(auth) ~> route ~> check {
status shouldEqual StatusCodes.OK
responseAs[Seq[PlayerResponse]] shouldEqual response
}
}
}
}

View File

@ -6,23 +6,24 @@ import org.scalatest.{BeforeAndAfterAll, Matchers, WordSpecLike}
class PartyTest extends WordSpecLike with Matchers with BeforeAndAfterAll {
private val partyDescription = PartyDescription.empty(Fixtures.partyId)
private val party =
Party(Fixtures.partyId, Seq.empty, Map(Fixtures.playerEmpty.playerId -> Fixtures.playerEmpty))
Party(partyDescription, Seq.empty, Map(Fixtures.playerEmpty.playerId -> Fixtures.playerEmpty))
"party model" must {
"accept player with same party id" in {
noException should be thrownBy
Party(Fixtures.partyId, Seq.empty, Map(Fixtures.playerEmpty.playerId -> Fixtures.playerEmpty))
Party(partyDescription, Seq.empty, Map(Fixtures.playerEmpty.playerId -> Fixtures.playerEmpty))
}
"fail on multiple party ids" in {
val anotherPlayer = Fixtures.playerEmpty.copy(partyId = Fixtures.partyId2)
an [IllegalArgumentException] should be thrownBy
Party(Fixtures.partyId, Seq.empty, Map(anotherPlayer.playerId -> anotherPlayer))
Party(partyDescription, Seq.empty, Map(anotherPlayer.playerId -> anotherPlayer))
an [IllegalArgumentException] should be thrownBy
Party(Fixtures.partyId, Seq.empty, Map(Fixtures.playerEmpty.playerId -> Fixtures.playerEmpty, anotherPlayer.playerId -> anotherPlayer))
Party(partyDescription, Seq.empty, Map(Fixtures.playerEmpty.playerId -> Fixtures.playerEmpty, anotherPlayer.playerId -> anotherPlayer))
}
"return player list" in {
@ -38,7 +39,7 @@ class PartyTest extends WordSpecLike with Matchers with BeforeAndAfterAll {
}
"add new player" in {
val newParty = Party(Fixtures.partyId, Seq.empty, Map.empty)
val newParty = Party(partyDescription, Seq.empty, Map.empty)
newParty.withPlayer(Fixtures.playerEmpty) shouldEqual party
}

View File

@ -16,7 +16,7 @@ class LootSelectorTest extends TestKit(ActorSystem("lootselector"))
import me.arcanis.ffxivbis.utils.Converters._
private var default: Party = Party(Some(Fixtures.partyId), Settings.config(Map.empty))
private var default: Party = Party(PartyDescription.empty(Fixtures.partyId), Settings.config(Map.empty), Map.empty, Seq.empty, Seq.empty)
private var dnc: Player = Player(-1, Fixtures.partyId, Job.DNC, "a nick", BiS(), Seq.empty, Some(Fixtures.link))
private var drg: Player = Player(-1, Fixtures.partyId, Job.DRG, "another nick", BiS(), Seq.empty, Some(Fixtures.link2))
private val timeout: FiniteDuration = 60 seconds