exception safety, github actions and scalafmt

This commit is contained in:
Evgenii Alekseev 2022-01-06 19:01:30 +03:00
parent 99ed2705a2
commit 53b42a6fa8
84 changed files with 1536 additions and 909 deletions

22
.github/workflows/run-tests.yml vendored Normal file
View File

@ -0,0 +1,22 @@
name: tests
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
jobs:
run-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Setup JDK
uses: actions/setup-java@v2
with:
distribution: temurin
java-version: 8
- name: Build and Test
run: sbt -v +test

35
.scalafmt.conf Normal file
View File

@ -0,0 +1,35 @@
version = 3.3.1
runner.dialect = "scala213"
maxColumn = 120
align.preset = none
continuationIndent {
defnSite = 2
extendSite = 2
}
rewrite {
rules = [
AvoidInfix,
RedundantBraces,
RedundantParens,
SortImports,
SortModifiers
]
redundantBraces {
generalExpressions = yes
ifElseExpressions = yes
includeUnitMethods = yes
methodBodies = yes
parensForOneLineApply = yes
stringInterpolation = yes
}
}
importSelectors = singleLine
trailingCommas = preserve

View File

@ -1,9 +0,0 @@
language: scala
scala:
- 2.13.1
sbt_args: -no-colors
script:
- sbt compile
- sbt test

View File

