8 Commits

Author SHA1 Message Date
a6991a0a91 Release 0.9.13 2022-01-07 15:49:05 +03:00
5ec372be87 add item cache 2022-01-07 15:24:29 +03:00
bcdc88fa2c swagger fixes 2022-01-06 19:24:05 +03:00
53b42a6fa8 exception safety, github actions and scalafmt 2022-01-06 19:01:30 +03:00
99ed2705a2 another test for bis part 2022-01-06 06:19:57 +03:00
0ed9e92441 release 0.9.12 2021-12-19 22:44:39 +03:00
1866a1bb12 endwalker support
* added sge and rpr
* changed way to define savage gear
* libraries update
2021-12-19 22:39:20 +03:00
08f7f4571e Release 0.9.11
change loggin, more tests, cosmetic changes
2020-12-12 20:15:14 +03:00
102 changed files with 3474 additions and 1008 deletions

40
.github/workflows/release.yml vendored Normal file
View File

@ -0,0 +1,40 @@
name: release
on:
push:
tags:
- '*.*.*'
jobs:
make-release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: extract version
id: version
run: echo ::set-output name=VERSION::${GITHUB_REF#refs/tags/}
- name: create changelog
id: changelog
uses: jaywcjlove/changelog-generator@main
with:
token: ${{ secrets.GITHUB_TOKEN }}
filter: 'Release \d+\.\d+\.\d+'
- name: setup JDK
uses: actions/setup-java@v2
with:
distribution: temurin
java-version: 8
- name: create dist
run: sbt -v dist
- name: release
uses: softprops/action-gh-release@v1
with:
body: |
${{ steps.changelog.outputs.compareurl }}
${{ steps.changelog.outputs.changelog }}
files: target/universal/ffxivbis-*.zip
fail_on_unmatched_files: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

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: run tests
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,8 +1,8 @@
# 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 managing savage loot distribution easy.
## Installation and usage ## Installation and usage
@ -12,7 +12,7 @@ In general compilation process looks like:
sbt dist sbt dist
``` ```
Or alternatively you can download latest distribution zip from the releases page. Service can be run by using command: Or alternatively you can download the latest distribution zip from the releases page. Service can be run by using command:
```bash ```bash
bin/ffxivbis bin/ffxivbis

View File

@ -1,6 +1,6 @@
name := "ffxivbis" name := "ffxivbis"
scalaVersion := "2.13.1" scalaVersion := "2.13.6"
scalacOptions ++= Seq("-deprecation", "-feature") scalacOptions ++= Seq("-deprecation", "-feature")

56
extract_items.py Normal file
View File

@ -0,0 +1,56 @@
import json
import requests
# NOTE: it does not cover all items, just workaround to extract most gear pieces from patches
MIN_ILVL = 580
MAX_ILVL = 605
TOME = (
'radiant',
)
SAVAGE = (
'asphodelos',
)
payload = {
'queries': [
{
'slots': []
},
{
'jobs': [],
'minItemLevel': 580,
'maxItemLevel': 605
}
],
'existing': []
}
# it does not support application/json
r = requests.post('https://ffxiv.ariyala.com/items.app', data=json.dumps(payload))
r.raise_for_status()
result = []
for item in r.json():
item_id = item['itemID']
source_dict = item['source']
name = item['name']['en']
if 'crafting' in source_dict:
source = 'Crafted'
elif 'gathering' in source_dict:
continue # some random shit
elif 'purchase' in source_dict:
if any(tome in name.lower() for tome in TOME):
source = 'Tome'
elif any(savage in name.lower() for savage in SAVAGE):
source = 'Savage'
else:
source = None
continue
else:
raise RuntimeError(f'Unknown source {source_dict}')
result.append({'id': item_id, 'source': source, 'name': name})
output = {'cached-items': result}
print(json.dumps(output, indent=4, sort_keys=True))

View File

@ -1,32 +1,32 @@
val AkkaVersion = "2.6.10" val AkkaVersion = "2.6.17"
val AkkaHttpVersion = "10.2.1" val AkkaHttpVersion = "10.2.7"
val ScalaTestVersion = "3.2.10"
val SlickVersion = "3.3.3" val SlickVersion = "3.3.3"
libraryDependencies += "ch.qos.logback" % "logback-classic" % "1.2.3" libraryDependencies += "ch.qos.logback" % "logback-classic" % "1.2.9"
libraryDependencies += "com.typesafe.scala-logging" %% "scala-logging" % "3.9.2" libraryDependencies += "com.typesafe.scala-logging" %% "scala-logging" % "3.9.4"
libraryDependencies += "com.typesafe.akka" %% "akka-http" % AkkaHttpVersion libraryDependencies += "com.typesafe.akka" %% "akka-http" % AkkaHttpVersion
libraryDependencies += "com.typesafe.akka" %% "akka-http-spray-json" % AkkaHttpVersion libraryDependencies += "com.typesafe.akka" %% "akka-http-spray-json" % AkkaHttpVersion
libraryDependencies += "com.typesafe.akka" %% "akka-actor-typed" % AkkaVersion libraryDependencies += "com.typesafe.akka" %% "akka-actor-typed" % AkkaVersion
libraryDependencies += "com.typesafe.akka" %% "akka-stream" % AkkaVersion libraryDependencies += "com.typesafe.akka" %% "akka-stream" % AkkaVersion
libraryDependencies += "com.github.swagger-akka-http" %% "swagger-akka-http" % "2.3.0" libraryDependencies += "com.github.swagger-akka-http" %% "swagger-akka-http" % "2.6.0"
libraryDependencies += "javax.ws.rs" % "javax.ws.rs-api" % "2.1.1" libraryDependencies += "jakarta.platform" % "jakarta.jakartaee-web-api" % "9.1.0"
libraryDependencies += "io.spray" %% "spray-json" % "1.3.6" libraryDependencies += "io.spray" %% "spray-json" % "1.3.6"
libraryDependencies += "com.lihaoyi" %% "scalatags" % "0.9.2" libraryDependencies += "com.lihaoyi" %% "scalatags" % "0.9.2"
libraryDependencies += "com.typesafe.slick" %% "slick" % SlickVersion libraryDependencies += "com.typesafe.slick" %% "slick" % SlickVersion
libraryDependencies += "com.typesafe.slick" %% "slick-hikaricp" % SlickVersion libraryDependencies += "com.typesafe.slick" %% "slick-hikaricp" % SlickVersion
libraryDependencies += "org.flywaydb" % "flyway-core" % "6.0.6" libraryDependencies += "org.flywaydb" % "flyway-core" % "8.2.2"
libraryDependencies += "org.xerial" % "sqlite-jdbc" % "3.32.3.2" libraryDependencies += "org.xerial" % "sqlite-jdbc" % "3.36.0.3"
libraryDependencies += "org.postgresql" % "postgresql" % "42.2.18" libraryDependencies += "org.postgresql" % "postgresql" % "42.3.1"
libraryDependencies += "org.mindrot" % "jbcrypt" % "0.3m"
libraryDependencies += "org.mindrot" % "jbcrypt" % "0.4"
// testing // testing
libraryDependencies += "org.scalactic" %% "scalactic" % "3.1.4" % "test" libraryDependencies += "org.scalactic" %% "scalactic" % ScalaTestVersion % "test"
libraryDependencies += "org.scalatest" %% "scalatest" % "3.1.4" % "test" libraryDependencies += "org.scalatest" %% "scalatest" % ScalaTestVersion % "test"
libraryDependencies += "com.typesafe.akka" %% "akka-actor-testkit-typed" % AkkaVersion % "test" libraryDependencies += "com.typesafe.akka" %% "akka-actor-testkit-typed" % AkkaVersion % "test"
libraryDependencies += "com.typesafe.akka" %% "akka-stream-testkit" % AkkaVersion % "test" libraryDependencies += "com.typesafe.akka" %% "akka-stream-testkit" % AkkaVersion % "test"

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

@ -1,7 +1,7 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<title>ReDoc</title> <title>FFXIV loot tracker API</title>
<!-- needed for adaptive design --> <!-- needed for adaptive design -->
<meta charset="utf-8"/> <meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">

File diff suppressed because it is too large Load Diff

View File

@ -8,9 +8,10 @@
</root> </root>
<logger name="me.arcanis.ffxivbis" level="DEBUG" /> <logger name="me.arcanis.ffxivbis" level="DEBUG" />
<logger name="http" level="DEBUG"> <logger name="http" level="DEBUG" additivity="false">
<appender-ref ref="http" /> <appender-ref ref="http" />
</logger> </logger>
<logger name="slick" level="INFO" /> <logger name="slick" level="INFO" />
<logger name="org.flywaydb.core.internal" level="INFO" />
</configuration> </configuration>

View File

@ -1,5 +1,8 @@
me.arcanis.ffxivbis { me.arcanis.ffxivbis {
bis-provider { bis-provider {
include "item_data.json"
# xivapi base url, string, required # xivapi base url, string, required
xivapi-url = "https://xivapi.com" xivapi-url = "https://xivapi.com"
# xivapi developer key, string, optional # xivapi developer key, string, optional

View File

@ -11,27 +11,29 @@ package me.arcanis.ffxivbis
import akka.actor.typed.{Behavior, PostStop, Signal} import akka.actor.typed.{Behavior, PostStop, Signal}
import akka.actor.typed.scaladsl.{AbstractBehavior, ActorContext, Behaviors} import akka.actor.typed.scaladsl.{AbstractBehavior, ActorContext, Behaviors}
import akka.http.scaladsl.Http import akka.http.scaladsl.Http
import akka.http.scaladsl.server.Route
import akka.stream.Materializer 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}
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
} }
private def startApplication(): Unit = { private def startApplication(): Unit = {
@ -42,14 +44,25 @@ class Application(context: ActorContext[Nothing])
implicit val executionContext: ExecutionContext = context.system.executionContext implicit val executionContext: ExecutionContext = context.system.executionContext
implicit val materializer: Materializer = Materializer(context) implicit val materializer: Materializer = Materializer(context)
Migration(config) Migration(config) match {
case Success(result) if result.success =>
val bisProvider = context.spawn(BisProvider(), "bis-provider")
val storage = context.spawn(Database(), "storage")
val party = context.spawn(PartyService(storage), "party")
val http = new RootEndpoint(context.system, party, bisProvider)
val bisProvider = context.spawn(BisProvider(), "bis-provider") val flow = Route.toFlow(http.route)(context.system)
val storage = context.spawn(Database(), "storage") Http(context.system).newServerAt(host, port).bindFlow(flow)
val party = context.spawn(PartyService(storage), "party")
val http = new RootEndpoint(context.system, party, bisProvider)
Http()(context.system).newServerAt(host, port).bindFlow(http.route) 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) =>
logger.error("exception during migration", exception)
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)
Future.traverse(bis.pieces)(addPieceBiS(playerId, _)) .flatMap { bis =>
}.map(_ => ()) Future.traverse(bis.pieces)(addPieceBiS(playerId, _))
}
.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] =
player.link match { storage
case Some(link) => .ask(ref => AddPlayer(player, ref))
downloadBiS(link, player.job).map { bis => .map { res =>
bis.pieces.map(piece => storage.ask(AddPieceToBis(player.playerId, piece, _))) player.link.map(_.trim).filter(_.nonEmpty) match {
}.map(_ => res) case Some(link) =>
case None => Future.successful(res) downloadBiS(link, player.job)
.map { bis =>
bis.pieces.map(piece => storage.ask(AddPieceToBis(player.playerId, piece, _)))
}
.map(_ => 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,35 +39,36 @@ 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
} }
} }
def route: Route = def route: Route =
withHttpLog { withHttpLog {
apiRoute ~ htmlRoute ~ swagger.routes ~ swaggerUIRoute ignoreTrailingSlash {
apiRoute ~ htmlRoute ~ swagger.routes ~ swaggerUIRoute
}
} }
private def apiRoute: Route = private def apiRoute: Route =
ignoreTrailingSlash { pathPrefix("api") {
pathPrefix("api") { pathPrefix(Segment) {
pathPrefix(Segment) { case "v1" => rootApiV1Endpoint.route
case "v1" => rootApiV1Endpoint.route case _ => reject
case _ => reject
}
} }
} }
private def htmlRoute: Route = private def htmlRoute: Route =
ignoreTrailingSlash { pathPrefix("static") {
pathPrefix("static") { getFromResourceDirectory("static")
getFromResourceDirectory("static") } ~ rootView.route
} ~ rootView.route
}
private def swaggerUIRoute: Route = private def swaggerUIRoute: Route =
path("swagger") { path("swagger") {
getFromResource("swagger/index.html") getFromResource("html/swagger.html")
} ~ getFromResourceDirectory("swagger") }
} }

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

