From 4a87ac875378ffd6fd3f8d88526503c21b7bb939 Mon Sep 17 00:00:00 2001 From: Evgeniy Alekseev Date: Thu, 27 Feb 2020 00:19:25 +0300 Subject: [PATCH] initial timestamp support --- .../arcanis/ffxivbis/http/Authorization.scala | 9 ++++----- .../http/api/v1/json/JsonSupport.scala | 12 +++++++++++ .../http/api/v1/json/LootResponse.scala | 17 ++++++++++++++++ .../http/api/v1/json/PlayerResponse.scala | 6 +++--- .../arcanis/ffxivbis/http/view/LootView.scala | 12 ++++++----- .../me/arcanis/ffxivbis/models/Loot.scala | 4 +++- .../me/arcanis/ffxivbis/models/Party.scala | 2 +- .../me/arcanis/ffxivbis/models/Player.scala | 20 +++++++++++-------- .../arcanis/ffxivbis/service/Database.scala | 6 +++--- .../service/impl/DatabasePartyHandler.scala | 2 +- .../arcanis/ffxivbis/storage/BiSProfile.scala | 4 +++- .../ffxivbis/storage/LootProfile.scala | 4 +++- .../scala/me/arcanis/ffxivbis/Fixtures.scala | 2 +- .../http/api/v1/LootEndpointTest.scala | 2 +- 14 files changed, 71 insertions(+), 31 deletions(-) create mode 100644 src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/LootResponse.scala diff --git a/src/main/scala/me/arcanis/ffxivbis/http/Authorization.scala b/src/main/scala/me/arcanis/ffxivbis/http/Authorization.scala index 6c21cda..b62d9e8 100644 --- a/src/main/scala/me/arcanis/ffxivbis/http/Authorization.scala +++ b/src/main/scala/me/arcanis/ffxivbis/http/Authorization.scala @@ -39,8 +39,7 @@ trait Authorization { } } - def authenticator(scope: Permission.Value)(partyId: String) - (username: String, password: String) + def authenticator(scope: Permission.Value, partyId: String)(username: String, password: String) (implicit executionContext: ExecutionContext, timeout: Timeout): Future[Option[String]] = (storage ? DatabaseUserHandler.GetUser(partyId, username)).mapTo[Option[User]].map { case Some(user) if user.verify(password) && user.verityScope(scope) => Some(username) @@ -49,13 +48,13 @@ trait Authorization { def authAdmin(partyId: String)(username: String, password: String) (implicit executionContext: ExecutionContext, timeout: Timeout): Future[Option[String]] = - authenticator(Permission.admin)(partyId)(username, password) + authenticator(Permission.admin, partyId)(username, password) def authGet(partyId: String)(username: String, password: String) (implicit executionContext: ExecutionContext, timeout: Timeout): Future[Option[String]] = - authenticator(Permission.get)(partyId)(username, password) + authenticator(Permission.get, partyId)(username, password) def authPost(partyId: String)(username: String, password: String) (implicit executionContext: ExecutionContext, timeout: Timeout): Future[Option[String]] = - authenticator(Permission.post)(partyId)(username, password) + authenticator(Permission.post, partyId)(username, password) } diff --git a/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/JsonSupport.scala b/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/JsonSupport.scala index 81df641..92e6d90 100644 --- a/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/JsonSupport.scala +++ b/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/JsonSupport.scala @@ -8,6 +8,8 @@ */ package me.arcanis.ffxivbis.http.api.v1.json +import java.time.Instant + import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport import me.arcanis.ffxivbis.models.Permission import spray.json._ @@ -24,12 +26,22 @@ trait JsonSupport extends SprayJsonSupport with DefaultJsonProtocol { } } + implicit val instantFormat: RootJsonFormat[Instant] = new RootJsonFormat[Instant] { + override def write(obj: Instant): JsValue = obj.toString.toJson + override def read(json: JsValue): Instant = json match { + case JsNumber(value) => Instant.ofEpochMilli(value.toLongExact) + case JsString(value) => Instant.parse(value) + case other => deserializationError(s"String or number expected, got $other") + } + } + implicit val actionFormat: RootJsonFormat[ApiAction.Value] = enumFormat(ApiAction) implicit val permissionFormat: RootJsonFormat[Permission.Value] = enumFormat(Permission) implicit val errorFormat: RootJsonFormat[ErrorResponse] = jsonFormat1(ErrorResponse.apply) 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 playerFormat: RootJsonFormat[PlayerResponse] = jsonFormat7(PlayerResponse.apply) implicit val playerActionFormat: RootJsonFormat[PlayerActionResponse] = jsonFormat2(PlayerActionResponse.apply) implicit val playerIdFormat: RootJsonFormat[PlayerIdResponse] = jsonFormat3(PlayerIdResponse.apply) diff --git a/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/LootResponse.scala b/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/LootResponse.scala new file mode 100644 index 0000000..a1f4f6e --- /dev/null +++ b/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/LootResponse.scala @@ -0,0 +1,17 @@ +package me.arcanis.ffxivbis.http.api.v1.json + +import java.time.Instant + +import io.swagger.v3.oas.annotations.media.Schema +import me.arcanis.ffxivbis.models.Loot + +case class LootResponse( + @Schema(description = "looted piece", required = true) piece: PieceResponse, + @Schema(description = "loot timestamp", required = true) timestamp: Instant = Instant.now) { + def toLoot: Loot = Loot(-1, piece.toPiece, timestamp) +} + +object LootResponse { + def fromLoot(loot: Loot): LootResponse = + LootResponse(PieceResponse.fromPiece(loot.piece), loot.timestamp) +} \ No newline at end of file diff --git a/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/PlayerResponse.scala b/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/PlayerResponse.scala index 011081b..e4860f0 100644 --- a/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/PlayerResponse.scala +++ b/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/PlayerResponse.scala @@ -16,18 +16,18 @@ case class PlayerResponse( @Schema(description = "job name", required = true, example = "DNC") job: String, @Schema(description = "player nick name", required = true, example = "Siuan Sanche") nick: String, @Schema(description = "pieces in best in slot") bis: Option[Seq[PieceResponse]], - @Schema(description = "looted pieces") loot: Option[Seq[PieceResponse]], + @Schema(description = "looted pieces") loot: Option[Seq[LootResponse]], @Schema(description = "link to best in slot", example = "https://ffxiv.ariyala.com/19V5R") link: Option[String], @Schema(description = "player loot priority", `type` = "number") priority: Option[Int]) { def toPlayer: Player = Player(partyId, Job.withName(job), nick, - BiS(bis.getOrElse(Seq.empty).map(_.toPiece)), loot.getOrElse(Seq.empty).map(_.toPiece), + BiS(bis.getOrElse(Seq.empty).map(_.toPiece)), loot.getOrElse(Seq.empty).map(_.toLoot), link, priority.getOrElse(0)) } object PlayerResponse { def fromPlayer(player: Player): PlayerResponse = PlayerResponse(player.partyId, player.job.toString, player.nick, - Some(player.bis.pieces.map(PieceResponse.fromPiece)), Some(player.loot.map(PieceResponse.fromPiece)), + Some(player.bis.pieces.map(PieceResponse.fromPiece)), Some(player.loot.map(LootResponse.fromLoot)), player.link, Some(player.priority)) } diff --git a/src/main/scala/me/arcanis/ffxivbis/http/view/LootView.scala b/src/main/scala/me/arcanis/ffxivbis/http/view/LootView.scala index 2b21a88..4ab84b8 100644 --- a/src/main/scala/me/arcanis/ffxivbis/http/view/LootView.scala +++ b/src/main/scala/me/arcanis/ffxivbis/http/view/LootView.scala @@ -111,17 +111,19 @@ object LootView { th("player"), th("piece"), th("is tome"), + th("timestamp"), th("") ), - for (player <- party; piece <- player.loot) yield tr( + for (player <- party; loot <- player.loot) yield tr( td(`class`:="include_search")(player.playerId.toString), - td(`class`:="include_search")(piece.piece), - td(piece.isTomeToString), + td(`class`:="include_search")(loot.piece.piece), + td(loot.piece.isTomeToString), + td(loot.timestamp.toString), td( form(action:=s"/party/$partyId/loot", method:="post")( input(name:="player", id:="player", `type`:="hidden", value:=player.playerId.toString), - input(name:="piece", id:="piece", `type`:="hidden", value:=piece.piece), - input(name:="is_tome", id:="is_tome", `type`:="hidden", value:=piece.isTomeToString), + input(name:="piece", id:="piece", `type`:="hidden", value:=loot.piece.piece), + input(name:="is_tome", id:="is_tome", `type`:="hidden", value:=loot.piece.isTomeToString), input(name:="action", id:="action", `type`:="hidden", value:="remove"), input(name:="remove", id:="remove", `type`:="submit", value:="x") ) diff --git a/src/main/scala/me/arcanis/ffxivbis/models/Loot.scala b/src/main/scala/me/arcanis/ffxivbis/models/Loot.scala index b07c2c3..367d872 100644 --- a/src/main/scala/me/arcanis/ffxivbis/models/Loot.scala +++ b/src/main/scala/me/arcanis/ffxivbis/models/Loot.scala @@ -8,4 +8,6 @@ */ package me.arcanis.ffxivbis.models -case class Loot(playerId: Long, piece: Piece) +import java.time.Instant + +case class Loot(playerId: Long, piece: Piece, timestamp: Instant) diff --git a/src/main/scala/me/arcanis/ffxivbis/models/Party.scala b/src/main/scala/me/arcanis/ffxivbis/models/Party.scala index 41d58d2..104b106 100644 --- a/src/main/scala/me/arcanis/ffxivbis/models/Party.scala +++ b/src/main/scala/me/arcanis/ffxivbis/models/Party.scala @@ -42,7 +42,7 @@ object Party { def apply(partyId: String, 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.mapValues(_.map(_.piece)) + val lootByPlayer = loot.groupBy(_.playerId).view val playersWithItems = players.foldLeft(Map.empty[PlayerId, Player]) { case (acc, (playerId, player)) => acc + (player.playerId -> player diff --git a/src/main/scala/me/arcanis/ffxivbis/models/Player.scala b/src/main/scala/me/arcanis/ffxivbis/models/Player.scala index 3bbf3bd..862ba08 100644 --- a/src/main/scala/me/arcanis/ffxivbis/models/Player.scala +++ b/src/main/scala/me/arcanis/ffxivbis/models/Player.scala @@ -8,11 +8,12 @@ */ package me.arcanis.ffxivbis.models -case class Player(partyId: String, +case class Player(id: Long, + partyId: String, job: Job.Job, nick: String, bis: BiS, - loot: Seq[Piece], + loot: Seq[Loot], link: Option[String] = None, priority: Int = 0) { require(job ne Job.AnyJob, "AnyJob is not allowed") @@ -27,10 +28,13 @@ case class Player(partyId: String, partyId, job, nick, isRequired(piece), priority, bisCountTotal(piece), lootCount(piece), lootCountBiS(piece), lootCountTotal(piece)) - def withLoot(piece: Piece): Player = withLoot(Seq(piece)) - def withLoot(list: Seq[Piece]): Player = list match { - case Nil => this - case _ => copy(loot = list) + def withLoot(piece: Loot): Player = withLoot(Seq(piece)) + def withLoot(list: Seq[Loot]): Player = { + require(loot.forall(_.playerId == id), "player id must be same") + list match { + case Nil => this + case _ => copy(loot = list) + } } def isRequired(piece: Option[Piece]): Boolean = { @@ -44,10 +48,10 @@ case class Player(partyId: String, def bisCountTotal(piece: Option[Piece]): Int = bis.pieces.count(!_.isTome) def lootCount(piece: Option[Piece]): Int = piece match { - case Some(p) => loot.count(_ == p) + case Some(p) => loot.count(_.piece == p) case None => lootCountTotal(piece) } - def lootCountBiS(piece: Option[Piece]): Int = loot.count(bis.hasPiece) + def lootCountBiS(piece: Option[Piece]): Int = loot.map(_.piece).count(bis.hasPiece) def lootCountTotal(piece: Option[Piece]): Int = loot.length def lootPriority(piece: Piece): Int = priority } diff --git a/src/main/scala/me/arcanis/ffxivbis/service/Database.scala b/src/main/scala/me/arcanis/ffxivbis/service/Database.scala index b7dba24..6033885 100644 --- a/src/main/scala/me/arcanis/ffxivbis/service/Database.scala +++ b/src/main/scala/me/arcanis/ffxivbis/service/Database.scala @@ -25,9 +25,9 @@ trait Database extends Actor with StrictLogging { } def filterParty(party: Party, maybePlayerId: Option[PlayerId]): Seq[Player] = - (party, maybePlayerId) match { - case (_, Some(playerId)) => party.player(playerId).map(Seq(_)).getOrElse(Seq.empty) - case (_, _) => party.getPlayers + maybePlayerId match { + case Some(playerId) => party.player(playerId).map(Seq(_)).getOrElse(Seq.empty) + case _ => party.getPlayers } def getParty(partyId: String, withBiS: Boolean, withLoot: Boolean): Future[Party] = diff --git a/src/main/scala/me/arcanis/ffxivbis/service/impl/DatabasePartyHandler.scala b/src/main/scala/me/arcanis/ffxivbis/service/impl/DatabasePartyHandler.scala index ed05b35..c96ba54 100644 --- a/src/main/scala/me/arcanis/ffxivbis/service/impl/DatabasePartyHandler.scala +++ b/src/main/scala/me/arcanis/ffxivbis/service/impl/DatabasePartyHandler.scala @@ -34,7 +34,7 @@ trait DatabasePartyHandler { this: Database => bis <- profile.getPiecesBiS(playerId) loot <- profile.getPieces(playerId) } yield Player(playerId.partyId, playerId.job, playerId.nick, - BiS(bis.map(_.piece)), loot.map(_.piece), + BiS(bis.map(_.piece)), loot, playerData.link, playerData.priority) } }.map(_.headOption) diff --git a/src/main/scala/me/arcanis/ffxivbis/storage/BiSProfile.scala b/src/main/scala/me/arcanis/ffxivbis/storage/BiSProfile.scala index 4f57138..71cbf06 100644 --- a/src/main/scala/me/arcanis/ffxivbis/storage/BiSProfile.scala +++ b/src/main/scala/me/arcanis/ffxivbis/storage/BiSProfile.scala @@ -8,6 +8,8 @@ */ package me.arcanis.ffxivbis.storage +import java.time.Instant + import me.arcanis.ffxivbis.models.{Job, Loot, Piece} import slick.lifted.ForeignKeyQuery @@ -17,7 +19,7 @@ trait BiSProfile { this: DatabaseProfile => import dbConfig.profile.api._ case class BiSRep(playerId: Long, created: Long, piece: String, isTome: Int, job: String) { - def toLoot: Loot = Loot(playerId, Piece(piece, isTome == 1, Job.withName(job))) + def toLoot: Loot = Loot(playerId, Piece(piece, isTome == 1, Job.withName(job)), Instant.ofEpochMilli(created)) } object BiSRep { def fromPiece(playerId: Long, piece: Piece) = diff --git a/src/main/scala/me/arcanis/ffxivbis/storage/LootProfile.scala b/src/main/scala/me/arcanis/ffxivbis/storage/LootProfile.scala index 03de0b0..795a295 100644 --- a/src/main/scala/me/arcanis/ffxivbis/storage/LootProfile.scala +++ b/src/main/scala/me/arcanis/ffxivbis/storage/LootProfile.scala @@ -8,6 +8,8 @@ */ package me.arcanis.ffxivbis.storage +import java.time.Instant + import me.arcanis.ffxivbis.models.{Job, Loot, Piece} import slick.lifted.{ForeignKeyQuery, Index} @@ -18,7 +20,7 @@ trait LootProfile { this: DatabaseProfile => case class LootRep(lootId: Option[Long], playerId: Long, created: Long, piece: String, isTome: Int, job: String) { - def toLoot: Loot = Loot(playerId, Piece(piece, isTome == 1, Job.withName(job))) + def toLoot: Loot = Loot(playerId, Piece(piece, isTome == 1, Job.withName(job)), Instant.ofEpochMilli(created)) } object LootRep { def fromPiece(playerId: Long, piece: Piece) = diff --git a/src/test/scala/me/arcanis/ffxivbis/Fixtures.scala b/src/test/scala/me/arcanis/ffxivbis/Fixtures.scala index aa5f800..45ffe11 100644 --- a/src/test/scala/me/arcanis/ffxivbis/Fixtures.scala +++ b/src/test/scala/me/arcanis/ffxivbis/Fixtures.scala @@ -33,7 +33,7 @@ object Fixtures { lazy val lootLeftRing: Piece = Ring(isTome = true, Job.AnyJob, "left ring") lazy val lootRightRing: Piece = Ring(isTome = true, Job.AnyJob, "right ring") lazy val lootUpgrade: Piece = BodyUpgrade - lazy val loot: Seq[Piece] = Seq(lootBody, lootHands, lootLegs, lootUpgrade) + lazy val loot: Seq[Loot] = Seq(lootBody, lootHands, lootLegs, lootUpgrade) lazy val partyId: String = Party.randomPartyId lazy val partyId2: String = Party.randomPartyId diff --git a/src/test/scala/me/arcanis/ffxivbis/http/api/v1/LootEndpointTest.scala b/src/test/scala/me/arcanis/ffxivbis/http/api/v1/LootEndpointTest.scala index 1ed7042..b82b8eb 100644 --- a/src/test/scala/me/arcanis/ffxivbis/http/api/v1/LootEndpointTest.scala +++ b/src/test/scala/me/arcanis/ffxivbis/http/api/v1/LootEndpointTest.scala @@ -59,7 +59,7 @@ class LootEndpointTest extends WordSpec "return looted items" in { val uri = endpoint.withQuery(Uri.Query(Map("nick" -> playerId.nick, "job" -> playerId.job))) - val response = Seq(PlayerResponse.fromPlayer(Fixtures.playerEmpty.withLoot(Fixtures.lootBody))) + val response = Seq(PlayerResponse.fromPlayer(Fixtures.playerEmpty.withLoot(Fixtures.lootBody.toLoot(-1)))) Get(uri).withHeaders(auth) ~> route ~> check { status shouldEqual StatusCodes.OK