@ -1,6 +1,6 @@
# FFXIV BiS # FFXIV BiS
[![Build Status](https://travis-ci.org/arcan1s/ffxivbis.svg?branch=master)](https://travis-ci.org/arcan1s/ffxivbis) ![GitHub release (latest by date)](https://img.shields.io/github/v/release/arcan1s/ffxivbis) [![Build status](https://github.com/arcan1s/ffxivbis/actions/workflows/run-tests.yml/badge.svg)](https://github.com/arcan1s/ffxivbis/actions/workflows/run-tests.yml) ![GitHub release (latest by date)](https://img.shields.io/github/v/release/arcan1s/ffxivbis)
Service which allows to manage savage loot distribution easy. Service which allows to manage savage loot distribution easy.

View File

@ -1,3 +1,4 @@
addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.14.6") addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.14.6")
addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.4.3")
addSbtPlugin("com.typesafe.sbt" % "sbt-native-packager" % "1.3.4") addSbtPlugin("com.typesafe.sbt" % "sbt-native-packager" % "1.3.4")
addSbtPlugin("net.virtual-void" % "sbt-dependency-graph" % "0.10.0-RC1") addSbtPlugin("net.virtual-void" % "sbt-dependency-graph" % "0.10.0-RC1")

View File

@ -16,22 +16,22 @@ import akka.stream.Materializer
import com.typesafe.scalalogging.StrictLogging import com.typesafe.scalalogging.StrictLogging
import me.arcanis.ffxivbis.http.RootEndpoint import me.arcanis.ffxivbis.http.RootEndpoint
import me.arcanis.ffxivbis.service.bis.BisProvider import me.arcanis.ffxivbis.service.bis.BisProvider
import me.arcanis.ffxivbis.service.{Database, PartyService} import me.arcanis.ffxivbis.service.database.Database
import me.arcanis.ffxivbis.service.PartyService
import me.arcanis.ffxivbis.storage.Migration import me.arcanis.ffxivbis.storage.Migration
import scala.concurrent.ExecutionContext import scala.concurrent.ExecutionContext
import scala.jdk.CollectionConverters._
import scala.util.{Failure, Success} import scala.util.{Failure, Success}
class Application(context: ActorContext[Nothing]) class Application(context: ActorContext[Nothing]) extends AbstractBehavior[Nothing](context) with StrictLogging {
extends AbstractBehavior[Nothing](context) with StrictLogging {
logger.info("root supervisor started") logger.info("root supervisor started")
startApplication() startApplication()
override def onMessage(msg: Nothing): Behavior[Nothing] = Behaviors.unhandled override def onMessage(msg: Nothing): Behavior[Nothing] = Behaviors.unhandled
override def onSignal: PartialFunction[Signal, Behavior[Nothing]] = { override def onSignal: PartialFunction[Signal, Behavior[Nothing]] = { case PostStop =>
case PostStop =>
logger.info("root supervisor stopped") logger.info("root supervisor stopped")
Behaviors.same Behaviors.same
} }
@ -45,7 +45,7 @@ class Application(context: ActorContext[Nothing])
implicit val materializer: Materializer = Materializer(context) implicit val materializer: Materializer = Materializer(context)
Migration(config) match { Migration(config) match {
case Success(_) => case Success(result) if result.success =>
val bisProvider = context.spawn(BisProvider(), "bis-provider") val bisProvider = context.spawn(BisProvider(), "bis-provider")
val storage = context.spawn(Database(), "storage") val storage = context.spawn(Database(), "storage")
val party = context.spawn(PartyService(storage), "party") val party = context.spawn(PartyService(storage), "party")
@ -54,6 +54,11 @@ class Application(context: ActorContext[Nothing])
val flow = Route.toFlow(http.route)(context.system) val flow = Route.toFlow(http.route)(context.system)
Http(context.system).newServerAt(host, port).bindFlow(flow) Http(context.system).newServerAt(host, port).bindFlow(flow)
case Success(result) =>
logger.error(s"migration completed with error, executed ${result.migrationsExecuted}")
result.migrations.asScala.foreach(o => logger.info(s"=> ${o.description} (${o.executionTime})"))
context.system.terminate()
case Failure(exception) => case Failure(exception) =>
logger.error("exception during migration", exception) logger.error("exception during migration", exception)
context.system.terminate() context.system.terminate()

View File

@ -25,8 +25,7 @@ trait Authorization {
def storage: ActorRef[Message] def storage: ActorRef[Message]
def authenticateBasicBCrypt[T](realm: String, def authenticateBasicBCrypt[T](realm: String, authenticate: (String, String) => Future[Option[T]]): Directive1[T] = {
authenticate: (String, String) => Future[Option[T]]): Directive1[T] = {
def challenge = HttpChallenges.basic(realm) def challenge = HttpChallenges.basic(realm)
extractCredentials.flatMap { extractCredentials.flatMap {
@ -39,22 +38,34 @@ 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
(implicit executionContext: ExecutionContext, timeout: Timeout, scheduler: Scheduler): Future[Option[String]] = executionContext: ExecutionContext,
timeout: Timeout,
scheduler: Scheduler
): Future[Option[String]] =
storage.ask(GetUser(partyId, username, _)).map { storage.ask(GetUser(partyId, username, _)).map {
case Some(user) if user.verify(password) && user.verityScope(scope) => Some(username) case Some(user) if user.verify(password) && user.verityScope(scope) => Some(username)
case _ => None case _ => None
} }
def authAdmin(partyId: String)(username: String, password: String) def authAdmin(partyId: String)(username: String, password: String)(implicit
(implicit executionContext: ExecutionContext, timeout: Timeout, scheduler: Scheduler): Future[Option[String]] = executionContext: ExecutionContext,
timeout: Timeout,
scheduler: Scheduler
): Future[Option[String]] =
authenticator(Permission.admin, partyId)(username, password) authenticator(Permission.admin, partyId)(username, password)
def authGet(partyId: String)(username: String, password: String) def authGet(partyId: String)(username: String, password: String)(implicit
(implicit executionContext: ExecutionContext, timeout: Timeout, scheduler: Scheduler): Future[Option[String]] = executionContext: ExecutionContext,
timeout: Timeout,
scheduler: Scheduler
): Future[Option[String]] =
authenticator(Permission.get, partyId)(username, password) authenticator(Permission.get, partyId)(username, password)
def authPost(partyId: String)(username: String, password: String) def authPost(partyId: String)(username: String, password: String)(implicit
(implicit executionContext: ExecutionContext, timeout: Timeout, scheduler: Scheduler): Future[Option[String]] = executionContext: ExecutionContext,
timeout: Timeout,
scheduler: Scheduler
): Future[Option[String]] =
authenticator(Permission.post, partyId)(username, password) authenticator(Permission.post, partyId)(username, password)
} }

View File

@ -21,32 +21,38 @@ trait BiSHelper extends BisProviderHelper {
def storage: ActorRef[Message] def storage: ActorRef[Message]
def addPieceBiS(playerId: PlayerId, piece: Piece) def addPieceBiS(playerId: PlayerId, piece: Piece)(implicit timeout: Timeout, scheduler: Scheduler): Future[Unit] =
(implicit timeout: Timeout, scheduler: Scheduler): Future[Unit] =
storage.ask(AddPieceToBis(playerId, piece.withJob(playerId.job), _)) storage.ask(AddPieceToBis(playerId, piece.withJob(playerId.job), _))
def bis(partyId: String, playerId: Option[PlayerId]) def bis(partyId: String, playerId: Option[PlayerId])(implicit
(implicit timeout: Timeout, scheduler: Scheduler): Future[Seq[Player]] = timeout: Timeout,
scheduler: Scheduler
): Future[Seq[Player]] =
storage.ask(GetBiS(partyId, playerId, _)) storage.ask(GetBiS(partyId, playerId, _))
def doModifyBiS(action: ApiAction.Value, playerId: PlayerId, piece: Piece) def doModifyBiS(action: ApiAction.Value, playerId: PlayerId, piece: Piece)(implicit
(implicit timeout: Timeout, scheduler: Scheduler): Future[Unit] = timeout: Timeout,
scheduler: Scheduler
): Future[Unit] =
action match { action match {
case ApiAction.add => addPieceBiS(playerId, piece) case ApiAction.add => addPieceBiS(playerId, piece)
case ApiAction.remove => removePieceBiS(playerId, piece) case ApiAction.remove => removePieceBiS(playerId, piece)
} }
def putBiS(playerId: PlayerId, link: String) def putBiS(playerId: PlayerId, link: String)(implicit
(implicit executionContext: ExecutionContext, timeout: Timeout, scheduler: Scheduler): Future[Unit] = { executionContext: ExecutionContext,
timeout: Timeout,
scheduler: Scheduler
): Future[Unit] =
storage.ask(RemovePiecesFromBiS(playerId, _)).flatMap { _ => storage.ask(RemovePiecesFromBiS(playerId, _)).flatMap { _ =>
downloadBiS(link, playerId.job).flatMap { bis => downloadBiS(link, playerId.job)
.flatMap { bis =>
Future.traverse(bis.pieces)(addPieceBiS(playerId, _)) Future.traverse(bis.pieces)(addPieceBiS(playerId, _))
}.map(_ => ())
} }
.map(_ => ())
} }
def removePieceBiS(playerId: PlayerId, piece: Piece) def removePieceBiS(playerId: PlayerId, piece: Piece)(implicit timeout: Timeout, scheduler: Scheduler): Future[Unit] =
(implicit timeout: Timeout, scheduler: Scheduler): Future[Unit] =
storage.ask(RemovePieceFromBiS(playerId, piece, _)) storage.ask(RemovePieceFromBiS(playerId, piece, _))
} }

View File

@ -20,7 +20,6 @@ trait BisProviderHelper {
def provider: ActorRef[BiSProviderMessage] def provider: ActorRef[BiSProviderMessage]
def downloadBiS(link: String, job: Job.Job) def downloadBiS(link: String, job: Job.Job)(implicit timeout: Timeout, scheduler: Scheduler): Future[BiS] =
(implicit timeout: Timeout, scheduler: Scheduler): Future[BiS] =
provider.ask(DownloadBiS(link, job, _)) provider.ask(DownloadBiS(link, job, _))
} }

View File

@ -21,28 +21,35 @@ trait LootHelper {
def storage: ActorRef[Message] def storage: ActorRef[Message]
def addPieceLoot(playerId: PlayerId, piece: Piece, isFreeLoot: Boolean) def addPieceLoot(playerId: PlayerId, piece: Piece, isFreeLoot: Boolean)(implicit
(implicit timeout: Timeout, scheduler: Scheduler): Future[Unit] = timeout: Timeout,
storage.ask( scheduler: Scheduler
AddPieceTo(playerId, piece, isFreeLoot, _)) ): Future[Unit] =
storage.ask(AddPieceTo(playerId, piece, isFreeLoot, _))
def doModifyLoot(action: ApiAction.Value, playerId: PlayerId, piece: Piece, maybeFree: Option[Boolean]) def doModifyLoot(action: ApiAction.Value, playerId: PlayerId, piece: Piece, maybeFree: Option[Boolean])(implicit
(implicit timeout: Timeout, scheduler: Scheduler): Future[Unit] = timeout: Timeout,
scheduler: Scheduler
): Future[Unit] =
(action, maybeFree) match { (action, maybeFree) match {
case (ApiAction.add, Some(isFreeLoot)) => addPieceLoot(playerId, piece, isFreeLoot) case (ApiAction.add, Some(isFreeLoot)) => addPieceLoot(playerId, piece, isFreeLoot)
case (ApiAction.remove, _) => removePieceLoot(playerId, piece) case (ApiAction.remove, _) => removePieceLoot(playerId, piece)
case _ => throw new IllegalArgumentException(s"Invalid combinantion of action $action and fee loot $maybeFree") case _ => throw new IllegalArgumentException(s"Invalid combinantion of action $action and fee loot $maybeFree")
} }
def loot(partyId: String, playerId: Option[PlayerId]) def loot(partyId: String, playerId: Option[PlayerId])(implicit
(implicit timeout: Timeout, scheduler: Scheduler): Future[Seq[Player]] = timeout: Timeout,
scheduler: Scheduler
): Future[Seq[Player]] =
storage.ask(GetLoot(partyId, playerId, _)) storage.ask(GetLoot(partyId, playerId, _))
def removePieceLoot(playerId: PlayerId, piece: Piece) def removePieceLoot(playerId: PlayerId, piece: Piece)(implicit timeout: Timeout, scheduler: Scheduler): Future[Unit] =
(implicit timeout: Timeout, scheduler: Scheduler): Future[Unit] =
storage.ask(RemovePieceFrom(playerId, piece, _)) storage.ask(RemovePieceFrom(playerId, piece, _))
def suggestPiece(partyId: String, piece: Piece) def suggestPiece(partyId: String, piece: Piece)(implicit
(implicit executionContext: ExecutionContext, timeout: Timeout, scheduler: Scheduler): Future[Seq[PlayerIdWithCounters]] = executionContext: ExecutionContext,
timeout: Timeout,
scheduler: Scheduler
): Future[Seq[PlayerIdWithCounters]] =
storage.ask(SuggestLoot(partyId, piece, _)).map(_.result) storage.ask(SuggestLoot(partyId, piece, _)).map(_.result)
} }

View File

@ -21,31 +21,42 @@ trait PlayerHelper extends BisProviderHelper {
def storage: ActorRef[Message] def storage: ActorRef[Message]
def addPlayer(player: Player) def addPlayer(
(implicit executionContext: ExecutionContext, timeout: Timeout, scheduler: Scheduler): Future[Unit] = player: Player
storage.ask(ref => AddPlayer(player, ref)).map { res => )(implicit executionContext: ExecutionContext, timeout: Timeout, scheduler: Scheduler): Future[Unit] =
storage
.ask(ref => AddPlayer(player, ref))
.map { res =>
player.link.map(_.trim).filter(_.nonEmpty) match { player.link.map(_.trim).filter(_.nonEmpty) match {
case Some(link) => case Some(link) =>
downloadBiS(link, player.job).map { bis => downloadBiS(link, player.job)
.map { bis =>
bis.pieces.map(piece => storage.ask(AddPieceToBis(player.playerId, piece, _))) bis.pieces.map(piece => storage.ask(AddPieceToBis(player.playerId, piece, _)))
}.map(_ => res) }
.map(_ => res)
case None => Future.successful(res) case None => Future.successful(res)
} }
}.flatten }
.flatten
def doModifyPlayer(action: ApiAction.Value, player: Player) def doModifyPlayer(action: ApiAction.Value, player: Player)(implicit
(implicit executionContext: ExecutionContext, timeout: Timeout, scheduler: Scheduler): Future[Unit] = executionContext: ExecutionContext,
timeout: Timeout,
scheduler: Scheduler
): Future[Unit] =
action match { action match {
case ApiAction.add => addPlayer(player) case ApiAction.add => addPlayer(player)
case ApiAction.remove => removePlayer(player.playerId) case ApiAction.remove => removePlayer(player.playerId)
} }
def getPartyDescription(partyId: String) def getPartyDescription(partyId: String)(implicit timeout: Timeout, scheduler: Scheduler): Future[PartyDescription] =
(implicit timeout: Timeout, scheduler: Scheduler): Future[PartyDescription] =
storage.ask(GetPartyDescription(partyId, _)) storage.ask(GetPartyDescription(partyId, _))
def getPlayers(partyId: String, maybePlayerId: Option[PlayerId]) def getPlayers(partyId: String, maybePlayerId: Option[PlayerId])(implicit
(implicit executionContext: ExecutionContext, timeout: Timeout, scheduler: Scheduler): Future[Seq[Player]] = executionContext: ExecutionContext,
timeout: Timeout,
scheduler: Scheduler
): Future[Seq[Player]] =
maybePlayerId match { maybePlayerId match {
case Some(playerId) => case Some(playerId) =>
storage.ask(GetPlayer(playerId, _)).map(_.toSeq) storage.ask(GetPlayer(playerId, _)).map(_.toSeq)
@ -53,11 +64,11 @@ trait PlayerHelper extends BisProviderHelper {
storage.ask(GetParty(partyId, _)).map(_.players.values.toSeq) storage.ask(GetParty(partyId, _)).map(_.players.values.toSeq)
} }
def removePlayer(playerId: PlayerId) def removePlayer(playerId: PlayerId)(implicit timeout: Timeout, scheduler: Scheduler): Future[Unit] =
(implicit timeout: Timeout, scheduler: Scheduler): Future[Unit] =
storage.ask(RemovePlayer(playerId, _)) storage.ask(RemovePlayer(playerId, _))
def updateDescription(partyDescription: PartyDescription) def updateDescription(
(implicit executionContext: ExecutionContext, timeout: Timeout, scheduler: Scheduler): Future[Unit] = partyDescription: PartyDescription
)(implicit executionContext: ExecutionContext, timeout: Timeout, scheduler: Scheduler): Future[Unit] =
storage.ask(UpdateParty(partyDescription, _)) storage.ask(UpdateParty(partyDescription, _))
} }

View File

@ -19,9 +19,7 @@ import me.arcanis.ffxivbis.http.api.v1.RootApiV1Endpoint
import me.arcanis.ffxivbis.http.view.RootView import me.arcanis.ffxivbis.http.view.RootView
import me.arcanis.ffxivbis.messages.{BiSProviderMessage, Message} import me.arcanis.ffxivbis.messages.{BiSProviderMessage, Message}
class RootEndpoint(system: ActorSystem[Nothing], class RootEndpoint(system: ActorSystem[Nothing], storage: ActorRef[Message], provider: ActorRef[BiSProviderMessage])
storage: ActorRef[Message],
provider: ActorRef[BiSProviderMessage])
extends StrictLogging { extends StrictLogging {
import me.arcanis.ffxivbis.utils.Implicits._ import me.arcanis.ffxivbis.utils.Implicits._
@ -41,7 +39,10 @@ class RootEndpoint(system: ActorSystem[Nothing],
val start = Instant.now.toEpochMilli val start = Instant.now.toEpochMilli
mapResponse { response => mapResponse { response =>
val time = (Instant.now.toEpochMilli - start) / 1000.0 val time = (Instant.now.toEpochMilli - start) / 1000.0
httpLogger.debug(s"""- - [${Instant.now}] "${context.request.method.name()} ${context.request.uri.path}" ${response.status.intValue()} ${response.entity.getContentLengthOption.getAsLong} $time""") httpLogger.debug(
s"""- - [${Instant.now}] "${context.request.method.name()} ${context.request.uri.path}" ${response.status
.intValue()} ${response.entity.getContentLengthOption.getAsLong} $time"""
)
response response
} }
} }

View File

@ -18,9 +18,12 @@ import scala.io.Source
class Swagger(config: Config) extends SwaggerHttpService { class Swagger(config: Config) extends SwaggerHttpService {
override val apiClasses: Set[Class[_]] = Set( override val apiClasses: Set[Class[_]] = Set(
classOf[api.v1.BiSEndpoint], classOf[api.v1.LootEndpoint], classOf[api.v1.BiSEndpoint],
classOf[api.v1.PartyEndpoint], classOf[api.v1.PlayerEndpoint], classOf[api.v1.LootEndpoint],
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( override val info: Info = Info(

View File

@ -20,22 +20,18 @@ trait UserHelper {
def storage: ActorRef[Message] def storage: ActorRef[Message]
def addUser(user: User, isHashedPassword: Boolean) def addUser(user: User, isHashedPassword: Boolean)(implicit timeout: Timeout, scheduler: Scheduler): Future[Unit] =
(implicit timeout: Timeout, scheduler: Scheduler): Future[Unit] =
storage.ask(AddUser(user, isHashedPassword, _)) storage.ask(AddUser(user, isHashedPassword, _))
def newPartyId(implicit timeout: Timeout, scheduler: Scheduler): Future[String] = def newPartyId(implicit timeout: Timeout, scheduler: Scheduler): Future[String] =
storage.ask(GetNewPartyId) storage.ask(GetNewPartyId)
def user(partyId: String, username: String) def user(partyId: String, username: String)(implicit timeout: Timeout, scheduler: Scheduler): Future[Option[User]] =
(implicit timeout: Timeout, scheduler: Scheduler): Future[Option[User]] =
storage.ask(GetUser(partyId, username, _)) storage.ask(GetUser(partyId, username, _))
def users(partyId: String) def users(partyId: String)(implicit timeout: Timeout, scheduler: Scheduler): Future[Seq[User]] =
(implicit timeout: Timeout, scheduler: Scheduler): Future[Seq[User]] =
storage.ask(GetUsers(partyId, _)) storage.ask(GetUsers(partyId, _))
def removeUser(partyId: String, username: String) def removeUser(partyId: String, username: String)(implicit timeout: Timeout, scheduler: Scheduler): Future[Unit] =
(implicit timeout: Timeout, scheduler: Scheduler): Future[Unit] =
storage.ask(DeleteUser(partyId, username, _)) storage.ask(DeleteUser(partyId, username, _))
} }

View File

@ -28,32 +28,51 @@ import me.arcanis.ffxivbis.models.PlayerId
import scala.util.{Failure, Success} import scala.util.{Failure, Success}
@Path("api/v1") @Path("api/v1")
class BiSEndpoint(override val storage: ActorRef[Message], class BiSEndpoint(override val storage: ActorRef[Message], override val provider: ActorRef[BiSProviderMessage])(implicit
override val provider: ActorRef[BiSProviderMessage]) timeout: Timeout,
(implicit timeout: Timeout, scheduler: Scheduler) scheduler: Scheduler
extends BiSHelper with Authorization with JsonSupport { ) extends BiSHelper
with Authorization
with JsonSupport {
def route: Route = createBiS ~ getBiS ~ modifyBiS def route: Route = createBiS ~ getBiS ~ modifyBiS
@PUT @PUT
@Path("party/{partyId}/bis") @Path("party/{partyId}/bis")
@Consumes(value = Array("application/json")) @Consumes(value = Array("application/json"))
@Operation(summary = "create best in slot", description = "Create the best in slot set", @Operation(
summary = "create best in slot",
description = "Create the best in slot set",
parameters = Array( parameters = Array(
new Parameter(name = "partyId", in = ParameterIn.PATH, description = "unique party ID", example = "abcdefgh"), new Parameter(name = "partyId", in = ParameterIn.PATH, description = "unique party ID", example = "abcdefgh"),
), ),
requestBody = new RequestBody(description = "player best in slot description", required = true, requestBody = new RequestBody(
content = Array(new Content(schema = new Schema(implementation = classOf[PlayerBiSLinkResponse])))), description = "player best in slot description",
required = true,
content = Array(new Content(schema = new Schema(implementation = classOf[PlayerBiSLinkResponse])))
),
responses = Array( responses = Array(
new ApiResponse(responseCode = "201", description = "Best in slot set has been created"), new ApiResponse(responseCode = "201", description = "Best in slot set has been created"),
new ApiResponse(responseCode = "400", description = "Invalid parameters were supplied", new ApiResponse(
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))), responseCode = "400",
new ApiResponse(responseCode = "401", description = "Supplied authorization is invalid", description = "Invalid parameters were supplied",
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))), 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(
new ApiResponse(responseCode = "500", description = "Internal server error", responseCode = "401",
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))), 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"))), security = Array(new SecurityRequirement(name = "basic auth", scopes = Array("post"))),
tags = Array("best in slot"), tags = Array("best in slot"),
@ -78,23 +97,44 @@ class BiSEndpoint(override val storage: ActorRef[Message],
@GET @GET
@Path("party/{partyId}/bis") @Path("party/{partyId}/bis")
@Produces(value = Array("application/json")) @Produces(value = Array("application/json"))
@Operation(summary = "get best in slot", description = "Return the best in slot items", @Operation(
summary = "get best in slot",
description = "Return the best in slot items",
parameters = Array( parameters = Array(
new Parameter(name = "partyId", in = ParameterIn.PATH, description = "unique party ID", example = "abcdefgh"), new Parameter(name = "partyId", in = ParameterIn.PATH, description = "unique party ID", example = "abcdefgh"),
new Parameter(name = "nick", in = ParameterIn.QUERY, description = "player nick name to filter", example = "Siuan Sanche"), new Parameter(
name = "nick",
in = ParameterIn.QUERY,
description = "player nick name to filter",
example = "Siuan Sanche"
),
new Parameter(name = "job", in = ParameterIn.QUERY, description = "player job to filter", example = "DNC"), new Parameter(name = "job", in = ParameterIn.QUERY, description = "player job to filter", example = "DNC"),
), ),
responses = Array( responses = Array(
new ApiResponse(responseCode = "200", description = "Best in slot", new ApiResponse(
content = Array(new Content( responseCode = "200",
description = "Best in slot",
content = Array(
new Content(
array = new ArraySchema(schema = new Schema(implementation = classOf[PlayerResponse])) array = new ArraySchema(schema = new Schema(implementation = classOf[PlayerResponse]))
))), )
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", new ApiResponse(
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))), responseCode = "401",
new ApiResponse(responseCode = "500", description = "Internal server error", description = "Supplied authorization is invalid",
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))), 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"))), security = Array(new SecurityRequirement(name = "basic auth", scopes = Array("get"))),
tags = Array("best in slot"), tags = Array("best in slot"),
@ -120,22 +160,39 @@ class BiSEndpoint(override val storage: ActorRef[Message],
@POST @POST
@Path("party/{partyId}/bis") @Path("party/{partyId}/bis")
@Consumes(value = Array("application/json")) @Consumes(value = Array("application/json"))
@Operation(summary = "modify best in slot", description = "Add or remove an item from the best in slot", @Operation(
summary = "modify best in slot",
description = "Add or remove an item from the best in slot",
parameters = Array( parameters = Array(
new Parameter(name = "partyId", in = ParameterIn.PATH, description = "unique party ID", example = "abcdefgh"), new Parameter(name = "partyId", in = ParameterIn.PATH, description = "unique party ID", example = "abcdefgh"),
), ),
requestBody = new RequestBody(description = "action and piece description", required = true, requestBody = new RequestBody(
content = Array(new Content(schema = new Schema(implementation = classOf[PieceActionResponse])))), description = "action and piece description",
required = true,
content = Array(new Content(schema = new Schema(implementation = classOf[PieceActionResponse])))
),
responses = Array( responses = Array(
new ApiResponse(responseCode = "202", description = "Best in slot set has been modified"), new ApiResponse(responseCode = "202", description = "Best in slot set has been modified"),
new ApiResponse(responseCode = "400", description = "Invalid parameters were supplied", new ApiResponse(
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))), responseCode = "400",
new ApiResponse(responseCode = "401", description = "Supplied authorization is invalid", description = "Invalid parameters were supplied",
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))), 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(
new ApiResponse(responseCode = "500", description = "Internal server error", responseCode = "401",
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))), 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"))), security = Array(new SecurityRequirement(name = "basic auth", scopes = Array("post"))),
tags = Array("best in slot"), tags = Array("best in slot"),

View File

@ -28,32 +28,55 @@ import me.arcanis.ffxivbis.models.PlayerId
import scala.util.{Failure, Success} import scala.util.{Failure, Success}
@Path("api/v1") @Path("api/v1")
class LootEndpoint(override val storage: ActorRef[Message]) class LootEndpoint(override val storage: ActorRef[Message])(implicit timeout: Timeout, scheduler: Scheduler)
(implicit timeout: Timeout, scheduler: Scheduler) extends LootHelper
extends LootHelper with Authorization with JsonSupport with HttpHandler { with Authorization
with JsonSupport
with HttpHandler {
def route: Route = getLoot ~ modifyLoot def route: Route = getLoot ~ modifyLoot
@GET @GET
@Path("party/{partyId}/loot") @Path("party/{partyId}/loot")
@Produces(value = Array("application/json")) @Produces(value = Array("application/json"))
@Operation(summary = "get loot list", description = "Return the looted items", @Operation(
summary = "get loot list",
description = "Return the looted items",
parameters = Array( parameters = Array(
new Parameter(name = "partyId", in = ParameterIn.PATH, description = "unique party ID", example = "abcdefgh"), new Parameter(name = "partyId", in = ParameterIn.PATH, description = "unique party ID", example = "abcdefgh"),
new Parameter(name = "nick", in = ParameterIn.QUERY, description = "player nick name to filter", example = "Siuan Sanche"), new Parameter(
name = "nick",
in = ParameterIn.QUERY,
description = "player nick name to filter",
example = "Siuan Sanche"
),
new Parameter(name = "job", in = ParameterIn.QUERY, description = "player job to filter", example = "DNC"), new Parameter(name = "job", in = ParameterIn.QUERY, description = "player job to filter", example = "DNC"),
), ),
responses = Array( responses = Array(
new ApiResponse(responseCode = "200", description = "Loot list", new ApiResponse(
content = Array(new Content( responseCode = "200",
description = "Loot list",
content = Array(
new Content(
array = new ArraySchema(schema = new Schema(implementation = classOf[PlayerResponse])) array = new ArraySchema(schema = new Schema(implementation = classOf[PlayerResponse]))
))), )
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", new ApiResponse(
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))), responseCode = "401",
new ApiResponse(responseCode = "500", description = "Internal server error", description = "Supplied authorization is invalid",
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))), 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"))), security = Array(new SecurityRequirement(name = "basic auth", scopes = Array("get"))),
tags = Array("loot"), tags = Array("loot"),
@ -78,22 +101,39 @@ class LootEndpoint(override val storage: ActorRef[Message])
@POST @POST
@Consumes(value = Array("application/json")) @Consumes(value = Array("application/json"))
@Path("party/{partyId}/loot") @Path("party/{partyId}/loot")
@Operation(summary = "modify loot list", description = "Add or remove an item from the loot list", @Operation(
summary = "modify loot list",
description = "Add or remove an item from the loot list",
parameters = Array( parameters = Array(
new Parameter(name = "partyId", in = ParameterIn.PATH, description = "unique party ID", example = "abcdefgh"), new Parameter(name = "partyId", in = ParameterIn.PATH, description = "unique party ID", example = "abcdefgh"),
), ),
requestBody = new RequestBody(description = "action and piece description", required = true, requestBody = new RequestBody(
content = Array(new Content(schema = new Schema(implementation = classOf[PieceActionResponse])))), description = "action and piece description",
required = true,
content = Array(new Content(schema = new Schema(implementation = classOf[PieceActionResponse])))
),
responses = Array( responses = Array(
new ApiResponse(responseCode = "202", description = "Loot list has been modified"), new ApiResponse(responseCode = "202", description = "Loot list has been modified"),
new ApiResponse(responseCode = "400", description = "Invalid parameters were supplied", new ApiResponse(
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))), responseCode = "400",
new ApiResponse(responseCode = "401", description = "Supplied authorization is invalid", description = "Invalid parameters were supplied",
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))), 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(
new ApiResponse(responseCode = "500", description = "Internal server error", responseCode = "401",
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))), 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"))), security = Array(new SecurityRequirement(name = "basic auth", scopes = Array("post"))),
tags = Array("loot"), tags = Array("loot"),
@ -119,25 +159,47 @@ class LootEndpoint(override val storage: ActorRef[Message])
@Path("party/{partyId}/loot") @Path("party/{partyId}/loot")
@Consumes(value = Array("application/json")) @Consumes(value = Array("application/json"))
@Produces(value = Array("application/json")) @Produces(value = Array("application/json"))
@Operation(summary = "suggest loot", description = "Suggest loot piece to party", @Operation(
summary = "suggest loot",
description = "Suggest loot piece to party",
parameters = Array( parameters = Array(
new Parameter(name = "partyId", in = ParameterIn.PATH, description = "unique party ID", example = "abcdefgh"), new Parameter(name = "partyId", in = ParameterIn.PATH, description = "unique party ID", example = "abcdefgh"),
), ),
requestBody = new RequestBody(description = "piece description", required = true, requestBody = new RequestBody(
content = Array(new Content(schema = new Schema(implementation = classOf[PieceResponse])))), description = "piece description",
required = true,
content = Array(new Content(schema = new Schema(implementation = classOf[PieceResponse])))
),
responses = Array( responses = Array(
new ApiResponse(responseCode = "200", description = "Players with counters ordered by priority to get this item", new ApiResponse(
content = Array(new Content( responseCode = "200",
description = "Players with counters ordered by priority to get this item",
content = Array(
new Content(
array = new ArraySchema(schema = new Schema(implementation = classOf[PlayerIdWithCountersResponse])), array = new ArraySchema(schema = new Schema(implementation = classOf[PlayerIdWithCountersResponse])),
))), )
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", new ApiResponse(
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))), responseCode = "400",
new ApiResponse(responseCode = "403", description = "Access is forbidden", description = "Invalid parameters were supplied",
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))), 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])))), 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"))), security = Array(new SecurityRequirement(name = "basic auth", scopes = Array("get"))),
tags = Array("loot"), tags = Array("loot"),

View File

@ -27,29 +27,47 @@ import me.arcanis.ffxivbis.messages.{BiSProviderMessage, Message}
import scala.util.{Failure, Success} import scala.util.{Failure, Success}
@Path("api/v1") @Path("api/v1")
class PartyEndpoint(override val storage: ActorRef[Message], class PartyEndpoint(override val storage: ActorRef[Message], override val provider: ActorRef[BiSProviderMessage])(
override val provider: ActorRef[BiSProviderMessage]) implicit
(implicit timeout: Timeout, scheduler: Scheduler) timeout: Timeout,
extends PlayerHelper with Authorization with JsonSupport with HttpHandler { scheduler: Scheduler
) extends PlayerHelper
with Authorization
with JsonSupport
with HttpHandler {
def route: Route = getPartyDescription ~ modifyPartyDescription def route: Route = getPartyDescription ~ modifyPartyDescription
@GET @GET
@Path("party/{partyId}/description") @Path("party/{partyId}/description")
@Produces(value = Array("application/json")) @Produces(value = Array("application/json"))
@Operation(summary = "get party description", description = "Return the party description", @Operation(
summary = "get party description",
description = "Return the party description",
parameters = Array( parameters = Array(
new Parameter(name = "partyId", in = ParameterIn.PATH, description = "unique party ID", example = "abcdefgh"), new Parameter(name = "partyId", in = ParameterIn.PATH, description = "unique party ID", example = "abcdefgh"),
), ),
responses = Array( responses = Array(
new ApiResponse(responseCode = "200", description = "Party description", new ApiResponse(
content = Array(new Content(schema = new Schema(implementation = classOf[PartyDescriptionResponse])))), responseCode = "200",
new ApiResponse(responseCode = "401", description = "Supplied authorization is invalid", description = "Party description",
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))), content = Array(new Content(schema = new Schema(implementation = classOf[PartyDescriptionResponse])))
new ApiResponse(responseCode = "403", description = "Access is forbidden", ),
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))), new ApiResponse(
new ApiResponse(responseCode = "500", description = "Internal server error", responseCode = "401",
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))), 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"))), security = Array(new SecurityRequirement(name = "basic auth", scopes = Array("get"))),
tags = Array("party"), tags = Array("party"),
@ -71,22 +89,39 @@ class PartyEndpoint(override val storage: ActorRef[Message],
@POST @POST
@Consumes(value = Array("application/json")) @Consumes(value = Array("application/json"))
@Path("party/{partyId}/description") @Path("party/{partyId}/description")
@Operation(summary = "modify party description", description = "Edit party description", @Operation(
summary = "modify party description",
description = "Edit party description",
parameters = Array( parameters = Array(
new Parameter(name = "partyId", in = ParameterIn.PATH, description = "unique party ID", example = "abcdefgh"), new Parameter(name = "partyId", in = ParameterIn.PATH, description = "unique party ID", example = "abcdefgh"),
), ),
requestBody = new RequestBody(description = "new party description", required = true, requestBody = new RequestBody(
content = Array(new Content(schema = new Schema(implementation = classOf[PartyDescriptionResponse])))), description = "new party description",
required = true,
content = Array(new Content(schema = new Schema(implementation = classOf[PartyDescriptionResponse])))
),
responses = Array( responses = Array(
new ApiResponse(responseCode = "202", description = "Party description has been modified"), new ApiResponse(responseCode = "202", description = "Party description has been modified"),
new ApiResponse(responseCode = "400", description = "Invalid parameters were supplied", new ApiResponse(
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))), responseCode = "400",
new ApiResponse(responseCode = "401", description = "Supplied authorization is invalid", description = "Invalid parameters were supplied",
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))), 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(
new ApiResponse(responseCode = "500", description = "Internal server error", responseCode = "401",
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))), 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"))), security = Array(new SecurityRequirement(name = "basic auth", scopes = Array("post"))),
tags = Array("party"), tags = Array("party"),

View File

@ -28,33 +28,58 @@ import me.arcanis.ffxivbis.models.PlayerId
import scala.util.{Failure, Success} import scala.util.{Failure, Success}
@Path("api/v1") @Path("api/v1")
class PlayerEndpoint(override val storage: ActorRef[Message], class PlayerEndpoint(override val storage: ActorRef[Message], override val provider: ActorRef[BiSProviderMessage])(
override val provider: ActorRef[BiSProviderMessage]) implicit
(implicit timeout: Timeout, scheduler: Scheduler) timeout: Timeout,
extends PlayerHelper with Authorization with JsonSupport with HttpHandler { scheduler: Scheduler
) extends PlayerHelper
with Authorization
with JsonSupport
with HttpHandler {
def route: Route = getParty ~ modifyParty def route: Route = getParty ~ modifyParty
@GET @GET
@Path("party/{partyId}") @Path("party/{partyId}")
@Produces(value = Array("application/json")) @Produces(value = Array("application/json"))
@Operation(summary = "get party", description = "Return the players who belong to the party", @Operation(
summary = "get party",
description = "Return the players who belong to the party",
parameters = Array( parameters = Array(
new Parameter(name = "partyId", in = ParameterIn.PATH, description = "unique party ID", example = "abcdefgh"), new Parameter(name = "partyId", in = ParameterIn.PATH, description = "unique party ID", example = "abcdefgh"),
new Parameter(name = "nick", in = ParameterIn.QUERY, description = "player nick name to filter", example = "Siuan Sanche"), new Parameter(
name = "nick",
in = ParameterIn.QUERY,
description = "player nick name to filter",
example = "Siuan Sanche"
),
new Parameter(name = "job", in = ParameterIn.QUERY, description = "player job to filter", example = "DNC"), new Parameter(name = "job", in = ParameterIn.QUERY, description = "player job to filter", example = "DNC"),
), ),
responses = Array( responses = Array(
new ApiResponse(responseCode = "200", description = "Players list", new ApiResponse(
content = Array(new Content( responseCode = "200",
description = "Players list",
content = Array(
new Content(
array = new ArraySchema(schema = new Schema(implementation = classOf[PlayerResponse])), array = new ArraySchema(schema = new Schema(implementation = classOf[PlayerResponse])),
))), )
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", new ApiResponse(
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))), responseCode = "401",
new ApiResponse(responseCode = "500", description = "Internal server error", description = "Supplied authorization is invalid",
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))), 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"))), security = Array(new SecurityRequirement(name = "basic auth", scopes = Array("get"))),
tags = Array("party"), tags = Array("party"),
@ -79,22 +104,39 @@ class PlayerEndpoint(override val storage: ActorRef[Message],
@POST @POST
@Path("party/{partyId}") @Path("party/{partyId}")
@Consumes(value = Array("application/json")) @Consumes(value = Array("application/json"))
@Operation(summary = "modify party", description = "Add or remove a player from party list", @Operation(
summary = "modify party",
description = "Add or remove a player from party list",
parameters = Array( parameters = Array(
new Parameter(name = "partyId", in = ParameterIn.PATH, description = "unique party ID", example = "abcdefgh"), new Parameter(name = "partyId", in = ParameterIn.PATH, description = "unique party ID", example = "abcdefgh"),
), ),
requestBody = new RequestBody(description = "player description", required = true, requestBody = new RequestBody(
content = Array(new Content(schema = new Schema(implementation = classOf[PlayerActionResponse])))), description = "player description",
required = true,
content = Array(new Content(schema = new Schema(implementation = classOf[PlayerActionResponse])))
),
responses = Array( responses = Array(
new ApiResponse(responseCode = "202", description = "Party has been modified"), new ApiResponse(responseCode = "202", description = "Party has been modified"),
new ApiResponse(responseCode = "400", description = "Invalid parameters were supplied", new ApiResponse(
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))), responseCode = "400",
new ApiResponse(responseCode = "401", description = "Supplied authorization is invalid", description = "Invalid parameters were supplied",
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))), 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(
new ApiResponse(responseCode = "500", description = "Internal server error", responseCode = "401",
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))), 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"))), security = Array(new SecurityRequirement(name = "basic auth", scopes = Array("post"))),
tags = Array("party"), tags = Array("party"),

View File

@ -16,10 +16,11 @@ import com.typesafe.config.Config
import me.arcanis.ffxivbis.http.api.v1.json.JsonSupport import me.arcanis.ffxivbis.http.api.v1.json.JsonSupport
import me.arcanis.ffxivbis.messages.{BiSProviderMessage, Message} import me.arcanis.ffxivbis.messages.{BiSProviderMessage, Message}
class RootApiV1Endpoint(storage: ActorRef[Message], class RootApiV1Endpoint(storage: ActorRef[Message], provider: ActorRef[BiSProviderMessage], config: Config)(implicit
provider: ActorRef[BiSProviderMessage], timeout: Timeout,
config: Config)(implicit timeout: Timeout, scheduler: Scheduler) scheduler: Scheduler
extends JsonSupport with HttpHandler { ) extends JsonSupport
with HttpHandler {
private val biSEndpoint = new BiSEndpoint(storage, provider) private val biSEndpoint = new BiSEndpoint(storage, provider)
private val lootEndpoint = new LootEndpoint(storage) private val lootEndpoint = new LootEndpoint(storage)

View File

@ -26,14 +26,24 @@ class TypesEndpoint(config: Config) extends JsonSupport {
@GET @GET
@Path("types/jobs") @Path("types/jobs")
@Produces(value = Array("application/json")) @Produces(value = Array("application/json"))
@Operation(summary = "jobs list", description = "Returns the available jobs", @Operation(
summary = "jobs list",
description = "Returns the available jobs",
responses = Array( responses = Array(
new ApiResponse(responseCode = "200", description = "List of available jobs", new ApiResponse(
content = Array(new Content( responseCode = "200",
description = "List of available jobs",
content = Array(
new Content(
array = new ArraySchema(schema = new Schema(implementation = classOf[String])) array = new ArraySchema(schema = new Schema(implementation = classOf[String]))
))), )
new ApiResponse(responseCode = "500", description = "Internal server error", )
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])))
),
), ),
tags = Array("types"), tags = Array("types"),
) )
@ -47,14 +57,24 @@ class TypesEndpoint(config: Config) extends JsonSupport {
@GET @GET
@Path("types/permissions") @Path("types/permissions")
@Produces(value = Array("application/json")) @Produces(value = Array("application/json"))
@Operation(summary = "permissions list", description = "Returns the available permissions", @Operation(
summary = "permissions list",
description = "Returns the available permissions",
responses = Array( responses = Array(
new ApiResponse(responseCode = "200", description = "List of available permissions", new ApiResponse(
content = Array(new Content( responseCode = "200",
description = "List of available permissions",
content = Array(
new Content(
array = new ArraySchema(schema = new Schema(implementation = classOf[String])) array = new ArraySchema(schema = new Schema(implementation = classOf[String]))
))), )
new ApiResponse(responseCode = "500", description = "Internal server error", )
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])))
),
), ),
tags = Array("types"), tags = Array("types"),
) )
@ -68,14 +88,24 @@ class TypesEndpoint(config: Config) extends JsonSupport {
@GET @GET
@Path("types/pieces") @Path("types/pieces")
@Produces(value = Array("application/json")) @Produces(value = Array("application/json"))
@Operation(summary = "pieces list", description = "Returns the available pieces", @Operation(
summary = "pieces list",
description = "Returns the available pieces",
responses = Array( responses = Array(
new ApiResponse(responseCode = "200", description = "List of available pieces", new ApiResponse(
content = Array(new Content( responseCode = "200",
description = "List of available pieces",
content = Array(
new Content(
array = new ArraySchema(schema = new Schema(implementation = classOf[String])) array = new ArraySchema(schema = new Schema(implementation = classOf[String]))
))), )
new ApiResponse(responseCode = "500", description = "Internal server error", )
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])))
),
), ),
tags = Array("types"), tags = Array("types"),
) )
@ -89,14 +119,24 @@ class TypesEndpoint(config: Config) extends JsonSupport {
@GET @GET
@Path("types/pieces/types") @Path("types/pieces/types")
@Produces(value = Array("application/json")) @Produces(value = Array("application/json"))
@Operation(summary = "piece types list", description = "Returns the available piece types", @Operation(
summary = "piece types list",
description = "Returns the available piece types",
responses = Array( responses = Array(
new ApiResponse(responseCode = "200", description = "List of available piece types", new ApiResponse(
content = Array(new Content( responseCode = "200",
description = "List of available piece types",
content = Array(
new Content(
array = new ArraySchema(schema = new Schema(implementation = classOf[String])) array = new ArraySchema(schema = new Schema(implementation = classOf[String]))
))), )
new ApiResponse(responseCode = "500", description = "Internal server error", )
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])))
),
), ),
tags = Array("types"), tags = Array("types"),
) )
@ -110,14 +150,24 @@ class TypesEndpoint(config: Config) extends JsonSupport {
@GET @GET
@Path("types/priority") @Path("types/priority")
@Produces(value = Array("application/json")) @Produces(value = Array("application/json"))
@Operation(summary = "priority list", description = "Returns the current priority list", @Operation(
summary = "priority list",
description = "Returns the current priority list",
responses = Array( responses = Array(
new ApiResponse(responseCode = "200", description = "Priority order", new ApiResponse(
content = Array(new Content( responseCode = "200",
description = "Priority order",
content = Array(
new Content(
array = new ArraySchema(schema = new Schema(implementation = classOf[String])) array = new ArraySchema(schema = new Schema(implementation = classOf[String]))
))), )
new ApiResponse(responseCode = "500", description = "Internal server error", )
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])))
),
), ),
tags = Array("types"), tags = Array("types"),
) )

View File

@ -28,26 +28,41 @@ import me.arcanis.ffxivbis.models.Permission
import scala.util.{Failure, Success} import scala.util.{Failure, Success}
@Path("api/v1") @Path("api/v1")
class UserEndpoint(override val storage: ActorRef[Message]) class UserEndpoint(override val storage: ActorRef[Message])(implicit timeout: Timeout, scheduler: Scheduler)
(implicit timeout: Timeout, scheduler: Scheduler) extends UserHelper
extends UserHelper with Authorization with JsonSupport { with Authorization
with JsonSupport {
def route: Route = createParty ~ createUser ~ deleteUser ~ getUsers def route: Route = createParty ~ createUser ~ deleteUser ~ getUsers
@PUT @PUT
@Path("party") @Path("party")
@Consumes(value = Array("application/json")) @Consumes(value = Array("application/json"))
@Operation(summary = "create new party", description = "Create new party with specified ID", @Operation(
requestBody = new RequestBody(description = "party administrator description", required = true, summary = "create new party",
content = Array(new Content(schema = new Schema(implementation = classOf[UserResponse])))), description = "Create new party with specified ID",
requestBody = new RequestBody(
description = "party administrator description",
required = true,
content = Array(new Content(schema = new Schema(implementation = classOf[UserResponse])))
),
responses = Array( responses = Array(
new ApiResponse(responseCode = "200", description = "Party has been created"), new ApiResponse(responseCode = "200", description = "Party has been created"),
new ApiResponse(responseCode = "400", description = "Invalid parameters were supplied", new ApiResponse(
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))), responseCode = "400",
new ApiResponse(responseCode = "406", description = "Party with the specified ID already exists", description = "Invalid parameters were supplied",
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))), 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])))), new ApiResponse(
responseCode = "406",
description = "Party with the specified ID already exists",
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])))
),
), ),
tags = Array("party"), tags = Array("party"),
) )
@ -73,22 +88,39 @@ class UserEndpoint(override val storage: ActorRef[Message])
@POST @POST
@Path("party/{partyId}/users") @Path("party/{partyId}/users")
@Consumes(value = Array("application/json")) @Consumes(value = Array("application/json"))
@Operation(summary = "create new user", description = "Add an user to the specified party", @Operation(
summary = "create new user",
description = "Add an user to the specified party",
parameters = Array( parameters = Array(
new Parameter(name = "partyId", in = ParameterIn.PATH, description = "unique party ID", example = "abcdefgh"), new Parameter(name = "partyId", in = ParameterIn.PATH, description = "unique party ID", example = "abcdefgh"),
), ),
requestBody = new RequestBody(description = "user description", required = true, requestBody = new RequestBody(
content = Array(new Content(schema = new Schema(implementation = classOf[UserResponse])))), description = "user description",
required = true,
content = Array(new Content(schema = new Schema(implementation = classOf[UserResponse])))
),
responses = Array( responses = Array(
new ApiResponse(responseCode = "201", description = "User has been created"), new ApiResponse(responseCode = "201", description = "User has been created"),
new ApiResponse(responseCode = "400", description = "Invalid parameters were supplied", new ApiResponse(
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))), responseCode = "400",
new ApiResponse(responseCode = "401", description = "Supplied authorization is invalid", description = "Invalid parameters were supplied",
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))), 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(
new ApiResponse(responseCode = "500", description = "Internal server error", responseCode = "401",
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))), 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("admin"))), security = Array(new SecurityRequirement(name = "basic auth", scopes = Array("admin"))),
tags = Array("users"), tags = Array("users"),
@ -112,19 +144,30 @@ class UserEndpoint(override val storage: ActorRef[Message])
@DELETE @DELETE
@Path("party/{partyId}/users/{username}") @Path("party/{partyId}/users/{username}")
@Operation(summary = "remove user", description = "Remove an user from the specified party", @Operation(
summary = "remove user",
description = "Remove an user from the specified party",
parameters = Array( parameters = Array(
new Parameter(name = "partyId", in = ParameterIn.PATH, description = "unique party ID", example = "abcdefgh"), new Parameter(name = "partyId", in = ParameterIn.PATH, description = "unique party ID", example = "abcdefgh"),
new Parameter(name = "username", in = ParameterIn.PATH, description = "username to remove", example = "siuan"), new Parameter(name = "username", in = ParameterIn.PATH, description = "username to remove", example = "siuan"),
), ),
responses = Array( responses = Array(
new ApiResponse(responseCode = "202", description = "User has been removed"), new ApiResponse(responseCode = "202", description = "User has been removed"),
new ApiResponse(responseCode = "401", description = "Supplied authorization is invalid", new ApiResponse(
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))), responseCode = "401",
new ApiResponse(responseCode = "403", description = "Access is forbidden", description = "Supplied authorization is invalid",
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))), 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])))), 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("admin"))), security = Array(new SecurityRequirement(name = "basic auth", scopes = Array("admin"))),
tags = Array("users"), tags = Array("users"),
@ -146,21 +189,37 @@ class UserEndpoint(override val storage: ActorRef[Message])
@GET @GET
@Path("party/{partyId}/users") @Path("party/{partyId}/users")
@Produces(value = Array("application/json")) @Produces(value = Array("application/json"))
@Operation(summary = "get users", description = "Return the list of users belong to party", @Operation(
summary = "get users",
description = "Return the list of users belong to party",
parameters = Array( parameters = Array(
new Parameter(name = "partyId", in = ParameterIn.PATH, description = "unique party ID", example = "abcdefgh"), new Parameter(name = "partyId", in = ParameterIn.PATH, description = "unique party ID", example = "abcdefgh"),
), ),
responses = Array( responses = Array(
new ApiResponse(responseCode = "200", description = "Users list", new ApiResponse(
content = Array(new Content( responseCode = "200",
description = "Users list",
content = Array(
new Content(
array = new ArraySchema(schema = new Schema(implementation = classOf[UserResponse])), array = new ArraySchema(schema = new Schema(implementation = classOf[UserResponse])),
))), )
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", new ApiResponse(
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))), responseCode = "401",
new ApiResponse(responseCode = "500", description = "Internal server error", description = "Supplied authorization is invalid",
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))), 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("admin"))), security = Array(new SecurityRequirement(name = "basic auth", scopes = Array("admin"))),
tags = Array("users"), tags = Array("users"),