@ -19,7 +19,7 @@ import io.swagger.v3.oas.annotations.parameters.RequestBody
import io.swagger.v3.oas.annotations.responses.ApiResponse import io.swagger.v3.oas.annotations.responses.ApiResponse
import io.swagger.v3.oas.annotations.security.SecurityRequirement import io.swagger.v3.oas.annotations.security.SecurityRequirement
import io.swagger.v3.oas.annotations.{Operation, Parameter} import io.swagger.v3.oas.annotations.{Operation, Parameter}
import javax.ws.rs._ import jakarta.ws.rs._
import me.arcanis.ffxivbis.http.api.v1.json._ import me.arcanis.ffxivbis.http.api.v1.json._
import me.arcanis.ffxivbis.http.{Authorization, BiSHelper} import me.arcanis.ffxivbis.http.{Authorization, BiSHelper}
import me.arcanis.ffxivbis.messages.{BiSProviderMessage, Message} import me.arcanis.ffxivbis.messages.{BiSProviderMessage, Message}
@ -27,33 +27,52 @@ 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",
array = new ArraySchema(schema = new Schema(implementation = classOf[PlayerResponse])) description = "Best in slot",
))), content = Array(
new ApiResponse(responseCode = "401", description = "Supplied authorization is invalid", new Content(
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))), array = new ArraySchema(schema = new Schema(implementation = classOf[PlayerResponse]))
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])))), 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("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

@ -17,7 +17,7 @@ import spray.json._
trait HttpHandler extends StrictLogging { this: JsonSupport => trait HttpHandler extends StrictLogging { this: JsonSupport =>
implicit def exceptionHandler: ExceptionHandler = ExceptionHandler { def exceptionHandler: ExceptionHandler = ExceptionHandler {
case ex: IllegalArgumentException => case ex: IllegalArgumentException =>
complete(StatusCodes.BadRequest, ErrorResponse(ex.getMessage)) complete(StatusCodes.BadRequest, ErrorResponse(ex.getMessage))
@ -26,7 +26,7 @@ trait HttpHandler extends StrictLogging { this: JsonSupport =>
complete(StatusCodes.InternalServerError, ErrorResponse("unknown server error")) complete(StatusCodes.InternalServerError, ErrorResponse("unknown server error"))
} }
implicit def rejectionHandler: RejectionHandler = def rejectionHandler: RejectionHandler =
RejectionHandler.default RejectionHandler.default
.mapRejectionResponse { .mapRejectionResponse {
case response @ HttpResponse(_, _, entity: HttpEntity.Strict, _) => case response @ HttpResponse(_, _, entity: HttpEntity.Strict, _) =>

View File

@ -19,7 +19,7 @@ import io.swagger.v3.oas.annotations.parameters.RequestBody
import io.swagger.v3.oas.annotations.responses.ApiResponse import io.swagger.v3.oas.annotations.responses.ApiResponse
import io.swagger.v3.oas.annotations.security.SecurityRequirement import io.swagger.v3.oas.annotations.security.SecurityRequirement
import io.swagger.v3.oas.annotations.{Operation, Parameter} import io.swagger.v3.oas.annotations.{Operation, Parameter}
import javax.ws.rs._ import jakarta.ws.rs._
import me.arcanis.ffxivbis.http.api.v1.json._ import me.arcanis.ffxivbis.http.api.v1.json._
import me.arcanis.ffxivbis.http.{Authorization, LootHelper} import me.arcanis.ffxivbis.http.{Authorization, LootHelper}
import me.arcanis.ffxivbis.messages.Message import me.arcanis.ffxivbis.messages.Message
@ -27,33 +27,56 @@ 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",
array = new ArraySchema(schema = new Schema(implementation = classOf[PlayerResponse])) description = "Loot list",
))), content = Array(
new ApiResponse(responseCode = "401", description = "Supplied authorization is invalid", new Content(
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))), array = new ArraySchema(schema = new Schema(implementation = classOf[PlayerResponse]))
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])))), 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"),
@ -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",
array = new ArraySchema(schema = new Schema(implementation = classOf[PlayerIdWithCountersResponse])), description = "Players with counters ordered by priority to get this item",
))), content = Array(
new ApiResponse(responseCode = "400", description = "Invalid parameters were supplied", new Content(
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))), array = new ArraySchema(schema = new Schema(implementation = classOf[PlayerIdWithCountersResponse])),
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(
new ApiResponse(responseCode = "500", description = "Internal server error", responseCode = "400",
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))), description = "Invalid parameters were supplied",
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))
),
new ApiResponse(
responseCode = "401",
description = "Supplied authorization is invalid",
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))
),
new ApiResponse(
responseCode = "403",
description = "Access is forbidden",
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))
),
new ApiResponse(
responseCode = "500",
description = "Internal server error",
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))
),
), ),
security = Array(new SecurityRequirement(name = "basic auth", scopes = Array("get"))), security = Array(new SecurityRequirement(name = "basic auth", scopes = Array("get"))),
tags = Array("loot"), tags = Array("loot"),

View File

@ -19,37 +19,55 @@ import io.swagger.v3.oas.annotations.parameters.RequestBody
import io.swagger.v3.oas.annotations.responses.ApiResponse import io.swagger.v3.oas.annotations.responses.ApiResponse
import io.swagger.v3.oas.annotations.security.SecurityRequirement import io.swagger.v3.oas.annotations.security.SecurityRequirement
import io.swagger.v3.oas.annotations.{Operation, Parameter} import io.swagger.v3.oas.annotations.{Operation, Parameter}
import javax.ws.rs._ import jakarta.ws.rs._
import me.arcanis.ffxivbis.http.api.v1.json._ import me.arcanis.ffxivbis.http.api.v1.json._
import me.arcanis.ffxivbis.http.{Authorization, PlayerHelper} import me.arcanis.ffxivbis.http.{Authorization, PlayerHelper}
import me.arcanis.ffxivbis.messages.{BiSProviderMessage, Message} 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

