1 Commits

Author SHA1 Message Date
abe3818ead fix assembly build 2019-11-02 15:24:26 +03:00
123 changed files with 1187 additions and 2545 deletions

View File

@ -1,9 +1,9 @@
language: scala sudo: required
scala: language: generic
- 2.13.1
sbt_args: -no-colors services:
- docker
script: script:
- sbt compile - docker run -it --rm -v "$(pwd):/opt/build" -w /opt/build mozilla/sbt sbt compile
- sbt test - docker run -it --rm -v "$(pwd):/opt/build" -w /opt/build mozilla/sbt sbt test

View File

@ -1,31 +1,23 @@
# 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)
Service which allows to manage savage loot distribution easy. Service which allows to manage savage loot distribution easy.
## Installation and usage ## Installation and usage
In general compilation process looks like: In general installation process looks like:
```bash ```bash
sbt dist sbt assembly
``` ```
Or alternatively you can download latest distribution zip from the releases page. Service can be run by using command: Service can be run by using command:
```bash ```bash
bin/ffxivbis java -cp ./target/scala-2.13/ffxivbis-scala-assembly-0.1.jar me.arcanis.ffxivbis.ffxivbis
``` ```
from the extracted archive root.
## Web service ## Web service
REST API documentation is available at `http://0.0.0.0:8000/swagger`. HTML representation is available at `http://0.0.0.0:8000`. REST API documentation is available at `http://0.0.0.0:8000/swagger`. HTML representation is available at `http://0.0.0.0:8000`.
*Note*: host and port depend on configuration settings. *Note*: host and port depend on configuration settings.
## Public service
There is also public service which is available at http://ffxivbis.arcanis.me.

View File