View File

@ -10,5 +10,4 @@ package me.arcanis.ffxivbis.http.api.v1.json
import io.swagger.v3.oas.annotations.media.Schema import io.swagger.v3.oas.annotations.media.Schema
case class ErrorResponse( case class ErrorResponse(@Schema(description = "error message", required = true) message: String)
@Schema(description = "error message", required = true) message: String)

View File

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

View File

@ -8,7 +8,8 @@ import me.arcanis.ffxivbis.models.Loot
case class LootResponse( case class LootResponse(
@Schema(description = "looted piece", required = true) piece: PieceResponse, @Schema(description = "looted piece", required = true) piece: PieceResponse,
@Schema(description = "loot timestamp", required = true) timestamp: Instant, @Schema(description = "loot timestamp", required = true) timestamp: Instant,
@Schema(description = "is loot free for all", required = true) isFreeLoot: Boolean) { @Schema(description = "is loot free for all", required = true) isFreeLoot: Boolean
) {
def toLoot: Loot = Loot(-1, piece.toPiece, timestamp, isFreeLoot) def toLoot: Loot = Loot(-1, piece.toPiece, timestamp, isFreeLoot)
} }

View File

@ -13,7 +13,8 @@ import me.arcanis.ffxivbis.models.PartyDescription
case class PartyDescriptionResponse( case class PartyDescriptionResponse(
@Schema(description = "party id", required = true) partyId: String, @Schema(description = "party id", required = true) partyId: String,
@Schema(description = "party name") partyAlias: Option[String]) { @Schema(description = "party name") partyAlias: Option[String]
) {
def toDescription: PartyDescription = PartyDescription(partyId, partyAlias) def toDescription: PartyDescription = PartyDescription(partyId, partyAlias)
} }

View File

@ -10,5 +10,4 @@ package me.arcanis.ffxivbis.http.api.v1.json
import io.swagger.v3.oas.annotations.media.Schema import io.swagger.v3.oas.annotations.media.Schema
case class PartyIdResponse( case class PartyIdResponse(@Schema(description = "party id", required = true) partyId: String)
@Schema(description = "party id", required = true) partyId: String)

View File

@ -11,7 +11,13 @@ package me.arcanis.ffxivbis.http.api.v1.json
import io.swagger.v3.oas.annotations.media.Schema import io.swagger.v3.oas.annotations.media.Schema
case class PieceActionResponse( case class PieceActionResponse(
@Schema(description = "action to perform", required = true, `type` = "string", allowableValues = Array("add", "remove")) action: ApiAction.Value, @Schema(
description = "action to perform",
required = true,
`type` = "string",
allowableValues = Array("add", "remove")
) action: ApiAction.Value,
@Schema(description = "piece description", required = true) piece: PieceResponse, @Schema(description = "piece description", required = true) piece: PieceResponse,
@Schema(description = "player description", required = true) playerId: PlayerIdResponse, @Schema(description = "player description", required = true) playerId: PlayerIdResponse,
@Schema(description = "is piece free to roll or not") isFreeLoot: Option[Boolean]) @Schema(description = "is piece free to roll or not") isFreeLoot: Option[Boolean]
)

View File

@ -14,7 +14,8 @@ import me.arcanis.ffxivbis.models.{Job, Piece, PieceType}
case class PieceResponse( case class PieceResponse(
@Schema(description = "piece type", required = true) pieceType: String, @Schema(description = "piece type", required = true) pieceType: String,
@Schema(description = "job name to which piece belong or AnyJob", required = true, example = "DNC") job: String, @Schema(description = "job name to which piece belong or AnyJob", required = true, example = "DNC") job: String,
@Schema(description = "piece name", required = true, example = "body") piece: String) { @Schema(description = "piece name", required = true, example = "body") piece: String
) {
def toPiece: Piece = Piece(piece, PieceType.withName(pieceType), Job.withName(job)) def toPiece: Piece = Piece(piece, PieceType.withName(pieceType), Job.withName(job))
} }

View File

@ -11,5 +11,12 @@ package me.arcanis.ffxivbis.http.api.v1.json
import io.swagger.v3.oas.annotations.media.Schema import io.swagger.v3.oas.annotations.media.Schema
case class PlayerActionResponse( case class PlayerActionResponse(
@Schema(description = "action to perform", required = true, `type` = "string", allowableValues = Array("add", "remove"), example = "add") action: ApiAction.Value, @Schema(
@Schema(description = "player description", required = true) playerId: PlayerResponse) description = "action to perform",
required = true,
`type` = "string",
allowableValues = Array("add", "remove"),
example = "add"
) action: ApiAction.Value,
@Schema(description = "player description", required = true) playerId: PlayerResponse
)

View File

@ -11,5 +11,10 @@ package me.arcanis.ffxivbis.http.api.v1.json
import io.swagger.v3.oas.annotations.media.Schema import io.swagger.v3.oas.annotations.media.Schema
case class PlayerBiSLinkResponse( case class PlayerBiSLinkResponse(
@Schema(description = "link to player best in slot", required = true, example = "https://ffxiv.ariyala.com/19V5R") link: String, @Schema(
@Schema(description = "player description", required = true) playerId: PlayerIdResponse) description = "link to player best in slot",
required = true,
example = "https://ffxiv.ariyala.com/19V5R"
) link: String,
@Schema(description = "player description", required = true) playerId: PlayerIdResponse
)

View File

@ -14,7 +14,8 @@ import me.arcanis.ffxivbis.models.{Job, PlayerId}
case class PlayerIdResponse( case class PlayerIdResponse(
@Schema(description = "unique party ID. Required in responses", example = "abcdefgh") partyId: Option[String], @Schema(description = "unique party ID. Required in responses", example = "abcdefgh") partyId: Option[String],
@Schema(description = "job name", required = true, example = "DNC") job: String, @Schema(description = "job name", required = true, example = "DNC") job: String,
@Schema(description = "player nick name", required = true, example = "Siuan Sanche") nick: String) { @Schema(description = "player nick name", required = true, example = "Siuan Sanche") nick: String
) {
def withPartyId(partyId: String): PlayerId = def withPartyId(partyId: String): PlayerId =
PlayerId(partyId, Job.withName(job), nick) PlayerId(partyId, Job.withName(job), nick)

View File

@ -20,7 +20,8 @@ case class PlayerIdWithCountersResponse(
@Schema(description = "count of savage pieces in best in slot", required = true) bisCountTotal: Int, @Schema(description = "count of savage pieces in best in slot", required = true) bisCountTotal: Int,
@Schema(description = "count of looted pieces", required = true) lootCount: Int, @Schema(description = "count of looted pieces", required = true) lootCount: Int,
@Schema(description = "count of looted pieces which are parts of best in slot", required = true) lootCountBiS: Int, @Schema(description = "count of looted pieces which are parts of best in slot", required = true) lootCountBiS: Int,
@Schema(description = "total count of looted pieces", required = true) lootCountTotal: Int) @Schema(description = "total count of looted pieces", required = true) lootCountTotal: Int
)
object PlayerIdWithCountersResponse { object PlayerIdWithCountersResponse {
@ -34,5 +35,6 @@ object PlayerIdWithCountersResponse {
playerIdWithCounters.bisCountTotal, playerIdWithCounters.bisCountTotal,
playerIdWithCounters.lootCount, playerIdWithCounters.lootCount,
playerIdWithCounters.lootCountBiS, playerIdWithCounters.lootCountBiS,
playerIdWithCounters.lootCountTotal) playerIdWithCounters.lootCountTotal
)
} }

View File

