mirror of
https://github.com/arcan1s/ffxivbis.git
synced 2025-06-28 23:01:41 +00:00
Feature/timestamp support (#8)
* initial timestamp support * compilation & test fixes * do not take default argument
This commit is contained in:
@ -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)
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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) {
|
||||
def toLoot: Loot = Loot(-1, piece.toPiece, timestamp)
|
||||
}
|
||||
|
||||
object LootResponse {
|
||||
def fromLoot(loot: Loot): LootResponse =
|
||||
LootResponse(PieceResponse.fromPiece(loot.piece), loot.timestamp)
|
||||
}
|
@ -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),
|
||||
Player(-1, partyId, Job.withName(job), nick,
|
||||
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))
|
||||
}
|
||||
|
@ -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")
|
||||
)
|
||||
|
@ -62,7 +62,7 @@ class PlayerView(override val storage: ActorRef, ariyala: ActorRef)(implicit tim
|
||||
(implicit executionContext: ExecutionContext, timeout: Timeout): Future[Unit] = {
|
||||
def maybePlayerId = PlayerId(partyId, Some(nick), Some(job))
|
||||
def player(playerId: PlayerId) =
|
||||
Player(partyId, playerId.job, playerId.nick, BiS(), Seq.empty, maybeLink, maybePriority.getOrElse(0))
|
||||
Player(-1, partyId, playerId.job, playerId.nick, BiS(), Seq.empty, maybeLink, maybePriority.getOrElse(0))
|
||||
|
||||
(action, maybePlayerId) match {
|
||||
case ("add", Some(playerId)) => addPlayer(player(playerId)).map(_ => ())
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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] =
|
||||
|
@ -33,8 +33,8 @@ trait DatabasePartyHandler { this: Database =>
|
||||
for {
|
||||
bis <- profile.getPiecesBiS(playerId)
|
||||
loot <- profile.getPieces(playerId)
|
||||
} yield Player(playerId.partyId, playerId.job, playerId.nick,
|
||||
BiS(bis.map(_.piece)), loot.map(_.piece),
|
||||
} yield Player(playerData.id, playerId.partyId, playerId.job,
|
||||
playerId.nick, BiS(bis.map(_.piece)), loot,
|
||||
playerData.link, playerData.priority)
|
||||
}
|
||||
}.map(_.headOption)
|
||||
|
@ -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) =
|
||||
|
@ -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) =
|
||||
|
@ -18,7 +18,7 @@ trait PlayersProfile { this: DatabaseProfile =>
|
||||
case class PlayerRep(partyId: String, playerId: Option[Long], created: Long, nick: String,
|
||||
job: String, link: Option[String], priority: Int) {
|
||||
def toPlayer: Player =
|
||||
Player(partyId, Job.withName(job), nick, BiS(Seq.empty), List.empty, link, priority)
|
||||
Player(playerId.getOrElse(-1), partyId, Job.withName(job), nick, BiS(Seq.empty), List.empty, link, priority)
|
||||
}
|
||||
object PlayerRep {
|
||||
def fromPlayer(player: Player, id: Option[Long]): PlayerRep =
|
||||
|
@ -39,7 +39,7 @@ object Fixtures {
|
||||
lazy val partyId2: String = Party.randomPartyId
|
||||
|
||||
lazy val playerEmpty: Player =
|
||||
Player(partyId, Job.DNC, "Siuan Sanche", BiS(), Seq.empty, Some(link))
|
||||
Player(1, partyId, Job.DNC, "Siuan Sanche", BiS(), Seq.empty, Some(link))
|
||||
lazy val playerWithBiS: Player = playerEmpty.copy(bis = bis)
|
||||
|
||||
lazy val userPassword: String = "password"
|
||||
|
@ -1,5 +1,7 @@
|
||||
package me.arcanis.ffxivbis.http.api.v1
|
||||
|
||||
import java.time.Instant
|
||||
|
||||
import akka.actor.ActorRef
|
||||
import akka.http.scaladsl.model.{StatusCodes, Uri}
|
||||
import akka.http.scaladsl.model.headers.{Authorization, BasicHttpCredentials}
|
||||
@ -58,12 +60,17 @@ class LootEndpointTest extends WordSpec
|
||||
}
|
||||
|
||||
"return looted items" in {
|
||||
import me.arcanis.ffxivbis.utils.Converters._
|
||||
|
||||
val uri = endpoint.withQuery(Uri.Query(Map("nick" -> playerId.nick, "job" -> playerId.job)))
|
||||
val response = Seq(PlayerResponse.fromPlayer(Fixtures.playerEmpty.withLoot(Fixtures.lootBody)))
|
||||
|
||||
Get(uri).withHeaders(auth) ~> route ~> check {
|
||||
status shouldEqual StatusCodes.OK
|
||||
responseAs[Seq[PlayerResponse]] shouldEqual response
|
||||
val withEmptyTimestamp = responseAs[Seq[PlayerResponse]].map { player =>
|
||||
player.copy(loot = player.loot.map(_.map(_.copy(timestamp = Instant.ofEpochMilli(0)))))
|
||||
}
|
||||
withEmptyTimestamp shouldEqual response
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -12,7 +12,9 @@ class PlayerTest extends WordSpecLike with Matchers with BeforeAndAfterAll {
|
||||
}
|
||||
|
||||
"add loot" in {
|
||||
Fixtures.playerEmpty.withLoot(Fixtures.loot).loot shouldEqual Fixtures.loot
|
||||
import me.arcanis.ffxivbis.utils.Converters._
|
||||
|
||||
Fixtures.playerEmpty.withLoot(Fixtures.loot.map(pieceToLoot)).loot.map(_.piece) shouldEqual Fixtures.loot
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -83,5 +83,7 @@ class DatabaseLootHandlerTest
|
||||
}
|
||||
|
||||
private def partyLootCompare[T](party: Seq[T], loot: Seq[Piece]): Boolean =
|
||||
Compare.seqEquals(party.foldLeft(Seq.empty[Piece]){ case (acc, player) => acc ++ player.asInstanceOf[Player].loot }, loot)
|
||||
Compare.seqEquals(party.foldLeft(Seq.empty[Piece]){ case (acc, player) =>
|
||||
acc ++ player.asInstanceOf[Player].loot.map(_.piece)
|
||||
}, loot)
|
||||
}
|
||||
|
@ -14,9 +14,11 @@ import scala.language.postfixOps
|
||||
class LootSelectorTest extends TestKit(ActorSystem("lootselector"))
|
||||
with ImplicitSender with WordSpecLike with Matchers with BeforeAndAfterAll {
|
||||
|
||||
import me.arcanis.ffxivbis.utils.Converters._
|
||||
|
||||
private var default: Party = Party(Some(Fixtures.partyId), Settings.config(Map.empty))
|
||||
private var dnc: Player = Player(Fixtures.partyId, Job.DNC, "a nick", BiS(), Seq.empty, Some(Fixtures.link))
|
||||
private var drg: Player = Player(Fixtures.partyId, Job.DRG, "another nick", BiS(), Seq.empty, Some(Fixtures.link2))
|
||||
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
|
||||
|
||||
override def beforeAll(): Unit = {
|
||||
@ -77,7 +79,7 @@ class LootSelectorTest extends TestKit(ActorSystem("lootselector"))
|
||||
"suggest loot by total piece got" in {
|
||||
val piece = Body(isTome = true, Job.AnyJob)
|
||||
val party = default
|
||||
.withPlayer(dnc.withLoot(Seq(piece, piece)))
|
||||
.withPlayer(dnc.withLoot(Seq(piece, piece).map(pieceToLoot)))
|
||||
.withPlayer(drg.withLoot(piece))
|
||||
|
||||
toPlayerId(party.suggestLoot(piece)) shouldEqual Seq(drg.playerId, dnc.playerId)
|
||||
|
11
src/test/scala/me/arcanis/ffxivbis/utils/Converters.scala
Normal file
11
src/test/scala/me/arcanis/ffxivbis/utils/Converters.scala
Normal file
@ -0,0 +1,11 @@
|
||||
package me.arcanis.ffxivbis.utils
|
||||
|
||||
import java.time.Instant
|
||||
|
||||
import me.arcanis.ffxivbis.models.{Loot, Piece}
|
||||
|
||||
import scala.language.implicitConversions
|
||||
|
||||
object Converters {
|
||||
implicit def pieceToLoot(piece: Piece): Loot = Loot(-1, piece, Instant.ofEpochMilli(0))
|
||||
}
|
Reference in New Issue
Block a user