@ -4,8 +4,6 @@ scalaVersion := "2.13.1"
scalacOptions ++= Seq("-deprecation", "-feature") scalacOptions ++= Seq("-deprecation", "-feature")
enablePlugins(JavaAppPackaging)
assemblyMergeStrategy in assembly := { assemblyMergeStrategy in assembly := {
case PathList("META-INF", xs @ _*) => MergeStrategy.discard case PathList("META-INF", xs @ _*) => MergeStrategy.discard
case "application.conf" => MergeStrategy.concat case "application.conf" => MergeStrategy.concat

View File

@ -1,33 +1,19 @@
val AkkaVersion = "2.6.10"
val AkkaHttpVersion = "10.2.1"
val SlickVersion = "3.3.3"
libraryDependencies += "ch.qos.logback" % "logback-classic" % "1.2.3" libraryDependencies += "ch.qos.logback" % "logback-classic" % "1.2.3"
libraryDependencies += "com.typesafe.scala-logging" %% "scala-logging" % "3.9.2" libraryDependencies += "com.typesafe.scala-logging" %% "scala-logging" % "3.9.2"
libraryDependencies += "com.typesafe.akka" %% "akka-http" % AkkaHttpVersion libraryDependencies += "com.typesafe.akka" %% "akka-http" % "10.1.10"
libraryDependencies += "com.typesafe.akka" %% "akka-http-spray-json" % AkkaHttpVersion libraryDependencies += "com.typesafe.akka" %% "akka-http-spray-json" % "10.1.10"
libraryDependencies += "com.typesafe.akka" %% "akka-actor-typed" % AkkaVersion libraryDependencies += "com.typesafe.akka" %% "akka-stream" % "2.5.23"
libraryDependencies += "com.typesafe.akka" %% "akka-stream" % AkkaVersion libraryDependencies += "com.github.swagger-akka-http" %% "swagger-akka-http" % "2.0.4"
libraryDependencies += "com.github.swagger-akka-http" %% "swagger-akka-http" % "2.3.0"
libraryDependencies += "javax.ws.rs" % "javax.ws.rs-api" % "2.1.1" libraryDependencies += "javax.ws.rs" % "javax.ws.rs-api" % "2.1.1"
libraryDependencies += "io.spray" %% "spray-json" % "1.3.6" libraryDependencies += "io.spray" %% "spray-json" % "1.3.5"
libraryDependencies += "com.lihaoyi" %% "scalatags" % "0.9.2" libraryDependencies += "com.lihaoyi" %% "scalatags" % "0.7.0"
libraryDependencies += "com.typesafe.slick" %% "slick" % SlickVersion libraryDependencies += "com.typesafe.slick" %% "slick" % "3.3.2"
libraryDependencies += "com.typesafe.slick" %% "slick-hikaricp" % SlickVersion libraryDependencies += "com.typesafe.slick" %% "slick-hikaricp" % "3.3.2"
libraryDependencies += "org.flywaydb" % "flyway-core" % "6.0.6" libraryDependencies += "org.flywaydb" % "flyway-core" % "6.0.6"
libraryDependencies += "org.xerial" % "sqlite-jdbc" % "3.32.3.2" libraryDependencies += "org.xerial" % "sqlite-jdbc" % "3.28.0"
libraryDependencies += "org.postgresql" % "postgresql" % "42.2.18" libraryDependencies += "org.postgresql" % "postgresql" % "9.3-1104-jdbc4"
libraryDependencies += "org.mindrot" % "jbcrypt" % "0.3m" libraryDependencies += "org.mindrot" % "jbcrypt" % "0.3m"
// testing
libraryDependencies += "org.scalactic" %% "scalactic" % "3.1.4" % "test"
libraryDependencies += "org.scalatest" %% "scalatest" % "3.1.4" % "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-http-testkit" % AkkaHttpVersion % "test"

1
project/assembly.sbt Normal file
View File

@ -0,0 +1 @@
addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.14.6")

1
project/dependency.sbt Normal file
View File

@ -0,0 +1 @@
addSbtPlugin("net.virtual-void" % "sbt-dependency-graph" % "0.10.0-RC1")

View File

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

View File

@ -1,6 +1,6 @@
create table players ( create table players (
party_id text not null, party_id text not null,
player_id bigserial unique, player_id bigserial,
created bigint not null, created bigint not null,
nick text not null, nick text not null,
job text not null, job text not null,
@ -9,7 +9,7 @@ create table players (
create unique index players_nick_job_idx on players(party_id, nick, job); create unique index players_nick_job_idx on players(party_id, nick, job);
create table loot ( create table loot (
loot_id bigserial unique, loot_id bigserial,
player_id bigint not null, player_id bigint not null,
created bigint not null, created bigint not null,
piece text not null, piece text not null,
@ -29,7 +29,7 @@ create unique index bis_piece_player_id_idx on bis(player_id, piece);
create table users ( create table users (
party_id text not null, party_id text not null,
user_id bigserial unique, user_id bigserial,
username text not null, username text not null,
password text not null, password text not null,
permission text not null); permission text not null);

View File

@ -1,5 +0,0 @@
update loot set piece = 'left ring' where piece = 'leftRing';
update loot set piece = 'right ring' where piece = 'rightRing';
update bis set piece = 'left ring' where piece = 'leftRing';
update bis set piece = 'right ring' where piece = 'rightRing';

View File

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

View File

@ -1,17 +0,0 @@
-- loot
alter table loot add column piece_type text;
update loot set piece_type = 'Tome' where is_tome = 1;
update loot set piece_type = 'Savage' where is_tome = 0;
alter table loot alter column piece_type set not null;
alter table loot drop column is_tome;
-- bis
alter table bis add column piece_type text;
update bis set piece_type = 'Tome' where is_tome = 1;
update bis set piece_type = 'Savage' where is_tome = 0;
alter table bis alter column piece_type set not null;
alter table bis drop column is_tome;

View File

@ -1 +0,0 @@
alter table loot add column is_free_loot integer not null default 0;

View File

@ -1,2 +0,0 @@
drop index bis_piece_player_id_idx;
create index bis_piece_type_player_id_idx on bis(player_id, piece, piece_type);

View File

@ -1 +0,0 @@
alter table parties rename column player_id to party_id;

View File

@ -1,5 +0,0 @@
update loot set piece = 'left ring' where piece = 'leftRing';
update loot set piece = 'right ring' where piece = 'rightRing';
update bis set piece = 'left ring' where piece = 'leftRing';
update bis set piece = 'right ring' where piece = 'rightRing';

View File

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

View File

@ -1,42 +0,0 @@
-- loot
alter table loot add column piece_type text;
update loot set piece_type = 'Tome' where is_tome = 1;
update loot set piece_type = 'Savage' where is_tome = 0;
create table loot_new (
loot_id integer primary key autoincrement,
player_id integer not null,
created integer not null,
piece text not null,
piece_type text not null,
job text not null,
foreign key (player_id) references players(player_id) on delete cascade);
insert into loot_new select loot_id, player_id, created, piece, piece_type, job from loot;
drop index loot_owner_idx;
drop table loot;
alter table loot_new rename to loot;
create index loot_owner_idx on loot(player_id);
-- bis
alter table bis add column piece_type text;
update bis set piece_type = 'Tome' where is_tome = 1;
update bis set piece_type = 'Savage' where is_tome = 0;
create table bis_new (
player_id integer not null,
created integer not null,
piece text not null,
piece_type text not null,
job text not null,
foreign key (player_id) references players(player_id) on delete cascade);
insert into bis_new select player_id, created, piece, piece_type, job from bis;
drop index bis_piece_player_id_idx;
drop table bis;
alter table bis_new rename to bis;
create unique index bis_piece_player_id_idx on bis(player_id, piece);

View File

@ -1,20 +0,0 @@
alter table loot add column is_free_loot integer;
update loot set is_free_loot = 0;
create table loot_new (
loot_id integer primary key autoincrement,
player_id integer not null,
created integer not null,
piece text not null,
piece_type text not null,
job text not null,
is_free_loot integer not null,
foreign key (player_id) references players(player_id) on delete cascade);
insert into loot_new select loot_id, player_id, created, piece, piece_type, job, is_free_loot from loot;
drop index loot_owner_idx;
drop table loot;
alter table loot_new rename to loot;
create index loot_owner_idx on loot(player_id);

View File

@ -1,2 +0,0 @@
drop index bis_piece_player_id_idx;
create index bis_piece_type_player_id_idx on bis(player_id, piece, piece_type);

View File

@ -1,11 +0,0 @@
create table parties_new (
party_id integer primary key autoincrement,
party_name text not null,
party_alias text);
insert into parties_new select player_id, party_name, party_alias from parties;
drop index parties_party_name_idx;
drop table parties;
alter table parties_new rename to parties;
create unique index parties_party_name_idx on parties(party_name);

View File

@ -1,9 +1,11 @@
me.arcanis.ffxivbis { me.arcanis.ffxivbis {
bis-provider { ariyala {
# ariyala base url, string, required
ariyala-url = "https://ffxiv.ariyala.com"
# 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
#xivapi-key = "abcdef" # xivapi-key = "abcdef"
} }
database { database {
@ -25,11 +27,8 @@ me.arcanis.ffxivbis {
profile = "slick.jdbc.PostgresProfile$" profile = "slick.jdbc.PostgresProfile$"
db { db {
url = "jdbc:postgresql://localhost/ffxivbis" url = "jdbc:postgresql://localhost/ffxivbis"
user = "ffxivbis" user = "user"
password = "ffxivbis" password = "password"
connectionPool = disabled
keepAliveConnection = yes
} }
numThreads = 10 numThreads = 10
} }
@ -49,19 +48,8 @@ me.arcanis.ffxivbis {
web { web {
# address to bind, string, required # address to bind, string, required
host = "127.0.0.1" host = "0.0.0.0"
# port to bind, int, required # port to bind, int, required
port = 8000 port = 8000
# hostname to use in docs, if not set host:port will be used
#hostname = "127.0.0.1:8000"
}
default-dispatcher {
type = Dispatcher
executor = "thread-pool-executor"
thread-pool-executor {
fixed-pool-size = 16
}
throughput = 1
} }
} }

View File

@ -1,24 +0,0 @@
REST json API description to interact with FFXIVBiS service.
# Basic workflow
* Create party using `PUT /api/v1/party` endpoint. It consumes username and password of administrator (which can't be restored). As the result it returns unique id of created party.
* Create new users which have access to this party. Note that user belongs to specific party id and in scope of the specified party it must be unique.
* Add players with their best in slot sets (probably by using ariyala links).
* Add loot items if any.
* By using `PUT /api/v1/party/{partyId}/loot` API find players which are better for the specified loot.
* Add new loot item to the selected player.
# Limitations
# Authentication
For the most party utils service requires user to be authenticated. User permission can be one of `get`, `post` or `admin`.
* `admin` permission means that the user is allowed to do anything, especially this permission is required to be able to add or modify users.
* `post` permission is required to deal with the most POST API endpoints, but to be precise only endpoints which modifies party content require this permission.
* `get` permission is required to have access to party.
`admin` permission includes any other permissions, `post` allows to perform get requests.
<security-definitions />

View File

@ -8,53 +8,45 @@
*/ */
package me.arcanis.ffxivbis package me.arcanis.ffxivbis
import akka.actor.typed.{Behavior, PostStop, Signal} import akka.actor.{Actor, Props}
import akka.actor.typed.scaladsl.{AbstractBehavior, ActorContext, Behaviors}
import akka.http.scaladsl.Http import akka.http.scaladsl.Http
import akka.stream.Materializer import akka.stream.ActorMaterializer
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.{Ariyala, PartyService}
import me.arcanis.ffxivbis.service.{Database, PartyService} import me.arcanis.ffxivbis.service.impl.DatabaseImpl
import me.arcanis.ffxivbis.storage.Migration import me.arcanis.ffxivbis.storage.Migration
import scala.concurrent.ExecutionContext import scala.concurrent.{Await, ExecutionContext}
import scala.concurrent.duration.Duration
import scala.util.{Failure, Success}
class Application(context: ActorContext[Nothing]) class Application extends Actor with StrictLogging {
extends AbstractBehavior[Nothing](context) with StrictLogging { implicit private val executionContext: ExecutionContext = context.system.dispatcher
implicit private val materializer: ActorMaterializer = ActorMaterializer()
logger.info("root supervisor started") private val config = context.system.settings.config
startApplication() private val host = config.getString("me.arcanis.ffxivbis.web.host")
private val port = config.getInt("me.arcanis.ffxivbis.web.port")
override def onMessage(msg: Nothing): Behavior[Nothing] = Behaviors.unhandled override def receive: Receive = Actor.emptyBehavior
override def onSignal: PartialFunction[Signal, Behavior[Nothing]] = { Migration(config).onComplete {
case PostStop => case Success(_) =>
logger.info("root supervisor stopped") val ariyala = context.system.actorOf(Ariyala.props, "ariyala")
Behaviors.same val storage = context.system.actorOf(DatabaseImpl.props, "storage")
} val party = context.system.actorOf(PartyService.props(storage), "party")
val http = new RootEndpoint(context.system, party, ariyala)
private def startApplication(): Unit = { logger.info(s"start server at $host:$port")
val config = context.system.settings.config val bind = Http()(context.system).bindAndHandle(http.route, host, port)
val host = config.getString("me.arcanis.ffxivbis.web.host") Await.result(context.system.whenTerminated, Duration.Inf)
val port = config.getInt("me.arcanis.ffxivbis.web.port") bind.foreach(_.unbind())
implicit val executionContext: ExecutionContext = context.system.executionContext case Failure(exception) => throw exception
implicit val materializer: Materializer = Materializer(context)
Migration(config)
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)
Http()(context.system).newServerAt(host, port).bindFlow(http.route)
} }
} }
object Application { object Application {
def props: Props = Props(new Application)
def apply(): Behavior[Nothing] =
Behaviors.setup[Nothing](context => new Application(context))
} }

View File

@ -8,13 +8,13 @@
*/ */
package me.arcanis.ffxivbis package me.arcanis.ffxivbis
import akka.actor.typed.ActorSystem import akka.actor.ActorSystem
import com.typesafe.config.ConfigFactory import com.typesafe.config.ConfigFactory
object ffxivbis { object ffxivbis {
def main(args: Array[String]): Unit = { def main(args: Array[String]): Unit = {
val config = ConfigFactory.load() val config = ConfigFactory.load()
ActorSystem[Nothing](Application(), "ffxivbis", config) val actorSystem = ActorSystem("ffxivbis", config)
actorSystem.actorOf(Application.props, "ffxivbis")
} }
} }

View File

@ -0,0 +1,24 @@
/*
* 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.http
import akka.actor.ActorRef
import akka.pattern.ask
import akka.util.Timeout
import me.arcanis.ffxivbis.models.{BiS, Job, Piece}
import me.arcanis.ffxivbis.service.Ariyala
import scala.concurrent.{ExecutionContext, Future}
class AriyalaHelper(ariyala: ActorRef) {
def downloadBiS(link: String, job: Job.Job)
(implicit executionContext: ExecutionContext, timeout: Timeout): Future[BiS] =
(ariyala ? Ariyala.GetBiS(link, job)).mapTo[BiS]
}

View File

@ -8,22 +8,22 @@
*/ */
package me.arcanis.ffxivbis.http package me.arcanis.ffxivbis.http
import akka.actor.typed.scaladsl.AskPattern.Askable import akka.actor.ActorRef
import akka.actor.typed.{ActorRef, Scheduler}
import akka.http.scaladsl.model.headers._ import akka.http.scaladsl.model.headers._
import akka.http.scaladsl.server.AuthenticationFailedRejection._ import akka.http.scaladsl.server.AuthenticationFailedRejection._
import akka.http.scaladsl.server.Directives._
import akka.http.scaladsl.server._ import akka.http.scaladsl.server._
import akka.http.scaladsl.server.Directives._
import akka.pattern.ask
import akka.util.Timeout import akka.util.Timeout
import me.arcanis.ffxivbis.messages.{GetUser, Message} import me.arcanis.ffxivbis.models.{Permission, User}
import me.arcanis.ffxivbis.models.Permission import me.arcanis.ffxivbis.service.impl.DatabaseUserHandler
import scala.concurrent.{ExecutionContext, Future} import scala.concurrent.{ExecutionContext, Future}
// idea comes from https://synkre.com/bcrypt-for-akka-http-password-encryption/ // idea comes from https://synkre.com/bcrypt-for-akka-http-password-encryption/
trait Authorization { trait Authorization {
def storage: ActorRef[Message] def storage: ActorRef
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] = {
@ -39,22 +39,23 @@ trait Authorization {
} }
} }
def authenticator(scope: Permission.Value, partyId: String)(username: String, password: String) def authenticator(scope: Permission.Value)(partyId: String)
(implicit executionContext: ExecutionContext, timeout: Timeout, scheduler: Scheduler): Future[Option[String]] = (username: String, password: String)
storage.ask(GetUser(partyId, username, _)).map { (implicit executionContext: ExecutionContext, timeout: Timeout): Future[Option[String]] =
(storage ? DatabaseUserHandler.GetUser(partyId, username)).mapTo[Option[User]].map {
case Some(user) if user.verify(password) && user.verityScope(scope) => Some(username) 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 executionContext: ExecutionContext, timeout: Timeout, scheduler: Scheduler): Future[Option[String]] = (implicit executionContext: ExecutionContext, timeout: Timeout): Future[Option[String]] =
authenticator(Permission.admin, partyId)(username, password) authenticator(Permission.admin)(partyId)(username, password)
def authGet(partyId: String)(username: String, password: String) def authGet(partyId: String)(username: String, password: String)
(implicit executionContext: ExecutionContext, timeout: Timeout, scheduler: Scheduler): Future[Option[String]] = (implicit executionContext: ExecutionContext, timeout: Timeout): Future[Option[String]] =
authenticator(Permission.get, partyId)(username, password) authenticator(Permission.get)(partyId)(username, password)
def authPost(partyId: String)(username: String, password: String) def authPost(partyId: String)(username: String, password: String)
(implicit executionContext: ExecutionContext, timeout: Timeout, scheduler: Scheduler): Future[Option[String]] = (implicit executionContext: ExecutionContext, timeout: Timeout): Future[Option[String]] =
authenticator(Permission.post, partyId)(username, password) authenticator(Permission.post)(partyId)(username, password)
} }

View File

@ -8,45 +8,40 @@
*/ */
package me.arcanis.ffxivbis.http package me.arcanis.ffxivbis.http
import akka.actor.typed.scaladsl.AskPattern.Askable import akka.actor.ActorRef
import akka.actor.typed.{ActorRef, Scheduler} import akka.pattern.ask
import akka.util.Timeout import akka.util.Timeout
import me.arcanis.ffxivbis.http.api.v1.json.ApiAction import me.arcanis.ffxivbis.http.api.v1.json.ApiAction
import me.arcanis.ffxivbis.messages.{AddPieceToBis, GetBiS, Message, RemovePieceFromBiS, RemovePiecesFromBiS}
import me.arcanis.ffxivbis.models.{Piece, Player, PlayerId} import me.arcanis.ffxivbis.models.{Piece, Player, PlayerId}
import me.arcanis.ffxivbis.service.impl.DatabaseBiSHandler
import scala.concurrent.{ExecutionContext, Future} import scala.concurrent.{ExecutionContext, Future}
trait BiSHelper extends BisProviderHelper { class BiSHelper(storage: ActorRef, ariyala: ActorRef) extends AriyalaHelper(ariyala) {
def storage: ActorRef[Message]
def addPieceBiS(playerId: PlayerId, piece: Piece) def addPieceBiS(playerId: PlayerId, piece: Piece)
(implicit timeout: Timeout, scheduler: Scheduler): Future[Unit] = (implicit executionContext: ExecutionContext, timeout: Timeout): Future[Int] =
storage.ask(AddPieceToBis(playerId, piece.withJob(playerId.job), _)) (storage ? DatabaseBiSHandler.AddPieceToBis(playerId, piece.withJob(playerId.job))).mapTo[Int]
def bis(partyId: String, playerId: Option[PlayerId]) def bis(partyId: String, playerId: Option[PlayerId])
(implicit timeout: Timeout, scheduler: Scheduler): Future[Seq[Player]] = (implicit executionContext: ExecutionContext, timeout: Timeout): Future[Seq[Player]] =
storage.ask(GetBiS(partyId, playerId, _)) (storage ? DatabaseBiSHandler.GetBiS(partyId, playerId)).mapTo[Seq[Player]]
def doModifyBiS(action: ApiAction.Value, playerId: PlayerId, piece: Piece) def doModifyBiS(action: ApiAction.Value, playerId: PlayerId, piece: Piece)
(implicit timeout: Timeout, scheduler: Scheduler): Future[Unit] = (implicit executionContext: ExecutionContext, timeout: Timeout): Future[Int] =
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 executionContext: ExecutionContext, timeout: Timeout, scheduler: Scheduler): Future[Unit] = { (implicit executionContext: ExecutionContext, timeout: Timeout): Future[Unit] =
storage.ask(RemovePiecesFromBiS(playerId, _)).flatMap { _ => downloadBiS(link, playerId.job).flatMap { bis =>
downloadBiS(link, playerId.job).flatMap { bis => Future.traverse(bis.pieces)(addPieceBiS(playerId, _))
Future.traverse(bis.pieces)(addPieceBiS(playerId, _)) }.map(_ => ())
}.map(_ => ())
}
}
def removePieceBiS(playerId: PlayerId, piece: Piece) def removePieceBiS(playerId: PlayerId, piece: Piece)
(implicit timeout: Timeout, scheduler: Scheduler): Future[Unit] = (implicit executionContext: ExecutionContext, timeout: Timeout): Future[Int] =
storage.ask(RemovePieceFromBiS(playerId, piece, _)) (storage ? DatabaseBiSHandler.RemovePieceFromBiS(playerId, piece)).mapTo[Int]
} }

View File

@ -1,26 +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.http
import akka.actor.typed.{ActorRef, Scheduler}
import akka.actor.typed.scaladsl.AskPattern.Askable
import akka.util.Timeout
import me.arcanis.ffxivbis.messages.{BiSProviderMessage, DownloadBiS}
import me.arcanis.ffxivbis.models.{BiS, Job}
import scala.concurrent.Future
trait BisProviderHelper {
def provider: ActorRef[BiSProviderMessage]
def downloadBiS(link: String, job: Job.Job)
(implicit timeout: Timeout, scheduler: Scheduler): Future[BiS] =
provider.ask(DownloadBiS(link, job, _))
}

View File

@ -8,41 +8,38 @@
*/ */
package me.arcanis.ffxivbis.http package me.arcanis.ffxivbis.http
import akka.actor.typed.scaladsl.AskPattern.Askable import akka.actor.ActorRef
import akka.actor.typed.{ActorRef, Scheduler} import akka.pattern.ask
import akka.util.Timeout import akka.util.Timeout
import me.arcanis.ffxivbis.http.api.v1.json.ApiAction import me.arcanis.ffxivbis.http.api.v1.json.ApiAction
import me.arcanis.ffxivbis.messages.{AddPieceTo, GetLoot, Message, RemovePieceFrom, SuggestLoot}
import me.arcanis.ffxivbis.models.{Piece, Player, PlayerId, PlayerIdWithCounters} import me.arcanis.ffxivbis.models.{Piece, Player, PlayerId, PlayerIdWithCounters}
import me.arcanis.ffxivbis.service.LootSelector.LootSelectorResult
import me.arcanis.ffxivbis.service.impl.DatabaseLootHandler
import scala.concurrent.{ExecutionContext, Future} import scala.concurrent.{ExecutionContext, Future}
trait LootHelper { class LootHelper(storage: ActorRef) {
def storage: ActorRef[Message] def addPieceLoot(playerId: PlayerId, piece: Piece)
(implicit executionContext: ExecutionContext, timeout: Timeout): Future[Int] =
(storage ? DatabaseLootHandler.AddPieceTo(playerId, piece)).mapTo[Int]
def addPieceLoot(playerId: PlayerId, piece: Piece, isFreeLoot: Boolean) def doModifyLoot(action: ApiAction.Value, playerId: PlayerId, piece: Piece)
(implicit timeout: Timeout, scheduler: Scheduler): Future[Unit] = (implicit executionContext: ExecutionContext, timeout: Timeout): Future[Int] =
storage.ask( action match {
AddPieceTo(playerId, piece, isFreeLoot, _)) case ApiAction.add => addPieceLoot(playerId, piece)
case ApiAction.remove => removePieceLoot(playerId, piece)
def doModifyLoot(action: ApiAction.Value, playerId: PlayerId, piece: Piece, maybeFree: Option[Boolean])
(implicit timeout: Timeout, scheduler: Scheduler): Future[Unit] =
(action, maybeFree) match {
case (ApiAction.add, Some(isFreeLoot)) => addPieceLoot(playerId, piece, isFreeLoot)
case (ApiAction.remove, _) => removePieceLoot(playerId, piece)
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 timeout: Timeout, scheduler: Scheduler): Future[Seq[Player]] = (implicit executionContext: ExecutionContext, timeout: Timeout): Future[Seq[Player]] =
storage.ask(GetLoot(partyId, playerId, _)) (storage ? DatabaseLootHandler.GetLoot(partyId, playerId)).mapTo[Seq[Player]]
def removePieceLoot(playerId: PlayerId, piece: Piece) def removePieceLoot(playerId: PlayerId, piece: Piece)
(implicit timeout: Timeout, scheduler: Scheduler): Future[Unit] = (implicit executionContext: ExecutionContext, timeout: Timeout): Future[Int] =
storage.ask(RemovePieceFrom(playerId, piece, _)) (storage ? DatabaseLootHandler.RemovePieceFrom(playerId, piece)).mapTo[Int]
def suggestPiece(partyId: String, piece: Piece) def suggestPiece(partyId: String, piece: Piece)
(implicit executionContext: ExecutionContext, timeout: Timeout, scheduler: Scheduler): Future[Seq[PlayerIdWithCounters]] = (implicit executionContext: ExecutionContext, timeout: Timeout): Future[Seq[PlayerIdWithCounters]] =
storage.ask(SuggestLoot(partyId, piece, _)).map(_.result) (storage ? DatabaseLootHandler.SuggestLoot(partyId, piece)).mapTo[LootSelectorResult].map(_.result)
} }

View File

@ -8,56 +8,46 @@
*/ */
package me.arcanis.ffxivbis.http package me.arcanis.ffxivbis.http
import akka.actor.typed.{ActorRef, Scheduler} import akka.actor.ActorRef
import akka.actor.typed.scaladsl.AskPattern.Askable import akka.pattern.ask
import akka.util.Timeout import akka.util.Timeout
import me.arcanis.ffxivbis.http.api.v1.json.ApiAction import me.arcanis.ffxivbis.http.api.v1.json.ApiAction
import me.arcanis.ffxivbis.messages.{AddPieceToBis, AddPlayer, GetParty, GetPartyDescription, GetPlayer, Message, RemovePlayer, UpdateParty} import me.arcanis.ffxivbis.models.{Party, Player, PlayerId}
import me.arcanis.ffxivbis.models.{PartyDescription, Player, PlayerId} import me.arcanis.ffxivbis.service.impl.{DatabaseBiSHandler, DatabasePartyHandler}
import scala.concurrent.{ExecutionContext, Future} import scala.concurrent.{ExecutionContext, Future}
trait PlayerHelper extends BisProviderHelper { class PlayerHelper(storage: ActorRef, ariyala: ActorRef) extends AriyalaHelper(ariyala) {
def storage: ActorRef[Message]
def addPlayer(player: Player) def addPlayer(player: Player)
(implicit executionContext: ExecutionContext, timeout: Timeout, scheduler: Scheduler): Future[Unit] = (implicit executionContext: ExecutionContext, timeout: Timeout): Future[Int] =
storage.ask(ref => AddPlayer(player, ref)).map { res => (storage ? DatabasePartyHandler.AddPlayer(player)).mapTo[Int].map { res =>
player.link match { player.link match {
case Some(link) => case Some(link) =>
downloadBiS(link, player.job).map { bis => downloadBiS(link, player.job).map { bis =>
bis.pieces.map(piece => storage.ask(AddPieceToBis(player.playerId, piece, _))) bis.pieces.map(storage ? DatabaseBiSHandler.AddPieceToBis(player.playerId, _))
}.map(_ => res) }.map(_ => res)
case None => Future.successful(res) case None => Future.successful(res)
} }
}.flatten }.flatten
def doModifyPlayer(action: ApiAction.Value, player: Player) def doModifyPlayer(action: ApiAction.Value, player: Player)
(implicit executionContext: ExecutionContext, timeout: Timeout, scheduler: Scheduler): Future[Unit] = (implicit executionContext: ExecutionContext, timeout: Timeout): Future[Int] =
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)
(implicit timeout: Timeout, scheduler: Scheduler): Future[PartyDescription] =
storage.ask(GetPartyDescription(partyId, _))
def getPlayers(partyId: String, maybePlayerId: Option[PlayerId]) def getPlayers(partyId: String, maybePlayerId: Option[PlayerId])
(implicit executionContext: ExecutionContext, timeout: Timeout, scheduler: Scheduler): Future[Seq[Player]] = (implicit executionContext: ExecutionContext, timeout: Timeout): Future[Seq[Player]] =
maybePlayerId match { maybePlayerId match {
case Some(playerId) => case Some(playerId) =>
storage.ask(GetPlayer(playerId, _)).map(_.toSeq) (storage ? DatabasePartyHandler.GetPlayer(playerId)).mapTo[Option[Player]].map(_.toSeq)
case None => case None =>
storage.ask(GetParty(partyId, _)).map(_.players.values.toSeq) (storage ? DatabasePartyHandler.GetParty(partyId)).mapTo[Party].map(_.players.values.toSeq)
} }
def removePlayer(playerId: PlayerId) def removePlayer(playerId: PlayerId)
(implicit timeout: Timeout, scheduler: Scheduler): Future[Unit] = (implicit executionContext: ExecutionContext, timeout: Timeout): Future[Int] =
storage.ask(RemovePlayer(playerId, _)) (storage ? DatabasePartyHandler.RemovePlayer(playerId)).mapTo[Int]
def updateDescription(partyDescription: PartyDescription)
(implicit executionContext: ExecutionContext, timeout: Timeout, scheduler: Scheduler): Future[Unit] =
storage.ask(UpdateParty(partyDescription, _))
} }

View File

@ -10,30 +10,25 @@ package me.arcanis.ffxivbis.http
import java.time.Instant import java.time.Instant
import akka.actor.typed.{ActorRef, ActorSystem, Scheduler} import akka.actor.{ActorRef, ActorSystem}
import akka.http.scaladsl.server.Directives._ import akka.http.scaladsl.server.Directives._
import akka.http.scaladsl.server._ import akka.http.scaladsl.server._
import akka.util.Timeout import akka.util.Timeout
import com.typesafe.scalalogging.{Logger, StrictLogging} import com.typesafe.scalalogging.{Logger, StrictLogging}
import me.arcanis.ffxivbis.http.api.v1.RootApiV1Endpoint 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}
class RootEndpoint(system: ActorSystem[Nothing], class RootEndpoint(system: ActorSystem, storage: ActorRef, ariyala: ActorRef)
storage: ActorRef[Message],
provider: ActorRef[BiSProviderMessage])
extends StrictLogging { extends StrictLogging {
import me.arcanis.ffxivbis.utils.Implicits._ import me.arcanis.ffxivbis.utils.Implicits._
private val config = system.settings.config private val config = system.settings.config
implicit val scheduler: Scheduler = system.scheduler
implicit val timeout: Timeout = implicit val timeout: Timeout =
config.getDuration("me.arcanis.ffxivbis.settings.request-timeout") config.getDuration("me.arcanis.ffxivbis.settings.request-timeout")
private val rootApiV1Endpoint: RootApiV1Endpoint = new RootApiV1Endpoint(storage, provider, config) private val rootApiV1Endpoint: RootApiV1Endpoint = new RootApiV1Endpoint(storage, ariyala)
private val rootView: RootView = new RootView(storage, provider) private val rootView: RootView = new RootView(storage, ariyala)
private val swagger: Swagger = new Swagger(config)
private val httpLogger = Logger("http") private val httpLogger = Logger("http")
private val withHttpLog: Directive0 = private val withHttpLog: Directive0 =
@ -48,7 +43,7 @@ class RootEndpoint(system: ActorSystem[Nothing],
def route: Route = def route: Route =
withHttpLog { withHttpLog {
apiRoute ~ htmlRoute ~ swagger.routes ~ swaggerUIRoute apiRoute ~ htmlRoute ~ Swagger.routes ~ swaggerUIRoute
} }
private def apiRoute: Route = private def apiRoute: Route =

View File

@ -9,37 +9,23 @@
package me.arcanis.ffxivbis.http package me.arcanis.ffxivbis.http
import com.github.swagger.akka.SwaggerHttpService import com.github.swagger.akka.SwaggerHttpService
import com.github.swagger.akka.model.{Info, License} import com.github.swagger.akka.model.Info
import com.typesafe.config.Config
import io.swagger.v3.oas.models.security.SecurityScheme import io.swagger.v3.oas.models.security.SecurityScheme
import scala.io.Source object Swagger 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.LootEndpoint],
classOf[api.v1.PartyEndpoint], classOf[api.v1.PlayerEndpoint], classOf[api.v1.PlayerEndpoint], classOf[api.v1.UserEndpoint]
classOf[api.v1.TypesEndpoint], classOf[api.v1.UserEndpoint]
) )
override val info: Info = Info( override val info: Info = Info()
description = Source.fromResource("swagger-info/description.md").mkString,
version = getClass.getPackage.getImplementationVersion,
title = "FFXIV static loot tracker",
license = Some(License("BSD", "https://raw.githubusercontent.com/arcan1s/ffxivbis/master/LICENSE"))
)
override val host: String =
if (config.hasPath("me.arcanis.ffxivbis.web.hostname")) config.getString("me.arcanis.ffxivbis.web.hostname")
else s"${config.getString("me.arcanis.ffxivbis.web.host")}:${config.getString("me.arcanis.ffxivbis.web.port")}"
private val basicAuth = new SecurityScheme() private val basicAuth = new SecurityScheme()
.description("basic http auth") .description("basic http auth")
.`type`(SecurityScheme.Type.HTTP) .`type`(SecurityScheme.Type.HTTP)
.in(SecurityScheme.In.HEADER) .in(SecurityScheme.In.HEADER)
.scheme("bearer") .scheme("bearer")
override val securitySchemes: Map[String, SecurityScheme] = Map("basic auth" -> basicAuth) override def securitySchemes: Map[String, SecurityScheme] = Map("basic auth" -> basicAuth)
override val unwantedDefinitions: Seq[String] = override val unwantedDefinitions: Seq[String] =
Seq("Function1", "Function1RequestContextFutureRouteResult") Seq("Function1", "Function1RequestContextFutureRouteResult")

View File

@ -8,34 +8,33 @@
*/ */
package me.arcanis.ffxivbis.http package me.arcanis.ffxivbis.http
import akka.actor.typed.scaladsl.AskPattern.Askable import akka.actor.ActorRef
import akka.actor.typed.{ActorRef, Scheduler} import akka.pattern.ask
import akka.util.Timeout import akka.util.Timeout
import me.arcanis.ffxivbis.messages.{AddUser, DeleteUser, GetNewPartyId, GetUser, GetUsers, Message}
import me.arcanis.ffxivbis.models.User import me.arcanis.ffxivbis.models.User
import me.arcanis.ffxivbis.service.PartyService
import me.arcanis.ffxivbis.service.impl.DatabaseUserHandler
import scala.concurrent.Future import scala.concurrent.{ExecutionContext, Future}
trait UserHelper { class UserHelper(storage: ActorRef) {
def storage: ActorRef[Message]
def addUser(user: User, isHashedPassword: Boolean) def addUser(user: User, isHashedPassword: Boolean)
(implicit timeout: Timeout, scheduler: Scheduler): Future[Unit] = (implicit executionContext: ExecutionContext, timeout: Timeout): Future[Int] =
storage.ask(AddUser(user, isHashedPassword, _)) (storage ? DatabaseUserHandler.AddUser(user, isHashedPassword)).mapTo[Int]
def newPartyId(implicit timeout: Timeout, scheduler: Scheduler): Future[String] = def newPartyId(implicit executionContext: ExecutionContext, timeout: Timeout): Future[String] =
storage.ask(GetNewPartyId) (storage ? PartyService.GetNewPartyId).mapTo[String]
def user(partyId: String, username: String) def user(partyId: String, username: String)
(implicit timeout: Timeout, scheduler: Scheduler): Future[Option[User]] = (implicit executionContext: ExecutionContext, timeout: Timeout): Future[Option[User]] =
storage.ask(GetUser(partyId, username, _)) (storage ? DatabaseUserHandler.GetUser(partyId, username)).mapTo[Option[User]]
def users(partyId: String) def users(partyId: String)
(implicit timeout: Timeout, scheduler: Scheduler): Future[Seq[User]] = (implicit executionContext: ExecutionContext, timeout: Timeout): Future[Seq[User]] =
storage.ask(GetUsers(partyId, _)) (storage ? DatabaseUserHandler.GetUsers(partyId)).mapTo[Seq[User]]
def removeUser(partyId: String, username: String) def removeUser(partyId: String, username: String)
(implicit timeout: Timeout, scheduler: Scheduler): Future[Unit] = (implicit executionContext: ExecutionContext, timeout: Timeout): Future[Int] =
storage.ask(DeleteUser(partyId, username, _)) (storage ? DatabaseUserHandler.DeleteUser(partyId, username)).mapTo[Int]
} }

View File

@ -8,30 +8,28 @@
*/ */
package me.arcanis.ffxivbis.http.api.v1 package me.arcanis.ffxivbis.http.api.v1
import akka.actor.typed.{ActorRef, Scheduler} import akka.actor.ActorRef
import akka.http.scaladsl.model.{HttpEntity, StatusCodes} import akka.http.scaladsl.model.{HttpEntity, StatusCodes}
import akka.http.scaladsl.server.Directives._ import akka.http.scaladsl.server.Directives._
import akka.http.scaladsl.server._ import akka.http.scaladsl.server._
import akka.util.Timeout import akka.util.Timeout
import com.typesafe.scalalogging.StrictLogging
import io.swagger.v3.oas.annotations.enums.ParameterIn import io.swagger.v3.oas.annotations.enums.ParameterIn
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.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.{Operation, Parameter} import io.swagger.v3.oas.annotations.{Operation, Parameter}
import javax.ws.rs._ import io.swagger.v3.oas.annotations.parameters.RequestBody
import me.arcanis.ffxivbis.http.api.v1.json._ import io.swagger.v3.oas.annotations.security.SecurityRequirement
import javax.ws.rs.{Consumes, GET, POST, PUT, Path, Produces}
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.http.api.v1.json._
import me.arcanis.ffxivbis.models.PlayerId 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, ariyala: ActorRef)(implicit timeout: Timeout)
override val provider: ActorRef[BiSProviderMessage]) extends BiSHelper(storage, ariyala) with Authorization with JsonSupport {
(implicit timeout: Timeout, scheduler: Scheduler)
extends BiSHelper with Authorization with JsonSupport {
def route: Route = createBiS ~ getBiS ~ modifyBiS def route: Route = createBiS ~ getBiS ~ modifyBiS
@ -46,14 +44,10 @@ class BiSEndpoint(override val storage: ActorRef[Message],
content = Array(new Content(schema = new Schema(implementation = classOf[PlayerBiSLinkResponse])))), 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(responseCode = "400", description = "Invalid parameters were supplied"),
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))), new ApiResponse(responseCode = "401", description = "Supplied authorization is invalid"),
new ApiResponse(responseCode = "401", description = "Supplied authorization is invalid", 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"),
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"),
@ -89,12 +83,9 @@ class BiSEndpoint(override val storage: ActorRef[Message],
content = Array(new Content( content = Array(new Content(
array = new ArraySchema(schema = new Schema(implementation = classOf[PlayerResponse])) array = new ArraySchema(schema = new Schema(implementation = classOf[PlayerResponse]))
))), ))),
new ApiResponse(responseCode = "401", description = "Supplied authorization is invalid", new ApiResponse(responseCode = "401", description = "Supplied authorization is invalid"),
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))), new ApiResponse(responseCode = "403", description = "Access is forbidden"),
new ApiResponse(responseCode = "403", description = "Access is forbidden", new ApiResponse(responseCode = "500", description = "Internal server error"),
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))),
new ApiResponse(responseCode = "500", description = "Internal server error",
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))),
), ),
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"),
@ -128,14 +119,10 @@ class BiSEndpoint(override val storage: ActorRef[Message],
content = Array(new Content(schema = new Schema(implementation = classOf[PieceActionResponse])))), 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(responseCode = "400", description = "Invalid parameters were supplied"),
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))), new ApiResponse(responseCode = "401", description = "Supplied authorization is invalid"),
new ApiResponse(responseCode = "401", description = "Supplied authorization is invalid", 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"),
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"),
@ -146,7 +133,7 @@ class BiSEndpoint(override val storage: ActorRef[Message],
authenticateBasicBCrypt(s"party $partyId", authPost(partyId)) { _ => authenticateBasicBCrypt(s"party $partyId", authPost(partyId)) { _ =>
post { post {
entity(as[PieceActionResponse]) { action => entity(as[PieceActionResponse]) { action =>
val playerId = action.playerId.withPartyId(partyId) val playerId = action.playerIdResponse.withPartyId(partyId)
onComplete(doModifyBiS(action.action, playerId, action.piece.toPiece)) { onComplete(doModifyBiS(action.action, playerId, action.piece.toPiece)) {
case Success(_) => complete(StatusCodes.Accepted, HttpEntity.Empty) case Success(_) => complete(StatusCodes.Accepted, HttpEntity.Empty)
case Failure(exception) => throw exception case Failure(exception) => throw exception

View File

@ -18,9 +18,6 @@ import spray.json._
trait HttpHandler extends StrictLogging { this: JsonSupport => trait HttpHandler extends StrictLogging { this: JsonSupport =>
implicit def exceptionHandler: ExceptionHandler = ExceptionHandler { implicit def exceptionHandler: ExceptionHandler = ExceptionHandler {
case ex: IllegalArgumentException =>
complete(StatusCodes.BadRequest, ErrorResponse(ex.getMessage))
case other: Exception => case other: Exception =>
logger.error("exception during request completion", other) logger.error("exception during request completion", other)
complete(StatusCodes.InternalServerError, ErrorResponse("unknown server error")) complete(StatusCodes.InternalServerError, ErrorResponse("unknown server error"))
@ -31,7 +28,7 @@ trait HttpHandler extends StrictLogging { this: JsonSupport =>
.mapRejectionResponse { .mapRejectionResponse {
case response @ HttpResponse(_, _, entity: HttpEntity.Strict, _) => case response @ HttpResponse(_, _, entity: HttpEntity.Strict, _) =>
val message = ErrorResponse(entity.data.utf8String).toJson val message = ErrorResponse(entity.data.utf8String).toJson
response.withEntity(HttpEntity(ContentTypes.`application/json`, message.compactPrint)) response.copy(entity = HttpEntity(ContentTypes.`application/json`, message.compactPrint))
case other => other case other => other
} }
} }

View File

@ -8,29 +8,27 @@
*/ */
package me.arcanis.ffxivbis.http.api.v1 package me.arcanis.ffxivbis.http.api.v1
import akka.actor.typed.{ActorRef, Scheduler} import akka.actor.ActorRef
import akka.http.scaladsl.model.{HttpEntity, StatusCodes} import akka.http.scaladsl.model.{HttpEntity, StatusCodes}
import akka.http.scaladsl.server.Directives._ import akka.http.scaladsl.server.Directives._
import akka.http.scaladsl.server._ import akka.http.scaladsl.server._
import akka.util.Timeout import akka.util.Timeout
import io.swagger.v3.oas.annotations.enums.ParameterIn import io.swagger.v3.oas.annotations.enums.ParameterIn
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.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.{Operation, Parameter} import io.swagger.v3.oas.annotations.{Operation, Parameter}
import javax.ws.rs._ import io.swagger.v3.oas.annotations.parameters.RequestBody
import me.arcanis.ffxivbis.http.api.v1.json._ import io.swagger.v3.oas.annotations.security.SecurityRequirement
import javax.ws.rs.{Consumes, GET, POST, PUT, Path, Produces}
import me.arcanis.ffxivbis.http.{Authorization, LootHelper} import me.arcanis.ffxivbis.http.{Authorization, LootHelper}
import me.arcanis.ffxivbis.messages.Message import me.arcanis.ffxivbis.http.api.v1.json._
import me.arcanis.ffxivbis.models.PlayerId 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)(implicit timeout: Timeout)
(implicit timeout: Timeout, scheduler: Scheduler) extends LootHelper(storage) with Authorization with JsonSupport with HttpHandler {
extends LootHelper with Authorization with JsonSupport with HttpHandler {
def route: Route = getLoot ~ modifyLoot def route: Route = getLoot ~ modifyLoot
@ -48,12 +46,9 @@ class LootEndpoint(override val storage: ActorRef[Message])
content = Array(new Content( content = Array(new Content(
array = new ArraySchema(schema = new Schema(implementation = classOf[PlayerResponse])) array = new ArraySchema(schema = new Schema(implementation = classOf[PlayerResponse]))
))), ))),
new ApiResponse(responseCode = "401", description = "Supplied authorization is invalid", new ApiResponse(responseCode = "401", description = "Supplied authorization is invalid"),
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))), new ApiResponse(responseCode = "403", description = "Access is forbidden"),
new ApiResponse(responseCode = "403", description = "Access is forbidden", new ApiResponse(responseCode = "500", description = "Internal server error"),
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))),
new ApiResponse(responseCode = "500", description = "Internal server error",
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))),
), ),
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"),
@ -86,14 +81,10 @@ class LootEndpoint(override val storage: ActorRef[Message])
content = Array(new Content(schema = new Schema(implementation = classOf[PieceActionResponse])))), 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(responseCode = "400", description = "Invalid parameters were supplied"),
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))), new ApiResponse(responseCode = "401", description = "Supplied authorization is invalid"),
new ApiResponse(responseCode = "401", description = "Supplied authorization is invalid", 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"),
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"),
@ -104,8 +95,8 @@ class LootEndpoint(override val storage: ActorRef[Message])
authenticateBasicBCrypt(s"party $partyId", authPost(partyId)) { _ => authenticateBasicBCrypt(s"party $partyId", authPost(partyId)) { _ =>
post { post {
entity(as[PieceActionResponse]) { action => entity(as[PieceActionResponse]) { action =>
val playerId = action.playerId.withPartyId(partyId) val playerId = action.playerIdResponse.withPartyId(partyId)
onComplete(doModifyLoot(action.action, playerId, action.piece.toPiece, action.isFreeLoot)) { onComplete(doModifyLoot(action.action, playerId, action.piece.toPiece)) {
case Success(_) => complete(StatusCodes.Accepted, HttpEntity.Empty) case Success(_) => complete(StatusCodes.Accepted, HttpEntity.Empty)
case Failure(exception) => throw exception case Failure(exception) => throw exception
} }
@ -130,14 +121,10 @@ class LootEndpoint(override val storage: ActorRef[Message])
content = Array(new Content( content = Array(new Content(
array = new ArraySchema(schema = new Schema(implementation = classOf[PlayerIdWithCountersResponse])), array = new ArraySchema(schema = new Schema(implementation = classOf[PlayerIdWithCountersResponse])),
))), ))),
new ApiResponse(responseCode = "400", description = "Invalid parameters were supplied", new ApiResponse(responseCode = "400", description = "Invalid parameters were supplied"),
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))), new ApiResponse(responseCode = "401", description = "Supplied authorization is invalid"),
new ApiResponse(responseCode = "401", description = "Supplied authorization is invalid", 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"),
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

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

View File

@ -8,30 +8,27 @@
*/ */
package me.arcanis.ffxivbis.http.api.v1 package me.arcanis.ffxivbis.http.api.v1
import akka.actor.typed.{ActorRef, Scheduler} import akka.actor.ActorRef
import akka.http.scaladsl.model.{HttpEntity, StatusCodes} import akka.http.scaladsl.model.{HttpEntity, StatusCodes}
import akka.http.scaladsl.server.Directives._ import akka.http.scaladsl.server.Directives._
import akka.http.scaladsl.server._ import akka.http.scaladsl.server._
import akka.util.Timeout import akka.util.Timeout
import io.swagger.v3.oas.annotations.enums.ParameterIn import io.swagger.v3.oas.annotations.enums.ParameterIn
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.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.{Operation, Parameter} import io.swagger.v3.oas.annotations.{Operation, Parameter}
import javax.ws.rs._ import io.swagger.v3.oas.annotations.parameters.RequestBody
import me.arcanis.ffxivbis.http.api.v1.json._ import io.swagger.v3.oas.annotations.security.SecurityRequirement
import javax.ws.rs.{Consumes, GET, POST, Path, Produces}
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.http.api.v1.json._
import me.arcanis.ffxivbis.models.PlayerId 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, ariyala: ActorRef)(implicit timeout: Timeout)
override val provider: ActorRef[BiSProviderMessage]) extends PlayerHelper(storage, ariyala) with Authorization with JsonSupport with HttpHandler {
(implicit timeout: Timeout, scheduler: Scheduler)
extends PlayerHelper with Authorization with JsonSupport with HttpHandler {
def route: Route = getParty ~ modifyParty def route: Route = getParty ~ modifyParty
@ -49,12 +46,9 @@ class PlayerEndpoint(override val storage: ActorRef[Message],
content = Array(new Content( content = Array(new Content(
array = new ArraySchema(schema = new Schema(implementation = classOf[PlayerResponse])), array = new ArraySchema(schema = new Schema(implementation = classOf[PlayerResponse])),
))), ))),
new ApiResponse(responseCode = "401", description = "Supplied authorization is invalid", new ApiResponse(responseCode = "401", description = "Supplied authorization is invalid"),
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))), new ApiResponse(responseCode = "403", description = "Access is forbidden"),
new ApiResponse(responseCode = "403", description = "Access is forbidden", new ApiResponse(responseCode = "500", description = "Internal server error"),
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))),
new ApiResponse(responseCode = "500", description = "Internal server error",
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))),
), ),
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"),
@ -87,14 +81,10 @@ class PlayerEndpoint(override val storage: ActorRef[Message],
content = Array(new Content(schema = new Schema(implementation = classOf[PlayerActionResponse])))), 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(responseCode = "400", description = "Invalid parameters were supplied"),
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))), new ApiResponse(responseCode = "401", description = "Supplied authorization is invalid"),
new ApiResponse(responseCode = "401", description = "Supplied authorization is invalid", 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"),
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"),
@ -104,7 +94,7 @@ class PlayerEndpoint(override val storage: ActorRef[Message],
extractExecutionContext { implicit executionContext => extractExecutionContext { implicit executionContext =>
authenticateBasicBCrypt(s"party $partyId", authPost(partyId)) { _ => authenticateBasicBCrypt(s"party $partyId", authPost(partyId)) { _ =>
entity(as[PlayerActionResponse]) { action => entity(as[PlayerActionResponse]) { action =>
val player = action.playerId.toPlayer.copy(partyId = partyId) val player = action.playerIdResponse.toPlayer.copy(partyId = partyId)
onComplete(doModifyPlayer(action.action, player)) { onComplete(doModifyPlayer(action.action, player)) {
case Success(_) => complete(StatusCodes.Accepted, HttpEntity.Empty) case Success(_) => complete(StatusCodes.Accepted, HttpEntity.Empty)
case Failure(exception) => throw exception case Failure(exception) => throw exception

View File

@ -8,31 +8,25 @@
*/ */
package me.arcanis.ffxivbis.http.api.v1 package me.arcanis.ffxivbis.http.api.v1
import akka.actor.typed.{ActorRef, Scheduler} import akka.actor.ActorRef
import akka.http.scaladsl.server.Directives._ import akka.http.scaladsl.server.Directives._
import akka.http.scaladsl.server.Route import akka.http.scaladsl.server.Route
import akka.util.Timeout import akka.util.Timeout
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}
class RootApiV1Endpoint(storage: ActorRef[Message], class RootApiV1Endpoint(storage: ActorRef, ariyala: ActorRef)
provider: ActorRef[BiSProviderMessage], (implicit timeout: Timeout)
config: Config)(implicit timeout: Timeout, scheduler: Scheduler)
extends JsonSupport with HttpHandler { extends JsonSupport with HttpHandler {
private val biSEndpoint = new BiSEndpoint(storage, provider) private val biSEndpoint = new BiSEndpoint(storage, ariyala)
private val lootEndpoint = new LootEndpoint(storage) private val lootEndpoint = new LootEndpoint(storage)
private val partyEndpoint = new PartyEndpoint(storage, provider) private val playerEndpoint = new PlayerEndpoint(storage, ariyala)
private val playerEndpoint = new PlayerEndpoint(storage, provider)
private val typesEndpoint = new TypesEndpoint(config)
private val userEndpoint = new UserEndpoint(storage) private val userEndpoint = new UserEndpoint(storage)
def route: Route = def route: Route =
handleExceptions(exceptionHandler) { handleExceptions(exceptionHandler) {
handleRejections(rejectionHandler) { handleRejections(rejectionHandler) {
biSEndpoint.route ~ lootEndpoint.route ~ partyEndpoint.route ~ biSEndpoint.route ~ lootEndpoint.route ~ playerEndpoint.route ~ userEndpoint.route
playerEndpoint.route ~ typesEndpoint.route ~ userEndpoint.route
} }
} }
} }

View File

@ -1,130 +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.http.api.v1
import akka.http.scaladsl.server.Directives._
import akka.http.scaladsl.server._
import com.typesafe.config.Config
import io.swagger.v3.oas.annotations.media.{ArraySchema, Content, Schema}
import io.swagger.v3.oas.annotations.responses.ApiResponse
import io.swagger.v3.oas.annotations.Operation
import javax.ws.rs._
import me.arcanis.ffxivbis.http.api.v1.json._
import me.arcanis.ffxivbis.models.{Job, Party, Permission, Piece, PieceType}
@Path("api/v1")
class TypesEndpoint(config: Config) extends JsonSupport {
def route: Route = getJobs ~ getPermissions ~ getPieces ~ getPieceTypes ~ getPriority
@GET
@Path("types/jobs")
@Produces(value = Array("application/json"))
@Operation(summary = "jobs list", description = "Returns the available jobs",
responses = Array(
new ApiResponse(responseCode = "200", description = "List of available jobs",
content = Array(new Content(
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"),
)
def getJobs: Route =
path("types" / "jobs") {
get {
complete(Job.availableWithAnyJob.map(_.toString))
}
}
@GET
@Path("types/permissions")
@Produces(value = Array("application/json"))
@Operation(summary = "permissions list", description = "Returns the available permissions",
responses = Array(
new ApiResponse(responseCode = "200", description = "List of available permissions",
content = Array(new Content(
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"),
)
def getPermissions: Route =
path("types" / "permissions") {
get {
complete(Permission.values.toSeq.sorted.map(_.toString))
}
}
@GET
@Path("types/pieces")
@Produces(value = Array("application/json"))
@Operation(summary = "pieces list", description = "Returns the available pieces",
responses = Array(
new ApiResponse(responseCode = "200", description = "List of available pieces",
content = Array(new Content(
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"),
)
def getPieces: Route =
path("types" / "pieces") {
get {
complete(Piece.available)
}
}
@GET
@Path("types/pieces/types")
@Produces(value = Array("application/json"))
@Operation(summary = "piece types list", description = "Returns the available piece types",
responses = Array(
new ApiResponse(responseCode = "200", description = "List of available piece types",
content = Array(new Content(
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"),
)
def getPieceTypes: Route =
path("types" / "pieces" / "types") {
get {
complete(PieceType.available.map(_.toString))
}
}
@GET
@Path("types/priority")
@Produces(value = Array("application/json"))
@Operation(summary = "priority list", description = "Returns the current priority list",
responses = Array(
new ApiResponse(responseCode = "200", description = "Priority order",
content = Array(new Content(
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"),
)
def getPriority: Route =
path("types" / "priority") {
get {
complete(Party.getRules(config))
}
}
}

View File

@ -8,29 +8,27 @@
*/ */
package me.arcanis.ffxivbis.http.api.v1 package me.arcanis.ffxivbis.http.api.v1
import akka.actor.typed.{ActorRef, Scheduler} import akka.actor.ActorRef
import akka.http.scaladsl.model.{HttpEntity, StatusCodes} import akka.http.scaladsl.model.{HttpEntity, StatusCodes}
import akka.http.scaladsl.server.Directives._ import akka.http.scaladsl.server.Directives._
import akka.http.scaladsl.server._ import akka.http.scaladsl.server._
import akka.util.Timeout import akka.util.Timeout
import io.swagger.v3.oas.annotations.enums.ParameterIn import io.swagger.v3.oas.annotations.enums.ParameterIn
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.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.{Operation, Parameter} import io.swagger.v3.oas.annotations.{Operation, Parameter}
import javax.ws.rs._ import io.swagger.v3.oas.annotations.parameters.RequestBody
import me.arcanis.ffxivbis.http.api.v1.json._ import io.swagger.v3.oas.annotations.security.SecurityRequirement
import javax.ws.rs.{Consumes, DELETE, GET, POST, PUT, Path, Produces}
import me.arcanis.ffxivbis.http.{Authorization, UserHelper} import me.arcanis.ffxivbis.http.{Authorization, UserHelper}
import me.arcanis.ffxivbis.messages.Message import me.arcanis.ffxivbis.http.api.v1.json._
import me.arcanis.ffxivbis.models.Permission 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)(implicit timeout: Timeout)
(implicit timeout: Timeout, scheduler: Scheduler) extends UserHelper(storage) with Authorization with JsonSupport {
extends UserHelper with Authorization with JsonSupport {
def route: Route = createParty ~ createUser ~ deleteUser ~ getUsers def route: Route = createParty ~ createUser ~ deleteUser ~ getUsers
@ -42,12 +40,9 @@ class UserEndpoint(override val storage: ActorRef[Message])
content = Array(new Content(schema = new Schema(implementation = classOf[UserResponse])))), 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(responseCode = "400", description = "Invalid parameters were supplied"),
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))), new ApiResponse(responseCode = "406", description = "Party with the specified ID already exists"),
new ApiResponse(responseCode = "406", description = "Party with the specified ID already exists", new ApiResponse(responseCode = "500", description = "Internal server error"),
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))),
new ApiResponse(responseCode = "500", description = "Internal server error",
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))),
), ),
tags = Array("party"), tags = Array("party"),
) )
@ -81,14 +76,10 @@ class UserEndpoint(override val storage: ActorRef[Message])
content = Array(new Content(schema = new Schema(implementation = classOf[UserResponse])))), 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(responseCode = "400", description = "Invalid parameters were supplied"),
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))), new ApiResponse(responseCode = "401", description = "Supplied authorization is invalid"),
new ApiResponse(responseCode = "401", description = "Supplied authorization is invalid", 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"),
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"),
@ -119,12 +110,9 @@ class UserEndpoint(override val storage: ActorRef[Message])
), ),
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(responseCode = "401", description = "Supplied authorization is invalid"),
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))), new ApiResponse(responseCode = "403", description = "Access is forbidden"),
new ApiResponse(responseCode = "403", description = "Access is forbidden", new ApiResponse(responseCode = "500", description = "Internal server error"),
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))),
new ApiResponse(responseCode = "500", description = "Internal server error",
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))),
), ),
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"),
@ -155,12 +143,9 @@ class UserEndpoint(override val storage: ActorRef[Message])
content = Array(new Content( content = Array(new Content(
array = new ArraySchema(schema = new Schema(implementation = classOf[UserResponse])), array = new ArraySchema(schema = new Schema(implementation = classOf[UserResponse])),
))), ))),
new ApiResponse(responseCode = "401", description = "Supplied authorization is invalid", new ApiResponse(responseCode = "401", description = "Supplied authorization is invalid"),
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))), new ApiResponse(responseCode = "403", description = "Access is forbidden"),
new ApiResponse(responseCode = "403", description = "Access is forbidden", new ApiResponse(responseCode = "500", description = "Internal server error"),
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))),
new ApiResponse(responseCode = "500", description = "Internal server error",
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))),
), ),
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