@ -18,20 +18,32 @@ case class PlayerResponse(
@Schema(description = "pieces in best in slot") bis: Option[Seq[PieceResponse]], @Schema(description = "pieces in best in slot") bis: Option[Seq[PieceResponse]],
@Schema(description = "looted pieces") loot: Option[Seq[LootResponse]], @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 = "link to best in slot", example = "https://ffxiv.ariyala.com/19V5R") link: Option[String],
@Schema(description = "player loot priority", `type` = "number") priority: Option[Int]) { @Schema(description = "player loot priority", `type` = "number") priority: Option[Int]
) {
def toPlayer: Player = def toPlayer: Player =
Player(-1, partyId, Job.withName(job), nick, Player(
-1,
partyId,
Job.withName(job),
nick,
BiS(bis.getOrElse(Seq.empty).map(_.toPiece)), BiS(bis.getOrElse(Seq.empty).map(_.toPiece)),
loot.getOrElse(Seq.empty).map(_.toLoot), loot.getOrElse(Seq.empty).map(_.toLoot),
link, priority.getOrElse(0)) link,
priority.getOrElse(0)
)
} }
object PlayerResponse { object PlayerResponse {
def fromPlayer(player: Player): PlayerResponse = def fromPlayer(player: Player): PlayerResponse =
PlayerResponse(player.partyId, player.job.toString, player.nick, PlayerResponse(
player.partyId,
player.job.toString,
player.nick,
Some(player.bis.pieces.map(PieceResponse.fromPiece)), Some(player.bis.pieces.map(PieceResponse.fromPiece)),
Some(player.loot.map(LootResponse.fromLoot)), Some(player.loot.map(LootResponse.fromLoot)),
player.link, Some(player.priority)) player.link,
Some(player.priority)
)
} }

View File