@ -19,7 +19,7 @@ import io.swagger.v3.oas.annotations.parameters.RequestBody
import io.swagger.v3.oas.annotations.responses.ApiResponse import io.swagger.v3.oas.annotations.responses.ApiResponse
import io.swagger.v3.oas.annotations.security.SecurityRequirement import io.swagger.v3.oas.annotations.security.SecurityRequirement
import io.swagger.v3.oas.annotations.{Operation, Parameter} import io.swagger.v3.oas.annotations.{Operation, Parameter}
import javax.ws.rs._ import jakarta.ws.rs._
import me.arcanis.ffxivbis.http.api.v1.json._ import me.arcanis.ffxivbis.http.api.v1.json._
import me.arcanis.ffxivbis.http.{Authorization, PlayerHelper} import me.arcanis.ffxivbis.http.{Authorization, PlayerHelper}
import me.arcanis.ffxivbis.messages.{BiSProviderMessage, Message} import me.arcanis.ffxivbis.messages.{BiSProviderMessage, Message}
@ -27,34 +27,59 @@ 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",
array = new ArraySchema(schema = new Schema(implementation = classOf[PlayerResponse])), description = "Players list",
))), content = Array(
new ApiResponse(responseCode = "401", description = "Supplied authorization is invalid", new Content(
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))), array = new ArraySchema(schema = new Schema(implementation = classOf[PlayerResponse])),
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])))), 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("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

@ -14,11 +14,11 @@ import com.typesafe.config.Config
import io.swagger.v3.oas.annotations.media.{ArraySchema, Content, Schema} import io.swagger.v3.oas.annotations.media.{ArraySchema, Content, Schema}
import io.swagger.v3.oas.annotations.responses.ApiResponse import io.swagger.v3.oas.annotations.responses.ApiResponse
import io.swagger.v3.oas.annotations.Operation import io.swagger.v3.oas.annotations.Operation
import javax.ws.rs._ import jakarta.ws.rs._
import me.arcanis.ffxivbis.http.api.v1.json._ import me.arcanis.ffxivbis.http.api.v1.json._
import me.arcanis.ffxivbis.models.{Job, Party, Permission, Piece, PieceType} import me.arcanis.ffxivbis.models.{Job, Party, Permission, Piece, PieceType}
@Path("api/v1") @Path("/api/v1")
class TypesEndpoint(config: Config) extends JsonSupport { class TypesEndpoint(config: Config) extends JsonSupport {
def route: Route = getJobs ~ getPermissions ~ getPieces ~ getPieceTypes ~ getPriority def route: Route = getJobs ~ getPermissions ~ getPieces ~ getPieceTypes ~ getPriority
@ -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",
array = new ArraySchema(schema = new Schema(implementation = classOf[String])) description = "List of available jobs",
))), content = Array(
new ApiResponse(responseCode = "500", description = "Internal server error", new Content(
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))), 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])))
),
), ),
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",
array = new ArraySchema(schema = new Schema(implementation = classOf[String])) description = "List of available permissions",
))), content = Array(
new ApiResponse(responseCode = "500", description = "Internal server error", new Content(
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))), 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])))
),
), ),
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",
array = new ArraySchema(schema = new Schema(implementation = classOf[String])) description = "List of available pieces",
))), content = Array(
new ApiResponse(responseCode = "500", description = "Internal server error", new Content(
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))), 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])))
),
), ),
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",
array = new ArraySchema(schema = new Schema(implementation = classOf[String])) description = "List of available piece types",
))), content = Array(
new ApiResponse(responseCode = "500", description = "Internal server error", new Content(
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))), 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])))
),
), ),
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",
array = new ArraySchema(schema = new Schema(implementation = classOf[String])) description = "Priority order",
))), content = Array(
new ApiResponse(responseCode = "500", description = "Internal server error", new Content(
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))), 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])))
),
), ),
tags = Array("types"), tags = Array("types"),
) )

View File