@ -8,8 +8,6 @@
*/ */
package me.arcanis.ffxivbis.http.api.v1.json package me.arcanis.ffxivbis.http.api.v1.json
import java.time.Instant
import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport
import me.arcanis.ffxivbis.models.Permission import me.arcanis.ffxivbis.models.Permission
import spray.json._ import spray.json._
@ -26,27 +24,16 @@ trait JsonSupport extends SprayJsonSupport with DefaultJsonProtocol {
} }
} }
implicit val instantFormat: RootJsonFormat[Instant] = new RootJsonFormat[Instant] {
override def write(obj: Instant): JsValue = obj.toString.toJson
override def read(json: JsValue): Instant = json match {
case JsNumber(value) => Instant.ofEpochMilli(value.toLongExact)
case JsString(value) => Instant.parse(value)
case other => deserializationError(s"String or number expected, got $other")
}
}
implicit val actionFormat: RootJsonFormat[ApiAction.Value] = enumFormat(ApiAction) implicit val actionFormat: RootJsonFormat[ApiAction.Value] = enumFormat(ApiAction)
implicit val permissionFormat: RootJsonFormat[Permission.Value] = enumFormat(Permission) implicit val permissionFormat: RootJsonFormat[Permission.Value] = enumFormat(Permission)
implicit val errorFormat: RootJsonFormat[ErrorResponse] = jsonFormat1(ErrorResponse.apply) implicit val errorFormat: RootJsonFormat[ErrorResponse] = jsonFormat1(ErrorResponse.apply)
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 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)
implicit val pieceActionFormat: RootJsonFormat[PieceActionResponse] = jsonFormat4(PieceActionResponse.apply) implicit val pieceActionFormat: RootJsonFormat[PieceActionResponse] = jsonFormat3(PieceActionResponse.apply)
implicit val playerBiSLinkFormat: RootJsonFormat[PlayerBiSLinkResponse] = jsonFormat2(PlayerBiSLinkResponse.apply) implicit val playerBiSLinkFormat: RootJsonFormat[PlayerBiSLinkResponse] = jsonFormat2(PlayerBiSLinkResponse.apply)
implicit val playerIdWithCountersFormat: RootJsonFormat[PlayerIdWithCountersResponse] = implicit val playerIdWithCountersFormat: RootJsonFormat[PlayerIdWithCountersResponse] =
jsonFormat9(PlayerIdWithCountersResponse.apply) jsonFormat9(PlayerIdWithCountersResponse.apply)

View File

@ -1,20 +0,0 @@
package me.arcanis.ffxivbis.http.api.v1.json
import java.time.Instant
import io.swagger.v3.oas.annotations.media.Schema
import me.arcanis.ffxivbis.models.Loot
case class LootResponse(
@Schema(description = "looted piece", required = true) piece: PieceResponse,
@Schema(description = "loot timestamp", required = true) timestamp: Instant,
@Schema(description = "is loot free for all", required = true) isFreeLoot: Boolean) {
def toLoot: Loot = Loot(-1, piece.toPiece, timestamp, isFreeLoot)
}
object LootResponse {
def fromLoot(loot: Loot): LootResponse =
LootResponse(PieceResponse.fromPiece(loot.piece), loot.timestamp, loot.isFreeLoot)
}

View File

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

View File

@ -13,5 +13,4 @@ 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) playerIdResponse: PlayerIdResponse)
@Schema(description = "is piece free to roll or not") isFreeLoot: Option[Boolean])

View File

@ -9,18 +9,16 @@
package me.arcanis.ffxivbis.http.api.v1.json package me.arcanis.ffxivbis.http.api.v1.json
import io.swagger.v3.oas.annotations.media.Schema import io.swagger.v3.oas.annotations.media.Schema
import me.arcanis.ffxivbis.models.{Job, Piece, PieceType} import me.arcanis.ffxivbis.models.{Job, Piece}
case class PieceResponse( case class PieceResponse(
@Schema(description = "piece type", required = true) pieceType: String, @Schema(description = "is piece tome gear", required = true) isTome: Boolean,
@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, isTome, Job.withName(job))
def toPiece: Piece = Piece(piece, PieceType.withName(pieceType), Job.withName(job))
} }
object PieceResponse { object PieceResponse {
def fromPiece(piece: Piece): PieceResponse = def fromPiece(piece: Piece): PieceResponse =
PieceResponse(piece.pieceType.toString, piece.job.toString, piece.piece) PieceResponse(piece.isTome, piece.job.toString, piece.piece)
} }

View File

@ -12,4 +12,4 @@ 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(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) @Schema(description = "player description", required = true) playerIdResponse: PlayerResponse)

View File

@ -15,13 +15,11 @@ 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)
} }
object PlayerIdResponse { object PlayerIdResponse {
def fromPlayerId(playerId: PlayerId): PlayerIdResponse = def fromPlayerId(playerId: PlayerId): PlayerIdResponse =
PlayerIdResponse(Some(playerId.partyId), playerId.job.toString, playerId.nick) PlayerIdResponse(Some(playerId.partyId), playerId.job.toString, playerId.nick)
} }

View File

@ -23,7 +23,6 @@ case class PlayerIdWithCountersResponse(
@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 {
def fromPlayerId(playerIdWithCounters: PlayerIdWithCounters): PlayerIdWithCountersResponse = def fromPlayerId(playerIdWithCounters: PlayerIdWithCounters): PlayerIdWithCountersResponse =
PlayerIdWithCountersResponse( PlayerIdWithCountersResponse(
playerIdWithCounters.partyId, playerIdWithCounters.partyId,

View File

@ -16,22 +16,18 @@ case class PlayerResponse(
@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,
@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[PieceResponse]],
@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") priority: Option[Int]) {
def toPlayer: Player = def toPlayer: Player =
Player(-1, partyId, Job.withName(job), nick, Player(partyId, Job.withName(job), nick,
BiS(bis.getOrElse(Seq.empty).map(_.toPiece)), BiS(bis.getOrElse(Seq.empty).map(_.toPiece)), loot.getOrElse(Seq.empty).map(_.toPiece),
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(PieceResponse.fromPiece)),
Some(player.loot.map(LootResponse.fromLoot)),
player.link, Some(player.priority)) player.link, Some(player.priority))
} }

View File

@ -16,13 +16,11 @@ case class UserResponse(
@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))
} }
object UserResponse { object UserResponse {
def fromUser(user: User): UserResponse = def fromUser(user: User): UserResponse =
UserResponse(user.partyId, user.username, "", Some(user.permission)) UserResponse(user.partyId, user.username, "", Some(user.permission))
} }

View File