@ -15,7 +15,12 @@ case class UserResponse(
@Schema(description = "unique party ID", required = true, example = "abcdefgh") partyId: String, @Schema(description = "unique party ID", required = true, example = "abcdefgh") partyId: String,
@Schema(description = "username to login to party", required = true, example = "siuan") username: String, @Schema(description = "username to login to party", required = true, example = "siuan") username: String,
@Schema(description = "password to login to party", required = true, example = "pa55w0rd") password: String, @Schema(description = "password to login to party", required = true, example = "pa55w0rd") password: String,
@Schema(description = "user permission", defaultValue = "get", allowableValues = Array("get", "post", "admin")) permission: Option[Permission.Value] = None) { @Schema(
description = "user permission",
defaultValue = "get",
allowableValues = Array("get", "post", "admin")
) permission: Option[Permission.Value] = None
) {
def toUser: User = def toUser: User =
User(partyId, username, password, permission.getOrElse(Permission.get)) User(partyId, username, password, permission.getOrElse(Permission.get))

View File

@ -18,10 +18,12 @@ import me.arcanis.ffxivbis.messages.{BiSProviderMessage, Message}
import scala.util.{Failure, Success} import scala.util.{Failure, Success}
class BasePartyView(override val storage: ActorRef[Message], class BasePartyView(override val storage: ActorRef[Message], override val provider: ActorRef[BiSProviderMessage])(
override val provider: ActorRef[BiSProviderMessage]) implicit
(implicit timeout: Timeout, scheduler: Scheduler) timeout: Timeout,
extends PlayerHelper with Authorization { scheduler: Scheduler
) extends PlayerHelper
with Authorization {
def route: Route = getIndex def route: Route = getIndex
@ -47,25 +49,25 @@ object BasePartyView {
import scalatags.Text.tags2.{title => titleTag} import scalatags.Text.tags2.{title => titleTag}
def root(partyId: String): Text.TypedTag[String] = def root(partyId: String): Text.TypedTag[String] =
a(href:=s"/party/$partyId", title:="root")("root") a(href := s"/party/$partyId", title := "root")("root")
def template(partyId: String, alias: 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\">" + "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01//EN\" \"http://www.w3.org/TR/html4/strict.dtd\">" +
html(lang:="en", html(
lang := "en",
head( head(
titleTag(s"Party $alias"), titleTag(s"Party $alias"),
link(rel:="stylesheet", `type`:="text/css", href:="/static/styles.css") link(rel := "stylesheet", `type` := "text/css", href := "/static/styles.css")
), ),
body( body(
h2(s"Party $alias"), h2(s"Party $alias"),
br, br,
h2(a(href:=s"/party/$partyId/players", title:="party")("party")), h2(a(href := s"/party/$partyId/players", title := "party")("party")),
h2(a(href:=s"/party/$partyId/bis", title:="bis management")("best in slot")), h2(a(href := s"/party/$partyId/bis", title := "bis management")("best in slot")),
h2(a(href:=s"/party/$partyId/loot", title:="loot management")("loot")), h2(a(href := s"/party/$partyId/loot", title := "loot management")("loot")),
h2(a(href:=s"/party/$partyId/suggest", title:="suggest loot")("suggest")), h2(a(href := s"/party/$partyId/suggest", title := "suggest loot")("suggest")),
hr, hr,
h2(a(href:=s"/party/$partyId/users", title:="user management")("users")) h2(a(href := s"/party/$partyId/users", title := "user management")("users"))
) )
) )
} }

View File

@ -20,10 +20,11 @@ import me.arcanis.ffxivbis.models.{Piece, PieceType, Player, PlayerId}
import scala.concurrent.{ExecutionContext, Future} import scala.concurrent.{ExecutionContext, Future}
import scala.util.Try import scala.util.Try
class BiSView(override val storage: ActorRef[Message], class BiSView(override val storage: ActorRef[Message], override val provider: ActorRef[BiSProviderMessage])(implicit
override val provider: ActorRef[BiSProviderMessage]) timeout: Timeout,
(implicit timeout: Timeout, scheduler: Scheduler) scheduler: Scheduler
extends BiSHelper with Authorization { ) extends BiSHelper
with Authorization {
def route: Route = getBiS ~ modifyBiS def route: Route = getBiS ~ modifyBiS
@ -33,9 +34,11 @@ class BiSView(override val storage: ActorRef[Message],
authenticateBasicBCrypt(s"party $partyId", authGet(partyId)) { _ => authenticateBasicBCrypt(s"party $partyId", authGet(partyId)) { _ =>
get { get {
complete { complete {
bis(partyId, None).map { players => bis(partyId, None)
.map { players =>
BiSView.template(partyId, players, None) BiSView.template(partyId, players, None)
}.map { text => }
.map { text =>
(StatusCodes.OK, RootView.toHtml(text)) (StatusCodes.OK, RootView.toHtml(text))
} }
} }
@ -49,8 +52,13 @@ class BiSView(override val storage: ActorRef[Message],
extractExecutionContext { implicit executionContext => extractExecutionContext { implicit executionContext =>
authenticateBasicBCrypt(s"party $partyId", authPost(partyId)) { _ => authenticateBasicBCrypt(s"party $partyId", authPost(partyId)) { _ =>
post { post {
formFields("player".as[String], "piece".as[String].?, "piece_type".as[String].?, "link".as[String].?, "action".as[String]) { formFields(
(player, maybePiece, maybePieceType, maybeLink, action) => "player".as[String],
"piece".as[String].?,
"piece_type".as[String].?,
"link".as[String].?,
"action".as[String]
) { (player, maybePiece, maybePieceType, maybeLink, action) =>
onComplete(modifyBiSCall(partyId, player, maybePiece, maybePieceType, maybeLink, action)) { _ => onComplete(modifyBiSCall(partyId, player, maybePiece, maybePieceType, maybeLink, action)) { _ =>
redirect(s"/party/$partyId/bis", StatusCodes.Found) redirect(s"/party/$partyId/bis", StatusCodes.Found)
} }
@ -60,10 +68,14 @@ class BiSView(override val storage: ActorRef[Message],
} }
} }
private def modifyBiSCall(partyId: String, player: String, private def modifyBiSCall(
maybePiece: Option[String], maybePieceType: Option[String], partyId: String,
maybeLink: Option[String], action: String) player: String,
(implicit executionContext: ExecutionContext, timeout: Timeout): Future[Unit] = { maybePiece: Option[String],
maybePieceType: Option[String],
maybeLink: Option[String],
action: String
)(implicit executionContext: ExecutionContext, timeout: Timeout): Future[Unit] = {
def getPiece(playerId: PlayerId, piece: String, pieceType: String) = def getPiece(playerId: PlayerId, piece: String, pieceType: String) =
Try(Piece(piece, PieceType.withName(pieceType), playerId.job)).toOption Try(Piece(piece, PieceType.withName(pieceType), playerId.job)).toOption
@ -74,7 +86,8 @@ class BiSView(override val storage: ActorRef[Message],
} }
PlayerId(partyId, player) match { PlayerId(partyId, player) match {
case Some(playerId) => (maybePiece, maybePieceType, action, maybeLink.map(_.trim).filter(_.nonEmpty)) match { case Some(playerId) =>
(maybePiece, maybePieceType, action, maybeLink.map(_.trim).filter(_.nonEmpty)) match {
case (Some(piece), Some(pieceType), "add", _) => case (Some(piece), Some(pieceType), "add", _) =>
bisAction(playerId, piece, pieceType)(addPieceBiS(playerId, _)) bisAction(playerId, piece, pieceType)(addPieceBiS(playerId, _))
case (Some(piece), Some(pieceType), "remove", _) => case (Some(piece), Some(pieceType), "remove", _) =>
@ -93,63 +106,68 @@ object BiSView {
def template(partyId: String, party: Seq[Player], error: Option[String]): String = def template(partyId: String, party: Seq[Player], error: Option[String]): String =
"<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01//EN\" \"http://www.w3.org/TR/html4/strict.dtd\">" + "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01//EN\" \"http://www.w3.org/TR/html4/strict.dtd\">" +
html(lang:="en", html(
lang := "en",
head( head(
titleTag("Best in slot"), titleTag("Best in slot"),
link(rel:="stylesheet", `type`:="text/css", href:="/static/styles.css") link(rel := "stylesheet", `type` := "text/css", href := "/static/styles.css")
), ),
body( body(
h2("Best in slot"), h2("Best in slot"),
ErrorView.template(error), ErrorView.template(error),
SearchLineView.template, SearchLineView.template,
form(action := s"/party/$partyId/bis", method := "post")(
form(action:=s"/party/$partyId/bis", method:="post")( select(name := "player", id := "player", title := "player")(
select(name:="player", id:="player", title:="player") for (player <- party) yield option(player.playerId.toString)
(for (player <- party) yield option(player.playerId.toString)),
select(name:="piece", id:="piece", title:="piece")
(for (piece <- Piece.available) yield option(piece)),
select(name:="piece_type", id:="piece_type", title:="piece type")
(for (pieceType <- PieceType.available) yield option(pieceType.toString)),
input(name:="action", id:="action", `type`:="hidden", value:="add"),
input(name:="add", id:="add", `type`:="submit", value:="add")
), ),
select(name := "piece", id := "piece", title := "piece")(
form(action:=s"/party/$partyId/bis", method:="post")( for (piece <- Piece.available) yield option(piece)
select(name:="player", id:="player", title:="player")
(for (player <- party) yield option(player.playerId.toString)),
input(name:="link", id:="link", placeholder:="player bis link", title:="link", `type`:="text"),
input(name:="action", id:="action", `type`:="hidden", value:="create"),
input(name:="add", id:="add", `type`:="submit", value:="add")
), ),
select(name := "piece_type", id := "piece_type", title := "piece type")(
table(id:="result")( for (pieceType <- PieceType.available) yield option(pieceType.toString)
),
input(name := "action", id := "action", `type` := "hidden", value := "add"),
input(name := "add", id := "add", `type` := "submit", value := "add")
),
form(action := s"/party/$partyId/bis", method := "post")(
select(name := "player", id := "player", title := "player")(
for (player <- party) yield option(player.playerId.toString)
),
input(name := "link", id := "link", placeholder := "player bis link", title := "link", `type` := "text"),
input(name := "action", id := "action", `type` := "hidden", value := "create"),
input(name := "add", id := "add", `type` := "submit", value := "add")
),
table(id := "result")(
tr( tr(
th("player"), th("player"),
th("piece"), th("piece"),
th("piece type"), th("piece type"),
th("") th("")
), ),
for (player <- party; piece <- player.bis.pieces) yield tr( for (player <- party; piece <- player.bis.pieces)
td(`class`:="include_search")(player.playerId.toString), yield tr(
td(`class`:="include_search")(piece.piece), td(`class` := "include_search")(player.playerId.toString),
td(`class` := "include_search")(piece.piece),
td(piece.pieceType.toString), td(piece.pieceType.toString),
td( td(
form(action:=s"/party/$partyId/bis", method:="post")( form(action := s"/party/$partyId/bis", method := "post")(
input(name:="player", id:="player", `type`:="hidden", value:=player.playerId.toString), input(name := "player", id := "player", `type` := "hidden", value := player.playerId.toString),
input(name:="piece", id:="piece", `type`:="hidden", value:=piece.piece), input(name := "piece", id := "piece", `type` := "hidden", value := piece.piece),
input(name:="piece_type", id:="piece_type", `type`:="hidden", value:=piece.pieceType.toString), input(
input(name:="action", id:="action", `type`:="hidden", value:="remove"), name := "piece_type",
input(name:="remove", id:="remove", `type`:="submit", value:="x") id := "piece_type",
`type` := "hidden",
value := piece.pieceType.toString
),
input(name := "action", id := "action", `type` := "hidden", value := "remove"),
input(name := "remove", id := "remove", `type` := "submit", value := "x")
) )
) )
) )
), ),
ExportToCSVView.template, ExportToCSVView.template,
BasePartyView.root(partyId), BasePartyView.root(partyId),
script(src:="/static/table_search.js", `type`:="text/javascript") script(src := "/static/table_search.js", `type` := "text/javascript")
) )
) )
} }

View File

@ -14,7 +14,7 @@ import scalatags.Text.all._
object ErrorView { object ErrorView {
def template(error: Option[String]): Text.TypedTag[String] = error match { def template(error: Option[String]): Text.TypedTag[String] = error match {
case Some(text) => p(id:="error", s"Error occurs: $text") case Some(text) => p(id := "error", s"Error occurs: $text")
case None => p("") case None => p("")
} }
} }

View File

@ -15,7 +15,7 @@ object ExportToCSVView {
def template: Text.TypedTag[String] = def template: Text.TypedTag[String] =
div( div(
button(onclick:="exportTableToCsv('result.csv')")("Export to CSV"), button(onclick := "exportTableToCsv('result.csv')")("Export to CSV"),
script(src:="/static/table_export.js", `type`:="text/javascript") script(src := "/static/table_export.js", `type` := "text/javascript")
) )
} }

View File

@ -20,10 +20,11 @@ import me.arcanis.ffxivbis.models.{PartyDescription, Permission, User}
import scala.concurrent.Future import scala.concurrent.Future
import scala.util.{Failure, Success} import scala.util.{Failure, Success}
class IndexView(override val storage: ActorRef[Message], class IndexView(override val storage: ActorRef[Message], override val provider: ActorRef[BiSProviderMessage])(implicit
override val provider: ActorRef[BiSProviderMessage]) timeout: Timeout,
(implicit timeout: Timeout, scheduler: Scheduler) scheduler: Scheduler
extends PlayerHelper with UserHelper { ) extends PlayerHelper
with UserHelper {
def route: Route = createParty ~ getIndex def route: Route = createParty ~ getIndex
@ -31,7 +32,8 @@ class IndexView(override val storage: ActorRef[Message],
path("party") { path("party") {
extractExecutionContext { implicit executionContext => extractExecutionContext { implicit executionContext =>
post { post {
formFields("username".as[String], "password".as[String], "alias".as[String].?) { (username, password, maybeAlias) => formFields("username".as[String], "password".as[String], "alias".as[String].?) {
(username, password, maybeAlias) =>
onComplete { onComplete {
newPartyId.flatMap { partyId => newPartyId.flatMap { partyId =>
val user = User(partyId, username, password, Permission.admin) val user = User(partyId, username, password, Permission.admin)
@ -69,26 +71,34 @@ object IndexView {
html( html(
head( head(
titleTag("FFXIV loot helper"), titleTag("FFXIV loot helper"),
link(rel:="stylesheet", `type`:="text/css", href:="/static/styles.css") link(rel := "stylesheet", `type` := "text/css", href := "/static/styles.css")
), ),
body( body(
form(action:=s"party", method:="post")( form(action := s"party", method := "post")(
label("create a new party"), label("create a new party"),
input(name:="alias", id:="alias", placeholder:="party alias", title:="alias", `type`:="text"), input(name := "alias", id := "alias", placeholder := "party alias", title := "alias", `type` := "text"),
input(name:="username", id:="username", placeholder:="username", title:="username", `type`:="text"), input(
input(name:="password", id:="password", placeholder:="password", title:="password", `type`:="password"), name := "username",
input(name:="add", id:="add", `type`:="submit", value:="add") 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")
), ),
br, br,
form(action := "/", method := "get")(
form(action:="/", method:="get")(
label("already have party?"), label("already have party?"),
input(name:="partyId", id:="partyId", placeholder:="party id", title:="party id", `type`:="text"), input(name := "partyId", id := "partyId", placeholder := "party id", title := "party id", `type` := "text"),
input(name:="go", id:="go", `type`:="submit", value:="go") input(name := "go", id := "go", `type` := "submit", value := "go")
) )
) )
) )
} }

View File

@ -20,9 +20,9 @@ import me.arcanis.ffxivbis.models.{Job, Piece, PieceType, PlayerIdWithCounters}
import scala.concurrent.{ExecutionContext, Future} import scala.concurrent.{ExecutionContext, Future}
import scala.util.{Failure, Success, Try} import scala.util.{Failure, Success, Try}
class LootSuggestView(override val storage: ActorRef[Message]) class LootSuggestView(override val storage: ActorRef[Message])(implicit timeout: Timeout, scheduler: Scheduler)
(implicit timeout: Timeout, scheduler: Scheduler) extends LootHelper
extends LootHelper with Authorization { with Authorization {
def route: Route = getIndex ~ suggestLoot def route: Route = getIndex ~ suggestLoot
@ -65,8 +65,10 @@ class LootSuggestView(override val storage: ActorRef[Message])
} }
} }
private def suggestLootCall(partyId: String, maybePiece: Option[Piece]) private def suggestLootCall(partyId: String, maybePiece: Option[Piece])(implicit
(implicit executionContext: ExecutionContext, timeout: Timeout): Future[Seq[PlayerIdWithCounters]] = executionContext: ExecutionContext,
timeout: Timeout
): Future[Seq[PlayerIdWithCounters]] =
maybePiece match { maybePiece match {
case Some(piece) => suggestPiece(partyId, piece) case Some(piece) => suggestPiece(partyId, piece)
case _ => Future.failed(new Error(s"Could not construct piece from `$maybePiece`")) case _ => Future.failed(new Error(s"Could not construct piece from `$maybePiece`"))
@ -77,36 +79,40 @@ object LootSuggestView {
import scalatags.Text.all._ import scalatags.Text.all._
import scalatags.Text.tags2.{title => titleTag} import scalatags.Text.tags2.{title => titleTag}
def template(partyId: String, party: Seq[PlayerIdWithCounters], piece: Option[Piece], def template(
isFreeLoot: Boolean, error: Option[String]): String = partyId: String,
party: Seq[PlayerIdWithCounters],
piece: Option[Piece],
isFreeLoot: Boolean,
error: Option[String]
): String =
"<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01//EN\" \"http://www.w3.org/TR/html4/strict.dtd\">" + "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01//EN\" \"http://www.w3.org/TR/html4/strict.dtd\">" +
html(lang:="en", html(
lang := "en",
head( head(
titleTag("Suggest loot"), titleTag("Suggest loot"),
link(rel:="stylesheet", `type`:="text/css", href:="/static/styles.css") link(rel := "stylesheet", `type` := "text/css", href := "/static/styles.css")
), ),
body( body(
h2("Suggest loot"), h2("Suggest loot"),
for (part <- piece) yield p(s"Piece ${part.piece} (${part.pieceType})"), for (part <- piece) yield p(s"Piece ${part.piece} (${part.pieceType})"),
ErrorView.template(error), ErrorView.template(error),
SearchLineView.template, SearchLineView.template,
form(action := s"/party/$partyId/suggest", method := "post")(
form(action:=s"/party/$partyId/suggest", method:="post")( select(name := "piece", id := "piece", title := "piece")(
select(name:="piece", id:="piece", title:="piece") for (piece <- Piece.available) yield option(piece)
(for (piece <- Piece.available) yield option(piece)),
select(name:="job", id:="job", title:="job")
(for (job <- Job.availableWithAnyJob) yield option(job.toString)),
select(name:="piece_type", id:="piece_type", title:="piece type")
(for (pieceType <- PieceType.available) yield option(pieceType.toString)),
input(name:="free_loot", id:="free_loot", title:="is free loot", `type`:="checkbox"),
label(`for`:="free_loot")("is free loot"),
input(name:="suggest", id:="suggest", `type`:="submit", value:="suggest")
), ),
select(name := "job", id := "job", title := "job")(
table(id:="result")( for (job <- Job.availableWithAnyJob) yield option(job.toString)
),
select(name := "piece_type", id := "piece_type", title := "piece type")(
for (pieceType <- PieceType.available) yield option(pieceType.toString)
),
input(name := "free_loot", id := "free_loot", title := "is free loot", `type` := "checkbox"),
label(`for` := "free_loot")("is free loot"),
input(name := "suggest", id := "suggest", `type` := "submit", value := "suggest")
),
table(id := "result")(
tr( tr(
th("player"), th("player"),
th("is required"), th("is required"),
@ -115,28 +121,43 @@ object LootSuggestView {
th("total pieces looted"), th("total pieces looted"),
th("") th("")
), ),
for (player <- party) yield tr( for (player <- party)
td(`class`:="include_search")(player.playerId.toString), yield tr(
td(`class` := "include_search")(player.playerId.toString),
td(player.isRequiredToString), td(player.isRequiredToString),
td(player.lootCount), td(player.lootCount),
td(player.lootCountBiS), td(player.lootCountBiS),
td(player.lootCountTotal), td(player.lootCountTotal),
td( td(
form(action:=s"/party/$partyId/loot", method:="post")( form(action := s"/party/$partyId/loot", method := "post")(
input(name:="player", id:="player", `type`:="hidden", value:=player.playerId.toString), input(name := "player", id := "player", `type` := "hidden", value := player.playerId.toString),
input(name:="piece", id:="piece", `type`:="hidden", value:=piece.map(_.piece).getOrElse("")), input(
input(name:="piece_type", id:="piece_type", `type`:="hidden", value:=piece.map(_.pieceType.toString).getOrElse("")), name := "piece",
input(name:="free_loot", id:="free_loot", `type`:="hidden", value:=(if (isFreeLoot) "yes" else "no")), id := "piece",
input(name:="action", id:="action", `type`:="hidden", value:="add"), `type` := "hidden",
input(name:="add", id:="add", `type`:="submit", value:="add") value := piece.map(_.piece).getOrElse("")
),
input(
name := "piece_type",
id := "piece_type",
`type` := "hidden",
value := piece.map(_.pieceType.toString).getOrElse("")
),
input(
name := "free_loot",
id := "free_loot",
`type` := "hidden",
value := (if (isFreeLoot) "yes" else "no")
),
input(name := "action", id := "action", `type` := "hidden", value := "add"),
input(name := "add", id := "add", `type` := "submit", value := "add")
) )
) )
) )
), ),
ExportToCSVView.template, ExportToCSVView.template,
BasePartyView.root(partyId), BasePartyView.root(partyId),
script(src:="/static/table_search.js", `type`:="text/javascript") script(src := "/static/table_search.js", `type` := "text/javascript")
) )
) )
} }

View File

@ -20,9 +20,9 @@ import me.arcanis.ffxivbis.models.{Piece, PieceType, Player, PlayerId}
import scala.concurrent.{ExecutionContext, Future} import scala.concurrent.{ExecutionContext, Future}
import scala.util.Try import scala.util.Try
class LootView(override val storage: ActorRef[Message]) class LootView(override val storage: ActorRef[Message])(implicit timeout: Timeout, scheduler: Scheduler)
(implicit timeout: Timeout, scheduler: Scheduler) extends LootHelper
extends LootHelper with Authorization { with Authorization {
def route: Route = getLoot ~ modifyLoot def route: Route = getLoot ~ modifyLoot
@ -32,9 +32,11 @@ class LootView(override val storage: ActorRef[Message])
authenticateBasicBCrypt(s"party $partyId", authGet(partyId)) { _ => authenticateBasicBCrypt(s"party $partyId", authGet(partyId)) { _ =>
get { get {
complete { complete {
loot(partyId, None).map { players => loot(partyId, None)
.map { players =>
LootView.template(partyId, players, None) LootView.template(partyId, players, None)
}.map { text => }
.map { text =>
(StatusCodes.OK, RootView.toHtml(text)) (StatusCodes.OK, RootView.toHtml(text))
} }
} }
@ -48,8 +50,13 @@ class LootView(override val storage: ActorRef[Message])
extractExecutionContext { implicit executionContext => extractExecutionContext { implicit executionContext =>
authenticateBasicBCrypt(s"party $partyId", authPost(partyId)) { _ => authenticateBasicBCrypt(s"party $partyId", authPost(partyId)) { _ =>
post { post {
formFields("player".as[String], "piece".as[String], "piece_type".as[String], "action".as[String], "free_loot".as[String].?) { formFields(
(player, piece, pieceType, action, isFreeLoot) => "player".as[String],
"piece".as[String],
"piece_type".as[String],
"action".as[String],
"free_loot".as[String].?
) { (player, piece, pieceType, action, isFreeLoot) =>
onComplete(modifyLootCall(partyId, player, piece, pieceType, isFreeLoot, action)) { _ => onComplete(modifyLootCall(partyId, player, piece, pieceType, isFreeLoot, action)) { _ =>
redirect(s"/party/$partyId/loot", StatusCodes.Found) redirect(s"/party/$partyId/loot", StatusCodes.Found)
} }
@ -59,17 +66,22 @@ class LootView(override val storage: ActorRef[Message])
} }
} }
private def modifyLootCall(partyId: String, player: String, maybePiece: String, private def modifyLootCall(
maybePieceType: String, maybeFreeLoot: Option[String], partyId: String,
action: String) player: String,
(implicit executionContext: ExecutionContext, timeout: Timeout): Future[Unit] = { maybePiece: String,
maybePieceType: String,
maybeFreeLoot: Option[String],
action: String
)(implicit executionContext: ExecutionContext, timeout: Timeout): Future[Unit] = {
import me.arcanis.ffxivbis.utils.Implicits._ import me.arcanis.ffxivbis.utils.Implicits._
def getPiece(playerId: PlayerId) = def getPiece(playerId: PlayerId) =
Try(Piece(maybePiece, PieceType.withName(maybePieceType), playerId.job)).toOption Try(Piece(maybePiece, PieceType.withName(maybePieceType), playerId.job)).toOption
PlayerId(partyId, player) match { PlayerId(partyId, player) match {
case Some(playerId) => (getPiece(playerId), action) match { case Some(playerId) =>
(getPiece(playerId), action) match {
case (Some(piece), "add") => addPieceLoot(playerId, piece, maybeFreeLoot) case (Some(piece), "add") => addPieceLoot(playerId, piece, maybeFreeLoot)
case (Some(piece), "remove") => removePieceLoot(playerId, piece) case (Some(piece), "remove") => removePieceLoot(playerId, piece)
case _ => Future.failed(new Error(s"Could not construct piece from `$maybePiece ($maybePieceType)`")) case _ => Future.failed(new Error(s"Could not construct piece from `$maybePiece ($maybePieceType)`"))
@ -85,32 +97,32 @@ object LootView {
def template(partyId: String, party: Seq[Player], error: Option[String]): String = def template(partyId: String, party: Seq[Player], error: Option[String]): String =
"<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01//EN\" \"http://www.w3.org/TR/html4/strict.dtd\">" + "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01//EN\" \"http://www.w3.org/TR/html4/strict.dtd\">" +
html(lang:="en", html(
lang := "en",
head( head(
titleTag("Loot"), titleTag("Loot"),
link(rel:="stylesheet", `type`:="text/css", href:="/static/styles.css") link(rel := "stylesheet", `type` := "text/css", href := "/static/styles.css")
), ),
body( body(
h2("Loot"), h2("Loot"),
ErrorView.template(error), ErrorView.template(error),
SearchLineView.template, SearchLineView.template,
form(action := s"/party/$partyId/loot", method := "post")(
form(action:=s"/party/$partyId/loot", method:="post")( select(name := "player", id := "player", title := "player")(
select(name:="player", id:="player", title:="player") for (player <- party) yield option(player.playerId.toString)
(for (player <- party) yield option(player.playerId.toString)),
select(name:="piece", id:="piece", title:="piece")
(for (piece <- Piece.available) yield option(piece)),
select(name:="piece_type", id:="piece_type", title:="piece type")
(for (pieceType <- PieceType.available) yield option(pieceType.toString)),
input(name:="free_loot", id:="free_loot", title:="is free loot", `type`:="checkbox"),
label(`for`:="free_loot")("is free loot"),
input(name:="action", id:="action", `type`:="hidden", value:="add"),
input(name:="add", id:="add", `type`:="submit", value:="add")
), ),
select(name := "piece", id := "piece", title := "piece")(
table(id:="result")( for (piece <- Piece.available) yield option(piece)
),
select(name := "piece_type", id := "piece_type", title := "piece type")(
for (pieceType <- PieceType.available) yield option(pieceType.toString)
),
input(name := "free_loot", id := "free_loot", title := "is free loot", `type` := "checkbox"),
label(`for` := "free_loot")("is free loot"),
input(name := "action", id := "action", `type` := "hidden", value := "add"),
input(name := "add", id := "add", `type` := "submit", value := "add")
),
table(id := "result")(
tr( tr(
th("player"), th("player"),
th("piece"), th("piece"),
@ -119,28 +131,33 @@ object LootView {
th("timestamp"), th("timestamp"),
th("") th("")
), ),
for (player <- party; loot <- player.loot) yield tr( for (player <- party; loot <- player.loot)
td(`class`:="include_search")(player.playerId.toString), yield tr(
td(`class`:="include_search")(loot.piece.piece), td(`class` := "include_search")(player.playerId.toString),
td(`class` := "include_search")(loot.piece.piece),
td(loot.piece.pieceType.toString), td(loot.piece.pieceType.toString),
td(loot.isFreeLootToString), td(loot.isFreeLootToString),
td(loot.timestamp.toString), td(loot.timestamp.toString),
td( td(
form(action:=s"/party/$partyId/loot", method:="post")( form(action := s"/party/$partyId/loot", method := "post")(
input(name:="player", id:="player", `type`:="hidden", value:=player.playerId.toString), input(name := "player", id := "player", `type` := "hidden", value := player.playerId.toString),
input(name:="piece", id:="piece", `type`:="hidden", value:=loot.piece.piece), input(name := "piece", id := "piece", `type` := "hidden", value := loot.piece.piece),
input(name:="piece_type", id:="piece_type", `type`:="hidden", value:=loot.piece.pieceType.toString), input(
input(name:="free_loot", id:="free_loot", `type`:="hidden", value:=loot.isFreeLootToString), name := "piece_type",
input(name:="action", id:="action", `type`:="hidden", value:="remove"), id := "piece_type",
input(name:="remove", id:="remove", `type`:="submit", value:="x") `type` := "hidden",
value := loot.piece.pieceType.toString
),
input(name := "free_loot", id := "free_loot", `type` := "hidden", value := loot.isFreeLootToString),
input(name := "action", id := "action", `type` := "hidden", value := "remove"),
input(name := "remove", id := "remove", `type` := "submit", value := "x")
) )
) )
) )
), ),
ExportToCSVView.template, ExportToCSVView.template,
BasePartyView.root(partyId), BasePartyView.root(partyId),
script(src:="/static/table_search.js", `type`:="text/javascript") script(src := "/static/table_search.js", `type` := "text/javascript")
) )
) )

View File

@ -19,10 +19,11 @@ import me.arcanis.ffxivbis.models._
import scala.concurrent.{ExecutionContext, Future} import scala.concurrent.{ExecutionContext, Future}
class PlayerView(override val storage: ActorRef[Message], class PlayerView(override val storage: ActorRef[Message], override val provider: ActorRef[BiSProviderMessage])(implicit
override val provider: ActorRef[BiSProviderMessage]) timeout: Timeout,
(implicit timeout: Timeout, scheduler: Scheduler) scheduler: Scheduler
extends PlayerHelper with Authorization { ) extends PlayerHelper
with Authorization {
def route: Route = getParty ~ modifyParty def route: Route = getParty ~ modifyParty
@ -32,9 +33,11 @@ class PlayerView(override val storage: ActorRef[Message],
authenticateBasicBCrypt(s"party $partyId", authGet(partyId)) { _ => authenticateBasicBCrypt(s"party $partyId", authGet(partyId)) { _ =>
get { get {
complete { complete {
getPlayers(partyId, None).map { players => getPlayers(partyId, None)
.map { players =>
PlayerView.template(partyId, players.map(_.withCounters(None)), None) PlayerView.template(partyId, players.map(_.withCounters(None)), None)
}.map { text => }
.map { text =>
(StatusCodes.OK, RootView.toHtml(text)) (StatusCodes.OK, RootView.toHtml(text))
} }
} }
@ -48,8 +51,13 @@ class PlayerView(override val storage: ActorRef[Message],
extractExecutionContext { implicit executionContext => extractExecutionContext { implicit executionContext =>
authenticateBasicBCrypt(s"party $partyId", authPost(partyId)) { _ => authenticateBasicBCrypt(s"party $partyId", authPost(partyId)) { _ =>
post { post {
formFields("nick".as[String], "job".as[String], "priority".as[Int].?, "link".as[String].?, "action".as[String]) { formFields(
(nick, job, maybePriority, maybeLink, action) => "nick".as[String],
"job".as[String],
"priority".as[Int].?,
"link".as[String].?,
"action".as[String]
) { (nick, job, maybePriority, maybeLink, action) =>
onComplete(modifyPartyCall(partyId, nick, job, maybePriority, maybeLink, action)) { _ => onComplete(modifyPartyCall(partyId, nick, job, maybePriority, maybeLink, action)) { _ =>
redirect(s"/party/$partyId/players", StatusCodes.Found) redirect(s"/party/$partyId/players", StatusCodes.Found)
} }
@ -59,10 +67,14 @@ class PlayerView(override val storage: ActorRef[Message],
} }
} }
private def modifyPartyCall(partyId: String, nick: String, job: String, private def modifyPartyCall(
maybePriority: Option[Int], maybeLink: Option[String], partyId: String,
action: String) nick: String,
(implicit executionContext: ExecutionContext, timeout: Timeout): Future[Unit] = { job: String,
maybePriority: Option[Int],
maybeLink: Option[String],
action: String
)(implicit executionContext: ExecutionContext, timeout: Timeout): Future[Unit] = {
def maybePlayerId = PlayerId(partyId, Some(nick), Some(job)) def maybePlayerId = PlayerId(partyId, Some(nick), Some(job))
def player(playerId: PlayerId) = def player(playerId: PlayerId) =
Player(-1, partyId, playerId.job, playerId.nick, BiS.empty, Seq.empty, maybeLink, maybePriority.getOrElse(0)) Player(-1, partyId, playerId.job, playerId.nick, BiS.empty, Seq.empty, maybeLink, maybePriority.getOrElse(0))
@ -81,29 +93,32 @@ object PlayerView {
def template(partyId: String, party: Seq[PlayerIdWithCounters], error: Option[String]): String = def template(partyId: String, party: Seq[PlayerIdWithCounters], error: Option[String]): String =
"<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01//EN\" \"http://www.w3.org/TR/html4/strict.dtd\">" + "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01//EN\" \"http://www.w3.org/TR/html4/strict.dtd\">" +
html(lang:="en", html(
lang := "en",
head( head(
titleTag("Party"), titleTag("Party"),
link(rel:="stylesheet", `type`:="text/css", href:="/static/styles.css") link(rel := "stylesheet", `type` := "text/css", href := "/static/styles.css")
), ),
body( body(
h2("Party"), h2("Party"),
ErrorView.template(error), ErrorView.template(error),
SearchLineView.template, SearchLineView.template,
form(action := s"/party/$partyId/players", method := "post")(
form(action:=s"/party/$partyId/players", method:="post")( input(name := "nick", id := "nick", placeholder := "nick", title := "nick", `type` := "nick"),
input(name:="nick", id:="nick", placeholder:="nick", title:="nick", `type`:="nick"), select(name := "job", id := "job", title := "job")(for (job <- Job.available) yield option(job.toString)),
select(name:="job", id:="job", title:="job") input(name := "link", id := "link", placeholder := "player bis link", title := "link", `type` := "text"),
(for (job <- Job.available) yield option(job.toString)), input(
input(name:="link", id:="link", placeholder:="player bis link", title:="link", `type`:="text"), name := "prioiry",
input(name:="prioiry", id:="priority", placeholder:="priority", title:="priority", `type`:="number", value:="0"), id := "priority",
input(name:="action", id:="action", `type`:="hidden", value:="add"), placeholder := "priority",
input(name:="add", id:="add", `type`:="submit", value:="add") title := "priority",
`type` := "number",
value := "0"
), ),
input(name := "action", id := "action", `type` := "hidden", value := "add"),
table(id:="result")( input(name := "add", id := "add", `type` := "submit", value := "add")
),
table(id := "result")(
tr( tr(
th("nick"), th("nick"),
th("job"), th("job"),
@ -112,26 +127,26 @@ object PlayerView {
th("priority"), th("priority"),
th("") th("")
), ),
for (player <- party) yield tr( for (player <- party)
td(`class`:="include_search")(player.nick), yield tr(
td(`class`:="include_search")(player.job.toString), td(`class` := "include_search")(player.nick),
td(`class` := "include_search")(player.job.toString),
td(player.lootCountBiS), td(player.lootCountBiS),
td(player.lootCountTotal), td(player.lootCountTotal),
td(player.priority), td(player.priority),
td( td(
form(action:=s"/party/$partyId/players", method:="post")( form(action := s"/party/$partyId/players", method := "post")(
input(name:="nick", id:="nick", `type`:="hidden", value:=player.nick), input(name := "nick", id := "nick", `type` := "hidden", value := player.nick),
input(name:="job", id:="job", `type`:="hidden", value:=player.job.toString), input(name := "job", id := "job", `type` := "hidden", value := player.job.toString),
input(name:="action", id:="action", `type`:="hidden", value:="remove"), input(name := "action", id := "action", `type` := "hidden", value := "remove"),
input(name:="remove", id:="remove", `type`:="submit", value:="x") input(name := "remove", id := "remove", `type` := "submit", value := "x")
) )
) )
) )
), ),
ExportToCSVView.template, ExportToCSVView.template,
BasePartyView.root(partyId), BasePartyView.root(partyId),
script(src:="/static/table_search.js", `type`:="text/javascript") script(src := "/static/table_search.js", `type` := "text/javascript")
) )
) )
} }

View File

@ -15,9 +15,10 @@ import akka.http.scaladsl.server.Route
import akka.util.Timeout import akka.util.Timeout
import me.arcanis.ffxivbis.messages.{BiSProviderMessage, Message} import me.arcanis.ffxivbis.messages.{BiSProviderMessage, Message}
class RootView(storage: ActorRef[Message], class RootView(storage: ActorRef[Message], provider: ActorRef[BiSProviderMessage])(implicit
provider: ActorRef[BiSProviderMessage]) timeout: Timeout,
(implicit timeout: Timeout, scheduler: Scheduler) { scheduler: Scheduler
) {
private val basePartyView = new BasePartyView(storage, provider) private val basePartyView = new BasePartyView(storage, provider)
private val indexView = new IndexView(storage, provider) private val indexView = new IndexView(storage, provider)

View File

@ -16,8 +16,11 @@ object SearchLineView {
def template: Text.TypedTag[String] = def template: Text.TypedTag[String] =
div( div(
input( input(
`type`:="text", id:="search", onkeyup:="searchTable()", `type` := "text",
placeholder:="search for data", title:="search" id := "search",
onkeyup := "searchTable()",
placeholder := "search for data",
title := "search"
) )
) )
} }

View File

@ -20,9 +20,9 @@ import me.arcanis.ffxivbis.models.{Permission, User}
import scala.concurrent.{ExecutionContext, Future} import scala.concurrent.{ExecutionContext, Future}
import scala.util.Try import scala.util.Try
class UserView(override val storage: ActorRef[Message]) class UserView(override val storage: ActorRef[Message])(implicit timeout: Timeout, scheduler: Scheduler)
(implicit timeout: Timeout, scheduler: Scheduler) extends UserHelper
extends UserHelper with Authorization { with Authorization {
def route: Route = getUsers ~ modifyUsers def route: Route = getUsers ~ modifyUsers
@ -32,9 +32,11 @@ class UserView(override val storage: ActorRef[Message])
authenticateBasicBCrypt(s"party $partyId", authAdmin(partyId)) { _ => authenticateBasicBCrypt(s"party $partyId", authAdmin(partyId)) { _ =>
get { get {
complete { complete {
users(partyId).map { users => users(partyId)
.map { users =>
UserView.template(partyId, users, None) UserView.template(partyId, users, None)
}.map { text => }
.map { text =>
(StatusCodes.OK, RootView.toHtml(text)) (StatusCodes.OK, RootView.toHtml(text))
} }
} }
@ -50,8 +52,8 @@ class UserView(override val storage: ActorRef[Message])
post { post {
formFields("username".as[String], "password".as[String].?, "permission".as[String].?, "action".as[String]) { formFields("username".as[String], "password".as[String].?, "permission".as[String].?, "action".as[String]) {
(username, maybePassword, maybePermission, action) => (username, maybePassword, maybePermission, action) =>
onComplete(modifyUsersCall(partyId, username, maybePassword, maybePermission, action)) { onComplete(modifyUsersCall(partyId, username, maybePassword, maybePermission, action)) { case _ =>
case _ => redirect(s"/party/$partyId/users", StatusCodes.Found) redirect(s"/party/$partyId/users", StatusCodes.Found)
} }
} }
} }
@ -59,17 +61,25 @@ class UserView(override val storage: ActorRef[Message])
} }
} }
private def modifyUsersCall(partyId: String, username: String, private def modifyUsersCall(
maybePassword: Option[String], maybePermission: Option[String], partyId: String,
action: String) username: String,
(implicit executionContext: ExecutionContext, timeout: Timeout): Future[Unit] = { maybePassword: Option[String],
maybePermission: Option[String],
action: String
)(implicit executionContext: ExecutionContext, timeout: Timeout): Future[Unit] = {
def permission: Option[Permission.Value] = def permission: Option[Permission.Value] =
maybePermission.flatMap(p => Try(Permission.withName(p)).toOption) maybePermission.flatMap(p => Try(Permission.withName(p)).toOption)
action match { action match {
case "add" => (maybePassword, permission) match { case "add" =>
case (Some(password), Some(permission)) => addUser(User(partyId, username, password, permission), isHashedPassword = false) (maybePassword, permission) match {
case _ => Future.failed(new Error(s"Could not construct permission/password from `$maybePermission`/`$maybePassword`")) case (Some(password), Some(permission)) =>
addUser(User(partyId, username, password, permission), isHashedPassword = false)
case _ =>
Future.failed(
new Error(s"Could not construct permission/password from `$maybePermission`/`$maybePassword`")
)
} }
case "remove" => removeUser(partyId, username) case "remove" => removeUser(partyId, username)
case _ => Future.failed(new Error(s"Could not perform $action")) case _ => Future.failed(new Error(s"Could not perform $action"))
@ -83,48 +93,57 @@ object UserView {
def template(partyId: String, users: Seq[User], error: Option[String]) = def template(partyId: String, users: Seq[User], error: Option[String]) =
"<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01//EN\" \"http://www.w3.org/TR/html4/strict.dtd\">" + "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01//EN\" \"http://www.w3.org/TR/html4/strict.dtd\">" +
html(lang:="en", html(
lang := "en",
head( head(
titleTag("Users"), titleTag("Users"),
link(rel:="stylesheet", `type`:="text/css", href:="/static/styles.css") link(rel := "stylesheet", `type` := "text/css", href := "/static/styles.css")
), ),
body( body(
h2("Users"), h2("Users"),
ErrorView.template(error), ErrorView.template(error),
SearchLineView.template, SearchLineView.template,
form(action := s"/party/$partyId/users", method := "post")(
form(action:=s"/party/$partyId/users", method:="post")( input(
input(name:="username", id:="username", placeholder:="username", title:="username", `type`:="text"), name := "username",
input(name:="password", id:="password", placeholder:="password", title:="password", `type`:="password"), id := "username",
select(name:="permission", id:="permission", title:="permission")(option("get"), option("post")), placeholder := "username",
input(name:="action", id:="action", `type`:="hidden", value:="add"), title := "username",
input(name:="add", id:="add", `type`:="submit", value:="add") `type` := "text"
), ),
input(
table(id:="result")( name := "password",
id := "password",
placeholder := "password",
title := "password",
`type` := "password"
),
select(name := "permission", id := "permission", title := "permission")(option("get"), option("post")),
input(name := "action", id := "action", `type` := "hidden", value := "add"),
input(name := "add", id := "add", `type` := "submit", value := "add")
),
table(id := "result")(
tr( tr(
th("username"), th("username"),
th("permission"), th("permission"),
th("") th("")
), ),
for (user <- users) yield tr( for (user <- users)
td(`class`:="include_search")(user.username), yield tr(
td(`class` := "include_search")(user.username),
td(user.permission.toString), td(user.permission.toString),
td( td(
form(action:=s"/party/$partyId/users", method:="post")( form(action := s"/party/$partyId/users", method := "post")(
input(name:="username", id:="username", `type`:="hidden", value:=user.username.toString), input(name := "username", id := "username", `type` := "hidden", value := user.username.toString),
input(name:="action", id:="action", `type`:="hidden", value:="remove"), input(name := "action", id := "action", `type` := "hidden", value := "remove"),
input(name:="remove", id:="remove", `type`:="submit", value:="x") input(name := "remove", id := "remove", `type` := "submit", value := "x")
) )
) )
) )
), ),
ExportToCSVView.template, ExportToCSVView.template,
BasePartyView.root(partyId), BasePartyView.root(partyId),
script(src:="/static/table_search.js", `type`:="text/javascript") script(src := "/static/table_search.js", `type` := "text/javascript")
) )
) )
} }

View File

@ -30,7 +30,8 @@ case class RemovePiecesFromBiS(playerId: PlayerId, replyTo: ActorRef[Unit]) exte
} }
// loot handler // loot handler
case class AddPieceTo(playerId: PlayerId, piece: Piece, isFreeLoot: Boolean, replyTo: ActorRef[Unit]) extends DatabaseMessage { case class AddPieceTo(playerId: PlayerId, piece: Piece, isFreeLoot: Boolean, replyTo: ActorRef[Unit])
extends DatabaseMessage {
override def partyId: String = playerId.partyId override def partyId: String = playerId.partyId
} }
@ -40,7 +41,8 @@ case class RemovePieceFrom(playerId: PlayerId, piece: Piece, replyTo: ActorRef[U
override def partyId: String = playerId.partyId override def partyId: String = playerId.partyId
} }
case class SuggestLoot(partyId: String, piece: Piece, replyTo: ActorRef[LootSelector.LootSelectorResult]) extends DatabaseMessage case class SuggestLoot(partyId: String, piece: Piece, replyTo: ActorRef[LootSelector.LootSelectorResult])
extends DatabaseMessage
// party handler // party handler
case class AddPlayer(player: Player, replyTo: ActorRef[Unit]) extends DatabaseMessage { case class AddPlayer(player: Player, replyTo: ActorRef[Unit]) extends DatabaseMessage {

View File

@ -16,18 +16,21 @@ case class BiS(pieces: Seq[Piece]) {
} }
def upgrades: Map[PieceUpgrade, Int] = def upgrades: Map[PieceUpgrade, Int] =
pieces.groupBy(_.upgrade).foldLeft(Map.empty[PieceUpgrade, Int]) { pieces
.groupBy(_.upgrade)
.foldLeft(Map.empty[PieceUpgrade, Int]) {
case (acc, (Some(k), v)) => acc + (k -> v.size) case (acc, (Some(k), v)) => acc + (k -> v.size)
case (acc, _) => acc case (acc, _) => acc
} withDefaultValue 0 }
.withDefaultValue(0)
def withPiece(piece: Piece): BiS = copy(pieces :+ piece) def withPiece(piece: Piece): BiS = copy(pieces :+ piece)
def withoutPiece(piece: Piece): BiS = copy(pieces.filterNot(_.strictEqual(piece))) def withoutPiece(piece: Piece): BiS = copy(pieces.filterNot(_.strictEqual(piece)))
override def equals(obj: Any): Boolean = { override def equals(obj: Any): Boolean = {
def comparePieces(left: Seq[Piece], right: Seq[Piece]): Boolean = def comparePieces(left: Seq[Piece], right: Seq[Piece]): Boolean =
left.groupBy(identity).view.mapValues(_.size).forall { left.groupBy(identity).view.mapValues(_.size).forall { case (key, count) =>
case (key, count) => right.count(_.strictEqual(key)) == count right.count(_.strictEqual(key)) == count
} }
obj match { obj match {

View File

@ -37,12 +37,16 @@ case class Party(partyDescription: PartyDescription, rules: Seq[String], players
object Party { object Party {
def apply(party: PartyDescription, config: Config, def apply(
players: Map[Long, Player], bis: Seq[Loot], loot: Seq[Loot]): Party = { 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 bisByPlayer = bis.groupBy(_.playerId).view.mapValues(piece => BiS(piece.map(_.piece)))
val lootByPlayer = loot.groupBy(_.playerId).view val lootByPlayer = loot.groupBy(_.playerId).view
val playersWithItems = players.foldLeft(Map.empty[PlayerId, Player]) { val playersWithItems = players.foldLeft(Map.empty[PlayerId, Player]) { case (acc, (playerId, player)) =>
case (acc, (playerId, player)) =>
acc + (player.playerId -> player acc + (player.playerId -> player
.withBiS(bisByPlayer.get(playerId)) .withBiS(bisByPlayer.get(playerId))
.withLoot(lootByPlayer.getOrElse(playerId, Seq.empty))) .withLoot(lootByPlayer.getOrElse(playerId, Seq.empty)))

View File

@ -76,8 +76,11 @@ case class Wrist(override val pieceType: PieceType.PieceType, override val job:
val piece: String = "wrist" val piece: String = "wrist"
def withJob(other: Job.Job): Piece = copy(job = other) def withJob(other: Job.Job): Piece = copy(job = other)
} }
case class Ring(override val pieceType: PieceType.PieceType, override val job: Job.Job, override val piece: String = "ring") case class Ring(
extends PieceAccessory { override val pieceType: PieceType.PieceType,
override val job: Job.Job,
override val piece: String = "ring"
) extends PieceAccessory {
def withJob(other: Job.Job): Piece = copy(job = other) def withJob(other: Job.Job): Piece = copy(job = other)
override def equals(obj: Any): Boolean = obj match { override def equals(obj: Any): Boolean = obj match {
@ -120,8 +123,20 @@ object Piece {
case other => throw new Error(s"Unknown item type $other") case other => throw new Error(s"Unknown item type $other")
} }
lazy val available: Seq[String] = Seq("weapon", lazy val available: Seq[String] = Seq(
"head", "body", "hands", "legs", "feet", "weapon",
"ears", "neck", "wrist", "left ring", "right ring", "head",
"accessory upgrade", "body upgrade", "weapon upgrade") "body",
"hands",
"legs",
"feet",
"ears",
"neck",
"wrist",
"left ring",
"right ring",
"accessory upgrade",
"body upgrade",
"weapon upgrade"
)
} }

View File

@ -8,14 +8,16 @@
*/ */
package me.arcanis.ffxivbis.models package me.arcanis.ffxivbis.models
case class Player(id: Long, case class Player(
id: Long,
partyId: String, partyId: String,
job: Job.Job, job: Job.Job,
nick: String, nick: String,
bis: BiS, bis: BiS,
loot: Seq[Loot], loot: Seq[Loot],
link: Option[String] = None, link: Option[String] = None,
priority: Int = 0) { priority: Int = 0
) {
require(job ne Job.AnyJob, "AnyJob is not allowed") require(job ne Job.AnyJob, "AnyJob is not allowed")
val playerId: PlayerId = PlayerId(partyId, job, nick) val playerId: PlayerId = PlayerId(partyId, job, nick)
@ -25,23 +27,29 @@ case class Player(id: Long,
} }
def withCounters(piece: Option[Piece]): PlayerIdWithCounters = def withCounters(piece: Option[Piece]): PlayerIdWithCounters =
PlayerIdWithCounters( PlayerIdWithCounters(
partyId, job, nick, isRequired(piece), priority, partyId,
bisCountTotal(piece), lootCount(piece), job,
lootCountBiS(piece), lootCountTotal(piece)) nick,
isRequired(piece),
priority,
bisCountTotal(piece),
lootCount(piece),
lootCountBiS(piece),
lootCountTotal(piece)
)
def withLoot(piece: Loot): Player = withLoot(Seq(piece)) def withLoot(piece: Loot): Player = withLoot(Seq(piece))
def withLoot(list: Seq[Loot]): Player = { def withLoot(list: Seq[Loot]): Player = {
require(loot.forall(_.playerId == id), "player id must be same") require(loot.forall(_.playerId == id), "player id must be same")
copy(loot = loot ++ list) copy(loot = loot ++ list)
} }
def isRequired(piece: Option[Piece]): Boolean = { def isRequired(piece: Option[Piece]): Boolean =
piece match { piece match {
case None => false case None => false
case Some(p) if !bis.hasPiece(p) => false case Some(p) if !bis.hasPiece(p) => false
case Some(p: PieceUpgrade) => bis.upgrades(p) > lootCount(piece) case Some(p: PieceUpgrade) => bis.upgrades(p) > lootCount(piece)
case Some(_) => lootCount(piece) == 0 case Some(_) => lootCount(piece) == 0
} }
}
def bisCountTotal(piece: Option[Piece]): Int = bis.pieces.count(_.pieceType == PieceType.Savage) def bisCountTotal(piece: Option[Piece]): Int = bis.pieces.count(_.pieceType == PieceType.Savage)
def lootCount(piece: Option[Piece]): Int = piece match { def lootCount(piece: Option[Piece]): Int = piece match {

View File

@ -8,7 +8,8 @@
*/ */
package me.arcanis.ffxivbis.models package me.arcanis.ffxivbis.models
case class PlayerIdWithCounters(partyId: String, case class PlayerIdWithCounters(
partyId: String,
job: Job.Job, job: Job.Job,
nick: String, nick: String,
isRequired: Boolean, isRequired: Boolean,
@ -16,8 +17,8 @@ case class PlayerIdWithCounters(partyId: String,
bisCountTotal: Int, bisCountTotal: Int,
lootCount: Int, lootCount: Int,
lootCountBiS: Int, lootCountBiS: Int,
lootCountTotal: Int) lootCountTotal: Int
extends PlayerIdBase { ) extends PlayerIdBase {
import PlayerIdWithCounters._ import PlayerIdWithCounters._
def gt(that: PlayerIdWithCounters, orderBy: Seq[String]): Boolean = def gt(that: PlayerIdWithCounters, orderBy: Seq[String]): Boolean =
@ -31,7 +32,8 @@ case class PlayerIdWithCounters(partyId: String,
"bisCountTotal" -> bisCountTotal, // the more pieces in bis the more priority "bisCountTotal" -> bisCountTotal, // the more pieces in bis the more priority
"lootCount" -> -lootCount, // the less loot got the more priority "lootCount" -> -lootCount, // the less loot got the more priority
"lootCountBiS" -> -lootCountBiS, // the less bis pieces looted the more priority "lootCountBiS" -> -lootCountBiS, // the less bis pieces looted the more priority
"lootCountTotal" -> -lootCountTotal) withDefaultValue 0 // the less pieces looted the more priority "lootCountTotal" -> -lootCountTotal
).withDefaultValue(0) // the less pieces looted the more priority
private def withCounters(orderBy: Seq[String]): PlayerCountersComparator = private def withCounters(orderBy: Seq[String]): PlayerCountersComparator =
PlayerCountersComparator(orderBy.map(counters): _*) PlayerCountersComparator(orderBy.map(counters): _*)

View File

@ -14,10 +14,7 @@ object Permission extends Enumeration {
val get, post, admin = Value val get, post, admin = Value
} }
case class User(partyId: String, case class User(partyId: String, username: String, password: String, permission: Permission.Value) {
username: String,
password: String,
permission: Permission.Value) {
def hash: String = BCrypt.hashpw(password, BCrypt.gensalt) def hash: String = BCrypt.hashpw(password, BCrypt.gensalt)
def verify(plain: String): Boolean = BCrypt.checkpw(plain, password) def verify(plain: String): Boolean = BCrypt.checkpw(plain, password)

View File

@ -20,7 +20,8 @@ import scala.concurrent.duration.FiniteDuration
import scala.concurrent.{ExecutionContext, Future} import scala.concurrent.{ExecutionContext, Future}
class PartyService(context: ActorContext[Message], storage: ActorRef[DatabaseMessage]) class PartyService(context: ActorContext[Message], storage: ActorRef[DatabaseMessage])
extends AbstractBehavior[Message](context) with StrictLogging { extends AbstractBehavior[Message](context)
with StrictLogging {
import me.arcanis.ffxivbis.utils.Implicits._ import me.arcanis.ffxivbis.utils.Implicits._
private val cacheTimeout: FiniteDuration = private val cacheTimeout: FiniteDuration =

View File

@ -1,44 +0,0 @@
/*
* Copyright (c) 2019 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.service.bis
import akka.http.scaladsl.model.Uri
import me.arcanis.ffxivbis.models.Job
import spray.json.{JsNumber, JsObject, JsString, deserializationError}
import scala.concurrent.{ExecutionContext, Future}
object Ariyala extends IdParser {
override def parse(job: Job.Job, js: JsObject)
(implicit executionContext: ExecutionContext): Future[Map[String, Long]] =
Future {
val apiJob = js.fields.get("content") match {
case Some(JsString(value)) => value
case other => throw deserializationError(s"Invalid job name $other")
}
js.fields.get("datasets") match {
case Some(datasets: JsObject) =>
val fields = datasets.fields
fields.getOrElse(apiJob, fields(job.toString)).asJsObject
.fields("normal").asJsObject
.fields("items").asJsObject
.fields.foldLeft(Map.empty[String, Long]) {
case (acc, (key, JsNumber(id))) => BisProvider.remapKey(key).map(k => acc + (k -> id.toLong)).getOrElse(acc)
case (acc, _) => acc
}
case other => throw deserializationError(s"Invalid json $other")
}
}
override def uri(root: Uri, id: String): Uri =
root
.withPath(Uri.Path / "store.app")
.withQuery(Uri.Query(Map("identifier" -> id)))
}

View File

@ -16,13 +16,17 @@ import akka.http.scaladsl.model._
import com.typesafe.scalalogging.StrictLogging import com.typesafe.scalalogging.StrictLogging
import me.arcanis.ffxivbis.messages.{BiSProviderMessage, DownloadBiS} import me.arcanis.ffxivbis.messages.{BiSProviderMessage, DownloadBiS}
import me.arcanis.ffxivbis.models.{BiS, Job, Piece, PieceType} import me.arcanis.ffxivbis.models.{BiS, Job, Piece, PieceType}
import me.arcanis.ffxivbis.service.bis.parser.Parser
import me.arcanis.ffxivbis.service.bis.parser.impl.{Ariyala, Etro}
import spray.json._ import spray.json._
import scala.concurrent.{ExecutionContext, Future} import scala.concurrent.{ExecutionContext, Future}
import scala.util.{Failure, Success} import scala.util.{Failure, Success}
class BisProvider(context: ActorContext[BiSProviderMessage]) class BisProvider(context: ActorContext[BiSProviderMessage])
extends AbstractBehavior[BiSProviderMessage](context) with XivApi with StrictLogging { extends AbstractBehavior[BiSProviderMessage](context)
with XivApi
with StrictLogging {
override def system: ClassicActorSystemProvider = context.system override def system: ClassicActorSystemProvider = context.system
@ -37,19 +41,21 @@ class BisProvider(context: ActorContext[BiSProviderMessage])
Behaviors.same Behaviors.same
} }
override def onSignal: PartialFunction[Signal, Behavior[BiSProviderMessage]] = { override def onSignal: PartialFunction[Signal, Behavior[BiSProviderMessage]] = { case PostStop =>
case PostStop =>
shutdown() shutdown()
Behaviors.same Behaviors.same
} }
private def get(link: String, job: Job.Job): Future[Seq[Piece]] = { private def get(link: String, job: Job.Job): Future[Seq[Piece]] =
try {
val url = Uri(link) val url = Uri(link)
val id = Paths.get(link).normalize.getFileName.toString val id = Paths.get(link).normalize.getFileName.toString
val parser = if (url.authority.host.address().contains("etro")) Etro else Ariyala val parser = if (url.authority.host.address().contains("etro")) Etro else Ariyala
val uri = parser.uri(url, id) val uri = parser.uri(url, id)
sendRequest(uri, BisProvider.parseBisJsonToPieces(job, parser, getPieceType)) sendRequest(uri, BisProvider.parseBisJsonToPieces(job, parser, getPieceType))
} catch {
case exception: Exception => Future.failed(exception)
} }
} }
@ -58,16 +64,19 @@ object BisProvider {
def apply(): Behavior[BiSProviderMessage] = def apply(): Behavior[BiSProviderMessage] =
Behaviors.setup[BiSProviderMessage](context => new BisProvider(context)) Behaviors.setup[BiSProviderMessage](context => new BisProvider(context))
private def parseBisJsonToPieces(job: Job.Job, private def parseBisJsonToPieces(
idParser: IdParser, job: Job.Job,
pieceTypes: Seq[Long] => Future[Map[Long, PieceType.PieceType]]) idParser: Parser,
(js: JsObject) pieceTypes: Seq[Long] => Future[Map[Long, PieceType.PieceType]]
(implicit executionContext: ExecutionContext): Future[Seq[Piece]] = )(js: JsObject)(implicit executionContext: ExecutionContext): Future[Seq[Piece]] =
idParser.parse(job, js).flatMap { pieces => idParser.parse(job, js).flatMap { pieces =>
pieceTypes(pieces.values.toSeq).map { types => pieceTypes(pieces.values.toSeq).map { types =>
pieces.view.mapValues(types).map { pieces.view
case (piece, pieceType) => Piece(piece, pieceType, job) .mapValues(types)
}.toSeq .map { case (piece, pieceType) =>
Piece(piece, pieceType, job)
}
.toSeq
} }
} }

View File

@ -1,30 +0,0 @@
/*
* Copyright (c) 2019 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.service.bis
import akka.http.scaladsl.model.Uri
import me.arcanis.ffxivbis.models.Job
import spray.json.{JsNumber, JsObject, deserializationError}
import scala.concurrent.{ExecutionContext, Future}
object Etro extends IdParser {
override def parse(job: Job.Job, js: JsObject)
(implicit executionContext: ExecutionContext): Future[Map[String, Long]] =
Future {
js.fields.foldLeft(Map.empty[String, Long]) {
case (acc, (key, JsNumber(id))) => BisProvider.remapKey(key).map(k => acc + (k -> id.toLong)).getOrElse(acc)
case (acc, _) => acc
}
}
override def uri(root: Uri, id: String): Uri =
root.withPath(Uri.Path / "api" / "gearsets" / id)
}

View File

@ -29,7 +29,9 @@ trait RequestExecutor {
system.classicSystem.dispatchers.lookup("me.arcanis.ffxivbis.default-dispatcher") system.classicSystem.dispatchers.lookup("me.arcanis.ffxivbis.default-dispatcher")
def sendRequest[T](uri: Uri, parser: JsObject => Future[T]): Future[T] = def sendRequest[T](uri: Uri, parser: JsObject => Future[T]): Future[T] =
http.singleRequest(HttpRequest(uri = uri)).map { http
.singleRequest(HttpRequest(uri = uri))
.map {
case r: HttpResponse if r.status.isRedirection() => case r: HttpResponse if r.status.isRedirection() =>
val location = r.header[Location].get.uri val location = r.header[Location].get.uri
sendRequest(uri.withPath(location.path), parser) sendRequest(uri.withPath(location.path), parser)
@ -39,9 +41,11 @@ trait RequestExecutor {
.map(_.utf8String) .map(_.utf8String)
.map(result => parser(result.parseJson.asJsObject)) .map(result => parser(result.parseJson.asJsObject))
.toMat(Sink.head)(Keep.right) .toMat(Sink.head)(Keep.right)
.run().flatten .run()
.flatten
case other => Future.failed(new Error(s"Invalid response from server $other")) case other => Future.failed(new Error(s"Invalid response from server $other"))
}.flatten }
.flatten
def shutdown(): Unit = http.shutdownAllConnectionPools() def shutdown(): Unit = http.shutdownAllConnectionPools()
} }

View File

@ -24,22 +24,30 @@ trait XivApi extends RequestExecutor {
def getPieceType(itemIds: Seq[Long]): Future[Map[Long, PieceType.PieceType]] = { def getPieceType(itemIds: Seq[Long]): Future[Map[Long, PieceType.PieceType]] = {
val uriForItems = Uri(xivapiUrl) val uriForItems = Uri(xivapiUrl)
.withPath(Uri.Path / "item") .withPath(Uri.Path / "item")
.withQuery(Uri.Query(Map( .withQuery(
Uri.Query(
Map(
"columns" -> Seq("ID", "GameContentLinks").mkString(","), "columns" -> Seq("ID", "GameContentLinks").mkString(","),
"ids" -> itemIds.mkString(","), "ids" -> itemIds.mkString(","),
"private_key" -> xivapiKey.getOrElse("") "private_key" -> xivapiKey.getOrElse("")
))) )
)
)
sendRequest(uriForItems, XivApi.parseXivapiJsonToShop).flatMap { shops => sendRequest(uriForItems, XivApi.parseXivapiJsonToShop).flatMap { shops =>
val shopIds = shops.values.map(_._2).toSet val shopIds = shops.values.map(_._2).toSet
val columns = shops.values.map(pair => s"ItemCost${pair._1}").toSet val columns = shops.values.map(pair => s"ItemCost${pair._1}").toSet
val uriForShops = Uri(xivapiUrl) val uriForShops = Uri(xivapiUrl)
.withPath(Uri.Path / "specialshop") .withPath(Uri.Path / "specialshop")
.withQuery(Uri.Query(Map( .withQuery(
Uri.Query(
Map(
"columns" -> (columns + "ID").mkString(","), "columns" -> (columns + "ID").mkString(","),
"ids" -> shopIds.mkString(","), "ids" -> shopIds.mkString(","),
"private_key" -> xivapiKey.getOrElse("") "private_key" -> xivapiKey.getOrElse("")
))) )
)
)
sendRequest(uriForShops, XivApi.parseXivapiJsonToType(shops)) sendRequest(uriForShops, XivApi.parseXivapiJsonToType(shops))
} }
@ -48,14 +56,15 @@ trait XivApi extends RequestExecutor {
object XivApi { object XivApi {
private def parseXivapiJsonToShop(js: JsObject) private def parseXivapiJsonToShop(
(implicit executionContext: ExecutionContext): Future[Map[Long, (String, Long)]] = { js: JsObject
def extractTraderId(js: JsObject) = { )(implicit executionContext: ExecutionContext): Future[Map[Long, (String, Long)]] = {
def extractTraderId(js: JsObject) =
js.fields js.fields
.get("Recipe").map(_ => "crafted" -> -1L) // you can craft this item .get("Recipe")
.map(_ => "crafted" -> -1L) // you can craft this item
.orElse { // lets try shop items .orElse { // lets try shop items
js.fields("SpecialShop").asJsObject js.fields("SpecialShop").asJsObject.fields.collectFirst {
.fields.collectFirst {
case (shopName, JsArray(array)) if shopName.startsWith("ItemReceive") => case (shopName, JsArray(array)) if shopName.startsWith("ItemReceive") =>
val shopId = array.head match { val shopId = array.head match {
case JsNumber(id) => id.toLong case JsNumber(id) => id.toLong
@ -63,30 +72,32 @@ object XivApi {
} }
shopName.replace("ItemReceive", "") -> shopId shopName.replace("ItemReceive", "") -> shopId
} }
}.getOrElse(throw deserializationError(s"Could not parse $js"))
} }
.getOrElse(throw deserializationError(s"Could not parse $js"))
Future { Future {
js.fields("Results") match { js.fields("Results") match {
case array: JsArray => case array: JsArray =>
array.elements.map(_.asJsObject.getFields("ID", "GameContentLinks") match { array.elements
.map(_.asJsObject.getFields("ID", "GameContentLinks") match {
case Seq(JsNumber(id), shop: JsObject) => id.toLong -> extractTraderId(shop.asJsObject) case Seq(JsNumber(id), shop: JsObject) => id.toLong -> extractTraderId(shop.asJsObject)
case other => throw deserializationError(s"Could not parse $other") case other => throw deserializationError(s"Could not parse $other")
}).toMap })
.toMap
case other => throw deserializationError(s"Could not parse $other") case other => throw deserializationError(s"Could not parse $other")
} }
} }
} }
private def parseXivapiJsonToType(shops: Map[Long, (String, Long)])(js: JsObject) private def parseXivapiJsonToType(
(implicit executionContext: ExecutionContext): Future[Map[Long, PieceType.PieceType]] = shops: Map[Long, (String, Long)]
)(js: JsObject)(implicit executionContext: ExecutionContext): Future[Map[Long, PieceType.PieceType]] =
Future { Future {
val shopMap = js.fields("Results") match { val shopMap = js.fields("Results") match {
case array: JsArray => case array: JsArray =>
array.elements.collect { array.elements.collect { case shop: JsObject =>
case shop: JsObject => shop.fields("ID") match {
shop.asJsObject.fields("ID") match { case JsNumber(id) => id.toLong -> shop
case JsNumber(id) => id.toLong -> shop.asJsObject
case other => throw deserializationError(s"Could not parse $other") case other => throw deserializationError(s"Could not parse $other")
} }
}.toMap }.toMap
@ -96,9 +107,8 @@ object XivApi {
shops.map { case (itemId, (index, shopId)) => shops.map { case (itemId, (index, shopId)) =>
val pieceType = val pieceType =
if (index == "crafted" && shopId == -1L) PieceType.Crafted if (index == "crafted" && shopId == -1L) PieceType.Crafted
else { else
Try(shopMap(shopId).fields(s"ItemCost$index").asJsObject) Try(shopMap(shopId).fields(s"ItemCost$index").asJsObject).toOption
.toOption
.getOrElse(throw new Exception(s"${shopMap(shopId).fields(s"ItemCost$index")}, $index")) .getOrElse(throw new Exception(s"${shopMap(shopId).fields(s"ItemCost$index")}, $index"))
.getFields("IsUnique", "StackSize") match { .getFields("IsUnique", "StackSize") match {
case Seq(JsNumber(isUnique), JsNumber(stackSize)) => case Seq(JsNumber(isUnique), JsNumber(stackSize)) =>
@ -106,7 +116,6 @@ object XivApi {
else PieceType.Savage else PieceType.Savage
case other => throw deserializationError(s"Could not parse $other") case other => throw deserializationError(s"Could not parse $other")
} }
}
itemId -> pieceType itemId -> pieceType
} }
} }

View File

@ -1,4 +1,4 @@
package me.arcanis.ffxivbis.service.bis package me.arcanis.ffxivbis.service.bis.parser
import akka.http.scaladsl.model.Uri import akka.http.scaladsl.model.Uri
import com.typesafe.scalalogging.StrictLogging import com.typesafe.scalalogging.StrictLogging
@ -7,10 +7,9 @@ import spray.json.JsObject
import scala.concurrent.{ExecutionContext, Future} import scala.concurrent.{ExecutionContext, Future}
trait IdParser extends StrictLogging { trait Parser extends StrictLogging {
def parse(job: Job.Job, js: JsObject) def parse(job: Job.Job, js: JsObject)(implicit executionContext: ExecutionContext): Future[Map[String, Long]]
(implicit executionContext: ExecutionContext): Future[Map[String, Long]]
def uri(root: Uri, id: String): Uri def uri(root: Uri, id: String): Uri
} }

View File

@ -0,0 +1,45 @@
package me.arcanis.ffxivbis.service.bis.parser.impl
import akka.http.scaladsl.model.Uri
import me.arcanis.ffxivbis.models.Job
import me.arcanis.ffxivbis.service.bis.BisProvider
import me.arcanis.ffxivbis.service.bis.parser.Parser
import spray.json.{deserializationError, JsNumber, JsObject, JsString}
import scala.concurrent.{ExecutionContext, Future}
object Ariyala extends Parser {
override def parse(job: Job.Job, js: JsObject)(implicit
executionContext: ExecutionContext
): Future[Map[String, Long]] =
Future {
val apiJob = js.fields.get("content") match {
case Some(JsString(value)) => value
case other => throw deserializationError(s"Invalid job name $other")
}
js.fields.get("datasets") match {
case Some(datasets: JsObject) =>
val fields = datasets.fields
fields
.getOrElse(apiJob, fields(job.toString))
.asJsObject
.fields("normal")
.asJsObject
.fields("items")
.asJsObject
.fields
.foldLeft(Map.empty[String, Long]) {
case (acc, (key, JsNumber(id))) =>
BisProvider.remapKey(key).map(k => acc + (k -> id.toLong)).getOrElse(acc)
case (acc, _) => acc
}
case other => throw deserializationError(s"Invalid json $other")
}
}
override def uri(root: Uri, id: String): Uri =
root
.withPath(Uri.Path / "store.app")
.withQuery(Uri.Query(Map("identifier" -> id)))
}

View File

@ -0,0 +1,25 @@
package me.arcanis.ffxivbis.service.bis.parser.impl
import akka.http.scaladsl.model.Uri
import me.arcanis.ffxivbis.models.Job
import me.arcanis.ffxivbis.service.bis.BisProvider
import me.arcanis.ffxivbis.service.bis.parser.Parser
import spray.json.{JsNumber, JsObject}
import scala.concurrent.{ExecutionContext, Future}
object Etro extends Parser {
override def parse(job: Job.Job, js: JsObject)(implicit
executionContext: ExecutionContext
): Future[Map[String, Long]] =
Future {
js.fields.foldLeft(Map.empty[String, Long]) {
case (acc, (key, JsNumber(id))) => BisProvider.remapKey(key).map(k => acc + (k -> id.toLong)).getOrElse(acc)
case (acc, _) => acc
}
}
override def uri(root: Uri, id: String): Uri =
root.withPath(Uri.Path / "api" / "gearsets" / id)
}

View File

@ -6,15 +6,15 @@
* *
* License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause * License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause
*/ */
package me.arcanis.ffxivbis.service package me.arcanis.ffxivbis.service.database
import akka.actor.typed.Behavior import akka.actor.typed.Behavior
import akka.actor.typed.scaladsl.Behaviors import akka.actor.typed.scaladsl.Behaviors
import com.typesafe.config.Config import com.typesafe.config.Config
import com.typesafe.scalalogging.StrictLogging import com.typesafe.scalalogging.StrictLogging
import me.arcanis.ffxivbis.messages.{DatabaseMessage} import me.arcanis.ffxivbis.messages.DatabaseMessage
import me.arcanis.ffxivbis.models.{Party, Player, PlayerId} import me.arcanis.ffxivbis.models.{Party, Player, PlayerId}
import me.arcanis.ffxivbis.service.impl.DatabaseImpl import me.arcanis.ffxivbis.service.database.impl.DatabaseImpl
import me.arcanis.ffxivbis.storage.DatabaseProfile import me.arcanis.ffxivbis.storage.DatabaseProfile
import scala.concurrent.{ExecutionContext, Future} import scala.concurrent.{ExecutionContext, Future}

View File

@ -6,11 +6,11 @@
* *
* License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause * License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause
*/ */
package me.arcanis.ffxivbis.service.impl package me.arcanis.ffxivbis.service.database.impl
import akka.actor.typed.scaladsl.Behaviors import akka.actor.typed.scaladsl.Behaviors
import me.arcanis.ffxivbis.messages.{AddPieceToBis, DatabaseMessage, GetBiS, RemovePieceFromBiS, RemovePiecesFromBiS} import me.arcanis.ffxivbis.messages.{AddPieceToBis, DatabaseMessage, GetBiS, RemovePieceFromBiS, RemovePiecesFromBiS}
import me.arcanis.ffxivbis.service.Database import me.arcanis.ffxivbis.service.database.Database
trait DatabaseBiSHandler { this: Database => trait DatabaseBiSHandler { this: Database =>
@ -34,4 +34,3 @@ trait DatabaseBiSHandler { this: Database =>
Behaviors.same Behaviors.same
} }
} }

View File

@ -6,23 +6,26 @@
* *
* License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause * License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause
*/ */
package me.arcanis.ffxivbis.service.impl package me.arcanis.ffxivbis.service.database.impl
import akka.actor.typed.{Behavior, DispatcherSelector} import akka.actor.typed.{Behavior, DispatcherSelector}
import akka.actor.typed.scaladsl.{AbstractBehavior, ActorContext} import akka.actor.typed.scaladsl.{AbstractBehavior, ActorContext}
import com.typesafe.config.Config import com.typesafe.config.Config
import me.arcanis.ffxivbis.messages.DatabaseMessage import me.arcanis.ffxivbis.messages.DatabaseMessage
import me.arcanis.ffxivbis.service.Database import me.arcanis.ffxivbis.service.database.Database
import me.arcanis.ffxivbis.storage.DatabaseProfile import me.arcanis.ffxivbis.storage.DatabaseProfile
import scala.concurrent.ExecutionContext import scala.concurrent.ExecutionContext
class DatabaseImpl(context: ActorContext[DatabaseMessage]) class DatabaseImpl(context: ActorContext[DatabaseMessage])
extends AbstractBehavior[DatabaseMessage](context) with Database extends AbstractBehavior[DatabaseMessage](context)
with DatabaseBiSHandler with DatabaseLootHandler with Database
with DatabasePartyHandler with DatabaseUserHandler { with DatabaseBiSHandler
with DatabaseLootHandler
with DatabasePartyHandler
with DatabaseUserHandler {
override implicit val executionContext: ExecutionContext = { implicit override val executionContext: ExecutionContext = {
val selector = DispatcherSelector.fromConfig("me.arcanis.ffxivbis.default-dispatcher") val selector = DispatcherSelector.fromConfig("me.arcanis.ffxivbis.default-dispatcher")
context.system.dispatchers.lookup(selector) context.system.dispatchers.lookup(selector)
} }
@ -32,5 +35,5 @@ class DatabaseImpl(context: ActorContext[DatabaseMessage])
override def onMessage(msg: DatabaseMessage): Behavior[DatabaseMessage] = handle(msg) override def onMessage(msg: DatabaseMessage): Behavior[DatabaseMessage] = handle(msg)
private def handle: DatabaseMessage.Handler = private def handle: DatabaseMessage.Handler =
bisHandler orElse lootHandler orElse partyHandler orElse userHandler bisHandler.orElse(lootHandler).orElse(partyHandler).orElse(userHandler)
} }

View File

@ -6,14 +6,13 @@
* *
* License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause * License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause
*/ */
package me.arcanis.ffxivbis.service.impl package me.arcanis.ffxivbis.service.database.impl
import java.time.Instant import java.time.Instant
import akka.actor.typed.scaladsl.Behaviors import akka.actor.typed.scaladsl.Behaviors
import me.arcanis.ffxivbis.messages.{AddPieceTo, DatabaseMessage, GetLoot, RemovePieceFrom, SuggestLoot} import me.arcanis.ffxivbis.messages.{AddPieceTo, DatabaseMessage, GetLoot, RemovePieceFrom, SuggestLoot}
import me.arcanis.ffxivbis.models.Loot import me.arcanis.ffxivbis.models.Loot
import me.arcanis.ffxivbis.service.Database import me.arcanis.ffxivbis.service.database.Database
trait DatabaseLootHandler { this: Database => trait DatabaseLootHandler { this: Database =>

View File

@ -6,12 +6,12 @@
* *
* License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause * License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause
*/ */
package me.arcanis.ffxivbis.service.impl package me.arcanis.ffxivbis.service.database.impl
import akka.actor.typed.scaladsl.Behaviors import akka.actor.typed.scaladsl.Behaviors
import me.arcanis.ffxivbis.messages.{AddPlayer, DatabaseMessage, GetParty, GetPartyDescription, GetPlayer, RemovePlayer, UpdateParty} import me.arcanis.ffxivbis.messages.{AddPlayer, DatabaseMessage, GetParty, GetPartyDescription, GetPlayer, RemovePlayer, UpdateParty}
import me.arcanis.ffxivbis.models.{BiS, Player} import me.arcanis.ffxivbis.models.{BiS, Player}
import me.arcanis.ffxivbis.service.Database import me.arcanis.ffxivbis.service.database.Database
import scala.concurrent.Future import scala.concurrent.Future
@ -31,16 +31,26 @@ trait DatabasePartyHandler { this: Database =>
Behaviors.same Behaviors.same
case GetPlayer(playerId, client) => case GetPlayer(playerId, client) =>
val player = profile.getPlayerFull(playerId).flatMap { maybePlayerData => val player = profile
.getPlayerFull(playerId)
.flatMap { maybePlayerData =>
Future.traverse(maybePlayerData.toSeq) { playerData => Future.traverse(maybePlayerData.toSeq) { playerData =>
for { for {
bis <- profile.getPiecesBiS(playerId) bis <- profile.getPiecesBiS(playerId)
loot <- profile.getPieces(playerId) loot <- profile.getPieces(playerId)
} yield Player(playerData.id, playerId.partyId, playerId.job, } yield Player(
playerId.nick, BiS(bis.map(_.piece)), loot, playerData.id,
playerData.link, playerData.priority) playerId.partyId,
playerId.job,
playerId.nick,
BiS(bis.map(_.piece)),
loot,
playerData.link,
playerData.priority
)
} }
}.map(_.headOption) }
.map(_.headOption)
player.foreach(client ! _) player.foreach(client ! _)
Behaviors.same Behaviors.same

View File

@ -6,11 +6,11 @@
* *
* License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause * License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause
*/ */
package me.arcanis.ffxivbis.service.impl package me.arcanis.ffxivbis.service.database.impl
import akka.actor.typed.scaladsl.Behaviors import akka.actor.typed.scaladsl.Behaviors
import me.arcanis.ffxivbis.messages.{AddUser, DatabaseMessage, DeleteUser, Exists, GetUser, GetUsers} import me.arcanis.ffxivbis.messages.{AddUser, DatabaseMessage, DeleteUser, Exists, GetUser, GetUsers}
import me.arcanis.ffxivbis.service.Database import me.arcanis.ffxivbis.service.database.Database
trait DatabaseUserHandler { this: Database => trait DatabaseUserHandler { this: Database =>

View File

@ -18,16 +18,17 @@ import scala.concurrent.Future
trait BiSProfile { this: DatabaseProfile => trait BiSProfile { this: DatabaseProfile =>
import dbConfig.profile.api._ import dbConfig.profile.api._
case class BiSRep(playerId: Long, created: Long, piece: String, case class BiSRep(playerId: Long, created: Long, piece: String, pieceType: String, job: String) {
pieceType: String, job: String) {
def toLoot: Loot = Loot( def toLoot: Loot = Loot(
playerId, Piece(piece, PieceType.withName(pieceType), Job.withName(job)), playerId,
Instant.ofEpochMilli(created), isFreeLoot = false) Piece(piece, PieceType.withName(pieceType), Job.withName(job)),
Instant.ofEpochMilli(created),
isFreeLoot = false
)
} }
object BiSRep { object BiSRep {
def fromPiece(playerId: Long, piece: Piece): BiSRep = def fromPiece(playerId: Long, piece: Piece): BiSRep =
BiSRep(playerId, DatabaseProfile.now, piece.piece, BiSRep(playerId, DatabaseProfile.now, piece.piece, piece.pieceType.toString, piece.job.toString)
piece.pieceType.toString, piece.job.toString)
} }
class BiSPieces(tag: Tag) extends Table[BiSRep](tag, "bis") { class BiSPieces(tag: Tag) extends Table[BiSRep](tag, "bis") {

View File

@ -18,7 +18,11 @@ import slick.jdbc.JdbcProfile
import scala.concurrent.{ExecutionContext, Future} import scala.concurrent.{ExecutionContext, Future}
class DatabaseProfile(context: ExecutionContext, config: Config) class DatabaseProfile(context: ExecutionContext, config: Config)
extends BiSProfile with LootProfile with PartyProfile with PlayersProfile with UsersProfile { extends BiSProfile
with LootProfile
with PartyProfile
with PlayersProfile
with UsersProfile {
implicit val executionContext: ExecutionContext = context implicit val executionContext: ExecutionContext = context

View File

@ -18,19 +18,33 @@ import scala.concurrent.Future
trait LootProfile { this: DatabaseProfile => trait LootProfile { this: DatabaseProfile =>
import dbConfig.profile.api._ import dbConfig.profile.api._
case class LootRep(lootId: Option[Long], playerId: Long, created: Long, case class LootRep(
piece: String, pieceType: String, job: String, lootId: Option[Long],
isFreeLoot: Int) { playerId: Long,
created: Long,
piece: String,
pieceType: String,
job: String,
isFreeLoot: Int
) {
def toLoot: Loot = Loot( def toLoot: Loot = Loot(
playerId, playerId,
Piece(piece, PieceType.withName(pieceType), Job.withName(job)), Piece(piece, PieceType.withName(pieceType), Job.withName(job)),
Instant.ofEpochMilli(created), isFreeLoot == 1) Instant.ofEpochMilli(created),
isFreeLoot == 1
)
} }
object LootRep { object LootRep {
def fromLoot(playerId: Long, loot: Loot): LootRep = def fromLoot(playerId: Long, loot: Loot): LootRep =
LootRep(None, playerId, loot.timestamp.toEpochMilli, loot.piece.piece, LootRep(
loot.piece.pieceType.toString, loot.piece.job.toString, None,
if (loot.isFreeLoot) 1 else 0) playerId,
loot.timestamp.toEpochMilli,
loot.piece.piece,
loot.piece.pieceType.toString,
loot.piece.job.toString,
if (loot.isFreeLoot) 1 else 0
)
} }
class LootPieces(tag: Tag) extends Table[LootRep](tag, "loot") { class LootPieces(tag: Tag) extends Table[LootRep](tag, "loot") {
@ -48,7 +62,7 @@ trait LootProfile { this: DatabaseProfile =>
def fkPlayerId: ForeignKeyQuery[Players, PlayerRep] = def fkPlayerId: ForeignKeyQuery[Players, PlayerRep] =
foreignKey("player_id", playerId, playersTable)(_.playerId, onDelete = ForeignKeyAction.Cascade) foreignKey("player_id", playerId, playersTable)(_.playerId, onDelete = ForeignKeyAction.Cascade)
def lootOwnerIdx: Index = def lootOwnerIdx: Index =
index("loot_owner_idx", (playerId), unique = false) index("loot_owner_idx", playerId, unique = false)
} }
def deletePieceById(loot: Loot)(playerId: Long): Future[Int] = def deletePieceById(loot: Loot)(playerId: Long): Future[Int] =

View File

@ -11,13 +11,13 @@ package me.arcanis.ffxivbis.storage
import com.typesafe.config.Config import com.typesafe.config.Config
import org.flywaydb.core.Flyway import org.flywaydb.core.Flyway
import org.flywaydb.core.api.configuration.ClassicConfiguration import org.flywaydb.core.api.configuration.ClassicConfiguration
import org.flywaydb.core.api.output.MigrateResult
import scala.concurrent.Future
import scala.util.Try import scala.util.Try
class Migration(config: Config) { class Migration(config: Config) {
def performMigration(): Try[Boolean] = { def performMigration(): Try[MigrateResult] = {
val section = DatabaseProfile.getSection(config) val section = DatabaseProfile.getSection(config)
val url = section.getString("db.url") val url = section.getString("db.url")
@ -34,11 +34,11 @@ class Migration(config: Config) {
flywayConfiguration.setDataSource(url, username, password) flywayConfiguration.setDataSource(url, username, password)
val flyway = new Flyway(flywayConfiguration) val flyway = new Flyway(flywayConfiguration)
Try(flyway.migrate().success) Try(flyway.migrate())
} }
} }
object Migration { object Migration {
def apply(config: Config): Try[Boolean] = new Migration(config).performMigration() def apply(config: Config): Try[MigrateResult] = new Migration(config).performMigration()
} }

View File

@ -15,8 +15,7 @@ import scala.concurrent.Future
trait PartyProfile { this: DatabaseProfile => trait PartyProfile { this: DatabaseProfile =>
import dbConfig.profile.api._ import dbConfig.profile.api._
case class PartyRep(partyId: Option[Long], partyName: String, case class PartyRep(partyId: Option[Long], partyName: String, partyAlias: Option[String]) {
partyAlias: Option[String]) {
def toDescription: PartyDescription = PartyDescription(partyName, partyAlias) def toDescription: PartyDescription = PartyDescription(partyName, partyAlias)
} }
object PartyRep { object PartyRep {
@ -34,7 +33,9 @@ trait PartyProfile { this: DatabaseProfile =>
} }
def getPartyDescription(partyId: String): Future[PartyDescription] = def getPartyDescription(partyId: String): Future[PartyDescription] =
db.run(partyDescription(partyId).result.headOption.map(_.map(_.toDescription).getOrElse(PartyDescription.empty(partyId)))) db.run(
partyDescription(partyId).result.headOption.map(_.map(_.toDescription).getOrElse(PartyDescription.empty(partyId)))
)
def getUniquePartyId(partyId: String): Future[Option[Long]] = def getUniquePartyId(partyId: String): Future[Option[Long]] =
db.run(partyDescription(partyId).map(_.partyId).result.headOption) db.run(partyDescription(partyId).map(_.partyId).result.headOption)
def insertPartyDescription(partyDescription: PartyDescription): Future[Int] = def insertPartyDescription(partyDescription: PartyDescription): Future[Int] =
@ -43,7 +44,6 @@ trait PartyProfile { this: DatabaseProfile =>
case _ => db.run(partiesTable.insertOrUpdate(PartyRep.fromDescription(partyDescription, None))) case _ => db.run(partiesTable.insertOrUpdate(PartyRep.fromDescription(partyDescription, None)))
} }
private def partyDescription(partyId: String) = private def partyDescription(partyId: String) =
partiesTable.filter(_.partyName === partyId) partiesTable.filter(_.partyName === partyId)
} }

View File

@ -15,16 +15,21 @@ import scala.concurrent.Future
trait PlayersProfile { this: DatabaseProfile => trait PlayersProfile { this: DatabaseProfile =>
import dbConfig.profile.api._ import dbConfig.profile.api._
case class PlayerRep(partyId: String, playerId: Option[Long], created: Long, case class PlayerRep(
nick: String, job: String, link: Option[String], priority: Int) { partyId: String,
playerId: Option[Long],
created: Long,
nick: String,
job: String,
link: Option[String],
priority: Int
) {
def toPlayer: Player = def toPlayer: Player =
Player(playerId.getOrElse(-1), partyId, Job.withName(job), nick, Player(playerId.getOrElse(-1), partyId, Job.withName(job), nick, BiS.empty, Seq.empty, link, priority)
BiS.empty, Seq.empty, link, priority)
} }
object PlayerRep { object PlayerRep {
def fromPlayer(player: Player, id: Option[Long]): PlayerRep = def fromPlayer(player: Player, id: Option[Long]): PlayerRep =
PlayerRep(player.partyId, id, DatabaseProfile.now, player.nick, PlayerRep(player.partyId, id, DatabaseProfile.now, player.nick, player.job.toString, player.link, player.priority)
player.job.toString, player.link, player.priority)
} }
class Players(tag: Tag) extends Table[PlayerRep](tag, "players") { class Players(tag: Tag) extends Table[PlayerRep](tag, "players") {
@ -42,7 +47,8 @@ trait PlayersProfile { this: DatabaseProfile =>
def deletePlayer(playerId: PlayerId): Future[Int] = db.run(player(playerId).delete) def deletePlayer(playerId: PlayerId): Future[Int] = db.run(player(playerId).delete)
def getParty(partyId: String): Future[Map[Long, Player]] = def getParty(partyId: String): Future[Map[Long, Player]] =
db.run(players(partyId).result).map(_.foldLeft(Map.empty[Long, Player]) { db.run(players(partyId).result)
.map(_.foldLeft(Map.empty[Long, Player]) {
case (acc, p @ PlayerRep(_, Some(id), _, _, _, _, _)) => acc + (id -> p.toPlayer) case (acc, p @ PlayerRep(_, Some(id), _, _, _, _, _)) => acc + (id -> p.toPlayer)
case (acc, _) => acc case (acc, _) => acc
}) })

View File

@ -16,8 +16,7 @@ import scala.concurrent.Future
trait UsersProfile { this: DatabaseProfile => trait UsersProfile { this: DatabaseProfile =>
import dbConfig.profile.api._ import dbConfig.profile.api._
case class UserRep(partyId: String, userId: Option[Long], username: String, case class UserRep(partyId: String, userId: Option[Long], username: String, password: String, permission: String) {
password: String, permission: String) {
def toUser: User = User(partyId, username, password, Permission.withName(permission)) def toUser: User = User(partyId, username, password, Permission.withName(permission))
} }
object UserRep { object UserRep {

View File

@ -12,7 +12,8 @@ import me.arcanis.ffxivbis.http.api.v1.json._
import me.arcanis.ffxivbis.messages.{AddPlayer, AddUser} import me.arcanis.ffxivbis.messages.{AddPlayer, AddUser}
import me.arcanis.ffxivbis.models.{BiS, Job} import me.arcanis.ffxivbis.models.{BiS, Job}
import me.arcanis.ffxivbis.service.bis.BisProvider import me.arcanis.ffxivbis.service.bis.BisProvider
import me.arcanis.ffxivbis.service.{Database, PartyService} import me.arcanis.ffxivbis.service.database.Database
import me.arcanis.ffxivbis.service.PartyService
import me.arcanis.ffxivbis.storage.Migration import me.arcanis.ffxivbis.storage.Migration
import me.arcanis.ffxivbis.utils.Compare import me.arcanis.ffxivbis.utils.Compare
import org.scalatest.matchers.should.Matchers import org.scalatest.matchers.should.Matchers

View File

@ -12,7 +12,8 @@ import com.typesafe.config.Config
import me.arcanis.ffxivbis.{Fixtures, Settings} import me.arcanis.ffxivbis.{Fixtures, Settings}
import me.arcanis.ffxivbis.http.api.v1.json._ import me.arcanis.ffxivbis.http.api.v1.json._
import me.arcanis.ffxivbis.messages.{AddPlayer, AddUser} import me.arcanis.ffxivbis.messages.{AddPlayer, AddUser}
import me.arcanis.ffxivbis.service.{Database, PartyService} import me.arcanis.ffxivbis.service.database.Database
import me.arcanis.ffxivbis.service.PartyService
import me.arcanis.ffxivbis.storage.Migration import me.arcanis.ffxivbis.storage.Migration
import org.scalatest.matchers.should.Matchers import org.scalatest.matchers.should.Matchers
import org.scalatest.wordspec.AnyWordSpecLike import org.scalatest.wordspec.AnyWordSpecLike

View File

@ -12,7 +12,8 @@ import me.arcanis.ffxivbis.http.api.v1.json._
import me.arcanis.ffxivbis.messages.AddUser import me.arcanis.ffxivbis.messages.AddUser
import me.arcanis.ffxivbis.models.PartyDescription import me.arcanis.ffxivbis.models.PartyDescription
import me.arcanis.ffxivbis.service.bis.BisProvider import me.arcanis.ffxivbis.service.bis.BisProvider
import me.arcanis.ffxivbis.service.{Database, PartyService} import me.arcanis.ffxivbis.service.database.Database
import me.arcanis.ffxivbis.service.PartyService
import me.arcanis.ffxivbis.storage.Migration import me.arcanis.ffxivbis.storage.Migration
import org.scalatest.matchers.should.Matchers import org.scalatest.matchers.should.Matchers
import org.scalatest.wordspec.AnyWordSpecLike import org.scalatest.wordspec.AnyWordSpecLike

View File

@ -11,7 +11,8 @@ import me.arcanis.ffxivbis.{Fixtures, Settings}
import me.arcanis.ffxivbis.http.api.v1.json._ import me.arcanis.ffxivbis.http.api.v1.json._
import me.arcanis.ffxivbis.messages.{AddPlayer, AddUser} import me.arcanis.ffxivbis.messages.{AddPlayer, AddUser}
import me.arcanis.ffxivbis.service.bis.BisProvider import me.arcanis.ffxivbis.service.bis.BisProvider
import me.arcanis.ffxivbis.service.{Database, PartyService} import me.arcanis.ffxivbis.service.database.Database
import me.arcanis.ffxivbis.service.PartyService
import me.arcanis.ffxivbis.storage.Migration import me.arcanis.ffxivbis.storage.Migration
import org.scalatest.matchers.should.Matchers import org.scalatest.matchers.should.Matchers
import org.scalatest.wordspec.AnyWordSpecLike import org.scalatest.wordspec.AnyWordSpecLike

View File

@ -8,12 +8,12 @@ import akka.testkit.TestKit
import com.typesafe.config.Config import com.typesafe.config.Config
import me.arcanis.ffxivbis.{Fixtures, Settings} import me.arcanis.ffxivbis.{Fixtures, Settings}
import me.arcanis.ffxivbis.http.api.v1.json._ import me.arcanis.ffxivbis.http.api.v1.json._
import me.arcanis.ffxivbis.service.{Database, PartyService} import me.arcanis.ffxivbis.service.PartyService
import me.arcanis.ffxivbis.service.database.Database
import me.arcanis.ffxivbis.storage.Migration import me.arcanis.ffxivbis.storage.Migration
import org.scalatest.matchers.should.Matchers import org.scalatest.matchers.should.Matchers
import org.scalatest.wordspec.AnyWordSpecLike import org.scalatest.wordspec.AnyWordSpecLike
import scala.concurrent.Await
import scala.concurrent.duration._ import scala.concurrent.duration._
import scala.language.postfixOps import scala.language.postfixOps

View File

@ -1,12 +1,12 @@
package me.arcanis.ffxivbis.service package me.arcanis.ffxivbis.service.database
import akka.actor.testkit.typed.scaladsl.ScalaTestWithActorTestKit import akka.actor.testkit.typed.scaladsl.ScalaTestWithActorTestKit
import akka.actor.typed.scaladsl.AskPattern.Askable import akka.actor.typed.scaladsl.AskPattern.Askable
import me.arcanis.ffxivbis.messages.{AddPieceToBis, AddPlayer, GetBiS, RemovePieceFromBiS} import me.arcanis.ffxivbis.messages.{AddPieceToBis, AddPlayer, GetBiS, RemovePieceFromBiS}
import me.arcanis.ffxivbis.{Fixtures, Settings}
import me.arcanis.ffxivbis.models._ import me.arcanis.ffxivbis.models._
import me.arcanis.ffxivbis.storage.Migration import me.arcanis.ffxivbis.storage.Migration
import me.arcanis.ffxivbis.utils.Compare import me.arcanis.ffxivbis.utils.Compare
import me.arcanis.ffxivbis.{Fixtures, Settings}
import org.scalatest.wordspec.AnyWordSpecLike import org.scalatest.wordspec.AnyWordSpecLike
import scala.concurrent.Await import scala.concurrent.Await

View File

@ -1,12 +1,12 @@
package me.arcanis.ffxivbis.service package me.arcanis.ffxivbis.service.database
import akka.actor.testkit.typed.scaladsl.ScalaTestWithActorTestKit import akka.actor.testkit.typed.scaladsl.ScalaTestWithActorTestKit
import akka.actor.typed.scaladsl.AskPattern.Askable import akka.actor.typed.scaladsl.AskPattern.Askable
import me.arcanis.ffxivbis.messages.{AddPieceTo, AddPlayer, GetLoot, RemovePieceFrom} import me.arcanis.ffxivbis.messages.{AddPieceTo, AddPlayer, GetLoot, RemovePieceFrom}
import me.arcanis.ffxivbis.{Fixtures, Settings}
import me.arcanis.ffxivbis.models._ import me.arcanis.ffxivbis.models._
import me.arcanis.ffxivbis.storage.Migration import me.arcanis.ffxivbis.storage.Migration
import me.arcanis.ffxivbis.utils.Compare import me.arcanis.ffxivbis.utils.Compare
import me.arcanis.ffxivbis.{Fixtures, Settings}
import org.scalatest.wordspec.AnyWordSpecLike import org.scalatest.wordspec.AnyWordSpecLike
import scala.concurrent.Await import scala.concurrent.Await

View File

@ -1,14 +1,13 @@
package me.arcanis.ffxivbis.service package me.arcanis.ffxivbis.service.database
import akka.actor.testkit.typed.scaladsl.ScalaTestWithActorTestKit import akka.actor.testkit.typed.scaladsl.ScalaTestWithActorTestKit
import me.arcanis.ffxivbis.messages.{AddPlayer, GetParty, GetPlayer, RemovePlayer} import me.arcanis.ffxivbis.messages.{AddPlayer, GetParty, GetPlayer, RemovePlayer}
import me.arcanis.ffxivbis.{Fixtures, Settings}
import me.arcanis.ffxivbis.models._ import me.arcanis.ffxivbis.models._
import me.arcanis.ffxivbis.storage.Migration import me.arcanis.ffxivbis.storage.Migration
import me.arcanis.ffxivbis.utils.Compare import me.arcanis.ffxivbis.utils.Compare
import me.arcanis.ffxivbis.{Fixtures, Settings}
import org.scalatest.wordspec.AnyWordSpecLike import org.scalatest.wordspec.AnyWordSpecLike
import scala.concurrent.Await
import scala.concurrent.duration._ import scala.concurrent.duration._
import scala.language.postfixOps import scala.language.postfixOps

View File

@ -1,14 +1,13 @@
package me.arcanis.ffxivbis.service package me.arcanis.ffxivbis.service.database
import akka.actor.testkit.typed.scaladsl.ScalaTestWithActorTestKit import akka.actor.testkit.typed.scaladsl.ScalaTestWithActorTestKit
import me.arcanis.ffxivbis.messages.{AddUser, DeleteUser, GetUser, GetUsers} import me.arcanis.ffxivbis.messages.{AddUser, DeleteUser, GetUser, GetUsers}
import me.arcanis.ffxivbis.models.User import me.arcanis.ffxivbis.models.User
import me.arcanis.ffxivbis.{Fixtures, Settings}
import me.arcanis.ffxivbis.storage.Migration import me.arcanis.ffxivbis.storage.Migration
import me.arcanis.ffxivbis.utils.Compare import me.arcanis.ffxivbis.utils.Compare
import me.arcanis.ffxivbis.{Fixtures, Settings}
import org.scalatest.wordspec.AnyWordSpecLike import org.scalatest.wordspec.AnyWordSpecLike
import scala.concurrent.Await
import scala.concurrent.duration._ import scala.concurrent.duration._
import scala.language.postfixOps import scala.language.postfixOps