@ -19,7 +19,7 @@ import io.swagger.v3.oas.annotations.parameters.RequestBody
import io.swagger.v3.oas.annotations.responses.ApiResponse import io.swagger.v3.oas.annotations.responses.ApiResponse
import io.swagger.v3.oas.annotations.security.SecurityRequirement import io.swagger.v3.oas.annotations.security.SecurityRequirement
import io.swagger.v3.oas.annotations.{Operation, Parameter} import io.swagger.v3.oas.annotations.{Operation, Parameter}
import javax.ws.rs._ import jakarta.ws.rs._
import me.arcanis.ffxivbis.http.api.v1.json._ import me.arcanis.ffxivbis.http.api.v1.json._
import me.arcanis.ffxivbis.http.{Authorization, UserHelper} import me.arcanis.ffxivbis.http.{Authorization, UserHelper}
import me.arcanis.ffxivbis.messages.Message import me.arcanis.ffxivbis.messages.Message
@ -27,27 +27,42 @@ 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",
array = new ArraySchema(schema = new Schema(implementation = classOf[UserResponse])), description = "Users list",
))), content = Array(
new ApiResponse(responseCode = "401", description = "Supplied authorization is invalid", new Content(
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))), array = new ArraySchema(schema = new Schema(implementation = classOf[UserResponse])),
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])))), 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("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)
} }
@ -17,4 +18,4 @@ object LootResponse {
def fromLoot(loot: Loot): LootResponse = def fromLoot(loot: Loot): LootResponse =
LootResponse(PieceResponse.fromPiece(loot.piece), loot.timestamp, loot.isFreeLoot) LootResponse(PieceResponse.fromPiece(loot.piece), loot.timestamp, loot.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,11 +34,13 @@ 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)
BiSView.template(partyId, players, None) .map { players =>
}.map { text => BiSView.template(partyId, players, None)
(StatusCodes.OK, RootView.toHtml(text)) }
} .map { text =>
(StatusCodes.OK, RootView.toHtml(text))
}
} }
} }
} }
@ -49,39 +52,49 @@ 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],
onComplete(modifyBiSCall(partyId, player, maybePiece, maybePieceType, maybeLink, action)) { _ => "piece".as[String].?,
redirect(s"/party/$partyId/bis", StatusCodes.Found) "piece_type".as[String].?,
} "link".as[String].?,
"action".as[String]
) { (player, maybePiece, maybePieceType, maybeLink, action) =>
onComplete(modifyBiSCall(partyId, player, maybePiece, maybePieceType, maybeLink, action)) { _ =>
redirect(s"/party/$partyId/bis", StatusCodes.Found)
}
} }
} }
} }
} }
} }
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
def bisAction(playerId: PlayerId, piece: String, pieceType: String)(fn: Piece => Future[Unit]) = def bisAction(playerId: PlayerId, piece: String, pieceType: String)(fn: Piece => Future[Unit]) =
getPiece(playerId, piece, pieceType) match { getPiece(playerId, piece, pieceType) match {
case Some(item) => fn(item).map(_ => ()) case Some(item) => fn(item)
case _ => Future.failed(new Error(s"Could not construct piece from `$piece ($pieceType)`")) case _ => Future.failed(new Error(s"Could not construct piece from `$piece ($pieceType)`"))
} }
PlayerId(partyId, player) match { PlayerId(partyId, player) match {
case Some(playerId) => (maybePiece, maybePieceType, action, maybeLink) match { case Some(playerId) =>
case (Some(piece), Some(pieceType), "add", _) => (maybePiece, maybePieceType, action, maybeLink.map(_.trim).filter(_.nonEmpty)) match {
bisAction(playerId, piece, pieceType)(addPieceBiS(playerId, _)) case (Some(piece), Some(pieceType), "add", _) =>
case (Some(piece), Some(pieceType), "remove", _) => bisAction(playerId, piece, pieceType)(addPieceBiS(playerId, _))
bisAction(playerId, piece, pieceType)(removePieceBiS(playerId, _)) case (Some(piece), Some(pieceType), "remove", _) =>
case (_, _, "create", Some(link)) => putBiS(playerId, link).map(_ => ()) bisAction(playerId, piece, pieceType)(removePieceBiS(playerId, _))
case _ => Future.failed(new Error(s"Could not perform $action")) case (_, _, "create", Some(link)) => putBiS(playerId, link)
} case _ => Future.failed(new Error(s"Could not perform $action"))
}
case _ => Future.failed(new Error(s"Could not construct player id from `$player`")) case _ => Future.failed(new Error(s"Could not construct player id from `$player`"))
} }
} }
@ -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") select(name := "piece", id := "piece", title := "piece")(
(for (piece <- Piece.available) yield option(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)), select(name := "piece_type", id := "piece_type", title := "piece type")(
input(name:="action", id:="action", `type`:="hidden", value:="add"), for (pieceType <- PieceType.available) yield option(pieceType.toString)
input(name:="add", id:="add", `type`:="submit", value:="add") ),
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")(
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)), ),
input(name:="link", id:="link", placeholder:="player bis link", title:="link", `type`:="text"), input(name := "link", id := "link", placeholder := "player bis link", title := "link", `type` := "text"),
input(name:="action", id:="action", `type`:="hidden", value:="create"), input(name := "action", id := "action", `type` := "hidden", value := "create"),
input(name:="add", id:="add", `type`:="submit", value:="add") input(name := "add", id := "add", `type` := "submit", value := "add")
), ),
table(id := "result")(
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(piece.pieceType.toString), td(`class` := "include_search")(piece.piece),
td( td(piece.pieceType.toString),
form(action:=s"/party/$partyId/bis", method:="post")( td(
input(name:="player", id:="player", `type`:="hidden", value:=player.playerId.toString), form(action := s"/party/$partyId/bis", method := "post")(
input(name:="piece", id:="piece", `type`:="hidden", value:=piece.piece), input(name := "player", id := "player", `type` := "hidden", value := player.playerId.toString),
input(name:="piece_type", id:="piece_type", `type`:="hidden", value:=piece.pieceType.toString), input(name := "piece", id := "piece", `type` := "hidden", value := piece.piece),
input(name:="action", id:="action", `type`:="hidden", value:="remove"), input(
input(name:="remove", id:="remove", `type`:="submit", value:="x") name := "piece_type",
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,19 +32,20 @@ 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].?) {
onComplete { (username, password, maybeAlias) =>
newPartyId.flatMap { partyId => onComplete {
val user = User(partyId, username, password, Permission.admin) newPartyId.flatMap { partyId =>
addUser(user, isHashedPassword = false).flatMap { _ => val user = User(partyId, username, password, Permission.admin)
if (maybeAlias.getOrElse("").isEmpty) Future.successful(partyId) addUser(user, isHashedPassword = false).flatMap { _ =>
else updateDescription(PartyDescription(partyId, maybeAlias)).map(_ => partyId) if (maybeAlias.getOrElse("").isEmpty) Future.successful(partyId)
else updateDescription(PartyDescription(partyId, maybeAlias)).map(_ => partyId)
}
} }
} {
case Success(partyId) => redirect(s"/party/$partyId", StatusCodes.Found)
case Failure(exception) => throw exception
} }
} {
case Success(partyId) => redirect(s"/party/$partyId", StatusCodes.Found)
case Failure(exception) => throw exception
}
} }
} }
} }
@ -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,34 +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})"),
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") select(name := "job", id := "job", title := "job")(
(for (job <- Job.availableWithAnyJob) yield option(job.toString)), 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)), select(name := "piece_type", id := "piece_type", title := "piece type")(
input(name:="free_loot", id:="free_loot", title:="is free loot", `type`:="checkbox"), for (pieceType <- PieceType.available) yield option(pieceType.toString)
label(`for`:="free_loot")("is free loot"), ),
input(name:="suggest", id:="suggest", `type`:="submit", value:="suggest") 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")(
table(id:="result")(
tr( tr(
th("player"), th("player"),
th("is required"), th("is required"),
@ -113,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(player.isRequiredToString), td(`class` := "include_search")(player.playerId.toString),
td(player.lootCount), td(player.isRequiredToString),
td(player.lootCountBiS), td(player.lootCount),
td(player.lootCountTotal), td(player.lootCountBiS),
td( td(player.lootCountTotal),
form(action:=s"/party/$partyId/loot", method:="post")( td(
input(name:="player", id:="player", `type`:="hidden", value:=player.playerId.toString), form(action := s"/party/$partyId/loot", method := "post")(
input(name:="piece", id:="piece", `type`:="hidden", value:=piece.map(_.piece).getOrElse("")), input(name := "player", id := "player", `type` := "hidden", value := player.playerId.toString),
input(name:="piece_type", id:="piece_type", `type`:="hidden", value:=piece.map(_.pieceType.toString).getOrElse("")), input(
input(name:="free_loot", id:="free_loot", `type`:="hidden", value:=(if (isFreeLoot) "yes" else "no")), name := "piece",
input(name:="action", id:="action", `type`:="hidden", value:="add"), id := "piece",
input(name:="add", id:="add", `type`:="submit", value:="add") `type` := "hidden",
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,11 +32,13 @@ 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)
LootView.template(partyId, players, None) .map { players =>
}.map { text => LootView.template(partyId, players, None)
(StatusCodes.OK, RootView.toHtml(text)) }
} .map { text =>
(StatusCodes.OK, RootView.toHtml(text))
}
} }
} }
} }
@ -48,32 +50,42 @@ 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],
onComplete(modifyLootCall(partyId, player, piece, pieceType, isFreeLoot, action)) { _ => "piece".as[String],
redirect(s"/party/$partyId/loot", StatusCodes.Found) "piece_type".as[String],
} "action".as[String],
"free_loot".as[String].?
) { (player, piece, pieceType, action, isFreeLoot) =>
onComplete(modifyLootCall(partyId, player, piece, pieceType, isFreeLoot, action)) { _ =>
redirect(s"/party/$partyId/loot", StatusCodes.Found)
}
} }
} }
} }
} }
} }
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) =>
case (Some(piece), "add") => addPieceLoot(playerId, piece, maybeFreeLoot).map(_ => ()) (getPiece(playerId), action) match {
case (Some(piece), "remove") => removePieceLoot(playerId, piece).map(_ => ()) case (Some(piece), "add") => addPieceLoot(playerId, piece, maybeFreeLoot)
case _ => Future.failed(new Error(s"Could not construct piece from `$maybePiece ($maybePieceType)`")) 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 player id from `$player`")) case _ => Future.failed(new Error(s"Could not construct player id from `$player`"))
} }
} }
@ -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") select(name := "piece", id := "piece", title := "piece")(
(for (piece <- Piece.available) yield option(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)), select(name := "piece_type", id := "piece_type", title := "piece type")(
input(name:="free_loot", id:="free_loot", title:="is free loot", `type`:="checkbox"), for (pieceType <- PieceType.available) yield option(pieceType.toString)
label(`for`:="free_loot")("is free loot"), ),
input(name:="action", id:="action", `type`:="hidden", value:="add"), input(name := "free_loot", id := "free_loot", title := "is free loot", `type` := "checkbox"),
input(name:="add", id:="add", `type`:="submit", value:="add") 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")(
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(loot.piece.pieceType.toString), td(`class` := "include_search")(loot.piece.piece),
td(loot.isFreeLootToString), td(loot.piece.pieceType.toString),
td(loot.timestamp.toString), td(loot.isFreeLootToString),
td( td(loot.timestamp.toString),
form(action:=s"/party/$partyId/loot", method:="post")( td(
input(name:="player", id:="player", `type`:="hidden", value:=player.playerId.toString), form(action := s"/party/$partyId/loot", method := "post")(
input(name:="piece", id:="piece", `type`:="hidden", value:=loot.piece.piece), input(name := "player", id := "player", `type` := "hidden", value := player.playerId.toString),
input(name:="piece_type", id:="piece_type", `type`:="hidden", value:=loot.piece.pieceType.toString), input(name := "piece", id := "piece", `type` := "hidden", value := loot.piece.piece),
input(name:="free_loot", id:="free_loot", `type`:="hidden", value:=loot.isFreeLootToString), 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 := 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,11 +33,13 @@ 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)
PlayerView.template(partyId, players.map(_.withCounters(None)), None) .map { players =>
}.map { text => PlayerView.template(partyId, players.map(_.withCounters(None)), None)
(StatusCodes.OK, RootView.toHtml(text)) }
} .map { text =>
(StatusCodes.OK, RootView.toHtml(text))
}
} }
} }
} }
@ -48,28 +51,37 @@ 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],
onComplete(modifyPartyCall(partyId, nick, job, maybePriority, maybeLink, action)) { _ => "job".as[String],
redirect(s"/party/$partyId/players", StatusCodes.Found) "priority".as[Int].?,
} "link".as[String].?,
"action".as[String]
) { (nick, job, maybePriority, maybeLink, action) =>
onComplete(modifyPartyCall(partyId, nick, job, maybePriority, maybeLink, action)) { _ =>
redirect(s"/party/$partyId/players", StatusCodes.Found)
}
} }
} }
} }
} }
} }
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))
(action, maybePlayerId) match { (action, maybePlayerId) match {
case ("add", Some(playerId)) => addPlayer(player(playerId)).map(_ => ()) case ("add", Some(playerId)) => addPlayer(player(playerId))
case ("remove", Some(playerId)) => removePlayer(playerId).map(_ => ()) case ("remove", Some(playerId)) => removePlayer(playerId)
case _ => Future.failed(new Error(s"Could not perform $action with $nick ($job)")) case _ => Future.failed(new Error(s"Could not perform $action with $nick ($job)"))
} }
} }
@ -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"),
input(name := "add", id := "add", `type` := "submit", value := "add")
), ),
table(id := "result")(
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(player.lootCountBiS), td(`class` := "include_search")(player.job.toString),
td(player.lootCountTotal), td(player.lootCountBiS),
td(player.priority), td(player.lootCountTotal),
td( td(player.priority),
form(action:=s"/party/$partyId/players", method:="post")( td(
input(name:="nick", id:="nick", `type`:="hidden", value:=player.nick), form(action := s"/party/$partyId/players", method := "post")(
input(name:="job", id:="job", `type`:="hidden", value:=player.job.toString), input(name := "nick", id := "nick", `type` := "hidden", value := player.nick),
input(name:="action", id:="action", `type`:="hidden", value:="remove"), input(name := "job", id := "job", `type` := "hidden", value := player.job.toString),
input(name:="remove", id:="remove", `type`:="submit", value:="x") 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

@ -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)
@ -30,7 +31,7 @@ class RootView(storage: ActorRef[Message],
def route: Route = def route: Route =
basePartyView.route ~ indexView.route ~ basePartyView.route ~ indexView.route ~
biSView.route ~ lootView.route ~ lootSuggestView.route ~ playerView.route ~ userView.route biSView.route ~ lootView.route ~ lootSuggestView.route ~ playerView.route ~ userView.route
} }
object RootView { object RootView {

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,11 +32,13 @@ 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)
UserView.template(partyId, users, None) .map { users =>
}.map { text => UserView.template(partyId, users, None)
(StatusCodes.OK, RootView.toHtml(text)) }
} .map { 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,19 +61,27 @@ 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).map(_ => ()) (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 "remove" => removeUser(partyId, username).map(_ => ()) case _ =>
Future.failed(
new Error(s"Could not construct permission/password from `$maybePermission`/`$maybePassword`")
)
}
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(
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")(
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(user.permission.toString), td(`class` := "include_search")(user.username),
td( td(user.permission.toString),
form(action:=s"/party/$partyId/users", method:="post")( td(
input(name:="username", id:="username", `type`:="hidden", value:=user.username.toString), form(action := s"/party/$partyId/users", method := "post")(
input(name:="action", id:="action", `type`:="hidden", value:="remove"), input(name := "username", id := "username", `type` := "hidden", value := user.username.toString),
input(name:="remove", id:="remove", `type`:="submit", value:="x") 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

@ -5,4 +5,7 @@ import me.arcanis.ffxivbis.models.{BiS, Job}
sealed trait BiSProviderMessage sealed trait BiSProviderMessage
case class DownloadBiS(link: String, job: Job.Job, replyTo: ActorRef[BiS]) extends BiSProviderMessage case class DownloadBiS(link: String, job: Job.Job, replyTo: ActorRef[BiS]) extends BiSProviderMessage {
require(link.nonEmpty && link.trim == link, "Link must be not empty and contain no spaces")
}

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 {
@ -74,4 +76,4 @@ case class Exists(partyId: String, replyTo: ActorRef[Boolean]) extends DatabaseM
case class GetUser(partyId: String, username: String, replyTo: ActorRef[Option[User]]) extends DatabaseMessage case class GetUser(partyId: String, username: String, replyTo: ActorRef[Option[User]]) extends DatabaseMessage
case class GetUsers(partyId: String, replyTo: ActorRef[Seq[User]]) extends DatabaseMessage case class GetUsers(partyId: String, replyTo: ActorRef[Seq[User]]) 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
case (acc, (Some(k), v)) => acc + (k -> v.size) .groupBy(_.upgrade)
case (acc, _) => acc .foldLeft(Map.empty[PieceUpgrade, Int]) {
} withDefaultValue 0 case (acc, (Some(k), v)) => acc + (k -> v.size)
case (acc, _) => acc
}
.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

@ -62,6 +62,10 @@ object Job {
val leftSide: LeftSide = BodyMnks val leftSide: LeftSide = BodyMnks
val rightSide: RightSide = AccessoriesStr val rightSide: RightSide = AccessoriesStr
} }
trait Drgs extends Job {
val leftSide: LeftSide = BodyDrgs
val rightSide: RightSide = AccessoriesStr
}
trait Tanks extends Job { trait Tanks extends Job {
val leftSide: LeftSide = BodyTanks val leftSide: LeftSide = BodyTanks
val rightSide: RightSide = AccessoriesVit val rightSide: RightSide = AccessoriesVit
@ -79,12 +83,11 @@ object Job {
case object WHM extends Healers case object WHM extends Healers
case object SCH extends Healers case object SCH extends Healers
case object AST extends Healers case object AST extends Healers
case object SGE extends Healers
case object MNK extends Mnks case object MNK extends Mnks
case object DRG extends Job { case object DRG extends Drgs
val leftSide: LeftSide = BodyDrgs case object RPR extends Drgs
val rightSide: RightSide = AccessoriesStr
}
case object NIN extends Job { case object NIN extends Job {
val leftSide: LeftSide = BodyNins val leftSide: LeftSide = BodyNins
val rightSide: RightSide = AccessoriesDex val rightSide: RightSide = AccessoriesDex
@ -100,7 +103,7 @@ object Job {
case object RDM extends Casters case object RDM extends Casters
lazy val available: Seq[Job] = lazy val available: Seq[Job] =
Seq(PLD, WAR, DRK, GNB, WHM, SCH, AST, MNK, DRG, NIN, SAM, BRD, MCH, DNC, BLM, SMN, RDM) Seq(PLD, WAR, DRK, GNB, WHM, SCH, AST, SGE, MNK, DRG, RPR, NIN, SAM, BRD, MCH, DNC, BLM, SMN, RDM)
lazy val availableWithAnyJob: Seq[Job] = available.prepended(AnyJob) lazy val availableWithAnyJob: Seq[Job] = available.prepended(AnyJob)
def withName(job: String): Job.Job = def withName(job: String): Job.Job =

View File

@ -37,15 +37,19 @@ 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)))
} }
Party(party, getRules(config), playersWithItems) Party(party, getRules(config), playersWithItems)
} }

View File

@ -16,4 +16,4 @@ case class PartyDescription(partyId: String, partyAlias: Option[String]) {
object PartyDescription { object PartyDescription {
def empty(partyId: String): PartyDescription = PartyDescription(partyId, None) def empty(partyId: String): PartyDescription = PartyDescription(partyId, None)
} }

View File

@ -16,12 +16,13 @@ sealed trait Piece extends Equals {
def withJob(other: Job.Job): Piece def withJob(other: Job.Job): Piece
def upgrade: Option[PieceUpgrade] = this match { def upgrade: Option[PieceUpgrade] = {
case _ if pieceType != PieceType.Tome => None val isTome = pieceType == PieceType.Tome
case _: Waist => Some(AccessoryUpgrade) Some(this).collect {
case _: PieceAccessory => Some(AccessoryUpgrade) case _: PieceAccessory if isTome => AccessoryUpgrade
case _: PieceBody => Some(BodyUpgrade) case _: PieceBody if isTome => BodyUpgrade
case _: PieceWeapon => Some(WeaponUpgrade) case _: PieceWeapon if isTome => WeaponUpgrade
}
} }
// used for ring comparison // used for ring comparison
@ -54,10 +55,6 @@ case class Hands(override val pieceType: PieceType.PieceType, override val job:
val piece: String = "hands" val piece: String = "hands"
def withJob(other: Job.Job): Piece = copy(job = other) def withJob(other: Job.Job): Piece = copy(job = other)
} }
case class Waist(override val pieceType: PieceType.PieceType, override val job: Job.Job) extends PieceBody {
val piece: String = "waist"
def withJob(other: Job.Job): Piece = copy(job = other)
}
case class Legs(override val pieceType: PieceType.PieceType, override val job: Job.Job) extends PieceBody { case class Legs(override val pieceType: PieceType.PieceType, override val job: Job.Job) extends PieceBody {
val piece: String = "legs" val piece: String = "legs"
def withJob(other: Job.Job): Piece = copy(job = other) def withJob(other: Job.Job): Piece = copy(job = other)
@ -79,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 {
@ -111,7 +111,6 @@ object Piece {
case "head" => Head(pieceType, job) case "head" => Head(pieceType, job)
case "body" => Body(pieceType, job) case "body" => Body(pieceType, job)
case "hands" => Hands(pieceType, job) case "hands" => Hands(pieceType, job)
case "waist" => Waist(pieceType, job)
case "legs" => Legs(pieceType, job) case "legs" => Legs(pieceType, job)
case "feet" => Feet(pieceType, job) case "feet" => Feet(pieceType, job)
case "ears" => Ears(pieceType, job) case "ears" => Ears(pieceType, job)
@ -124,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", "waist", "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

@ -7,9 +7,10 @@ object PieceType {
case object Crafted extends PieceType case object Crafted extends PieceType
case object Tome extends PieceType case object Tome extends PieceType
case object Savage extends PieceType case object Savage extends PieceType
case object Artifact extends PieceType
lazy val available: Seq[PieceType] = lazy val available: Seq[PieceType] =
Seq(Crafted, Tome, Savage) Seq(Crafted, Tome, Savage, Artifact)
def withName(pieceType: String): PieceType = def withName(pieceType: String): PieceType =
available.find(_.toString.equalsIgnoreCase(pieceType)) match { available.find(_.toString.equalsIgnoreCase(pieceType)) match {

View File

@ -8,14 +8,16 @@
*/ */
package me.arcanis.ffxivbis.models package me.arcanis.ffxivbis.models
case class Player(id: Long, case class Player(
partyId: String, id: Long,
job: Job.Job, partyId: String,
nick: String, job: Job.Job,
bis: BiS, nick: String,
loot: Seq[Loot], bis: BiS,
link: Option[String] = None, loot: Seq[Loot],
priority: Int = 0) { link: Option[String] = None,
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,16 +8,17 @@
*/ */
package me.arcanis.ffxivbis.models package me.arcanis.ffxivbis.models
case class PlayerIdWithCounters(partyId: String, case class PlayerIdWithCounters(
job: Job.Job, partyId: String,
nick: String, job: Job.Job,
isRequired: Boolean, nick: String,
priority: Int, isRequired: Boolean,
bisCountTotal: Int, priority: Int,
lootCount: Int, bisCountTotal: Int,
lootCountBiS: Int, lootCount: Int,
lootCountTotal: Int) lootCountBiS: Int,
extends PlayerIdBase { lootCountTotal: Int
) 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 {
def idParser(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")
}
}
def uri(root: Uri, id: String): Uri =
root
.withPath(Uri.Path / "store.app")
.withQuery(Uri.Query(Map("identifier" -> id)))
}

View File

@ -9,7 +9,6 @@
package me.arcanis.ffxivbis.service.bis package me.arcanis.ffxivbis.service.bis
import java.nio.file.Paths import java.nio.file.Paths
import akka.actor.ClassicActorSystemProvider import akka.actor.ClassicActorSystemProvider
import akka.actor.typed.{Behavior, PostStop, Signal} import akka.actor.typed.{Behavior, PostStop, Signal}
import akka.actor.typed.scaladsl.{AbstractBehavior, ActorContext, Behaviors} import akka.actor.typed.scaladsl.{AbstractBehavior, ActorContext, Behaviors}
@ -17,41 +16,47 @@ 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}
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
override def onMessage(msg: BiSProviderMessage): Behavior[BiSProviderMessage] = override def onMessage(msg: BiSProviderMessage): Behavior[BiSProviderMessage] =
msg match { msg match {
case DownloadBiS(link, job, client) => case DownloadBiS(link, job, client) =>
get(link, job).map(BiS(_)).foreach(client ! _) get(link, job).onComplete {
case Success(items) => client ! BiS(items)
case Failure(exception) =>
logger.error("received exception while getting items", exception)
}
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]] =
val url = Uri(link) try {
val id = Paths.get(link).normalize.getFileName.toString val url = Uri(link)
val id = Paths.get(link).normalize.getFileName.toString
val (idParser, uri) = val parser = if (url.authority.host.address().contains("etro")) Etro else Ariyala
if (url.authority.host.address().contains("etro")) { val uri = parser.uri(url, id)
(Etro.idParser(_, _), Etro.uri(url, id)) sendRequest(uri, BisProvider.parseBisJsonToPieces(job, parser, getPieceType))
} else { } catch {
(Ariyala.idParser(_, _), Ariyala.uri(url, id)) case exception: Exception => Future.failed(exception)
} }
sendRequest(uri, BisProvider.parseBisJsonToPieces(job, idParser, getPieceType))
}
} }
object BisProvider { object BisProvider {
@ -59,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: (Job.Job, JsObject) => Future[Map[String, Long]], 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(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
} }
} }
@ -77,7 +85,7 @@ object BisProvider {
case "chest" => Some("body") case "chest" => Some("body")
case "ringLeft" | "fingerL" => Some("left ring") case "ringLeft" | "fingerL" => Some("left ring")
case "ringRight" | "fingerR" => Some("right ring") case "ringRight" | "fingerR" => Some("right ring")
case "weapon" | "head" | "body" | "hands" | "waist" | "legs" | "feet" | "ears" | "neck" | "wrist" | "wrists" => Some(key) case "weapon" | "head" | "body" | "hands" | "legs" | "feet" | "ears" | "neck" | "wrist" | "wrists" => Some(key)
case _ => None case _ => None
} }
} }

View File

@ -29,19 +29,23 @@ 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
case r: HttpResponse if r.status.isRedirection() => .singleRequest(HttpRequest(uri = uri))
val location = r.header[Location].get.uri .map {
sendRequest(uri.withPath(location.path), parser) case r: HttpResponse if r.status.isRedirection() =>
case HttpResponse(status, _, entity, _) if status.isSuccess() => val location = r.header[Location].get.uri
entity.dataBytes sendRequest(uri.withPath(location.path), parser)
.fold(ByteString.empty)(_ ++ _) case HttpResponse(status, _, entity, _) if status.isSuccess() =>
.map(_.utf8String) entity.dataBytes
.map(result => parser(result.parseJson.asJsObject)) .fold(ByteString.empty)(_ ++ _)
.toMat(Sink.head)(Keep.right) .map(_.utf8String)
.run().flatten .map(result => parser(result.parseJson.asJsObject))
case other => Future.failed(new Error(s"Invalid response from server $other")) .toMat(Sink.head)(Keep.right)
}.flatten .run()
.flatten
case other => Future.failed(new Error(s"Invalid response from server $other"))
}
.flatten
def shutdown(): Unit = http.shutdownAllConnectionPools() def shutdown(): Unit = http.shutdownAllConnectionPools()
} }

View File

@ -13,6 +13,7 @@ import me.arcanis.ffxivbis.models.PieceType
import spray.json._ import spray.json._
import scala.concurrent.{ExecutionContext, Future} import scala.concurrent.{ExecutionContext, Future}
import scala.jdk.CollectionConverters._
import scala.util.Try import scala.util.Try
trait XivApi extends RequestExecutor { trait XivApi extends RequestExecutor {
@ -21,25 +22,52 @@ trait XivApi extends RequestExecutor {
private val xivapiUrl = config.getString("me.arcanis.ffxivbis.bis-provider.xivapi-url") private val xivapiUrl = config.getString("me.arcanis.ffxivbis.bis-provider.xivapi-url")
private val xivapiKey = Try(config.getString("me.arcanis.ffxivbis.bis-provider.xivapi-key")).toOption private val xivapiKey = Try(config.getString("me.arcanis.ffxivbis.bis-provider.xivapi-key")).toOption
private val preloadedItems: Map[Long, PieceType.PieceType] =
config
.getConfigList("me.arcanis.ffxivbis.bis-provider.cached-items")
.asScala
.map { item =>
item.getLong("id") -> PieceType.withName(item.getString("source"))
}
.toMap
def getPieceType(itemIds: Seq[Long]): Future[Map[Long, PieceType.PieceType]] = { def getPieceType(itemIds: Seq[Long]): Future[Map[Long, PieceType.PieceType]] = {
val (local, remote) = itemIds.foldLeft((Map.empty[Long, PieceType.PieceType], Seq.empty[Long])) {
case ((l, r), id) =>
if (preloadedItems.contains(id)) (l.updated(id, preloadedItems(id)), r)
else (l, r :+ id)
}
if (remote.isEmpty) Future.successful(local)
else remotePieceType(remote).map(_ ++ local)
}
private def remotePieceType(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(
"columns" -> Seq("ID", "GameContentLinks").mkString(","), Uri.Query(
"ids" -> itemIds.mkString(","), Map(
"private_key" -> xivapiKey.getOrElse("") "columns" -> Seq("ID", "GameContentLinks").mkString(","),
))) "ids" -> itemIds.mkString(","),
"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(
"columns" -> (columns + "ID").mkString(","), Uri.Query(
"ids" -> shopIds.mkString(","), Map(
"private_key" -> xivapiKey.getOrElse("") "columns" -> (columns + "ID").mkString(","),
))) "ids" -> shopIds.mkString(","),
"private_key" -> xivapiKey.getOrElse("")
)
)
)
sendRequest(uriForShops, XivApi.parseXivapiJsonToType(shops)) sendRequest(uriForShops, XivApi.parseXivapiJsonToType(shops))
} }
@ -48,14 +76,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")
.orElse { // lets try shop items .map(_ => "crafted" -> -1L) // you can craft this item
js.fields("SpecialShop").asJsObject .orElse { // lets try shop items
.fields.collectFirst { js.fields("SpecialShop").asJsObject.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,29 +92,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
case Seq(JsNumber(id), shop) => id.toLong -> extractTraderId(shop.asJsObject) .map(_.asJsObject.getFields("ID", "GameContentLinks") match {
case other => throw deserializationError(s"Could not parse $other") case Seq(JsNumber(id), shop: JsObject) => id.toLong -> extractTraderId(shop.asJsObject)
}).toMap case other => throw deserializationError(s"Could not parse $other")
})
.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.map { shop => array.elements.collect { case shop: JsObject =>
shop.asJsObject.fields("ID") match { shop.fields("ID") match {
case JsNumber(id) => id.toLong -> shop.asJsObject case JsNumber(id) => id.toLong -> shop
case other => throw deserializationError(s"Could not parse $other") case other => throw deserializationError(s"Could not parse $other")
} }
}.toMap }.toMap
@ -94,18 +126,16 @@ object XivApi {
shops.map { case (itemId, (index, shopId)) => shops.map { case (itemId, (index, shopId)) =>
val pieceType = val pieceType =
if (index == "crafted" && shopId == -1) 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
.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)) =>
if (isUnique == 1 || stackSize.toLong == 2000) PieceType.Tome // either upgraded gear or tomes found if (isUnique == 1 || stackSize.toLong != 999) PieceType.Tome // either upgraded gear or tomes found
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

@ -0,0 +1,15 @@
package me.arcanis.ffxivbis.service.bis.parser
import akka.http.scaladsl.model.Uri
import com.typesafe.scalalogging.StrictLogging
import me.arcanis.ffxivbis.models.Job
import spray.json.JsObject
import scala.concurrent.{ExecutionContext, Future}
trait Parser extends StrictLogging {
def parse(job: Job.Job, js: JsObject)(implicit executionContext: ExecutionContext): Future[Map[String, Long]]
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

@ -1,23 +1,18 @@
/* package me.arcanis.ffxivbis.service.bis.parser.impl
* 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 akka.http.scaladsl.model.Uri
import me.arcanis.ffxivbis.models.Job 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 spray.json.{JsNumber, JsObject}
import scala.concurrent.{ExecutionContext, Future} import scala.concurrent.{ExecutionContext, Future}
object Etro { object Etro extends Parser {
def idParser(job: Job.Job, js: JsObject) override def parse(job: Job.Job, js: JsObject)(implicit
(implicit executionContext: ExecutionContext): Future[Map[String, Long]] = executionContext: ExecutionContext
): Future[Map[String, Long]] =
Future { Future {
js.fields.foldLeft(Map.empty[String, Long]) { 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, (key, JsNumber(id))) => BisProvider.remapKey(key).map(k => acc + (k -> id.toLong)).getOrElse(acc)
@ -25,6 +20,6 @@ object Etro {
} }
} }
def uri(root: Uri, id: String): Uri = override def uri(root: Uri, id: String): Uri =
root.withPath(Uri.Path / "api" / "gearsets" / id) 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,13 +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 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 =>
def bisHandler: DatabaseMessage.Handler = { def bisHandler: DatabaseMessage.Handler = {
case AddPieceToBis(playerId, piece, client) => case AddPieceToBis(playerId, piece, client) =>
@ -23,7 +23,7 @@ trait DatabaseBiSHandler { this: Database =>
getParty(partyId, withBiS = true, withLoot = false) getParty(partyId, withBiS = true, withLoot = false)
.map(filterParty(_, maybePlayerId)) .map(filterParty(_, maybePlayerId))
.foreach(client ! _) .foreach(client ! _)
Behaviors.same Behaviors.same
case RemovePieceFromBiS(playerId, piece, client) => case RemovePieceFromBiS(playerId, piece, client) =>
profile.deletePieceBiS(playerId, piece).foreach(_ => client ! ()) profile.deletePieceBiS(playerId, piece).foreach(_ => client ! ())
@ -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,16 +6,16 @@
* *
* 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
trait DatabasePartyHandler { this: Database => trait DatabasePartyHandler { this: Database =>
def partyHandler: DatabaseMessage.Handler = { def partyHandler: DatabaseMessage.Handler = {
case AddPlayer(player, client) => case AddPlayer(player, client) =>
@ -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
Future.traverse(maybePlayerData.toSeq) { playerData => .getPlayerFull(playerId)
for { .flatMap { maybePlayerData =>
bis <- profile.getPiecesBiS(playerId) Future.traverse(maybePlayerData.toSeq) { playerData =>
loot <- profile.getPieces(playerId) for {
} yield Player(playerData.id, playerId.partyId, playerId.job, bis <- profile.getPiecesBiS(playerId)
playerId.nick, BiS(bis.map(_.piece)), loot, loot <- profile.getPieces(playerId)
playerData.link, playerData.priority) } yield Player(
playerData.id,
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,12 +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
class Migration(config: Config) { class Migration(config: Config) {
def performMigration(): Future[Int] = { 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")
@ -33,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)
Future.successful(flyway.migrate()) Try(flyway.migrate())
} }
} }
object Migration { object Migration {
def apply(config: Config): Future[Int] = 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,10 +47,11 @@ 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)
case (acc, p @ PlayerRep(_, Some(id), _, _, _, _, _)) => acc + (id -> p.toPlayer) .map(_.foldLeft(Map.empty[Long, Player]) {
case (acc, _) => acc case (acc, p @ PlayerRep(_, Some(id), _, _, _, _, _)) => acc + (id -> p.toPlayer)
}) case (acc, _) => acc
})
def getPlayer(playerId: PlayerId): Future[Option[Long]] = def getPlayer(playerId: PlayerId): Future[Option[Long]] =
db.run(player(playerId).map(_.playerId).result.headOption) db.run(player(playerId).map(_.playerId).result.headOption)
def getPlayerFull(playerId: PlayerId): Future[Option[Player]] = def getPlayerFull(playerId: PlayerId): Future[Option[Player]] =

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

@ -17,6 +17,7 @@ import scala.concurrent.duration.FiniteDuration
import scala.language.implicitConversions import scala.language.implicitConversions
object Implicits { object Implicits {
implicit def getBooleanFromOptionString(maybeYes: Option[String]): Boolean = maybeYes.map(_.toLowerCase) match { implicit def getBooleanFromOptionString(maybeYes: Option[String]): Boolean = maybeYes.map(_.toLowerCase) match {
case Some("yes" | "on") => true case Some("yes" | "on") => true
case _ => false case _ => false

View File

@ -9,7 +9,6 @@ object Fixtures {
Head(pieceType = PieceType.Savage, Job.DNC), Head(pieceType = PieceType.Savage, Job.DNC),
Body(pieceType = PieceType.Savage, Job.DNC), Body(pieceType = PieceType.Savage, Job.DNC),
Hands(pieceType = PieceType.Tome, Job.DNC), Hands(pieceType = PieceType.Tome, Job.DNC),
Waist(pieceType = PieceType.Tome, Job.DNC),
Legs(pieceType = PieceType.Tome, Job.DNC), Legs(pieceType = PieceType.Tome, Job.DNC),
Feet(pieceType = PieceType.Savage, Job.DNC), Feet(pieceType = PieceType.Savage, Job.DNC),
Ears(pieceType = PieceType.Savage, Job.DNC), Ears(pieceType = PieceType.Savage, Job.DNC),
@ -19,16 +18,47 @@ object Fixtures {
Ring(pieceType = PieceType.Tome, Job.DNC, "right ring") Ring(pieceType = PieceType.Tome, Job.DNC, "right ring")
) )
) )
lazy val bis2: BiS = BiS(
Seq(
Weapon(pieceType = PieceType.Savage ,Job.DNC),
Head(pieceType = PieceType.Tome, Job.DNC),
Body(pieceType = PieceType.Savage, Job.DNC),
Hands(pieceType = PieceType.Tome, Job.DNC),
Legs(pieceType = PieceType.Savage, Job.DNC),
Feet(pieceType = PieceType.Tome, Job.DNC),
Ears(pieceType = PieceType.Savage, Job.DNC),
Neck(pieceType = PieceType.Savage, Job.DNC),
Wrist(pieceType = PieceType.Savage, Job.DNC),
Ring(pieceType = PieceType.Tome, Job.DNC, "left ring"),
Ring(pieceType = PieceType.Savage, Job.DNC, "right ring")
)
)
lazy val bis3: BiS = BiS(
Seq(
Weapon(pieceType = PieceType.Savage ,Job.SGE),
Head(pieceType = PieceType.Tome, Job.SGE),
Body(pieceType = PieceType.Savage, Job.SGE),
Hands(pieceType = PieceType.Tome, Job.SGE),
Legs(pieceType = PieceType.Tome, Job.SGE),
Feet(pieceType = PieceType.Savage, Job.SGE),
Ears(pieceType = PieceType.Savage, Job.SGE),
Neck(pieceType = PieceType.Tome, Job.SGE),
Wrist(pieceType = PieceType.Savage, Job.SGE),
Ring(pieceType = PieceType.Savage, Job.SGE, "left ring"),
Ring(pieceType = PieceType.Tome, Job.SGE, "right ring")
)
)
lazy val link: String = "https://ffxiv.ariyala.com/19V5R" lazy val link: String = "https://ffxiv.ariyala.com/19V5R"
lazy val link2: String = "https://ffxiv.ariyala.com/1A0WM" lazy val link2: String = "https://ffxiv.ariyala.com/1A0WM"
lazy val link3: String = "https://etro.gg/gearset/26a67536-b4ce-4adc-a46a-f70e348bb138" lazy val link3: String = "https://etro.gg/gearset/26a67536-b4ce-4adc-a46a-f70e348bb138"
lazy val link4: String = "https://etro.gg/gearset/865fc886-994f-4c28-8fc1-4379f160a916"
lazy val link5: String = "https://ffxiv.ariyala.com/1FGU0"
lazy val lootWeapon: Piece = Weapon(pieceType = PieceType.Tome, Job.AnyJob) lazy val lootWeapon: Piece = Weapon(pieceType = PieceType.Tome, Job.AnyJob)
lazy val lootBody: Piece = Body(pieceType = PieceType.Savage, Job.AnyJob) lazy val lootBody: Piece = Body(pieceType = PieceType.Savage, Job.AnyJob)
lazy val lootBodyCrafted: Piece = Body(pieceType = PieceType.Crafted, Job.AnyJob) lazy val lootBodyCrafted: Piece = Body(pieceType = PieceType.Crafted, Job.AnyJob)
lazy val lootHands: Piece = Hands(pieceType = PieceType.Tome, Job.AnyJob) lazy val lootHands: Piece = Hands(pieceType = PieceType.Tome, Job.AnyJob)
lazy val lootWaist: Piece = Waist(pieceType = PieceType.Tome, Job.AnyJob)
lazy val lootLegs: Piece = Legs(pieceType = PieceType.Savage, Job.AnyJob) lazy val lootLegs: Piece = Legs(pieceType = PieceType.Savage, Job.AnyJob)
lazy val lootEars: Piece = Ears(pieceType = PieceType.Savage, Job.AnyJob) lazy val lootEars: Piece = Ears(pieceType = PieceType.Savage, Job.AnyJob)
lazy val lootRing: Piece = Ring(pieceType = PieceType.Tome, Job.AnyJob) lazy val lootRing: Piece = Ring(pieceType = PieceType.Tome, Job.AnyJob)

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
@ -39,17 +40,18 @@ class BiSEndpointTest extends AnyWordSpecLike with Matchers with ScalatestRouteT
private val party = testKit.spawn(PartyService(storage)) private val party = testKit.spawn(PartyService(storage))
private val route = new BiSEndpoint(party, provider)(askTimeout, testKit.scheduler).route private val route = new BiSEndpoint(party, provider)(askTimeout, testKit.scheduler).route
override def beforeAll: Unit = { override def beforeAll(): Unit = {
Await.result(Migration(testConfig), askTimeout) super.beforeAll()
Migration(testConfig)
Await.result(storage.ask(AddUser(Fixtures.userAdmin, isHashedPassword = true, _))(askTimeout, testKit.scheduler), askTimeout) Await.result(storage.ask(AddUser(Fixtures.userAdmin, isHashedPassword = true, _))(askTimeout, testKit.scheduler), askTimeout)
Await.result(storage.ask(AddPlayer(Fixtures.playerEmpty, _))(askTimeout, testKit.scheduler), askTimeout) Await.result(storage.ask(AddPlayer(Fixtures.playerEmpty, _))(askTimeout, testKit.scheduler), askTimeout)
} }
override def afterAll: Unit = { override def afterAll(): Unit = {
super.afterAll()
Settings.clearDatabase(testConfig) Settings.clearDatabase(testConfig)
TestKit.shutdownActorSystem(system) TestKit.shutdownActorSystem(system)
testKit.shutdownTestKit() testKit.shutdownTestKit()
super.afterAll()
} }
private def compareBiSResponse(actual: PlayerResponse, expected: PlayerResponse): Unit = { private def compareBiSResponse(actual: PlayerResponse, expected: PlayerResponse): Unit = {

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
@ -37,17 +38,18 @@ class LootEndpointTest extends AnyWordSpecLike with Matchers with ScalatestRoute
private val party = testKit.spawn(PartyService(storage)) private val party = testKit.spawn(PartyService(storage))
private val route = new LootEndpoint(party)(askTimeout, testKit.scheduler).route private val route = new LootEndpoint(party)(askTimeout, testKit.scheduler).route
override def beforeAll: Unit = { override def beforeAll(): Unit = {
Await.result(Migration(testConfig), askTimeout) super.beforeAll()
Migration(testConfig)
Await.result(storage.ask(AddUser(Fixtures.userAdmin, isHashedPassword = true, _))(askTimeout, testKit.scheduler), askTimeout) Await.result(storage.ask(AddUser(Fixtures.userAdmin, isHashedPassword = true, _))(askTimeout, testKit.scheduler), askTimeout)
Await.result(storage.ask(AddPlayer(Fixtures.playerEmpty, _))(askTimeout, testKit.scheduler), askTimeout) Await.result(storage.ask(AddPlayer(Fixtures.playerEmpty, _))(askTimeout, testKit.scheduler), askTimeout)
} }
override def afterAll: Unit = { override def afterAll(): Unit = {
super.afterAll()
Settings.clearDatabase(testConfig) Settings.clearDatabase(testConfig)
TestKit.shutdownActorSystem(system) TestKit.shutdownActorSystem(system)
testKit.shutdownTestKit() testKit.shutdownTestKit()
super.afterAll()
} }
"api v1 loot endpoint" must { "api v1 loot endpoint" must {

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
@ -37,16 +38,17 @@ class PartyEndpointTest extends AnyWordSpecLike with Matchers with ScalatestRout
private val party = testKit.spawn(PartyService(storage)) private val party = testKit.spawn(PartyService(storage))
private val route = new PartyEndpoint(party, provider)(askTimeout, testKit.scheduler).route private val route = new PartyEndpoint(party, provider)(askTimeout, testKit.scheduler).route
override def beforeAll: Unit = { override def beforeAll(): Unit = {
Await.result(Migration(testConfig), askTimeout) super.beforeAll()
Migration(testConfig)
Await.result(storage.ask(AddUser(Fixtures.userAdmin, isHashedPassword = true, _))(askTimeout, testKit.scheduler), askTimeout) Await.result(storage.ask(AddUser(Fixtures.userAdmin, isHashedPassword = true, _))(askTimeout, testKit.scheduler), askTimeout)
} }
override def afterAll: Unit = { override def afterAll(): Unit = {
super.afterAll()
Settings.clearDatabase(testConfig) Settings.clearDatabase(testConfig)
TestKit.shutdownActorSystem(system) TestKit.shutdownActorSystem(system)
testKit.shutdownTestKit() testKit.shutdownTestKit()
super.afterAll()
} }
"api v1 party endpoint" must { "api v1 party endpoint" must {

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
@ -36,17 +37,18 @@ class PlayerEndpointTest extends AnyWordSpecLike with Matchers with ScalatestRou
private val party = testKit.spawn(PartyService(storage)) private val party = testKit.spawn(PartyService(storage))
private val route = new PlayerEndpoint(party, provider)(askTimeout, testKit.scheduler).route private val route = new PlayerEndpoint(party, provider)(askTimeout, testKit.scheduler).route
override def beforeAll: Unit = { override def beforeAll(): Unit = {
Await.result(Migration(testConfig), askTimeout) super.beforeAll()
Migration(testConfig)
Await.result(storage.ask(AddUser(Fixtures.userAdmin, isHashedPassword = true, _))(askTimeout, testKit.scheduler), askTimeout) Await.result(storage.ask(AddUser(Fixtures.userAdmin, isHashedPassword = true, _))(askTimeout, testKit.scheduler), askTimeout)
Await.result(storage.ask(AddPlayer(Fixtures.playerEmpty, _))(askTimeout, testKit.scheduler), askTimeout) Await.result(storage.ask(AddPlayer(Fixtures.playerEmpty, _))(askTimeout, testKit.scheduler), askTimeout)
} }
override def afterAll: Unit = { override def afterAll(): Unit = {
super.afterAll()
Settings.clearDatabase(testConfig) Settings.clearDatabase(testConfig)
TestKit.shutdownActorSystem(system) TestKit.shutdownActorSystem(system)
testKit.shutdownTestKit() testKit.shutdownTestKit()
super.afterAll()
} }
"api v1 player endpoint" must { "api v1 player endpoint" must {

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
@ -33,15 +33,16 @@ class UserEndpointTest extends AnyWordSpecLike with Matchers with ScalatestRoute
private val party = testKit.spawn(PartyService(storage)) private val party = testKit.spawn(PartyService(storage))
private val route = new UserEndpoint(party)(askTimeout, testKit.scheduler).route private val route = new UserEndpoint(party)(askTimeout, testKit.scheduler).route
override def beforeAll: Unit = { override def beforeAll(): Unit = {
Await.result(Migration(testConfig), askTimeout) super.beforeAll()
Migration(testConfig)
} }
override def afterAll: Unit = { override def afterAll(): Unit = {
super.afterAll()
Settings.clearDatabase(testConfig) Settings.clearDatabase(testConfig)
TestKit.shutdownActorSystem(system) TestKit.shutdownActorSystem(system)
testKit.shutdownTestKit() testKit.shutdownTestKit()
super.afterAll()
} }
"api v1 users endpoint" must { "api v1 users endpoint" must {

View File

@ -46,7 +46,7 @@ class BiSTest extends AnyWordSpecLike with Matchers {
} }
"return upgrade list" in { "return upgrade list" in {
Compare.mapEquals(Fixtures.bis.upgrades, Map[PieceUpgrade, Int](BodyUpgrade -> 2, AccessoryUpgrade -> 4)) shouldEqual true Compare.mapEquals(Fixtures.bis.upgrades, Map[PieceUpgrade, Int](BodyUpgrade -> 2, AccessoryUpgrade -> 3)) shouldEqual true
} }
} }

View File

@ -12,7 +12,6 @@ class PieceTest extends AnyWordSpecLike with Matchers {
Fixtures.lootWeapon.upgrade shouldEqual Some(WeaponUpgrade) Fixtures.lootWeapon.upgrade shouldEqual Some(WeaponUpgrade)
Fixtures.lootBody.upgrade shouldEqual None Fixtures.lootBody.upgrade shouldEqual None
Fixtures.lootHands.upgrade shouldEqual Some(BodyUpgrade) Fixtures.lootHands.upgrade shouldEqual Some(BodyUpgrade)
Fixtures.lootWaist.upgrade shouldEqual Some(AccessoryUpgrade)
Fixtures.lootLegs.upgrade shouldEqual None Fixtures.lootLegs.upgrade shouldEqual None
Fixtures.lootEars.upgrade shouldEqual None Fixtures.lootEars.upgrade shouldEqual None
Fixtures.lootLeftRing.upgrade shouldEqual Some(AccessoryUpgrade) Fixtures.lootLeftRing.upgrade shouldEqual Some(AccessoryUpgrade)

View File

@ -24,6 +24,7 @@ class LootSelectorTest extends AnyWordSpecLike with Matchers with BeforeAndAfter
private val timeout: FiniteDuration = 60 seconds private val timeout: FiniteDuration = 60 seconds
override def beforeAll(): Unit = { override def beforeAll(): Unit = {
super.beforeAll()
val testKit = ActorTestKit(Settings.withRandomDatabase) val testKit = ActorTestKit(Settings.withRandomDatabase)
val provider = testKit.spawn(BisProvider()) val provider = testKit.spawn(BisProvider())

View File

@ -23,11 +23,23 @@ class BisProviderTest extends ScalaTestWithActorTestKit(Settings.withRandomDatab
probe.expectMessage(askTimeout, Fixtures.bis) probe.expectMessage(askTimeout, Fixtures.bis)
} }
"get best in slot set (ariyala 2)" in {
val probe = testKit.createTestProbe[BiS]()
provider ! DownloadBiS(Fixtures.link5, Job.SGE, probe.ref)
probe.expectMessage(askTimeout, Fixtures.bis3)
}
"get best in slot set (etro)" in { "get best in slot set (etro)" in {
val probe = testKit.createTestProbe[BiS]() val probe = testKit.createTestProbe[BiS]()
provider ! DownloadBiS(Fixtures.link3, Job.DNC, probe.ref) provider ! DownloadBiS(Fixtures.link3, Job.DNC, probe.ref)
probe.expectMessage(askTimeout, Fixtures.bis) probe.expectMessage(askTimeout, Fixtures.bis)
} }
"get best in slot set (etro 2)" in {
val probe = testKit.createTestProbe[BiS]()
provider ! DownloadBiS(Fixtures.link4, Job.DNC, probe.ref)
probe.expectMessage(askTimeout, Fixtures.bis2)
}
} }
} }

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
@ -19,14 +19,15 @@ class DatabaseBiSHandlerTest extends ScalaTestWithActorTestKit(Settings.withRand
private val database = testKit.spawn(Database()) private val database = testKit.spawn(Database())
private val askTimeout: FiniteDuration = 60 seconds private val askTimeout: FiniteDuration = 60 seconds
override def beforeAll: Unit = { override def beforeAll(): Unit = {
Await.result(Migration(testKit.system.settings.config), askTimeout) super.beforeAll()
Migration(testKit.system.settings.config)
Await.result(database.ask(AddPlayer(Fixtures.playerEmpty, _))(askTimeout, testKit.scheduler), askTimeout) Await.result(database.ask(AddPlayer(Fixtures.playerEmpty, _))(askTimeout, testKit.scheduler), askTimeout)
} }
override def afterAll: Unit = { override def afterAll(): Unit = {
super.afterAll()
Settings.clearDatabase(testKit.system.settings.config) Settings.clearDatabase(testKit.system.settings.config)
super.afterAll()
} }
"database bis handler" must { "database bis handler" must {

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
@ -19,14 +19,15 @@ class DatabaseLootHandlerTest extends ScalaTestWithActorTestKit(Settings.withRan
private val database = testKit.spawn(Database()) private val database = testKit.spawn(Database())
private val askTimeout = 60 seconds private val askTimeout = 60 seconds
override def beforeAll: Unit = { override def beforeAll(): Unit = {
Await.result(Migration(testKit.system.settings.config), askTimeout) super.beforeAll()
Migration(testKit.system.settings.config)
Await.result(database.ask(AddPlayer(Fixtures.playerEmpty, _))(askTimeout, testKit.scheduler), askTimeout) Await.result(database.ask(AddPlayer(Fixtures.playerEmpty, _))(askTimeout, testKit.scheduler), askTimeout)
} }
override def afterAll: Unit = { override def afterAll(): Unit = {
super.afterAll()
Settings.clearDatabase(testKit.system.settings.config) Settings.clearDatabase(testKit.system.settings.config)
super.afterAll()
} }
"database loot handler actor" must { "database loot handler actor" must {

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
@ -18,13 +17,14 @@ class DatabasePartyHandlerTest extends ScalaTestWithActorTestKit(Settings.withRa
private val database = testKit.spawn(Database()) private val database = testKit.spawn(Database())
private val askTimeout = 60 seconds private val askTimeout = 60 seconds
override def beforeAll: Unit = { override def beforeAll(): Unit = {
Await.result(Migration(testKit.system.settings.config), askTimeout) super.beforeAll()
Migration(testKit.system.settings.config)
} }
override def afterAll: Unit = { override def afterAll(): Unit = {
super.afterAll()
Settings.clearDatabase(testKit.system.settings.config) Settings.clearDatabase(testKit.system.settings.config)
super.afterAll()
} }
"database party handler actor" must { "database party handler actor" must {

Some files were not shown because too many files have changed in this diff Show More