@ -8,20 +8,15 @@
*/ */
package me.arcanis.ffxivbis.http.view package me.arcanis.ffxivbis.http.view
import akka.actor.typed.{ActorRef, Scheduler} import akka.actor.ActorRef
import akka.http.scaladsl.model.StatusCodes import akka.http.scaladsl.model.StatusCodes
import akka.http.scaladsl.server.Directives._ import akka.http.scaladsl.server.Directives._
import akka.http.scaladsl.server._ import akka.http.scaladsl.server._
import akka.util.Timeout import akka.util.Timeout
import me.arcanis.ffxivbis.http.{Authorization, PlayerHelper} import me.arcanis.ffxivbis.http.Authorization
import me.arcanis.ffxivbis.messages.{BiSProviderMessage, Message}
import scala.util.{Failure, Success} class BasePartyView(override val storage: ActorRef)(implicit timeout: Timeout)
extends Authorization {
class BasePartyView(override val storage: ActorRef[Message],
override val provider: ActorRef[BiSProviderMessage])
(implicit timeout: Timeout, scheduler: Scheduler)
extends PlayerHelper with Authorization {
def route: Route = getIndex def route: Route = getIndex
@ -30,10 +25,8 @@ class BasePartyView(override val storage: ActorRef[Message],
extractExecutionContext { implicit executionContext => extractExecutionContext { implicit executionContext =>
authenticateBasicBCrypt(s"party $partyId", authGet(partyId)) { _ => authenticateBasicBCrypt(s"party $partyId", authGet(partyId)) { _ =>
get { get {
onComplete(getPartyDescription(partyId)) { complete {
case Success(description) => (StatusCodes.OK, RootView.toHtml(BasePartyView.template(partyId)))
complete(StatusCodes.OK, RootView.toHtml(BasePartyView.template(partyId, description.alias)))
case Failure(exception) => throw exception
} }
} }
} }
@ -44,21 +37,20 @@ class BasePartyView(override val storage: ActorRef[Message],
object BasePartyView { object BasePartyView {
import scalatags.Text import scalatags.Text
import scalatags.Text.all._ import scalatags.Text.all._
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): 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"), title:=s"Party $partyId",
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 $partyId"),
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")),

View File

@ -8,22 +8,19 @@
*/ */
package me.arcanis.ffxivbis.http.view package me.arcanis.ffxivbis.http.view
import akka.actor.typed.{ActorRef, Scheduler} import akka.actor.ActorRef
import akka.http.scaladsl.model.StatusCodes import akka.http.scaladsl.model.StatusCodes
import akka.http.scaladsl.server.Directives._ import akka.http.scaladsl.server.Directives._
import akka.http.scaladsl.server._ import akka.http.scaladsl.server._
import akka.util.Timeout import akka.util.Timeout
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.models.{Piece, Player, PlayerId}
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, ariyala: ActorRef)(implicit timeout: Timeout)
override val provider: ActorRef[BiSProviderMessage]) extends BiSHelper(storage, ariyala) with Authorization {
(implicit timeout: Timeout, scheduler: Scheduler)
extends BiSHelper with Authorization {
def route: Route = getBiS ~ modifyBiS def route: Route = getBiS ~ modifyBiS
@ -49,10 +46,10 @@ 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".as[String], "piece".as[String].?, "is_tome".as[String].?, "link".as[String].?, "action".as[String]) {
(player, maybePiece, maybePieceType, maybeLink, action) => (player, maybePiece, maybeIsTome, maybeLink, action) =>
onComplete(modifyBiSCall(partyId, player, maybePiece, maybePieceType, maybeLink, action)) { _ => onComplete(modifyBiSCall(partyId, player, maybePiece, maybeIsTome, maybeLink, action)) {
redirect(s"/party/$partyId/bis", StatusCodes.Found) case _ => redirect(s"/party/$partyId/bis", StatusCodes.Found)
} }
} }
} }
@ -61,25 +58,25 @@ class BiSView(override val storage: ActorRef[Message],
} }
private def modifyBiSCall(partyId: String, player: String, private def modifyBiSCall(partyId: String, player: String,
maybePiece: Option[String], maybePieceType: Option[String], maybePiece: Option[String], maybeIsTome: Option[String],
maybeLink: Option[String], action: String) maybeLink: Option[String], action: String)
(implicit executionContext: ExecutionContext, timeout: Timeout): Future[Unit] = { (implicit executionContext: ExecutionContext, timeout: Timeout): Future[Unit] = {
def getPiece(playerId: PlayerId, piece: String, pieceType: String) = import me.arcanis.ffxivbis.utils.Implicits._
Try(Piece(piece, PieceType.withName(pieceType), playerId.job)).toOption
def bisAction(playerId: PlayerId, piece: String, pieceType: String)(fn: Piece => Future[Unit]) = def getPiece(playerId: PlayerId, piece: String) =
getPiece(playerId, piece, pieceType) match { Try(Piece(piece, maybeIsTome, playerId.job)).toOption
case Some(item) => fn(item).map(_ => ())
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) => (maybePiece, action, maybeLink) match {
case (Some(piece), Some(pieceType), "add", _) => case (Some(piece), "add", _) => getPiece(playerId, piece) match {
bisAction(playerId, piece, pieceType)(addPieceBiS(playerId, _)) case Some(item) => addPieceBiS(playerId, item).map(_ => ())
case (Some(piece), Some(pieceType), "remove", _) => case _ => Future.failed(new Error(s"Could not construct piece from `$piece`"))
bisAction(playerId, piece, pieceType)(removePieceBiS(playerId, _)) }
case (_, _, "create", Some(link)) => putBiS(playerId, link).map(_ => ()) case (Some(piece), "remove", _) => getPiece(playerId, piece) match {
case Some(item) => removePieceBiS(playerId, item).map(_ => ())
case _ => Future.failed(new Error(s"Could not construct piece from `$piece`"))
}
case (_, "create", Some(link)) => putBiS(playerId, link).map(_ => ())
case _ => Future.failed(new Error(s"Could not perform $action")) 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`"))
@ -89,13 +86,12 @@ class BiSView(override val storage: ActorRef[Message],
object BiSView { object BiSView {
import scalatags.Text.all._ import scalatags.Text.all._
import scalatags.Text.tags2.{title => titleTag}
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"), title:="Best in slot",
link(rel:="stylesheet", `type`:="text/css", href:="/static/styles.css") link(rel:="stylesheet", `type`:="text/css", href:="/static/styles.css")
), ),
@ -110,13 +106,13 @@ object BiSView {
(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") input(name:="is_tome", id:="is_tome", title:="is tome", `type`:="checkbox"),
(for (pieceType <- PieceType.available) yield option(pieceType.toString)), label(`for`:="is_tome")("is tome gear"),
input(name:="action", id:="action", `type`:="hidden", value:="add"), input(name:="action", id:="action", `type`:="hidden", value:="add"),
input(name:="add", id:="add", `type`:="submit", value:="add") input(name:="add", id:="add", `type`:="submit", value:="add")
), ),
form(action:=s"/party/$partyId/bis", method:="post")( form(action:="/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"),
@ -128,18 +124,18 @@ object BiSView {
tr( tr(
th("player"), th("player"),
th("piece"), th("piece"),
th("piece type"), th("is tome"),
th("") th("")
), ),
for (player <- party; piece <- player.bis.pieces) yield tr( for (player <- party; piece <- player.bis.pieces) yield tr(
td(`class`:="include_search")(player.playerId.toString), td(`class`:="include_search")(player.playerId.toString),
td(`class`:="include_search")(piece.piece), td(`class`:="include_search")(piece.piece),
td(piece.pieceType.toString), td(piece.isTomeToString),
td( td(
form(action:=s"/party/$partyId/bis", method:="post")( form(action:=s"/party/$partyId/bis", method:="post")(
input(name:="player", id:="player", `type`:="hidden", value:=player.playerId.toString), input(name:="player", id:="player", `type`:="hidden", value:=player.playerId.toString),
input(name:="piece", id:="piece", `type`:="hidden", value:=piece.piece), input(name:="piece", id:="piece", `type`:="hidden", value:=piece.piece),
input(name:="piece_type", id:="piece_type", `type`:="hidden", value:=piece.pieceType.toString), input(name:="is_tome", id:="is_tome", `type`:="hidden", value:=piece.isTomeToString),
input(name:="action", id:="action", `type`:="hidden", value:="remove"), input(name:="action", id:="action", `type`:="hidden", value:="remove"),
input(name:="remove", id:="remove", `type`:="submit", value:="x") input(name:="remove", id:="remove", `type`:="submit", value:="x")
) )

View File

@ -12,7 +12,6 @@ import scalatags.Text
import scalatags.Text.all._ 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

@ -12,7 +12,6 @@ import scalatags.Text
import scalatags.Text.all._ import scalatags.Text.all._
object ExportToCSVView { 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"),

View File

@ -8,22 +8,18 @@
*/ */
package me.arcanis.ffxivbis.http.view package me.arcanis.ffxivbis.http.view
import akka.actor.typed.{ActorRef, Scheduler} import akka.actor.ActorRef
import akka.http.scaladsl.model.StatusCodes import akka.http.scaladsl.model.StatusCodes
import akka.http.scaladsl.server.Directives._ import akka.http.scaladsl.server.Directives._
import akka.http.scaladsl.server._ import akka.http.scaladsl.server._
import akka.util.Timeout import akka.util.Timeout
import me.arcanis.ffxivbis.http.{PlayerHelper, UserHelper} import me.arcanis.ffxivbis.http.UserHelper
import me.arcanis.ffxivbis.messages.{BiSProviderMessage, Message} import me.arcanis.ffxivbis.models.{Party, Permission, User}
import me.arcanis.ffxivbis.models.{PartyDescription, Permission, User}
import scala.concurrent.Future
import scala.util.{Failure, Success} import scala.util.{Failure, Success}
class IndexView(override val storage: ActorRef[Message], class IndexView(storage: ActorRef)(implicit timeout: Timeout)
override val provider: ActorRef[BiSProviderMessage]) extends UserHelper(storage) {
(implicit timeout: Timeout, scheduler: Scheduler)
extends PlayerHelper with UserHelper {
def route: Route = createParty ~ getIndex def route: Route = createParty ~ getIndex
@ -31,17 +27,13 @@ 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]) { (username, password) =>
onComplete { onComplete(newPartyId) {
newPartyId.flatMap { partyId => case Success(partyId) =>
val user = User(partyId, username, password, Permission.admin) val user = User(partyId, username, password, Permission.admin)
addUser(user, isHashedPassword = false).flatMap { _ => onComplete(addUser(user, isHashedPassword = false)) {
if (maybeAlias.getOrElse("").isEmpty) Future.successful(partyId) case _ => redirect(s"/party/$partyId", StatusCodes.Found)
else updateDescription(PartyDescription(partyId, maybeAlias)).map(_ => partyId)
} }
}
} {
case Success(partyId) => redirect(s"/party/$partyId", StatusCodes.Found)
case Failure(exception) => throw exception case Failure(exception) => throw exception
} }
} }
@ -62,20 +54,18 @@ class IndexView(override val storage: ActorRef[Message],
object IndexView { object IndexView {
import scalatags.Text.all._ import scalatags.Text.all._
import scalatags.Text.tags2.{title => titleTag}
def template: String = def template: 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( html(
head( head(
titleTag("FFXIV loot helper"), title:="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:="username", id:="username", placeholder:="username", title:="username", `type`:="text"), input(name:="username", id:="username", placeholder:="username", title:="username", `type`:="text"),
input(name:="password", id:="password", placeholder:="password", title:="password", `type`:="password"), input(name:="password", id:="password", placeholder:="password", title:="password", `type`:="password"),
input(name:="add", id:="add", `type`:="submit", value:="add") input(name:="add", id:="add", `type`:="submit", value:="add")

View File

@ -8,21 +8,19 @@
*/ */
package me.arcanis.ffxivbis.http.view package me.arcanis.ffxivbis.http.view
import akka.actor.typed.{ActorRef, Scheduler} import akka.actor.ActorRef
import akka.http.scaladsl.model.StatusCodes import akka.http.scaladsl.model.StatusCodes
import akka.http.scaladsl.server.Directives._ import akka.http.scaladsl.server.Directives._
import akka.http.scaladsl.server.Route import akka.http.scaladsl.server.Route
import akka.util.Timeout import akka.util.Timeout
import me.arcanis.ffxivbis.http.{Authorization, LootHelper} import me.arcanis.ffxivbis.http.{Authorization, LootHelper}
import me.arcanis.ffxivbis.messages.Message import me.arcanis.ffxivbis.models.{Piece, PlayerIdWithCounters}
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)(implicit timeout: Timeout)
(implicit timeout: Timeout, scheduler: Scheduler) extends LootHelper(storage) with Authorization {
extends LootHelper with Authorization {
def route: Route = getIndex ~ suggestLoot def route: Route = getIndex ~ suggestLoot
@ -32,7 +30,7 @@ class LootSuggestView(override val storage: ActorRef[Message])
authenticateBasicBCrypt(s"party $partyId", authGet(partyId)) { _ => authenticateBasicBCrypt(s"party $partyId", authGet(partyId)) { _ =>
get { get {
complete { complete {
val text = LootSuggestView.template(partyId, Seq.empty, None, false, None) val text = LootSuggestView.template(partyId, Seq.empty, None, None)
(StatusCodes.OK, RootView.toHtml(text)) (StatusCodes.OK, RootView.toHtml(text))
} }
} }
@ -45,20 +43,18 @@ class LootSuggestView(override val storage: ActorRef[Message])
extractExecutionContext { implicit executionContext => extractExecutionContext { implicit executionContext =>
authenticateBasicBCrypt(s"party $partyId", authGet(partyId)) { _ => authenticateBasicBCrypt(s"party $partyId", authGet(partyId)) { _ =>
post { post {
formFields("piece".as[String], "job".as[String], "piece_type".as[String], "free_loot".as[String].?) { formFields("piece".as[String], "is_tome".as[String].?) { (piece, maybeTome) =>
(piece, job, pieceType, maybeFreeLoot) => import me.arcanis.ffxivbis.utils.Implicits._
import me.arcanis.ffxivbis.utils.Implicits._ val maybePiece = Try(Piece(piece, maybeTome)).toOption
val maybePiece = Try(Piece(piece, PieceType.withName(pieceType), Job.withName(job))).toOption onComplete(suggestLootCall(partyId, maybePiece)) {
case Success(players) =>
onComplete(suggestLootCall(partyId, maybePiece)) { val text = LootSuggestView.template(partyId, players, maybePiece, None)
case Success(players) => complete(StatusCodes.OK, RootView.toHtml(text))
val text = LootSuggestView.template(partyId, players, maybePiece, maybeFreeLoot, None) case Failure(exception) =>
complete(StatusCodes.OK, RootView.toHtml(text)) val text = LootSuggestView.template(partyId, Seq.empty, maybePiece, Some(exception.getMessage))
case Failure(exception) => complete(StatusCodes.OK, RootView.toHtml(text))
val text = LootSuggestView.template(partyId, Seq.empty, None, false, Some(exception.getMessage)) }
complete(StatusCodes.OK, RootView.toHtml(text))
}
} }
} }
} }
@ -75,14 +71,12 @@ class LootSuggestView(override val storage: ActorRef[Message])
object LootSuggestView { object LootSuggestView {
import scalatags.Text.all._ import scalatags.Text.all._
import scalatags.Text.tags2.{title => titleTag}
def template(partyId: String, party: Seq[PlayerIdWithCounters], piece: Option[Piece], def template(partyId: String, party: Seq[PlayerIdWithCounters], piece: Option[Piece], error: Option[String]): String =
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"), title:="Suggest loot",
link(rel:="stylesheet", `type`:="text/css", href:="/static/styles.css") link(rel:="stylesheet", `type`:="text/css", href:="/static/styles.css")
), ),
@ -95,12 +89,8 @@ object LootSuggestView {
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") input(name:="is_tome", id:="is_tome", title:="is tome", `type`:="checkbox"),
(for (job <- Job.availableWithAnyJob) yield option(job.toString)), label(`for`:="is_tome")("is tome gear"),
select(name:="piece_type", id:="piece_type", title:="piece type")
(for (pieceType <- PieceType.available) yield option(pieceType.toString)),
input(name:="free_loot", id:="free_loot", title:="is free loot", `type`:="checkbox"),
label(`for`:="free_loot")("is free loot"),
input(name:="suggest", id:="suggest", `type`:="submit", value:="suggest") input(name:="suggest", id:="suggest", `type`:="submit", value:="suggest")
), ),
@ -123,8 +113,7 @@ object LootSuggestView {
form(action:=s"/party/$partyId/loot", method:="post")( form(action:=s"/party/$partyId/loot", method:="post")(
input(name:="player", id:="player", `type`:="hidden", value:=player.playerId.toString), input(name:="player", id:="player", `type`:="hidden", value:=player.playerId.toString),
input(name:="piece", id:="piece", `type`:="hidden", value:=piece.map(_.piece).getOrElse("")), input(name:="piece", id:="piece", `type`:="hidden", value:=piece.map(_.piece).getOrElse("")),
input(name:="piece_type", id:="piece_type", `type`:="hidden", value:=piece.map(_.pieceType.toString).getOrElse("")), input(name:="is_tome", id:="is_tome", `type`:="hidden", value:=piece.map(_.isTomeToString).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:="action", id:="action", `type`:="hidden", value:="add"),
input(name:="add", id:="add", `type`:="submit", value:="add") input(name:="add", id:="add", `type`:="submit", value:="add")
) )

View File

@ -8,21 +8,19 @@
*/ */
package me.arcanis.ffxivbis.http.view package me.arcanis.ffxivbis.http.view
import akka.actor.typed.{ActorRef, Scheduler} import akka.actor.ActorRef
import akka.http.scaladsl.model.StatusCodes import akka.http.scaladsl.model.StatusCodes
import akka.http.scaladsl.server.Directives._ import akka.http.scaladsl.server.Directives._
import akka.http.scaladsl.server.Route import akka.http.scaladsl.server.Route
import akka.util.Timeout import akka.util.Timeout
import me.arcanis.ffxivbis.http.{Authorization, LootHelper} import me.arcanis.ffxivbis.http.{Authorization, LootHelper}
import me.arcanis.ffxivbis.messages.Message import me.arcanis.ffxivbis.models.{Piece, Player, PlayerId}
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)(implicit timeout: Timeout)
(implicit timeout: Timeout, scheduler: Scheduler) extends LootHelper(storage) with Authorization {
extends LootHelper with Authorization {
def route: Route = getLoot ~ modifyLoot def route: Route = getLoot ~ modifyLoot
@ -48,10 +46,10 @@ 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".as[String], "piece".as[String], "is_tome".as[String].?, "action".as[String]) {
(player, piece, pieceType, action, isFreeLoot) => (player, maybePiece, maybeIsTome, action) =>
onComplete(modifyLootCall(partyId, player, piece, pieceType, isFreeLoot, action)) { _ => onComplete(modifyLootCall(partyId, player, maybePiece, maybeIsTome, action)) {
redirect(s"/party/$partyId/loot", StatusCodes.Found) case _ => redirect(s"/party/$partyId/loot", StatusCodes.Found)
} }
} }
} }
@ -59,20 +57,20 @@ class LootView(override val storage: ActorRef[Message])
} }
} }
private def modifyLootCall(partyId: String, player: String, maybePiece: String, private def modifyLootCall(partyId: String, player: String,
maybePieceType: String, maybeFreeLoot: Option[String], maybePiece: String, maybeIsTome: Option[String],
action: String) action: String)
(implicit executionContext: ExecutionContext, timeout: Timeout): Future[Unit] = { (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, maybeIsTome, playerId.job)).toOption
PlayerId(partyId, player) match { PlayerId(partyId, player) match {
case Some(playerId) => (getPiece(playerId), action) match { case Some(playerId) => (getPiece(playerId), action) match {
case (Some(piece), "add") => addPieceLoot(playerId, piece, maybeFreeLoot).map(_ => ()) case (Some(piece), "add") => addPieceLoot(playerId, piece).map(_ => ())
case (Some(piece), "remove") => removePieceLoot(playerId, piece).map(_ => ()) case (Some(piece), "remove") => removePieceLoot(playerId, piece).map(_ => ())
case _ => Future.failed(new Error(s"Could not construct piece from `$maybePiece ($maybePieceType)`")) case _ => Future.failed(new Error(s"Could not construct piece from `$maybePiece`"))
} }
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`"))
} }
@ -81,13 +79,12 @@ class LootView(override val storage: ActorRef[Message])
object LootView { object LootView {
import scalatags.Text.all._ import scalatags.Text.all._
import scalatags.Text.tags2.{title => titleTag}
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"), title:="Loot",
link(rel:="stylesheet", `type`:="text/css", href:="/static/styles.css") link(rel:="stylesheet", `type`:="text/css", href:="/static/styles.css")
), ),
@ -102,10 +99,8 @@ object LootView {
(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") input(name:="is_tome", id:="is_tome", title:="is tome", `type`:="checkbox"),
(for (pieceType <- PieceType.available) yield option(pieceType.toString)), label(`for`:="is_tome")("is tome gear"),
input(name:="free_loot", id:="free_loot", title:="is free loot", `type`:="checkbox"),
label(`for`:="free_loot")("is free loot"),
input(name:="action", id:="action", `type`:="hidden", value:="add"), input(name:="action", id:="action", `type`:="hidden", value:="add"),
input(name:="add", id:="add", `type`:="submit", value:="add") input(name:="add", id:="add", `type`:="submit", value:="add")
), ),
@ -114,23 +109,18 @@ object LootView {
tr( tr(
th("player"), th("player"),
th("piece"), th("piece"),
th("piece type"), th("is tome"),
th("is free loot"),
th("timestamp"),
th("") th("")
), ),
for (player <- party; loot <- player.loot) yield tr( for (player <- party; piece <- player.bis.pieces) yield tr(
td(`class`:="include_search")(player.playerId.toString), td(`class`:="include_search")(player.playerId.toString),
td(`class`:="include_search")(loot.piece.piece), td(`class`:="include_search")(piece.piece),
td(loot.piece.pieceType.toString), td(piece.isTomeToString),
td(loot.isFreeLootToString),
td(loot.timestamp.toString),
td( td(
form(action:=s"/party/$partyId/loot", method:="post")( form(action:=s"/party/$partyId/loot", method:="post")(
input(name:="player", id:="player", `type`:="hidden", value:=player.playerId.toString), input(name:="player", id:="player", `type`:="hidden", value:=player.playerId.toString),
input(name:="piece", id:="piece", `type`:="hidden", value:=loot.piece.piece), input(name:="piece", id:="piece", `type`:="hidden", value:=piece.piece),
input(name:="piece_type", id:="piece_type", `type`:="hidden", value:=loot.piece.pieceType.toString), input(name:="is_tome", id:="is_tome", `type`:="hidden", value:=piece.isTomeToString),
input(name:="free_loot", id:="free_loot", `type`:="hidden", value:=loot.isFreeLootToString),
input(name:="action", id:="action", `type`:="hidden", value:="remove"), input(name:="action", id:="action", `type`:="hidden", value:="remove"),
input(name:="remove", id:="remove", `type`:="submit", value:="x") input(name:="remove", id:="remove", `type`:="submit", value:="x")
) )

View File

@ -8,21 +8,18 @@
*/ */
package me.arcanis.ffxivbis.http.view package me.arcanis.ffxivbis.http.view
import akka.actor.typed.{ActorRef, Scheduler} import akka.actor.ActorRef
import akka.http.scaladsl.model.StatusCodes import akka.http.scaladsl.model.StatusCodes
import akka.http.scaladsl.server.Directives._ import akka.http.scaladsl.server.Directives._
import akka.http.scaladsl.server.Route import akka.http.scaladsl.server.Route
import akka.util.Timeout import akka.util.Timeout
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.models.{BiS, Job, Player, PlayerId, PlayerIdWithCounters}
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, ariyala: ActorRef)(implicit timeout: Timeout)
override val provider: ActorRef[BiSProviderMessage]) extends PlayerHelper(storage, ariyala) with Authorization {
(implicit timeout: Timeout, scheduler: Scheduler)
extends PlayerHelper with Authorization {
def route: Route = getParty ~ modifyParty def route: Route = getParty ~ modifyParty
@ -50,8 +47,8 @@ class PlayerView(override val storage: ActorRef[Message],
post { post {
formFields("nick".as[String], "job".as[String], "priority".as[Int].?, "link".as[String].?, "action".as[String]) { formFields("nick".as[String], "job".as[String], "priority".as[Int].?, "link".as[String].?, "action".as[String]) {
(nick, job, maybePriority, maybeLink, action) => (nick, job, maybePriority, maybeLink, action) =>
onComplete(modifyPartyCall(partyId, nick, job, maybePriority, maybeLink, action)) { _ => onComplete(modifyPartyCall(partyId, nick, job, maybePriority, maybeLink, action)) {
redirect(s"/party/$partyId/players", StatusCodes.Found) case _ => redirect(s"/party/$partyId/players", StatusCodes.Found)
} }
} }
} }
@ -65,7 +62,7 @@ class PlayerView(override val storage: ActorRef[Message],
(implicit executionContext: ExecutionContext, timeout: Timeout): Future[Unit] = { (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(partyId, playerId.job, playerId.nick, BiS(), 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)).map(_ => ())
@ -77,13 +74,12 @@ class PlayerView(override val storage: ActorRef[Message],
object PlayerView { object PlayerView {
import scalatags.Text.all._ import scalatags.Text.all._
import scalatags.Text.tags2.{title => titleTag}
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"), title:="Party",
link(rel:="stylesheet", `type`:="text/css", href:="/static/styles.css") link(rel:="stylesheet", `type`:="text/css", href:="/static/styles.css")
), ),
@ -96,7 +92,7 @@ object PlayerView {
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") select(name:="job", id:="job", title:="job")
(for (job <- Job.available) yield option(job.toString)), (for (job <- Job.jobs) yield option(job.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:="prioiry", id:="priority", placeholder:="priority", title:="priority", `type`:="number", value:="0"), input(name:="prioiry", id:="priority", placeholder:="priority", title:="priority", `type`:="number", value:="0"),
input(name:="action", id:="action", `type`:="hidden", value:="add"), input(name:="action", id:="action", `type`:="hidden", value:="add"),

View File

@ -8,24 +8,21 @@
*/ */
package me.arcanis.ffxivbis.http.view package me.arcanis.ffxivbis.http.view
import akka.actor.typed.{ActorRef, Scheduler} import akka.actor.ActorRef
import akka.http.scaladsl.model.{ContentTypes, HttpEntity} import akka.http.scaladsl.model.{ContentTypes, HttpEntity}
import akka.http.scaladsl.server.Directives._ import akka.http.scaladsl.server.Directives._
import akka.http.scaladsl.server.Route import akka.http.scaladsl.server.Route
import akka.util.Timeout import akka.util.Timeout
import me.arcanis.ffxivbis.messages.{BiSProviderMessage, Message}
class RootView(storage: ActorRef[Message], class RootView(storage: ActorRef, ariyala: ActorRef)(implicit timeout: Timeout) {
provider: ActorRef[BiSProviderMessage])
(implicit timeout: Timeout, scheduler: Scheduler) {
private val basePartyView = new BasePartyView(storage, provider) private val basePartyView = new BasePartyView(storage)
private val indexView = new IndexView(storage, provider) private val indexView = new IndexView(storage)
private val biSView = new BiSView(storage, provider) private val biSView = new BiSView(storage, ariyala)
private val lootView = new LootView(storage) private val lootView = new LootView(storage)
private val lootSuggestView = new LootSuggestView(storage) private val lootSuggestView = new LootSuggestView(storage)
private val playerView = new PlayerView(storage, provider) private val playerView = new PlayerView(storage, ariyala)
private val userView = new UserView(storage) private val userView = new UserView(storage)
def route: Route = def route: Route =
@ -34,7 +31,6 @@ class RootView(storage: ActorRef[Message],
} }
object RootView { object RootView {
def toHtml(template: String): HttpEntity.Strict = def toHtml(template: String): HttpEntity.Strict =
HttpEntity(ContentTypes.`text/html(UTF-8)`, template) HttpEntity(ContentTypes.`text/html(UTF-8)`, template)
} }

View File

@ -12,7 +12,6 @@ import scalatags.Text
import scalatags.Text.all._ import scalatags.Text.all._
object SearchLineView { object SearchLineView {
def template: Text.TypedTag[String] = def template: Text.TypedTag[String] =
div( div(
input( input(

View File

@ -8,21 +8,19 @@
*/ */
package me.arcanis.ffxivbis.http.view package me.arcanis.ffxivbis.http.view
import akka.actor.typed.{ActorRef, Scheduler} import akka.actor.ActorRef
import akka.http.scaladsl.model.StatusCodes import akka.http.scaladsl.model.StatusCodes
import akka.http.scaladsl.server.Directives._ import akka.http.scaladsl.server.Directives._
import akka.http.scaladsl.server.Route import akka.http.scaladsl.server.Route
import akka.util.Timeout import akka.util.Timeout
import me.arcanis.ffxivbis.http.{Authorization, UserHelper} import me.arcanis.ffxivbis.http.{Authorization, UserHelper}
import me.arcanis.ffxivbis.messages.Message
import me.arcanis.ffxivbis.models.{Permission, User} 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)(implicit timeout: Timeout)
(implicit timeout: Timeout, scheduler: Scheduler) extends UserHelper(storage) with Authorization {
extends UserHelper with Authorization {
def route: Route = getUsers ~ modifyUsers def route: Route = getUsers ~ modifyUsers
@ -79,13 +77,12 @@ class UserView(override val storage: ActorRef[Message])
object UserView { object UserView {
import scalatags.Text.all._ import scalatags.Text.all._
import scalatags.Text.tags2.{title => titleTag}
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"), title:="Users",
link(rel:="stylesheet", `type`:="text/css", href:="/static/styles.css") link(rel:="stylesheet", `type`:="text/css", href:="/static/styles.css")
), ),

View File

@ -1,8 +0,0 @@
package me.arcanis.ffxivbis.messages
import akka.actor.typed.ActorRef
import me.arcanis.ffxivbis.models.{BiS, Job}
sealed trait BiSProviderMessage
case class DownloadBiS(link: String, job: Job.Job, replyTo: ActorRef[BiS]) extends BiSProviderMessage

View File

@ -1,10 +0,0 @@
package me.arcanis.ffxivbis.messages
import akka.actor.typed.ActorRef
import me.arcanis.ffxivbis.models.Party
case class ForgetParty(partyId: String) extends Message
case class GetNewPartyId(replyTo: ActorRef[String]) extends Message
case class StoreParty(partyId: String, party: Party) extends Message

View File

@ -1,77 +0,0 @@
package me.arcanis.ffxivbis.messages
import akka.actor.typed.{ActorRef, Behavior}
import me.arcanis.ffxivbis.models.{Party, PartyDescription, Piece, Player, PlayerId, User}
import me.arcanis.ffxivbis.service.LootSelector
sealed trait DatabaseMessage extends Message {
def partyId: String
}
object DatabaseMessage {
type Handler = PartialFunction[DatabaseMessage, Behavior[DatabaseMessage]]
}
// bis handler
case class AddPieceToBis(playerId: PlayerId, piece: Piece, replyTo: ActorRef[Unit]) extends DatabaseMessage {
override def partyId: String = playerId.partyId
}
case class GetBiS(partyId: String, playerId: Option[PlayerId], replyTo: ActorRef[Seq[Player]]) extends DatabaseMessage
case class RemovePieceFromBiS(playerId: PlayerId, piece: Piece, replyTo: ActorRef[Unit]) extends DatabaseMessage {
override def partyId: String = playerId.partyId
}
case class RemovePiecesFromBiS(playerId: PlayerId, replyTo: ActorRef[Unit]) extends DatabaseMessage {
override def partyId: String = playerId.partyId
}
// loot handler
case class AddPieceTo(playerId: PlayerId, piece: Piece, isFreeLoot: Boolean, replyTo: ActorRef[Unit]) extends DatabaseMessage {
override def partyId: String = playerId.partyId
}
case class GetLoot(partyId: String, playerId: Option[PlayerId], replyTo: ActorRef[Seq[Player]]) extends DatabaseMessage
case class RemovePieceFrom(playerId: PlayerId, piece: Piece, replyTo: ActorRef[Unit]) extends DatabaseMessage {
override def partyId: String = playerId.partyId
}
case class SuggestLoot(partyId: String, piece: Piece, replyTo: ActorRef[LootSelector.LootSelectorResult]) extends DatabaseMessage
// party handler
case class AddPlayer(player: Player, replyTo: ActorRef[Unit]) extends DatabaseMessage {
override def partyId: String = player.partyId
}
case class GetParty(partyId: String, replyTo: ActorRef[Party]) extends DatabaseMessage
case class GetPartyDescription(partyId: String, replyTo: ActorRef[PartyDescription]) extends DatabaseMessage
case class GetPlayer(playerId: PlayerId, replyTo: ActorRef[Option[Player]]) extends DatabaseMessage {
override def partyId: String = playerId.partyId
}
case class RemovePlayer(playerId: PlayerId, replyTo: ActorRef[Unit]) extends DatabaseMessage {
override def partyId: String = playerId.partyId
}
case class UpdateParty(partyDescription: PartyDescription, replyTo: ActorRef[Unit]) extends DatabaseMessage {
override def partyId: String = partyDescription.partyId
}
// user handler
case class AddUser(user: User, isHashedPassword: Boolean, replyTo: ActorRef[Unit]) extends DatabaseMessage {
override def partyId: String = user.partyId
}
case class DeleteUser(partyId: String, username: String, replyTo: ActorRef[Unit]) extends DatabaseMessage
case class Exists(partyId: String, replyTo: ActorRef[Boolean]) 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

View File

@ -1,9 +0,0 @@
package me.arcanis.ffxivbis.messages
import akka.actor.typed.Behavior
trait Message
object Message {
type Handler = PartialFunction[Message, Behavior[Message]]
}

View File

@ -8,7 +8,21 @@
*/ */
package me.arcanis.ffxivbis.models package me.arcanis.ffxivbis.models
case class BiS(pieces: Seq[Piece]) { case class BiS(weapon: Option[Piece],
head: Option[Piece],
body: Option[Piece],
hands: Option[Piece],
waist: Option[Piece],
legs: Option[Piece],
feet: Option[Piece],
ears: Option[Piece],
neck: Option[Piece],
wrist: Option[Piece],
leftRing: Option[Piece],
rightRing: Option[Piece]) {
val pieces: Seq[Piece] =
Seq(weapon, head, body, hands, waist, legs, feet, ears, neck, wrist, leftRing, rightRing).flatten
def hasPiece(piece: Piece): Boolean = piece match { def hasPiece(piece: Piece): Boolean = piece match {
case upgrade: PieceUpgrade => upgrades.contains(upgrade) case upgrade: PieceUpgrade => upgrades.contains(upgrade)
@ -17,27 +31,50 @@ case class BiS(pieces: Seq[Piece]) {
def upgrades: Map[PieceUpgrade, Int] = def upgrades: Map[PieceUpgrade, Int] =
pieces.groupBy(_.upgrade).foldLeft(Map.empty[PieceUpgrade, Int]) { pieces.groupBy(_.upgrade).foldLeft(Map.empty[PieceUpgrade, Int]) {
case (acc, (Some(k), v)) => acc + (k -> v.size) case (acc, (Some(k), v)) => acc + (k -> v.length)
case (acc, _) => acc case (acc, _) => acc
} withDefaultValue 0 } withDefaultValue 0
def withPiece(piece: Piece): BiS = copy(pieces :+ piece) def withPiece(piece: Piece): BiS = copyWithPiece(piece.piece, Some(piece))
def withoutPiece(piece: Piece): BiS = copy(pieces.filterNot(_.strictEqual(piece))) def withoutPiece(piece: Piece): BiS = copyWithPiece(piece.piece, None)
override def equals(obj: Any): Boolean = { private def copyWithPiece(name: String, piece: Option[Piece]): BiS = {
def comparePieces(left: Seq[Piece], right: Seq[Piece]): Boolean = val params = Map(
left.groupBy(identity).view.mapValues(_.size).forall { "weapon" -> weapon,
case (key, count) => right.count(_.strictEqual(key)) == count "head" -> head,
} "body" -> body,
"hands" -> hands,
obj match { "waist" -> waist,
case left: BiS => comparePieces(left.pieces, pieces) "legs" -> legs,
case _ => false "feet" -> feet,
} "ears" -> ears,
"neck" -> neck,
"wrist" -> wrist,
"leftRing" -> leftRing,
"rightRing" -> rightRing
) + (name -> piece)
BiS(params)
} }
} }
object BiS { object BiS {
def apply(data: Map[String, Option[Piece]]): BiS =
BiS(
data.get("weapon").flatten,
data.get("head").flatten,
data.get("body").flatten,
data.get("hands").flatten,
data.get("waist").flatten,
data.get("legs").flatten,
data.get("feet").flatten,
data.get("ears").flatten,
data.get("neck").flatten,
data.get("wrist").flatten,
data.get("leftRing").flatten,
data.get("rightRing").flatten)
def empty: BiS = BiS(Seq.empty) def apply(): BiS = BiS(Seq.empty)
def apply(pieces: Seq[Piece]): BiS =
BiS(pieces.map(piece => piece.piece -> Some(piece)).toMap)
} }

View File

@ -9,7 +9,6 @@
package me.arcanis.ffxivbis.models package me.arcanis.ffxivbis.models
object Job { object Job {
sealed trait RightSide sealed trait RightSide
object AccessoriesDex extends RightSide object AccessoriesDex extends RightSide
object AccessoriesInt extends RightSide object AccessoriesInt extends RightSide
@ -26,19 +25,17 @@ object Job {
object BodyTanks extends LeftSide object BodyTanks extends LeftSide
object BodyRanges extends LeftSide object BodyRanges extends LeftSide
sealed trait Job extends Equals { sealed trait Job {
def leftSide: LeftSide def leftSide: LeftSide
def rightSide: RightSide def rightSide: RightSide
// conversion to string to avoid recursion // conversion to string to avoid recursion
override def canEqual(that: Any): Boolean = that.isInstanceOf[Job]
override def equals(obj: Any): Boolean = { override def equals(obj: Any): Boolean = {
def canEqual(obj: Any): Boolean = obj.isInstanceOf[Job]
def equality(objRepr: String): Boolean = objRepr match { def equality(objRepr: String): Boolean = objRepr match {
case _ if objRepr == AnyJob.toString => true case _ if objRepr == AnyJob.toString => true
case _ if this.toString == AnyJob.toString => true case _ if this.toString == AnyJob.toString => true
case _ => this.toString == objRepr case _ => this.toString == obj.toString
} }
canEqual(obj) && equality(obj.toString) canEqual(obj) && equality(obj.toString)
@ -99,14 +96,8 @@ object Job {
case object SMN extends Casters case object SMN extends Casters
case object RDM extends Casters case object RDM extends Casters
lazy val available: Seq[Job] = lazy val jobs: 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, MNK, DRG, NIN, SAM, BRD, MCH, DNC, BLM, SMN, RDM)
lazy val availableWithAnyJob: Seq[Job] = available.prepended(AnyJob)
def withName(job: String): Job.Job = def withName(job: String): Job.Job = jobs.find(_.toString == job.toUpperCase).getOrElse(AnyJob)
availableWithAnyJob.find(_.toString.equalsIgnoreCase(job)) match {
case Some(value) => value
case None if job.isEmpty => AnyJob
case _ => throw new IllegalArgumentException(s"Invalid or unknown job $job")
}
} }

View File

@ -8,9 +8,4 @@
*/ */
package me.arcanis.ffxivbis.models package me.arcanis.ffxivbis.models
import java.time.Instant case class Loot(playerId: Long, piece: Piece)
case class Loot(playerId: Long, piece: Piece, timestamp: Instant, isFreeLoot: Boolean) {
def isFreeLootToString: String = if (isFreeLoot) "yes" else "no"
}

View File

@ -15,15 +15,15 @@ import me.arcanis.ffxivbis.service.LootSelector
import scala.jdk.CollectionConverters._ import scala.jdk.CollectionConverters._
import scala.util.Random import scala.util.Random
case class Party(partyDescription: PartyDescription, rules: Seq[String], players: Map[PlayerId, Player]) case class Party(partyId: String, rules: Seq[String], players: Map[PlayerId, Player])
extends StrictLogging { extends StrictLogging {
require(players.keys.forall(_.partyId == partyDescription.partyId), "party id must be same") require(players.keys.forall(_.partyId == partyId), "party id must be same")
def getPlayers: Seq[Player] = players.values.toSeq def getPlayers: Seq[Player] = players.values.toSeq
def player(playerId: PlayerId): Option[Player] = players.get(playerId) def player(playerId: PlayerId): Option[Player] = players.get(playerId)
def withPlayer(player: Player): Party = def withPlayer(player: Player): Party =
try { try {
require(player.partyId == partyDescription.partyId, "player must belong to this party") require(player.partyId == partyId, "player must belong to this party")
copy(players = players + (player.playerId -> player)) copy(players = players + (player.playerId -> player))
} catch { } catch {
case exception: Exception => case exception: Exception =>
@ -36,22 +36,24 @@ case class Party(partyDescription: PartyDescription, rules: Seq[String], players
} }
object Party { object Party {
private def getRules(config: Config): Seq[String] =
config.getStringList("me.arcanis.ffxivbis.settings.priority").asScala.toSeq
def apply(party: PartyDescription, config: Config, def apply(partyId: Option[String], config: Config): Party =
new Party(partyId.getOrElse(randomPartyId), getRules(config), Map.empty)
def apply(partyId: String, config: Config,
players: Map[Long, Player], bis: Seq[Loot], loot: Seq[Loot]): Party = { 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.mapValues(_.map(_.piece))
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(partyId, getRules(config), playersWithItems)
} }
def getRules(config: Config): Seq[String] =
config.getStringList("me.arcanis.ffxivbis.settings.priority").asScala.toSeq
def randomPartyId: String = Random.alphanumeric.take(20).mkString def randomPartyId: String = Random.alphanumeric.take(20).mkString
} }

View File

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

View File

@ -8,88 +8,79 @@
*/ */
package me.arcanis.ffxivbis.models package me.arcanis.ffxivbis.models
sealed trait Piece extends Equals { sealed trait Piece {
def isTome: Boolean
def pieceType: PieceType.PieceType
def job: Job.Job def job: Job.Job
def piece: String def piece: String
def withJob(other: Job.Job): Piece def withJob(other: Job.Job): Piece
def isTomeToString: String = if (isTome) "yes" else "no"
def upgrade: Option[PieceUpgrade] = this match { def upgrade: Option[PieceUpgrade] = this match {
case _ if pieceType != PieceType.Tome => None case _ if !isTome => None
case _: Waist => Some(AccessoryUpgrade) case _: Waist => Some(AccessoryUpgrade)
case _: PieceAccessory => Some(AccessoryUpgrade) case _: PieceAccessory => Some(AccessoryUpgrade)
case _: PieceBody => Some(BodyUpgrade) case _: PieceBody => Some(BodyUpgrade)
case _: PieceWeapon => Some(WeaponUpgrade) case _: PieceWeapon => Some(WeaponUpgrade)
} }
// used for ring comparison
def strictEqual(obj: Any): Boolean = equals(obj)
} }
trait PieceAccessory extends Piece trait PieceAccessory extends Piece
trait PieceBody extends Piece trait PieceBody extends Piece
trait PieceUpgrade extends Piece { trait PieceUpgrade extends Piece {
val pieceType: PieceType.PieceType = PieceType.Tome val isTome: Boolean = true
val job: Job.Job = Job.AnyJob val job: Job.Job = Job.AnyJob
def withJob(other: Job.Job): Piece = this def withJob(other: Job.Job): Piece = this
} }
trait PieceWeapon extends Piece trait PieceWeapon extends Piece
case class Weapon(override val pieceType: PieceType.PieceType, override val job: Job.Job) extends PieceWeapon { case class Weapon(override val isTome: Boolean, override val job: Job.Job) extends PieceWeapon {
val piece: String = "weapon" val piece: String = "weapon"
def withJob(other: Job.Job): Piece = copy(job = other) def withJob(other: Job.Job): Piece = copy(job = other)
} }
case class Head(override val pieceType: PieceType.PieceType, override val job: Job.Job) extends PieceBody { case class Head(override val isTome: Boolean, override val job: Job.Job) extends PieceBody {
val piece: String = "head" val piece: String = "head"
def withJob(other: Job.Job): Piece = copy(job = other) def withJob(other: Job.Job): Piece = copy(job = other)
} }
case class Body(override val pieceType: PieceType.PieceType, override val job: Job.Job) extends PieceBody { case class Body(override val isTome: Boolean, override val job: Job.Job) extends PieceBody {
val piece: String = "body" val piece: String = "body"
def withJob(other: Job.Job): Piece = copy(job = other) def withJob(other: Job.Job): Piece = copy(job = other)
} }
case class Hands(override val pieceType: PieceType.PieceType, override val job: Job.Job) extends PieceBody { case class Hands(override val isTome: Boolean, override val job: Job.Job) extends PieceBody {
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 { case class Waist(override val isTome: Boolean, override val job: Job.Job) extends PieceBody {
val piece: String = "waist" val piece: String = "waist"
def withJob(other: Job.Job): Piece = copy(job = other) 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 isTome: Boolean, 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)
} }
case class Feet(override val pieceType: PieceType.PieceType, override val job: Job.Job) extends PieceBody { case class Feet(override val isTome: Boolean, override val job: Job.Job) extends PieceBody {
val piece: String = "feet" val piece: String = "feet"
def withJob(other: Job.Job): Piece = copy(job = other) def withJob(other: Job.Job): Piece = copy(job = other)
} }
case class Ears(override val pieceType: PieceType.PieceType, override val job: Job.Job) extends PieceAccessory { case class Ears(override val isTome: Boolean, override val job: Job.Job) extends PieceAccessory {
val piece: String = "ears" val piece: String = "ears"
def withJob(other: Job.Job): Piece = copy(job = other) def withJob(other: Job.Job): Piece = copy(job = other)
} }
case class Neck(override val pieceType: PieceType.PieceType, override val job: Job.Job) extends PieceAccessory { case class Neck(override val isTome: Boolean, override val job: Job.Job) extends PieceAccessory {
val piece: String = "neck" val piece: String = "neck"
def withJob(other: Job.Job): Piece = copy(job = other) def withJob(other: Job.Job): Piece = copy(job = other)
} }
case class Wrist(override val pieceType: PieceType.PieceType, override val job: Job.Job) extends PieceAccessory { case class Wrist(override val isTome: Boolean, override val job: Job.Job) extends PieceAccessory {
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(override val isTome: Boolean, override val job: Job.Job, override val piece: String = "ring")
extends PieceAccessory { 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 {
case Ring(thatPieceType, thatJob, _) => (thatPieceType == pieceType) && (thatJob == job) case Ring(thatIsTome, thatJob, _) => (thatIsTome == isTome) && (thatJob == job)
case _ => false
}
override def strictEqual(obj: Any): Boolean = obj match {
case ring: Ring => equals(obj) && (ring.piece == this.piece)
case _ => false case _ => false
} }
} }
@ -105,19 +96,21 @@ case object WeaponUpgrade extends PieceUpgrade {
} }
object Piece { object Piece {
def apply(piece: String, pieceType: PieceType.PieceType, job: Job.Job = Job.AnyJob): Piece = def apply(piece: String, isTome: Boolean, job: Job.Job = Job.AnyJob): Piece =
piece.toLowerCase match { piece.toLowerCase match {
case "weapon" => Weapon(pieceType, job) case "weapon" => Weapon(isTome, job)
case "head" => Head(pieceType, job) case "head" => Head(isTome, job)
case "body" => Body(pieceType, job) case "body" => Body(isTome, job)
case "hands" => Hands(pieceType, job) case "hands" => Hands(isTome, job)
case "waist" => Waist(pieceType, job) case "waist" => Waist(isTome, job)
case "legs" => Legs(pieceType, job) case "legs" => Legs(isTome, job)
case "feet" => Feet(pieceType, job) case "feet" => Feet(isTome, job)
case "ears" => Ears(pieceType, job) case "ears" => Ears(isTome, job)
case "neck" => Neck(pieceType, job) case "neck" => Neck(isTome, job)
case "wrist" | "wrists" => Wrist(pieceType, job) case "wrist" => Wrist(isTome, job)
case ring @ ("ring" | "left ring" | "right ring") => Ring(pieceType, job, ring) case "ring" => Ring(isTome, job)
case "leftring" => Ring(isTome, job).copy(piece = "leftRing")
case "rightring" => Ring(isTome, job).copy(piece = "rightRing")
case "accessory upgrade" => AccessoryUpgrade case "accessory upgrade" => AccessoryUpgrade
case "body upgrade" => BodyUpgrade case "body upgrade" => BodyUpgrade
case "weapon upgrade" => WeaponUpgrade case "weapon upgrade" => WeaponUpgrade
@ -126,6 +119,5 @@ object Piece {
lazy val available: Seq[String] = Seq("weapon", lazy val available: Seq[String] = Seq("weapon",
"head", "body", "hands", "waist", "legs", "feet", "head", "body", "hands", "waist", "legs", "feet",
"ears", "neck", "wrist", "left ring", "right ring", "ears", "neck", "wrist", "leftRing", "rightRing")
"accessory upgrade", "body upgrade", "weapon upgrade")
} }

View File

@ -1,19 +0,0 @@
package me.arcanis.ffxivbis.models
object PieceType {
sealed trait PieceType
case object Crafted extends PieceType
case object Tome extends PieceType
case object Savage extends PieceType
lazy val available: Seq[PieceType] =
Seq(Crafted, Tome, Savage)
def withName(pieceType: String): PieceType =
available.find(_.toString.equalsIgnoreCase(pieceType)) match {
case Some(value) => value
case _ => throw new IllegalArgumentException(s"Invalid or unknown piece type $pieceType")
}
}

View File

@ -8,12 +8,11 @@
*/ */
package me.arcanis.ffxivbis.models package me.arcanis.ffxivbis.models
case class Player(id: Long, case class Player(partyId: String,
partyId: String,
job: Job.Job, job: Job.Job,
nick: String, nick: String,
bis: BiS, bis: BiS,
loot: Seq[Loot], loot: Seq[Piece],
link: Option[String] = None, link: Option[String] = None,
priority: Int = 0) { priority: Int = 0) {
require(job ne Job.AnyJob, "AnyJob is not allowed") require(job ne Job.AnyJob, "AnyJob is not allowed")
@ -28,10 +27,10 @@ case class Player(id: Long,
partyId, job, nick, isRequired(piece), priority, partyId, job, nick, isRequired(piece), priority,
bisCountTotal(piece), lootCount(piece), bisCountTotal(piece), lootCount(piece),
lootCountBiS(piece), lootCountTotal(piece)) lootCountBiS(piece), lootCountTotal(piece))
def withLoot(piece: Loot): Player = withLoot(Seq(piece)) def withLoot(piece: Piece): Player = withLoot(Seq(piece))
def withLoot(list: Seq[Loot]): Player = { def withLoot(list: Seq[Piece]): Player = list match {
require(loot.forall(_.playerId == id), "player id must be same") case Nil => this
copy(loot = loot ++ list) case _ => copy(loot = list)
} }
def isRequired(piece: Option[Piece]): Boolean = { def isRequired(piece: Option[Piece]): Boolean = {
@ -43,12 +42,12 @@ case class Player(id: Long,
} }
} }
def bisCountTotal(piece: Option[Piece]): Int = bis.pieces.count(_.pieceType == PieceType.Savage) def bisCountTotal(piece: Option[Piece]): Int = bis.pieces.count(!_.isTome)
def lootCount(piece: Option[Piece]): Int = piece match { def lootCount(piece: Option[Piece]): Int = piece match {
case Some(p) => loot.count(item => !item.isFreeLoot && item.piece == p) case Some(p) => loot.count(_ == p)
case None => lootCountTotal(piece) case None => lootCountTotal(piece)
} }
def lootCountBiS(piece: Option[Piece]): Int = loot.map(_.piece).count(bis.hasPiece) def lootCountBiS(piece: Option[Piece]): Int = loot.count(bis.hasPiece)
def lootCountTotal(piece: Option[Piece]): Int = loot.count(!_.isFreeLoot) def lootCountTotal(piece: Option[Piece]): Int = loot.length
def lootPriority(piece: Piece): Int = priority def lootPriority(piece: Piece): Int = priority
} }

View File

@ -12,7 +12,6 @@ import scala.util.Try
import scala.util.matching.Regex import scala.util.matching.Regex
trait PlayerIdBase { trait PlayerIdBase {
def job: Job.Job def job: Job.Job
def nick: String def nick: String
@ -22,7 +21,6 @@ trait PlayerIdBase {
case class PlayerId(partyId: String, job: Job.Job, nick: String) extends PlayerIdBase case class PlayerId(partyId: String, job: Job.Job, nick: String) extends PlayerIdBase
object PlayerId { object PlayerId {
def apply(partyId: String, maybeNick: Option[String], maybeJob: Option[String]): Option[PlayerId] = def apply(partyId: String, maybeNick: Option[String], maybeJob: Option[String]): Option[PlayerId] =
(maybeNick, maybeJob) match { (maybeNick, maybeJob) match {
case (Some(nick), Some(job)) => Try(PlayerId(partyId, Job.withName(job), nick)).toOption case (Some(nick), Some(job)) => Try(PlayerId(partyId, Job.withName(job), nick)).toOption

View File

@ -38,7 +38,6 @@ case class PlayerIdWithCounters(partyId: String,
} }
object PlayerIdWithCounters { object PlayerIdWithCounters {
private case class PlayerCountersComparator(values: Int*) { private case class PlayerCountersComparator(values: Int*) {
def >(that: PlayerCountersComparator): Boolean = { def >(that: PlayerCountersComparator): Boolean = {
@scala.annotation.tailrec @scala.annotation.tailrec

View File

@ -0,0 +1,136 @@
/*
* 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
import java.nio.file.Paths
import akka.actor.{Actor, Props}
import akka.http.scaladsl.model._
import akka.http.scaladsl.Http
import akka.pattern.pipe
import akka.stream.ActorMaterializer
import akka.stream.scaladsl.{Keep, Sink}
import akka.util.ByteString
import com.typesafe.scalalogging.StrictLogging
import me.arcanis.ffxivbis.models.{BiS, Job, Piece}
import spray.json._
import scala.concurrent.{ExecutionContext, Future}
import scala.util.Try
class Ariyala extends Actor with StrictLogging {
import Ariyala._
private val settings = context.system.settings.config
private val ariyalaUrl = settings.getString("me.arcanis.ffxivbis.ariyala.ariyala-url")
private val xivapiUrl = settings.getString("me.arcanis.ffxivbis.ariyala.xivapi-url")
private val xivapiKey = Try(settings.getString("me.arcanis.ffxivbis.ariyala.xivapi-key")).toOption
private val http = Http()(context.system)
implicit private val materializer: ActorMaterializer = ActorMaterializer()
implicit private val executionContext: ExecutionContext = context.dispatcher
override def receive: Receive = {
case GetBiS(link, job) =>
val client = sender()
get(link, job).map(BiS(_)).pipeTo(client)
}
private def get(link: String, job: Job.Job): Future[Seq[Piece]] = {
val id = Paths.get(link).normalize.getFileName.toString
val uri = Uri(ariyalaUrl)
.withPath(Uri.Path / "store.app")
.withQuery(Uri.Query(Map("identifier" -> id)))
sendRequest(uri, Ariyala.parseAriyalaJsonToPieces(job, getIsTome))
}
private def getIsTome(itemIds: Seq[Long]): Future[Map[Long, Boolean]] = {
val uri = Uri(xivapiUrl)
.withPath(Uri.Path / "item")
.withQuery(Uri.Query(Map(
"columns" -> Seq("ID", "IsEquippable").mkString(","),
"ids" -> itemIds.mkString(","),
"private_key" -> xivapiKey.getOrElse("")
)))
sendRequest(uri, Ariyala.parseXivapiJson)
}
private def sendRequest[T](uri: Uri, parser: JsObject => Future[T]): Future[T] =
http.singleRequest(HttpRequest(uri = uri)).map {
case HttpResponse(status, _, entity, _) if status.isSuccess() =>
entity.dataBytes
.fold(ByteString.empty)(_ ++ _)
.map(_.utf8String)
.map(result => parser(result.parseJson.asJsObject))
.toMat(Sink.head)(Keep.right)
.run().flatten
case other => Future.failed(new Error(s"Invalid response from server $other"))
}.flatten
}
object Ariyala {
def props: Props = Props(new Ariyala)
case class GetBiS(link: String, job: Job.Job)
private def parseAriyalaJson(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))) => remapKey(key).map(k => acc + (k -> id.toLong)).getOrElse(acc)
case (acc, _) => acc
}
case other => throw deserializationError(s"Invalid json $other")
}
}
private def parseAriyalaJsonToPieces(job: Job.Job, isTome: Seq[Long] => Future[Map[Long, Boolean]])(js: JsObject)
(implicit executionContext: ExecutionContext): Future[Seq[Piece]] =
parseAriyalaJson(job)(js).flatMap { pieces =>
isTome(pieces.values.toSeq).map { tomePieces =>
pieces.view.mapValues(tomePieces).map {
case (piece, isTomePiece) => Piece(piece, isTomePiece, job)
}.toSeq
}
}
private def parseXivapiJson(js: JsObject)
(implicit executionContext: ExecutionContext): Future[Map[Long, Boolean]] =
Future {
js.fields("Results") match {
case array: JsArray =>
array.elements.map(_.asJsObject.getFields("ID", "IsEquippable") match {
case Seq(JsNumber(id), JsNumber(isTome)) => id.toLong -> (isTome == 0)
case other => throw deserializationError(s"Could not parse $other")
}).toMap
case other => throw deserializationError(s"Could not parse $other")
}
}
private def remapKey(key: String): Option[String] = key match {
case "mainhand" => Some("weapon")
case "chest" => Some("body")
case "ringLeft" => Some("leftRing")
case "ringRight" => Some("rightRing")
case "head" | "hands" | "waist" | "legs" | "feet" | "ears" | "neck" | "wrist" => Some(key)
case _ => None
}
}

View File

@ -8,40 +8,38 @@
*/ */
package me.arcanis.ffxivbis.service package me.arcanis.ffxivbis.service
import akka.actor.typed.Behavior import akka.actor.Actor
import akka.actor.typed.scaladsl.Behaviors
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.models.{Party, Player, PlayerId} import me.arcanis.ffxivbis.models.{Party, Player, PlayerId}
import me.arcanis.ffxivbis.service.impl.DatabaseImpl
import me.arcanis.ffxivbis.storage.DatabaseProfile import me.arcanis.ffxivbis.storage.DatabaseProfile
import scala.concurrent.{ExecutionContext, Future} import scala.concurrent.{ExecutionContext, Future}
trait Database extends StrictLogging { trait Database extends Actor with StrictLogging {
implicit def executionContext: ExecutionContext implicit def executionContext: ExecutionContext
def config: Config
def profile: DatabaseProfile def profile: DatabaseProfile
override def postStop(): Unit = {
profile.db.close()
super.postStop()
}
def filterParty(party: Party, maybePlayerId: Option[PlayerId]): Seq[Player] = def filterParty(party: Party, maybePlayerId: Option[PlayerId]): Seq[Player] =
maybePlayerId match { (party, maybePlayerId) match {
case Some(playerId) => party.player(playerId).map(Seq(_)).getOrElse(Seq.empty) case (_, Some(playerId)) => party.player(playerId).map(Seq(_)).getOrElse(Seq.empty)
case _ => party.getPlayers case (_, _) => party.getPlayers
} }
def getParty(partyId: String, withBiS: Boolean, withLoot: Boolean): Future[Party] = def getParty(partyId: String, withBiS: Boolean, withLoot: Boolean): Future[Party] =
for { for {
partyDescription <- profile.getPartyDescription(partyId)
players <- profile.getParty(partyId) players <- profile.getParty(partyId)
bis <- if (withBiS) profile.getPiecesBiS(partyId) else Future(Seq.empty) bis <- if (withBiS) profile.getPiecesBiS(partyId) else Future(Seq.empty)
loot <- if (withLoot) profile.getPieces(partyId) else Future(Seq.empty) loot <- if (withLoot) profile.getPieces(partyId) else Future(Seq.empty)
} yield Party(partyDescription, config, players, bis, loot) } yield Party(partyId, context.system.settings.config, players, bis, loot)
} }
object Database { object Database {
trait DatabaseRequest {
def apply(): Behavior[DatabaseMessage] = def partyId: String
Behaviors.setup[DatabaseMessage](context => new DatabaseImpl(context)) }
} }

View File

@ -21,7 +21,6 @@ class LootSelector(players: Seq[Player], piece: Piece, orderBy: Seq[String]) {
} }
object LootSelector { object LootSelector {
def apply(players: Seq[Player], piece: Piece, orderBy: Seq[String]): LootSelectorResult = def apply(players: Seq[Player], piece: Piece, orderBy: Seq[String]): LootSelectorResult =
new LootSelector(players, piece, orderBy).suggest new LootSelector(players, piece, orderBy).suggest

View File

@ -8,65 +8,56 @@
*/ */
package me.arcanis.ffxivbis.service package me.arcanis.ffxivbis.service
import akka.actor.typed.scaladsl.AskPattern.Askable import akka.actor.{Actor, ActorRef, Props}
import akka.actor.typed.{ActorRef, Behavior, DispatcherSelector, Scheduler} import akka.pattern.{ask, pipe}
import akka.actor.typed.scaladsl.{AbstractBehavior, ActorContext, Behaviors}
import akka.util.Timeout import akka.util.Timeout
import com.typesafe.scalalogging.StrictLogging import com.typesafe.scalalogging.StrictLogging
import me.arcanis.ffxivbis.messages.{DatabaseMessage, Exists, ForgetParty, GetNewPartyId, GetParty, Message, StoreParty}
import me.arcanis.ffxivbis.models.Party import me.arcanis.ffxivbis.models.Party
import scala.concurrent.duration.FiniteDuration 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(storage: ActorRef) extends Actor with StrictLogging {
extends AbstractBehavior[Message](context) with StrictLogging { import PartyService._
import me.arcanis.ffxivbis.utils.Implicits._ import me.arcanis.ffxivbis.utils.Implicits._
private val cacheTimeout: FiniteDuration = private val cacheTimeout: FiniteDuration =
context.system.settings.config.getDuration("me.arcanis.ffxivbis.settings.cache-timeout") context.system.settings.config.getDuration("me.arcanis.ffxivbis.settings.cache-timeout")
implicit private val executionContext: ExecutionContext = { implicit private val executionContext: ExecutionContext = context.dispatcher
val selector = DispatcherSelector.fromConfig("me.arcanis.ffxivbis.default-dispatcher")
context.system.dispatchers.lookup(selector)
}
implicit private val timeout: Timeout = implicit private val timeout: Timeout =
context.system.settings.config.getDuration("me.arcanis.ffxivbis.settings.request-timeout") context.system.settings.config.getDuration("me.arcanis.ffxivbis.settings.request-timeout")
implicit private val scheduler: Scheduler = context.system.scheduler
override def onMessage(msg: Message): Behavior[Message] = handle(Map.empty)(msg) override def receive: Receive = handle(Map.empty)
private def handle(cache: Map[String, Party]): Message.Handler = { private def handle(cache: Map[String, Party]): Receive = {
case ForgetParty(partyId) => case ForgetParty(partyId) =>
Behaviors.receiveMessage(handle(cache - partyId)) context become handle(cache - partyId)
case GetNewPartyId(client) => case GetNewPartyId =>
getPartyId.foreach(client ! _) val client = sender()
Behaviors.same getPartyId.pipeTo(client)
case StoreParty(partyId, party) => case req @ impl.DatabasePartyHandler.GetParty(partyId) =>
Behaviors.receiveMessage(handle(cache.updated(partyId, party))) val client = sender()
case GetParty(partyId, client) =>
val party = cache.get(partyId) match { val party = cache.get(partyId) match {
case Some(party) => Future.successful(party) case Some(party) => Future.successful(party)
case None => case None =>
storage.ask(ref => GetParty(partyId, ref)).map { party => (storage ? req).mapTo[Party].map { party =>
context.self ! StoreParty(partyId, party) context become handle(cache + (partyId -> party))
context.system.scheduler.scheduleOnce(cacheTimeout, () => context.self ! ForgetParty(partyId)) context.system.scheduler.scheduleOnce(cacheTimeout, self, ForgetParty(partyId))
party party
} }
} }
party.foreach(client ! _) party.pipeTo(client)
Behaviors.same
case req: DatabaseMessage => case req: Database.DatabaseRequest =>
storage ! req self ! ForgetParty(req.partyId)
Behaviors.receiveMessage(handle(cache - req.partyId)) storage.forward(req)
} }
private def getPartyId: Future[String] = { private def getPartyId: Future[String] = {
val partyId = Party.randomPartyId val partyId = Party.randomPartyId
storage.ask(ref => Exists(partyId, ref)).flatMap { (storage ? impl.DatabaseUserHandler.Exists(partyId)).mapTo[Boolean].flatMap {
case true => getPartyId case true => getPartyId
case false => Future.successful(partyId) case false => Future.successful(partyId)
} }
@ -74,7 +65,8 @@ class PartyService(context: ActorContext[Message], storage: ActorRef[DatabaseMes
} }
object PartyService { object PartyService {
def props(storage: ActorRef): Props = Props(new PartyService(storage))
def apply(storage: ActorRef[DatabaseMessage]): Behavior[Message] = case class ForgetParty(partyId: String)
Behaviors.setup[Message](context => new PartyService(context, storage)) case object GetNewPartyId
} }

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

@ -1,83 +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 java.nio.file.Paths
import akka.actor.ClassicActorSystemProvider
import akka.actor.typed.{Behavior, PostStop, Signal}
import akka.actor.typed.scaladsl.{AbstractBehavior, ActorContext, Behaviors}
import akka.http.scaladsl.model._
import com.typesafe.scalalogging.StrictLogging
import me.arcanis.ffxivbis.messages.{BiSProviderMessage, DownloadBiS}
import me.arcanis.ffxivbis.models.{BiS, Job, Piece, PieceType}
import spray.json._
import scala.concurrent.{ExecutionContext, Future}
class BisProvider(context: ActorContext[BiSProviderMessage])
extends AbstractBehavior[BiSProviderMessage](context) with XivApi with StrictLogging {
override def system: ClassicActorSystemProvider = context.system
override def onMessage(msg: BiSProviderMessage): Behavior[BiSProviderMessage] =
msg match {
case DownloadBiS(link, job, client) =>
get(link, job).map(BiS(_)).foreach(client ! _)
Behaviors.same
}
override def onSignal: PartialFunction[Signal, Behavior[BiSProviderMessage]] = {
case PostStop =>
shutdown()
Behaviors.same
}
private def get(link: String, job: Job.Job): Future[Seq[Piece]] = {
val url = Uri(link)
val id = Paths.get(link).normalize.getFileName.toString
val (idParser, uri) =
if (url.authority.host.address().contains("etro")) {
(Etro.idParser(_, _), Etro.uri(url, id))
} else {
(Ariyala.idParser(_, _), Ariyala.uri(url, id))
}
sendRequest(uri, BisProvider.parseBisJsonToPieces(job, idParser, getPieceType))
}
}
object BisProvider {
def apply(): Behavior[BiSProviderMessage] =
Behaviors.setup[BiSProviderMessage](context => new BisProvider(context))
private def parseBisJsonToPieces(job: Job.Job,
idParser: (Job.Job, JsObject) => Future[Map[String, Long]],
pieceTypes: Seq[Long] => Future[Map[Long, PieceType.PieceType]])
(js: JsObject)
(implicit executionContext: ExecutionContext): Future[Seq[Piece]] =
idParser(job, js).flatMap { pieces =>
pieceTypes(pieces.values.toSeq).map { types =>
pieces.view.mapValues(types).map {
case (piece, pieceType) => Piece(piece, pieceType, job)
}.toSeq
}
}
def remapKey(key: String): Option[String] = key match {
case "mainhand" => Some("weapon")
case "chest" => Some("body")
case "ringLeft" | "fingerL" => Some("left ring")
case "ringRight" | "fingerR" => Some("right ring")
case "weapon" | "head" | "body" | "hands" | "waist" | "legs" | "feet" | "ears" | "neck" | "wrist" | "wrists" => Some(key)
case _ => None
}
}

View File

@ -1,30 +0,0 @@
/*
* Copyright (c) 2019 Evgeniy Alekseev.
*
* This file is part of ffxivbis
* (see https://github.com/arcan1s/ffxivbis).
*
* License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause
*/
package me.arcanis.ffxivbis.service.bis
import akka.http.scaladsl.model.Uri
import me.arcanis.ffxivbis.models.Job
import spray.json.{JsNumber, JsObject}
import scala.concurrent.{ExecutionContext, Future}
object Etro {
def idParser(job: Job.Job, js: JsObject)
(implicit executionContext: ExecutionContext): Future[Map[String, Long]] =
Future {
js.fields.foldLeft(Map.empty[String, Long]) {
case (acc, (key, JsNumber(id))) => BisProvider.remapKey(key).map(k => acc + (k -> id.toLong)).getOrElse(acc)
case (acc, _) => acc
}
}
def uri(root: Uri, id: String): Uri =
root.withPath(Uri.Path / "api" / "gearsets" / id)
}

View File

@ -1,47 +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.actor.ClassicActorSystemProvider
import akka.http.scaladsl.Http
import akka.http.scaladsl.model.headers.Location
import akka.http.scaladsl.model.{HttpRequest, HttpResponse, Uri}
import akka.stream.Materializer
import akka.stream.scaladsl.{Keep, Sink}
import akka.util.ByteString
import spray.json._
import scala.concurrent.{ExecutionContext, Future}
trait RequestExecutor {
def system: ClassicActorSystemProvider
private val http = Http()(system)
implicit val materializer: Materializer = Materializer.createMaterializer(system)
implicit val executionContext: ExecutionContext =
system.classicSystem.dispatchers.lookup("me.arcanis.ffxivbis.default-dispatcher")
def sendRequest[T](uri: Uri, parser: JsObject => Future[T]): Future[T] =
http.singleRequest(HttpRequest(uri = uri)).map {
case r: HttpResponse if r.status.isRedirection() =>
val location = r.header[Location].get.uri
sendRequest(uri.withPath(location.path), parser)
case HttpResponse(status, _, entity, _) if status.isSuccess() =>
entity.dataBytes
.fold(ByteString.empty)(_ ++ _)
.map(_.utf8String)
.map(result => parser(result.parseJson.asJsObject))
.toMat(Sink.head)(Keep.right)
.run().flatten
case other => Future.failed(new Error(s"Invalid response from server $other"))
}.flatten
def shutdown(): Unit = http.shutdownAllConnectionPools()
}

View File

@ -1,112 +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.PieceType
import spray.json._
import scala.concurrent.{ExecutionContext, Future}
import scala.util.Try
trait XivApi extends RequestExecutor {
private val config = system.classicSystem.settings.config
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
def getPieceType(itemIds: Seq[Long]): Future[Map[Long, PieceType.PieceType]] = {
val uriForItems = Uri(xivapiUrl)
.withPath(Uri.Path / "item")
.withQuery(Uri.Query(Map(
"columns" -> Seq("ID", "GameContentLinks").mkString(","),
"ids" -> itemIds.mkString(","),
"private_key" -> xivapiKey.getOrElse("")
)))
sendRequest(uriForItems, XivApi.parseXivapiJsonToShop).flatMap { shops =>
val shopIds = shops.values.map(_._2).toSet
val columns = shops.values.map(pair => s"ItemCost${pair._1}").toSet
val uriForShops = Uri(xivapiUrl)
.withPath(Uri.Path / "specialshop")
.withQuery(Uri.Query(Map(
"columns" -> (columns + "ID").mkString(","),
"ids" -> shopIds.mkString(","),
"private_key" -> xivapiKey.getOrElse("")
)))
sendRequest(uriForShops, XivApi.parseXivapiJsonToType(shops))
}
}
}
object XivApi {
private def parseXivapiJsonToShop(js: JsObject)
(implicit executionContext: ExecutionContext): Future[Map[Long, (String, Long)]] = {
def extractTraderId(js: JsObject) = {
js.fields
.get("Recipe").map(_ => "crafted" -> -1L) // you can craft this item
.orElse { // lets try shop items
js.fields("SpecialShop").asJsObject
.fields.collectFirst {
case (shopName, JsArray(array)) if shopName.startsWith("ItemReceive") =>
val shopId = array.head match {
case JsNumber(id) => id.toLong
case other => throw deserializationError(s"Could not parse $other")
}
shopName.replace("ItemReceive", "") -> shopId
}
}.getOrElse(throw deserializationError(s"Could not parse $js"))
}
Future {
js.fields("Results") match {
case array: JsArray =>
array.elements.map(_.asJsObject.getFields("ID", "GameContentLinks") match {
case Seq(JsNumber(id), shop) => id.toLong -> extractTraderId(shop.asJsObject)
case other => throw deserializationError(s"Could not parse $other")
}).toMap
case other => throw deserializationError(s"Could not parse $other")
}
}
}
private def parseXivapiJsonToType(shops: Map[Long, (String, Long)])(js: JsObject)
(implicit executionContext: ExecutionContext): Future[Map[Long, PieceType.PieceType]] =
Future {
val shopMap = js.fields("Results") match {
case array: JsArray =>
array.elements.map { shop =>
shop.asJsObject.fields("ID") match {
case JsNumber(id) => id.toLong -> shop.asJsObject
case other => throw deserializationError(s"Could not parse $other")
}
}.toMap
case other => throw deserializationError(s"Could not parse $other")
}
shops.map { case (itemId, (index, shopId)) =>
val pieceType =
if (index == "crafted" && shopId == -1) PieceType.Crafted
else {
Try(shopMap(shopId).fields(s"ItemCost$index").asJsObject)
.toOption
.getOrElse(throw new Exception(s"${shopMap(shopId).fields(s"ItemCost$index")}, $index"))
.getFields("IsUnique", "StackSize") match {
case Seq(JsNumber(isUnique), JsNumber(stackSize)) =>
if (isUnique == 1 || stackSize.toLong == 2000) PieceType.Tome // either upgraded gear or tomes found
else PieceType.Savage
case other => throw deserializationError(s"Could not parse $other")
}
}
itemId -> pieceType
}
}
}

View File

@ -8,30 +8,36 @@
*/ */
package me.arcanis.ffxivbis.service.impl package me.arcanis.ffxivbis.service.impl
import akka.actor.typed.scaladsl.Behaviors import akka.pattern.pipe
import me.arcanis.ffxivbis.messages.{AddPieceToBis, DatabaseMessage, GetBiS, RemovePieceFromBiS, RemovePiecesFromBiS} import me.arcanis.ffxivbis.models.{Piece, PlayerId}
import me.arcanis.ffxivbis.service.Database import me.arcanis.ffxivbis.service.Database
trait DatabaseBiSHandler { this: Database => trait DatabaseBiSHandler { this: Database =>
import DatabaseBiSHandler._
def bisHandler: DatabaseMessage.Handler = { def bisHandler: Receive = {
case AddPieceToBis(playerId, piece, client) => case AddPieceToBis(playerId, piece) =>
profile.insertPieceBiS(playerId, piece).foreach(_ => client ! ()) val client = sender()
Behaviors.same profile.insertPieceBiS(playerId, piece).pipeTo(client)
case GetBiS(partyId, maybePlayerId, client) => case GetBiS(partyId, maybePlayerId) =>
val client = sender()
getParty(partyId, withBiS = true, withLoot = false) getParty(partyId, withBiS = true, withLoot = false)
.map(filterParty(_, maybePlayerId)) .map(filterParty(_, maybePlayerId))
.foreach(client ! _) .pipeTo(client)
Behaviors.same
case RemovePieceFromBiS(playerId, piece, client) => case RemovePieceFromBiS(playerId, piece) =>
profile.deletePieceBiS(playerId, piece).foreach(_ => client ! ()) val client = sender()
Behaviors.same profile.deletePieceBiS(playerId, piece).pipeTo(client)
case RemovePiecesFromBiS(playerId, client) =>
profile.deletePiecesBiS(playerId).foreach(_ => client ! ())
Behaviors.same
} }
} }
object DatabaseBiSHandler {
case class AddPieceToBis(playerId: PlayerId, piece: Piece) extends Database.DatabaseRequest {
override def partyId: String = playerId.partyId
}
case class GetBiS(partyId: String, playerId: Option[PlayerId]) extends Database.DatabaseRequest
case class RemovePieceFromBiS(playerId: PlayerId, piece: Piece) extends Database.DatabaseRequest {
override def partyId: String = playerId.partyId
}
}

View File

@ -8,29 +8,23 @@
*/ */
package me.arcanis.ffxivbis.service.impl package me.arcanis.ffxivbis.service.impl
import akka.actor.typed.{Behavior, DispatcherSelector} import akka.actor.Props
import akka.actor.typed.scaladsl.{AbstractBehavior, ActorContext}
import com.typesafe.config.Config
import me.arcanis.ffxivbis.messages.DatabaseMessage
import me.arcanis.ffxivbis.service.Database import me.arcanis.ffxivbis.service.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 extends Database
extends AbstractBehavior[DatabaseMessage](context) with Database
with DatabaseBiSHandler with DatabaseLootHandler with DatabaseBiSHandler with DatabaseLootHandler
with DatabasePartyHandler with DatabaseUserHandler { with DatabasePartyHandler with DatabaseUserHandler {
override implicit val executionContext: ExecutionContext = { implicit val executionContext: ExecutionContext = context.dispatcher
val selector = DispatcherSelector.fromConfig("me.arcanis.ffxivbis.default-dispatcher") val profile = new DatabaseProfile(executionContext, context.system.settings.config)
context.system.dispatchers.lookup(selector)
}
override val config: Config = context.system.settings.config
override val profile: DatabaseProfile = new DatabaseProfile(executionContext, config)
override def onMessage(msg: DatabaseMessage): Behavior[DatabaseMessage] = handle(msg) override def receive: Receive =
private def handle: DatabaseMessage.Handler =
bisHandler orElse lootHandler orElse partyHandler orElse userHandler bisHandler orElse lootHandler orElse partyHandler orElse userHandler
} }
object DatabaseImpl {
def props: Props = Props(new DatabaseImpl)
}

View File

@ -8,35 +8,41 @@
*/ */
package me.arcanis.ffxivbis.service.impl package me.arcanis.ffxivbis.service.impl
import java.time.Instant import akka.pattern.pipe
import me.arcanis.ffxivbis.models.{Piece, PlayerId}
import akka.actor.typed.scaladsl.Behaviors
import me.arcanis.ffxivbis.messages.{AddPieceTo, DatabaseMessage, GetLoot, RemovePieceFrom, SuggestLoot}
import me.arcanis.ffxivbis.models.Loot
import me.arcanis.ffxivbis.service.Database import me.arcanis.ffxivbis.service.Database
trait DatabaseLootHandler { this: Database => trait DatabaseLootHandler { this: Database =>
import DatabaseLootHandler._
def lootHandler: DatabaseMessage.Handler = { def lootHandler: Receive = {
case AddPieceTo(playerId, piece, isFreeLoot, client) => case AddPieceTo(playerId, piece) =>
val loot = Loot(-1, piece, Instant.now, isFreeLoot) val client = sender()
profile.insertPiece(playerId, loot).foreach(_ => client ! ()) profile.insertPiece(playerId, piece).pipeTo(client)
Behaviors.same
case GetLoot(partyId, maybePlayerId, client) => case GetLoot(partyId, maybePlayerId) =>
val client = sender()
getParty(partyId, withBiS = false, withLoot = true) getParty(partyId, withBiS = false, withLoot = true)
.map(filterParty(_, maybePlayerId)) .map(filterParty(_, maybePlayerId))
.foreach(client ! _) .pipeTo(client)
Behaviors.same
case RemovePieceFrom(playerId, piece, client) => case RemovePieceFrom(playerId, piece) =>
profile.deletePiece(playerId, piece).foreach(_ => client ! ()) val client = sender()
Behaviors.same profile.deletePiece(playerId, piece).pipeTo(client)
case SuggestLoot(partyId, piece, client) => case SuggestLoot(partyId, piece) =>
getParty(partyId, withBiS = true, withLoot = true) val client = sender()
.map(_.suggestLoot(piece)) getParty(partyId, withBiS = true, withLoot = true).map(_.suggestLoot(piece)).pipeTo(client)
.foreach(client ! _)
Behaviors.same
} }
} }
object DatabaseLootHandler {
case class AddPieceTo(playerId: PlayerId, piece: Piece) extends Database.DatabaseRequest {
override def partyId: String = playerId.partyId
}
case class GetLoot(partyId: String, playerId: Option[PlayerId]) extends Database.DatabaseRequest
case class RemovePieceFrom(playerId: PlayerId, piece: Piece) extends Database.DatabaseRequest {
override def partyId: String = playerId.partyId
}
case class SuggestLoot(partyId: String, piece: Piece) extends Database.DatabaseRequest
}

View File

@ -8,48 +8,53 @@
*/ */
package me.arcanis.ffxivbis.service.impl package me.arcanis.ffxivbis.service.impl
import akka.actor.typed.scaladsl.Behaviors import akka.pattern.pipe
import me.arcanis.ffxivbis.messages.{AddPlayer, DatabaseMessage, GetParty, GetPartyDescription, GetPlayer, RemovePlayer, UpdateParty} import me.arcanis.ffxivbis.models.{BiS, Player, PlayerId}
import me.arcanis.ffxivbis.models.{BiS, Player}
import me.arcanis.ffxivbis.service.Database import me.arcanis.ffxivbis.service.Database
import scala.concurrent.Future import scala.concurrent.Future
trait DatabasePartyHandler { this: Database => trait DatabasePartyHandler { this: Database =>
import DatabasePartyHandler._
def partyHandler: DatabaseMessage.Handler = { def partyHandler: Receive = {
case AddPlayer(player, client) => case AddPlayer(player) =>
profile.insertPlayer(player).foreach(_ => client ! ()) val client = sender()
Behaviors.same profile.insertPlayer(player).pipeTo(client)
case GetParty(partyId, client) => case GetParty(partyId) =>
getParty(partyId, withBiS = true, withLoot = true).foreach(client ! _) val client = sender()
Behaviors.same getParty(partyId, withBiS = true, withLoot = true).pipeTo(client)
case GetPartyDescription(partyId, client) => case GetPlayer(playerId) =>
profile.getPartyDescription(partyId).foreach(client ! _) val client = sender()
Behaviors.same
case GetPlayer(playerId, client) =>
val player = profile.getPlayerFull(playerId).flatMap { maybePlayerData => val player = profile.getPlayerFull(playerId).flatMap { maybePlayerData =>
Future.traverse(maybePlayerData.toSeq) { playerData => Future.traverse(maybePlayerData.toSeq) { playerData =>
for { for {
bis <- profile.getPiecesBiS(playerId) bis <- profile.getPiecesBiS(playerId)
loot <- profile.getPieces(playerId) loot <- profile.getPieces(playerId)
} yield Player(playerData.id, playerId.partyId, playerId.job, } yield Player(playerId.partyId, playerId.job, playerId.nick,
playerId.nick, BiS(bis.map(_.piece)), loot, BiS(bis.map(_.piece)), loot.map(_.piece),
playerData.link, playerData.priority) playerData.link, playerData.priority)
} }
}.map(_.headOption) }.map(_.headOption)
player.foreach(client ! _) player.pipeTo(client)
Behaviors.same
case RemovePlayer(playerId, client) => case RemovePlayer(playerId) =>
profile.deletePlayer(playerId).foreach(_ => client ! ()) val client = sender()
Behaviors.same profile.deletePlayer(playerId).pipeTo(client)
}
case UpdateParty(description, client) => }
profile.insertPartyDescription(description).foreach(_ => client ! ())
Behaviors.same object DatabasePartyHandler {
case class AddPlayer(player: Player) extends Database.DatabaseRequest {
override def partyId: String = player.partyId
}
case class GetParty(partyId: String) extends Database.DatabaseRequest
case class GetPlayer(playerId: PlayerId) extends Database.DatabaseRequest {
override def partyId: String = playerId.partyId
}
case class RemovePlayer(playerId: PlayerId) extends Database.DatabaseRequest {
override def partyId: String = playerId.partyId
} }
} }

View File

@ -8,32 +8,43 @@
*/ */
package me.arcanis.ffxivbis.service.impl package me.arcanis.ffxivbis.service.impl
import akka.actor.typed.scaladsl.Behaviors import akka.pattern.pipe
import me.arcanis.ffxivbis.messages.{AddUser, DatabaseMessage, DeleteUser, Exists, GetUser, GetUsers} import me.arcanis.ffxivbis.models.User
import me.arcanis.ffxivbis.service.Database import me.arcanis.ffxivbis.service.Database
trait DatabaseUserHandler { this: Database => trait DatabaseUserHandler { this: Database =>
import DatabaseUserHandler._
def userHandler: DatabaseMessage.Handler = { def userHandler: Receive = {
case AddUser(user, isHashedPassword, client) => case AddUser(user, isHashedPassword) =>
val client = sender()
val toInsert = if (isHashedPassword) user else user.withHashedPassword val toInsert = if (isHashedPassword) user else user.withHashedPassword
profile.insertUser(toInsert).foreach(_ => client ! ()) profile.insertUser(toInsert).pipeTo(client)
Behaviors.same
case DeleteUser(partyId, username, client) => case DeleteUser(partyId, username) =>
profile.deleteUser(partyId, username).foreach(_ => client ! ()) val client = sender()
Behaviors.same profile.deleteUser(partyId, username).pipeTo(client)
case Exists(partyId, client) => case Exists(partyId) =>
profile.exists(partyId).foreach(client ! _) val client = sender()
Behaviors.same profile.exists(partyId).pipeTo(client)
case GetUser(partyId, username, client) => case GetUser(partyId, username) =>
profile.getUser(partyId, username).foreach(client ! _) val client = sender()
Behaviors.same profile.getUser(partyId, username).pipeTo(client)
case GetUsers(partyId, client) => case GetUsers(partyId) =>
profile.getUsers(partyId).foreach(client ! _) val client = sender()
Behaviors.same profile.getUsers(partyId).pipeTo(client)
} }
} }
object DatabaseUserHandler {
case class AddUser(user: User, isHashedPassword: Boolean) extends Database.DatabaseRequest {
override def partyId: String = user.partyId
}
case class DeleteUser(partyId: String, username: String) extends Database.DatabaseRequest
case class Exists(partyId: String) extends Database.DatabaseRequest
case class GetUser(partyId: String, username: String) extends Database.DatabaseRequest
case class GetUsers(partyId: String) extends Database.DatabaseRequest
}

View File

@ -8,37 +8,32 @@
*/ */
package me.arcanis.ffxivbis.storage package me.arcanis.ffxivbis.storage
import java.time.Instant import me.arcanis.ffxivbis.models.{Job, Loot, Piece}
import slick.lifted.{ForeignKeyQuery, Index, PrimaryKey}
import me.arcanis.ffxivbis.models.{Job, Loot, Piece, PieceType}
import slick.lifted.ForeignKeyQuery
import scala.concurrent.Future 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, isTome: Int, job: String) {
pieceType: String, job: String) { def toLoot: Loot = Loot(playerId, Piece(piece, isTome == 1, Job.withName(job)))
def toLoot: Loot = Loot(
playerId, 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(playerId, DatabaseProfile.now, piece.piece, BiSRep(playerId, DatabaseProfile.now, piece.piece, if (piece.isTome) 1 else 0,
piece.pieceType.toString, piece.job.toString) piece.job.toString)
} }
class BiSPieces(tag: Tag) extends Table[BiSRep](tag, "bis") { class BiSPieces(tag: Tag) extends Table[BiSRep](tag, "bis") {
def playerId: Rep[Long] = column[Long]("player_id", O.PrimaryKey) def playerId: Rep[Long] = column[Long]("player_id", O.PrimaryKey)
def created: Rep[Long] = column[Long]("created") def created: Rep[Long] = column[Long]("created")
def piece: Rep[String] = column[String]("piece", O.PrimaryKey) def piece: Rep[String] = column[String]("piece", O.PrimaryKey)
def pieceType: Rep[String] = column[String]("piece_type") def isTome: Rep[Int] = column[Int]("is_tome")
def job: Rep[String] = column[String]("job") def job: Rep[String] = column[String]("job")
def * = def * =
(playerId, created, piece, pieceType, job) <> ((BiSRep.apply _).tupled, BiSRep.unapply) (playerId, created, piece, isTome, job) <> ((BiSRep.apply _).tupled, BiSRep.unapply)
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)
@ -46,21 +41,14 @@ trait BiSProfile { this: DatabaseProfile =>
def deletePieceBiSById(piece: Piece)(playerId: Long): Future[Int] = def deletePieceBiSById(piece: Piece)(playerId: Long): Future[Int] =
db.run(pieceBiS(BiSRep.fromPiece(playerId, piece)).delete) db.run(pieceBiS(BiSRep.fromPiece(playerId, piece)).delete)
def deletePiecesBiSById(playerId: Long): Future[Int] =
db.run(piecesBiS(Seq(playerId)).delete)
def getPiecesBiSById(playerId: Long): Future[Seq[Loot]] = getPiecesBiSById(Seq(playerId)) def getPiecesBiSById(playerId: Long): Future[Seq[Loot]] = getPiecesBiSById(Seq(playerId))
def getPiecesBiSById(playerIds: Seq[Long]): Future[Seq[Loot]] = def getPiecesBiSById(playerIds: Seq[Long]): Future[Seq[Loot]] =
db.run(piecesBiS(playerIds).result).map(_.map(_.toLoot)) db.run(piecesBiS(playerIds).result).map(_.map(_.toLoot))
def insertPieceBiSById(piece: Piece)(playerId: Long): Future[Int] = def insertPieceBiSById(piece: Piece)(playerId: Long): Future[Int] =
getPiecesBiSById(playerId).flatMap { db.run(bisTable.insertOrUpdate(BiSRep.fromPiece(playerId, piece)))
case pieces if pieces.exists(loot => loot.piece.strictEqual(piece)) => Future.successful(0)
case _ => db.run(bisTable.insertOrUpdate(BiSRep.fromPiece(playerId, piece)))
}
private def pieceBiS(piece: BiSRep) = private def pieceBiS(piece: BiSRep) =
piecesBiS(Seq(piece.playerId)).filter { stored => piecesBiS(Seq(piece.playerId)).filter(_.piece === piece.piece)
(stored.piece === piece.piece) && (stored.pieceType === piece.pieceType)
}
private def piecesBiS(playerIds: Seq[Long]) = private def piecesBiS(playerIds: Seq[Long]) =
bisTable.filter(_.playerId.inSet(playerIds.toSet)) bisTable.filter(_.playerId.inSet(playerIds.toSet))
} }

View File

@ -18,7 +18,7 @@ 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 PlayersProfile with UsersProfile {
implicit val executionContext: ExecutionContext = context implicit val executionContext: ExecutionContext = context
@ -29,15 +29,12 @@ class DatabaseProfile(context: ExecutionContext, config: Config)
val bisTable: TableQuery[BiSPieces] = TableQuery[BiSPieces] val bisTable: TableQuery[BiSPieces] = TableQuery[BiSPieces]
val lootTable: TableQuery[LootPieces] = TableQuery[LootPieces] val lootTable: TableQuery[LootPieces] = TableQuery[LootPieces]
val partiesTable: TableQuery[Parties] = TableQuery[Parties]
val playersTable: TableQuery[Players] = TableQuery[Players] val playersTable: TableQuery[Players] = TableQuery[Players]
val usersTable: TableQuery[Users] = TableQuery[Users] val usersTable: TableQuery[Users] = TableQuery[Users]
// generic bis api // generic bis api
def deletePieceBiS(playerId: PlayerId, piece: Piece): Future[Int] = def deletePieceBiS(playerId: PlayerId, piece: Piece): Future[Int] =
byPlayerId(playerId, deletePieceBiSById(piece)) byPlayerId(playerId, deletePieceBiSById(piece))
def deletePiecesBiS(playerId: PlayerId): Future[Int] =
byPlayerId(playerId, deletePiecesBiSById)
def getPiecesBiS(playerId: PlayerId): Future[Seq[Loot]] = def getPiecesBiS(playerId: PlayerId): Future[Seq[Loot]] =
byPlayerId(playerId, getPiecesBiSById) byPlayerId(playerId, getPiecesBiSById)
def getPiecesBiS(partyId: String): Future[Seq[Loot]] = def getPiecesBiS(partyId: String): Future[Seq[Loot]] =
@ -46,29 +43,25 @@ class DatabaseProfile(context: ExecutionContext, config: Config)
byPlayerId(playerId, insertPieceBiSById(piece)) byPlayerId(playerId, insertPieceBiSById(piece))
// generic loot api // generic loot api
def deletePiece(playerId: PlayerId, piece: Piece): Future[Int] = { def deletePiece(playerId: PlayerId, piece: Piece): Future[Int] =
// we don't really care here about loot byPlayerId(playerId, deletePieceById(piece))
val loot = Loot(-1, piece, Instant.now, isFreeLoot = false)
byPlayerId(playerId, deletePieceById(loot))
}
def getPieces(playerId: PlayerId): Future[Seq[Loot]] = def getPieces(playerId: PlayerId): Future[Seq[Loot]] =
byPlayerId(playerId, getPiecesById) byPlayerId(playerId, getPiecesById)
def getPieces(partyId: String): Future[Seq[Loot]] = def getPieces(partyId: String): Future[Seq[Loot]] =
byPartyId(partyId, getPiecesById) byPartyId(partyId, getPiecesById)
def insertPiece(playerId: PlayerId, loot: Loot): Future[Int] = def insertPiece(playerId: PlayerId, piece: Piece): Future[Int] =
byPlayerId(playerId, insertPieceById(loot)) byPlayerId(playerId, insertPieceById(piece))
private def byPartyId[T](partyId: String, callback: Seq[Long] => Future[T]): Future[T] = private def byPartyId[T](partyId: String, callback: Seq[Long] => Future[T]): Future[T] =
getPlayers(partyId).flatMap(callback) getPlayers(partyId).map(callback).flatten
private def byPlayerId[T](playerId: PlayerId, callback: Long => Future[T]): Future[T] = private def byPlayerId[T](playerId: PlayerId, callback: Long => Future[T]): Future[T] =
getPlayer(playerId).flatMap { getPlayer(playerId).map {
case Some(id) => callback(id) case Some(id) => callback(id)
case None => Future.failed(new Error(s"Could not find player $playerId")) case None => Future.failed(new Error(s"Could not find player $playerId"))
} }.flatten
} }
object DatabaseProfile { object DatabaseProfile {
def now: Long = Instant.now.toEpochMilli def now: Long = Instant.now.toEpochMilli
def getSection(config: Config): Config = { def getSection(config: Config): Config = {
val section = config.getString("me.arcanis.ffxivbis.database.mode") val section = config.getString("me.arcanis.ffxivbis.database.mode")

View File

@ -8,9 +8,7 @@
*/ */
package me.arcanis.ffxivbis.storage package me.arcanis.ffxivbis.storage
import java.time.Instant import me.arcanis.ffxivbis.models.{Job, Loot, Piece}
import me.arcanis.ffxivbis.models.{Job, Loot, Piece, PieceType}
import slick.lifted.{ForeignKeyQuery, Index} import slick.lifted.{ForeignKeyQuery, Index}
import scala.concurrent.Future import scala.concurrent.Future
@ -18,19 +16,14 @@ 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(lootId: Option[Long], playerId: Long, created: Long, piece: String,
piece: String, pieceType: String, job: String, isTome: Int, job: String) {
isFreeLoot: Int) { def toLoot: Loot = Loot(playerId, Piece(piece, isTome == 1, Job.withName(job)))
def toLoot: Loot = Loot(
playerId,
Piece(piece, PieceType.withName(pieceType), Job.withName(job)),
Instant.ofEpochMilli(created), isFreeLoot == 1)
} }
object LootRep { object LootRep {
def fromLoot(playerId: Long, loot: Loot): LootRep = def fromPiece(playerId: Long, piece: Piece) =
LootRep(None, playerId, loot.timestamp.toEpochMilli, loot.piece.piece, LootRep(None, playerId, DatabaseProfile.now, piece.piece, if (piece.isTome) 1 else 0,
loot.piece.pieceType.toString, loot.piece.job.toString, 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") {
@ -38,12 +31,11 @@ trait LootProfile { this: DatabaseProfile =>
def playerId: Rep[Long] = column[Long]("player_id") def playerId: Rep[Long] = column[Long]("player_id")
def created: Rep[Long] = column[Long]("created") def created: Rep[Long] = column[Long]("created")
def piece: Rep[String] = column[String]("piece") def piece: Rep[String] = column[String]("piece")
def pieceType: Rep[String] = column[String]("piece_type") def isTome: Rep[Int] = column[Int]("is_tome")
def job: Rep[String] = column[String]("job") def job: Rep[String] = column[String]("job")
def isFreeLoot: Rep[Int] = column[Int]("is_free_loot")
def * = def * =
(lootId.?, playerId, created, piece, pieceType, job, isFreeLoot) <> ((LootRep.apply _).tupled, LootRep.unapply) (lootId.?, playerId, created, piece, isTome, job) <> ((LootRep.apply _).tupled, LootRep.unapply)
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)
@ -51,16 +43,16 @@ trait LootProfile { this: DatabaseProfile =>
index("loot_owner_idx", (playerId), unique = false) index("loot_owner_idx", (playerId), unique = false)
} }
def deletePieceById(loot: Loot)(playerId: Long): Future[Int] = def deletePieceById(piece: Piece)(playerId: Long): Future[Int] =
db.run(pieceLoot(LootRep.fromLoot(playerId, loot)).map(_.lootId).max.result).flatMap { db.run(pieceLoot(LootRep.fromPiece(playerId, piece)).map(_.lootId).max.result).flatMap {
case Some(id) => db.run(lootTable.filter(_.lootId === id).delete) case Some(id) => db.run(lootTable.filter(_.lootId === id).delete)
case _ => throw new IllegalArgumentException(s"Could not find piece $loot belong to $playerId") case _ => throw new IllegalArgumentException(s"Could not find piece $piece belong to $playerId")
} }
def getPiecesById(playerId: Long): Future[Seq[Loot]] = getPiecesById(Seq(playerId)) def getPiecesById(playerId: Long): Future[Seq[Loot]] = getPiecesById(Seq(playerId))
def getPiecesById(playerIds: Seq[Long]): Future[Seq[Loot]] = def getPiecesById(playerIds: Seq[Long]): Future[Seq[Loot]] =
db.run(piecesLoot(playerIds).result).map(_.map(_.toLoot)) db.run(piecesLoot(playerIds).result).map(_.map(_.toLoot))
def insertPieceById(loot: Loot)(playerId: Long): Future[Int] = def insertPieceById(piece: Piece)(playerId: Long): Future[Int] =
db.run(lootTable.insertOrUpdate(LootRep.fromLoot(playerId, loot))) db.run(lootTable.insertOrUpdate(LootRep.fromPiece(playerId, piece)))
private def pieceLoot(piece: LootRep) = private def pieceLoot(piece: LootRep) =
piecesLoot(Seq(piece.playerId)).filter(_.piece === piece.piece) piecesLoot(Seq(piece.playerId)).filter(_.piece === piece.piece)

View File

@ -15,7 +15,6 @@ import org.flywaydb.core.api.configuration.ClassicConfiguration
import scala.concurrent.Future import scala.concurrent.Future
class Migration(config: Config) { class Migration(config: Config) {
def performMigration(): Future[Int] = { def performMigration(): Future[Int] = {
val section = DatabaseProfile.getSection(config) val section = DatabaseProfile.getSection(config)
@ -38,6 +37,5 @@ class Migration(config: Config) {
} }
object Migration { object Migration {
def apply(config: Config): Future[Int] = new Migration(config).performMigration() def apply(config: Config): Future[Int] = new Migration(config).performMigration()
} }

View File

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

View File

@ -9,17 +9,17 @@
package me.arcanis.ffxivbis.storage package me.arcanis.ffxivbis.storage
import me.arcanis.ffxivbis.models.{BiS, Job, Player, PlayerId} import me.arcanis.ffxivbis.models.{BiS, Job, Player, PlayerId}
import slick.lifted.{Index, PrimaryKey}
import scala.concurrent.Future 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(partyId: String, playerId: Option[Long], created: Long, nick: String,
nick: String, job: String, link: Option[String], priority: Int) { job: String, link: Option[String], priority: Int) {
def toPlayer: Player = def toPlayer: Player =
Player(playerId.getOrElse(-1), partyId, Job.withName(job), nick, Player(partyId, Job.withName(job), nick, BiS(Seq.empty), List.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 =
@ -40,6 +40,7 @@ trait PlayersProfile { this: DatabaseProfile =>
(partyId, playerId.?, created, nick, job, bisLink, priority) <> ((PlayerRep.apply _).tupled, PlayerRep.unapply) (partyId, playerId.?, created, nick, job, bisLink, priority) <> ((PlayerRep.apply _).tupled, PlayerRep.unapply)
} }
def deletePlayer(playerId: PlayerId): Future[Int] = db.run(player(playerId).delete) def deletePlayer(playerId: PlayerId): Future[Int] = db.run(player(playerId).delete)
def getParty(partyId: String): Future[Map[Long, Player]] = def getParty(partyId: String): Future[Map[Long, Player]] =
db.run(players(partyId).result).map(_.foldLeft(Map.empty[Long, Player]) { db.run(players(partyId).result).map(_.foldLeft(Map.empty[Long, Player]) {
@ -53,10 +54,10 @@ trait PlayersProfile { this: DatabaseProfile =>
def getPlayers(partyId: String): Future[Seq[Long]] = def getPlayers(partyId: String): Future[Seq[Long]] =
db.run(players(partyId).map(_.playerId).result) db.run(players(partyId).map(_.playerId).result)
def insertPlayer(playerObj: Player): Future[Int] = def insertPlayer(playerObj: Player): Future[Int] =
getPlayer(playerObj.playerId).flatMap { getPlayer(playerObj.playerId).map {
case Some(id) => db.run(playersTable.update(PlayerRep.fromPlayer(playerObj, Some(id)))) case Some(id) => db.run(playersTable.update(PlayerRep.fromPlayer(playerObj, Some(id))))
case _ => db.run(playersTable.insertOrUpdate(PlayerRep.fromPlayer(playerObj, None))) case _ => db.run(playersTable.insertOrUpdate(PlayerRep.fromPlayer(playerObj, None)))
} }.flatten
private def player(playerId: PlayerId) = private def player(playerId: PlayerId) =
playersTable playersTable

View File

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

View File

@ -5,35 +5,33 @@ import me.arcanis.ffxivbis.models._
object Fixtures { object Fixtures {
lazy val bis: BiS = BiS( lazy val bis: BiS = BiS(
Seq( Seq(
Weapon(pieceType = PieceType.Savage ,Job.DNC), Weapon(isTome = false ,Job.DNC),
Head(pieceType = PieceType.Savage, Job.DNC), Head(isTome = false, Job.DNC),
Body(pieceType = PieceType.Savage, Job.DNC), Body(isTome = false, Job.DNC),
Hands(pieceType = PieceType.Tome, Job.DNC), Hands(isTome = true, Job.DNC),
Waist(pieceType = PieceType.Tome, Job.DNC), Waist(isTome = true, Job.DNC),
Legs(pieceType = PieceType.Tome, Job.DNC), Legs(isTome = true, Job.DNC),
Feet(pieceType = PieceType.Savage, Job.DNC), Feet(isTome = false, Job.DNC),
Ears(pieceType = PieceType.Savage, Job.DNC), Ears(isTome = false, Job.DNC),
Neck(pieceType = PieceType.Tome, Job.DNC), Neck(isTome = true, Job.DNC),
Wrist(pieceType = PieceType.Savage, Job.DNC), Wrist(isTome = false, Job.DNC),
Ring(pieceType = PieceType.Tome, Job.DNC, "left ring"), Ring(isTome = true, Job.DNC, "leftRing"),
Ring(pieceType = PieceType.Tome, Job.DNC, "right ring") Ring(isTome = true, Job.DNC, "rightRing")
) )
) )
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 lootWeapon: Piece = Weapon(pieceType = PieceType.Tome, Job.AnyJob) lazy val lootWeapon: Piece = Weapon(isTome = true, Job.AnyJob)
lazy val lootBody: Piece = Body(pieceType = PieceType.Savage, Job.AnyJob) lazy val lootBody: Piece = Body(isTome = false, Job.AnyJob)
lazy val lootBodyCrafted: Piece = Body(pieceType = PieceType.Crafted, Job.AnyJob) lazy val lootHands: Piece = Hands(isTome = true, Job.AnyJob)
lazy val lootHands: Piece = Hands(pieceType = PieceType.Tome, Job.AnyJob) lazy val lootWaist: Piece = Waist(isTome = true, Job.AnyJob)
lazy val lootWaist: Piece = Waist(pieceType = PieceType.Tome, Job.AnyJob) lazy val lootLegs: Piece = Legs(isTome = false, Job.AnyJob)
lazy val lootLegs: Piece = Legs(pieceType = PieceType.Savage, Job.AnyJob) lazy val lootEars: Piece = Ears(isTome = false, Job.AnyJob)
lazy val lootEars: Piece = Ears(pieceType = PieceType.Savage, Job.AnyJob) lazy val lootRing: Piece = Ring(isTome = true, Job.AnyJob)
lazy val lootRing: Piece = Ring(pieceType = PieceType.Tome, Job.AnyJob) lazy val lootLeftRing: Piece = Ring(isTome = true, Job.AnyJob, "leftRing")
lazy val lootLeftRing: Piece = Ring(pieceType = PieceType.Tome, Job.AnyJob, "left ring") lazy val lootRightRing: Piece = Ring(isTome = true, Job.AnyJob, "rightRing")
lazy val lootRightRing: Piece = Ring(pieceType = PieceType.Tome, Job.AnyJob, "right ring")
lazy val lootUpgrade: Piece = BodyUpgrade lazy val lootUpgrade: Piece = BodyUpgrade
lazy val loot: Seq[Piece] = Seq(lootBody, lootHands, lootLegs, lootUpgrade) lazy val loot: Seq[Piece] = Seq(lootBody, lootHands, lootLegs, lootUpgrade)
@ -41,7 +39,7 @@ object Fixtures {
lazy val partyId2: String = Party.randomPartyId lazy val partyId2: String = Party.randomPartyId
lazy val playerEmpty: Player = lazy val playerEmpty: Player =
Player(1, partyId, Job.DNC, "Siuan Sanche", BiS.empty, Seq.empty, Some(link)) Player(partyId, Job.DNC, "Siuan Sanche", BiS(), Seq.empty, Some(link))
lazy val playerWithBiS: Player = playerEmpty.copy(bis = bis) lazy val playerWithBiS: Player = playerEmpty.copy(bis = bis)
lazy val userPassword: String = "password" lazy val userPassword: String = "password"

View File

@ -16,13 +16,13 @@ object Settings {
replace(default, values.toList) replace(default, values.toList)
} }
def clearDatabase(config: Config): Unit = def clearDatabase(config: Config): Unit = {
config.getString("me.arcanis.ffxivbis.database.sqlite.db.url").split(":") val databasePath =
.lastOption.foreach { databasePath => config.getString("me.arcanis.ffxivbis.database.sqlite.db.url").split(":").last
val databaseFile = new File(databasePath) val databaseFile = new File(databasePath)
if (databaseFile.exists) if (databaseFile.exists)
databaseFile.delete() databaseFile.delete()
} }
def randomDatabasePath: String = File.createTempFile("ffxivdb-",".db").toPath.toString def randomDatabasePath: String = File.createTempFile("ffxivdb-",".db").toPath.toString
def withRandomDatabase: Config = def withRandomDatabase: Config =
config(Map("me.arcanis.ffxivbis.database.sqlite.db.url" -> s"jdbc:sqlite:$randomDatabasePath")) config(Map("me.arcanis.ffxivbis.database.sqlite.db.url" -> s"jdbc:sqlite:$randomDatabasePath"))

View File

@ -1,64 +1,50 @@
package me.arcanis.ffxivbis.http.api.v1 package me.arcanis.ffxivbis.http.api.v1
import akka.actor.testkit.typed.scaladsl.ActorTestKit import akka.actor.ActorRef
import akka.actor.typed.scaladsl.AskPattern.Askable
import akka.http.scaladsl.model.{StatusCodes, Uri} import akka.http.scaladsl.model.{StatusCodes, Uri}
import akka.http.scaladsl.model.headers.{Authorization, BasicHttpCredentials} import akka.http.scaladsl.model.headers.{Authorization, BasicHttpCredentials}
import akka.http.scaladsl.testkit.{RouteTestTimeout, ScalatestRouteTest} import akka.http.scaladsl.testkit.{RouteTestTimeout, ScalatestRouteTest}
import akka.http.scaladsl.server._
import akka.pattern.ask
import akka.testkit.TestKit 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.messages.{AddPlayer, AddUser} import me.arcanis.ffxivbis.models.BiS
import me.arcanis.ffxivbis.models.{BiS, Job} import me.arcanis.ffxivbis.service.{Ariyala, PartyService, impl}
import me.arcanis.ffxivbis.service.bis.BisProvider
import me.arcanis.ffxivbis.service.{Database, PartyService}
import me.arcanis.ffxivbis.storage.Migration import me.arcanis.ffxivbis.storage.Migration
import me.arcanis.ffxivbis.utils.Compare import org.scalatest.{Matchers, WordSpec}
import org.scalatest.matchers.should.Matchers
import org.scalatest.wordspec.AnyWordSpecLike
import scala.concurrent.Await import scala.concurrent.Await
import scala.concurrent.duration._ import scala.concurrent.duration._
import scala.language.postfixOps import scala.language.postfixOps
class BiSEndpointTest extends AnyWordSpecLike with Matchers with ScalatestRouteTest with JsonSupport { class BiSEndpointTest extends WordSpec
with Matchers with ScalatestRouteTest with JsonSupport {
private val testKit = ActorTestKit(Settings.withRandomDatabase) private val auth: Authorization =
override val testConfig: Config = testKit.system.settings.config
private val auth =
Authorization(BasicHttpCredentials(Fixtures.userAdmin.username, Fixtures.userPassword)) Authorization(BasicHttpCredentials(Fixtures.userAdmin.username, Fixtures.userPassword))
private val endpoint = Uri(s"/party/${Fixtures.partyId}/bis") private val endpoint: Uri = Uri(s"/party/${Fixtures.partyId}/bis")
private val playerId = PlayerIdResponse.fromPlayerId(Fixtures.playerEmpty.playerId) private val playerId = PlayerIdResponse.fromPlayerId(Fixtures.playerEmpty.playerId)
private val askTimeout = 60 seconds private val timeout: FiniteDuration = 60 seconds
implicit private val routeTimeout: RouteTestTimeout = RouteTestTimeout(askTimeout) implicit private val routeTimeout: RouteTestTimeout = RouteTestTimeout(timeout)
private val storage = testKit.spawn(Database()) private val storage: ActorRef = system.actorOf(impl.DatabaseImpl.props)
private val provider = testKit.spawn(BisProvider()) private val ariyala: ActorRef = system.actorOf(Ariyala.props)
private val party = testKit.spawn(PartyService(storage)) private val party: ActorRef = system.actorOf(PartyService.props(storage))
private val route = new BiSEndpoint(party, provider)(askTimeout, testKit.scheduler).route private val route: Route = new BiSEndpoint(party, ariyala)(timeout).route
override def testConfig: Config = Settings.withRandomDatabase
override def beforeAll: Unit = { override def beforeAll: Unit = {
Await.result(Migration(testConfig), askTimeout) Await.result(Migration(system.settings.config), timeout)
Await.result(storage.ask(AddUser(Fixtures.userAdmin, isHashedPassword = true, _))(askTimeout, testKit.scheduler), askTimeout) Await.result((storage ? impl.DatabaseUserHandler.AddUser(Fixtures.userAdmin, isHashedPassword = true))(timeout).mapTo[Int], timeout)
Await.result(storage.ask(AddPlayer(Fixtures.playerEmpty, _))(askTimeout, testKit.scheduler), askTimeout) Await.result((storage ? impl.DatabasePartyHandler.AddPlayer(Fixtures.playerEmpty))(timeout).mapTo[Int], timeout)
} }
override def afterAll: Unit = { override def afterAll: Unit = {
super.afterAll()
Settings.clearDatabase(testConfig)
TestKit.shutdownActorSystem(system) TestKit.shutdownActorSystem(system)
testKit.shutdownTestKit() Settings.clearDatabase(system.settings.config)
}
private def compareBiSResponse(actual: PlayerResponse, expected: PlayerResponse): Unit = {
actual.partyId shouldEqual expected.partyId
actual.nick shouldEqual expected.nick
actual.job shouldEqual expected.job
Compare.seqEquals(actual.bis.get, expected.bis.get) shouldEqual true
actual.link shouldEqual expected.link
actual.priority shouldEqual expected.priority
} }
"api v1 bis endpoint" must { "api v1 bis endpoint" must {
@ -74,19 +60,17 @@ class BiSEndpointTest extends AnyWordSpecLike with Matchers with ScalatestRouteT
"return best in slot set" in { "return best in slot set" in {
val uri = endpoint.withQuery(Uri.Query(Map("nick" -> playerId.nick, "job" -> playerId.job))) val uri = endpoint.withQuery(Uri.Query(Map("nick" -> playerId.nick, "job" -> playerId.job)))
val response = PlayerResponse.fromPlayer(Fixtures.playerEmpty.withBiS(Some(Fixtures.bis))) val response = Seq(PlayerResponse.fromPlayer(Fixtures.playerEmpty.withBiS(Some(Fixtures.bis))))
Get(uri).withHeaders(auth) ~> route ~> check { Get(uri).withHeaders(auth) ~> route ~> check {
status shouldEqual StatusCodes.OK status shouldEqual StatusCodes.OK
val actual = responseAs[Seq[PlayerResponse]] responseAs[Seq[PlayerResponse]] shouldEqual response
actual.length shouldEqual 1
actual.foreach(compareBiSResponse(_, response))
} }
} }
"remove item from best in slot set" in { "remove item from best in slot set" in {
val piece = PieceResponse.fromPiece(Fixtures.lootBody) val piece = PieceResponse.fromPiece(Fixtures.lootBody)
val entity = PieceActionResponse(ApiAction.remove, piece, playerId, None) val entity = PieceActionResponse(ApiAction.remove, piece, playerId)
Post(endpoint, entity).withHeaders(auth) ~> route ~> check { Post(endpoint, entity).withHeaders(auth) ~> route ~> check {
status shouldEqual StatusCodes.Accepted status shouldEqual StatusCodes.Accepted
@ -95,19 +79,17 @@ class BiSEndpointTest extends AnyWordSpecLike with Matchers with ScalatestRouteT
val uri = endpoint.withQuery(Uri.Query(Map("nick" -> playerId.nick, "job" -> playerId.job))) val uri = endpoint.withQuery(Uri.Query(Map("nick" -> playerId.nick, "job" -> playerId.job)))
val bis = BiS(Fixtures.bis.pieces.filterNot(_ == Fixtures.lootBody)) val bis = BiS(Fixtures.bis.pieces.filterNot(_ == Fixtures.lootBody))
val response = PlayerResponse.fromPlayer(Fixtures.playerEmpty.withBiS(Some(bis))) val response = Seq(PlayerResponse.fromPlayer(Fixtures.playerEmpty.withBiS(Some(bis))))
Get(uri).withHeaders(auth) ~> route ~> check { Get(uri).withHeaders(auth) ~> route ~> check {
status shouldEqual StatusCodes.OK status shouldEqual StatusCodes.OK
val actual = responseAs[Seq[PlayerResponse]] responseAs[Seq[PlayerResponse]] shouldEqual response
actual.length shouldEqual 1
actual.foreach(compareBiSResponse(_, response))
} }
} }
"add item to best in slot set" in { "add item to best in slot set" in {
val piece = PieceResponse.fromPiece(Fixtures.lootBody) val piece = PieceResponse.fromPiece(Fixtures.lootBody)
val entity = PieceActionResponse(ApiAction.add, piece, playerId, None) val entity = PieceActionResponse(ApiAction.add, piece, playerId)
Post(endpoint, entity).withHeaders(auth) ~> route ~> check { Post(endpoint, entity).withHeaders(auth) ~> route ~> check {
status shouldEqual StatusCodes.Accepted status shouldEqual StatusCodes.Accepted
@ -115,102 +97,11 @@ class BiSEndpointTest extends AnyWordSpecLike with Matchers with ScalatestRouteT
} }
val uri = endpoint.withQuery(Uri.Query(Map("nick" -> playerId.nick, "job" -> playerId.job))) val uri = endpoint.withQuery(Uri.Query(Map("nick" -> playerId.nick, "job" -> playerId.job)))
val response = PlayerResponse.fromPlayer(Fixtures.playerEmpty.withBiS(Some(Fixtures.bis))) val response = Seq(PlayerResponse.fromPlayer(Fixtures.playerEmpty.withBiS(Some(Fixtures.bis))))
Get(uri).withHeaders(auth) ~> route ~> check { Get(uri).withHeaders(auth) ~> route ~> check {
status shouldEqual StatusCodes.OK status shouldEqual StatusCodes.OK
val actual = responseAs[Seq[PlayerResponse]] responseAs[Seq[PlayerResponse]] shouldEqual response
actual.length shouldEqual 1
actual.foreach(compareBiSResponse(_, response))
}
}
"do not allow to add same item to best in slot set" in {
val piece = PieceResponse.fromPiece(Fixtures.lootBody.withJob(Job.DNC))
val entity = PieceActionResponse(ApiAction.add, piece, playerId, None)
Post(endpoint, entity).withHeaders(auth) ~> route ~> check {
status shouldEqual StatusCodes.Accepted
responseAs[String] shouldEqual ""
}
val uri = endpoint.withQuery(Uri.Query(Map("nick" -> playerId.nick, "job" -> playerId.job)))
val response = PlayerResponse.fromPlayer(Fixtures.playerEmpty.withBiS(Some(Fixtures.bis)))
Get(uri).withHeaders(auth) ~> route ~> check {
status shouldEqual StatusCodes.OK
val actual = responseAs[Seq[PlayerResponse]]
actual.length shouldEqual 1
actual.foreach(compareBiSResponse(_, response))
}
}
"allow to add item with another type to best in slot set" in {
val piece = PieceResponse.fromPiece(Fixtures.lootBodyCrafted.withJob(Job.DNC))
val entity = PieceActionResponse(ApiAction.add, piece, playerId, None)
Post(endpoint, entity).withHeaders(auth) ~> route ~> check {
status shouldEqual StatusCodes.Accepted
responseAs[String] shouldEqual ""
}
val uri = endpoint.withQuery(Uri.Query(Map("nick" -> playerId.nick, "job" -> playerId.job)))
val bis = Fixtures.bis.withPiece(piece.toPiece)
val response = PlayerResponse.fromPlayer(Fixtures.playerEmpty.withBiS(Some(bis)))
Get(uri).withHeaders(auth) ~> route ~> check {
status shouldEqual StatusCodes.OK
val actual = responseAs[Seq[PlayerResponse]]
actual.length shouldEqual 1
actual.foreach(compareBiSResponse(_, response))
}
}
"remove only specific item from best in slot set" in {
val piece = PieceResponse.fromPiece(Fixtures.lootBodyCrafted.withJob(Job.DNC))
val entity = PieceActionResponse(ApiAction.remove, piece, playerId, None)
Post(endpoint, entity).withHeaders(auth) ~> route ~> check {
status shouldEqual StatusCodes.Accepted
responseAs[String] shouldEqual ""
}
val uri = endpoint.withQuery(Uri.Query(Map("nick" -> playerId.nick, "job" -> playerId.job)))
val response = PlayerResponse.fromPlayer(Fixtures.playerEmpty.withBiS(Some(Fixtures.bis)))
Get(uri).withHeaders(auth) ~> route ~> check {
status shouldEqual StatusCodes.OK
val actual = responseAs[Seq[PlayerResponse]]
actual.length shouldEqual 1
actual.foreach(compareBiSResponse(_, response))
}
}
"totaly replace player bis" in {
// add random item first
val piece = PieceResponse.fromPiece(Fixtures.lootBodyCrafted.withJob(Job.DNC))
val entity = PieceActionResponse(ApiAction.add, piece, playerId, None)
Post(endpoint, entity).withHeaders(auth) ~> route ~> check {
status shouldEqual StatusCodes.Accepted
responseAs[String] shouldEqual ""
}
val bisEntity = PlayerBiSLinkResponse(Fixtures.link, playerId)
Put(endpoint, bisEntity).withHeaders(auth) ~> route ~> check {
status shouldEqual StatusCodes.Created
responseAs[String] shouldEqual ""
}
val uri = endpoint.withQuery(Uri.Query(Map("nick" -> playerId.nick, "job" -> playerId.job)))
val response = PlayerResponse.fromPlayer(Fixtures.playerEmpty.withBiS(Some(Fixtures.bis)))
Get(uri).withHeaders(auth) ~> route ~> check {
status shouldEqual StatusCodes.OK
val actual = responseAs[Seq[PlayerResponse]]
actual.length shouldEqual 1
actual.foreach(compareBiSResponse(_, response))
} }
} }

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