1 Commits

Author SHA1 Message Date
bb08132237 fix api urls in swagger 2019-12-29 03:44:18 +03:00
116 changed files with 1189 additions and 2228 deletions

View File

@ -28,4 +28,4 @@ REST API documentation is available at `http://0.0.0.0:8000/swagger`. HTML repre
## Public service
There is also public service which is available at http://ffxivbis.arcanis.me.
There is also public service which is available at https://ffxivbis.arcanis.me.

View File

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

View File

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

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 @@
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

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

View File

@ -1,5 +1,7 @@
me.arcanis.ffxivbis {
bis-provider {
ariyala {
# ariyala base url, string, required
ariyala-url = "https://ffxiv.ariyala.com"
# xivapi base url, string, required
xivapi-url = "https://xivapi.com"
# xivapi developer key, string, optional
@ -54,6 +56,12 @@ me.arcanis.ffxivbis {
port = 8000
# hostname to use in docs, if not set host:port will be used
#hostname = "127.0.0.1:8000"
# rate limits
limits {
intetval = 1m
max-count = 60
}
}
default-dispatcher {

View File

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

View File

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

View File

@ -8,45 +8,40 @@
*/
package me.arcanis.ffxivbis.http
import akka.actor.typed.scaladsl.AskPattern.Askable
import akka.actor.typed.{ActorRef, Scheduler}
import akka.actor.ActorRef
import akka.pattern.ask
import akka.util.Timeout
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.service.impl.DatabaseBiSHandler
import scala.concurrent.{ExecutionContext, Future}
trait BiSHelper extends BisProviderHelper {
def storage: ActorRef[Message]
class BiSHelper(storage: ActorRef, ariyala: ActorRef) extends AriyalaHelper(ariyala) {
def addPieceBiS(playerId: PlayerId, piece: Piece)
(implicit timeout: Timeout, scheduler: Scheduler): Future[Unit] =
storage.ask(AddPieceToBis(playerId, piece.withJob(playerId.job), _))
(implicit executionContext: ExecutionContext, timeout: Timeout): Future[Int] =
(storage ? DatabaseBiSHandler.AddPieceToBis(playerId, piece.withJob(playerId.job))).mapTo[Int]
def bis(partyId: String, playerId: Option[PlayerId])
(implicit timeout: Timeout, scheduler: Scheduler): Future[Seq[Player]] =
storage.ask(GetBiS(partyId, playerId, _))
(implicit executionContext: ExecutionContext, timeout: Timeout): Future[Seq[Player]] =
(storage ? DatabaseBiSHandler.GetBiS(partyId, playerId)).mapTo[Seq[Player]]
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 {
case ApiAction.add => addPieceBiS(playerId, piece)
case ApiAction.remove => removePieceBiS(playerId, piece)
}
def putBiS(playerId: PlayerId, link: String)
(implicit executionContext: ExecutionContext, timeout: Timeout, scheduler: Scheduler): Future[Unit] = {
storage.ask(RemovePiecesFromBiS(playerId, _)).flatMap { _ =>
downloadBiS(link, playerId.job).flatMap { bis =>
Future.traverse(bis.pieces)(addPieceBiS(playerId, _))
}.map(_ => ())
}
}
(implicit executionContext: ExecutionContext, timeout: Timeout): Future[Unit] =
downloadBiS(link, playerId.job).flatMap { bis =>
Future.traverse(bis.pieces)(addPieceBiS(playerId, _))
}.map(_ => ())
def removePieceBiS(playerId: PlayerId, piece: Piece)
(implicit timeout: Timeout, scheduler: Scheduler): Future[Unit] =
storage.ask(RemovePieceFromBiS(playerId, piece, _))
(implicit executionContext: ExecutionContext, timeout: Timeout): Future[Int] =
(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
import akka.actor.typed.scaladsl.AskPattern.Askable
import akka.actor.typed.{ActorRef, Scheduler}
import akka.actor.ActorRef
import akka.pattern.ask
import akka.util.Timeout
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.service.LootSelector.LootSelectorResult
import me.arcanis.ffxivbis.service.impl.DatabaseLootHandler
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)
(implicit timeout: Timeout, scheduler: Scheduler): Future[Unit] =
storage.ask(
AddPieceTo(playerId, piece, isFreeLoot, _))
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 doModifyLoot(action: ApiAction.Value, playerId: PlayerId, piece: Piece)
(implicit executionContext: ExecutionContext, timeout: Timeout): Future[Int] =
action match {
case ApiAction.add => addPieceLoot(playerId, piece)
case ApiAction.remove => removePieceLoot(playerId, piece)
}
def loot(partyId: String, playerId: Option[PlayerId])
(implicit timeout: Timeout, scheduler: Scheduler): Future[Seq[Player]] =
storage.ask(GetLoot(partyId, playerId, _))
(implicit executionContext: ExecutionContext, timeout: Timeout): Future[Seq[Player]] =
(storage ? DatabaseLootHandler.GetLoot(partyId, playerId)).mapTo[Seq[Player]]
def removePieceLoot(playerId: PlayerId, piece: Piece)
(implicit timeout: Timeout, scheduler: Scheduler): Future[Unit] =
storage.ask(RemovePieceFrom(playerId, piece, _))
(implicit executionContext: ExecutionContext, timeout: Timeout): Future[Int] =
(storage ? DatabaseLootHandler.RemovePieceFrom(playerId, piece)).mapTo[Int]
def suggestPiece(partyId: String, piece: Piece)
(implicit executionContext: ExecutionContext, timeout: Timeout, scheduler: Scheduler): Future[Seq[PlayerIdWithCounters]] =
storage.ask(SuggestLoot(partyId, piece, _)).map(_.result)
(implicit executionContext: ExecutionContext, timeout: Timeout): Future[Seq[PlayerIdWithCounters]] =
(storage ? DatabaseLootHandler.SuggestLoot(partyId, piece)).mapTo[LootSelectorResult].map(_.result)
}

View File

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

View File

@ -10,29 +10,25 @@ package me.arcanis.ffxivbis.http
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._
import akka.util.Timeout
import com.typesafe.scalalogging.{Logger, StrictLogging}
import me.arcanis.ffxivbis.http.api.v1.RootApiV1Endpoint
import me.arcanis.ffxivbis.http.view.RootView
import me.arcanis.ffxivbis.messages.{BiSProviderMessage, Message}
class RootEndpoint(system: ActorSystem[Nothing],
storage: ActorRef[Message],
provider: ActorRef[BiSProviderMessage])
class RootEndpoint(system: ActorSystem, storage: ActorRef, ariyala: ActorRef)
extends StrictLogging {
import me.arcanis.ffxivbis.utils.Implicits._
private val config = system.settings.config
implicit val scheduler: Scheduler = system.scheduler
implicit val timeout: Timeout =
config.getDuration("me.arcanis.ffxivbis.settings.request-timeout")
private val rootApiV1Endpoint: RootApiV1Endpoint = new RootApiV1Endpoint(storage, provider, config)
private val rootView: RootView = new RootView(storage, provider)
private val rootApiV1Endpoint: RootApiV1Endpoint = new RootApiV1Endpoint(storage, ariyala, config)
private val rootView: RootView = new RootView(storage, ariyala)
private val swagger: Swagger = new Swagger(config)
private val httpLogger = Logger("http")

View File

@ -16,11 +16,10 @@ import io.swagger.v3.oas.models.security.SecurityScheme
import scala.io.Source
class Swagger(config: Config) extends SwaggerHttpService {
override val apiClasses: Set[Class[_]] = Set(
classOf[api.v1.BiSEndpoint], classOf[api.v1.LootEndpoint],
classOf[api.v1.PartyEndpoint], classOf[api.v1.PlayerEndpoint],
classOf[api.v1.TypesEndpoint], classOf[api.v1.UserEndpoint]
classOf[api.v1.PlayerEndpoint], classOf[api.v1.TypesEndpoint],
classOf[api.v1.UserEndpoint]
)
override val info: Info = Info(

View File

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

View File

@ -8,7 +8,7 @@
*/
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.server.Directives._
import akka.http.scaladsl.server._
@ -22,16 +22,13 @@ 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, BiSHelper}
import me.arcanis.ffxivbis.messages.{BiSProviderMessage, Message}
import me.arcanis.ffxivbis.models.PlayerId
import scala.util.{Failure, Success}
@Path("api/v1")
class BiSEndpoint(override val storage: ActorRef[Message],
override val provider: ActorRef[BiSProviderMessage])
(implicit timeout: Timeout, scheduler: Scheduler)
extends BiSHelper with Authorization with JsonSupport {
class BiSEndpoint(override val storage: ActorRef, ariyala: ActorRef)(implicit timeout: Timeout)
extends BiSHelper(storage, ariyala) with Authorization with JsonSupport {
def route: Route = createBiS ~ getBiS ~ modifyBiS

View File

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

View File

@ -8,7 +8,7 @@
*/
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.server.Directives._
import akka.http.scaladsl.server._
@ -22,15 +22,13 @@ 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, LootHelper}
import me.arcanis.ffxivbis.messages.Message
import me.arcanis.ffxivbis.models.PlayerId
import scala.util.{Failure, Success}
@Path("api/v1")
class LootEndpoint(override val storage: ActorRef[Message])
(implicit timeout: Timeout, scheduler: Scheduler)
extends LootHelper with Authorization with JsonSupport with HttpHandler {
class LootEndpoint(override val storage: ActorRef)(implicit timeout: Timeout)
extends LootHelper(storage) with Authorization with JsonSupport with HttpHandler {
def route: Route = getLoot ~ modifyLoot
@ -105,7 +103,7 @@ class LootEndpoint(override val storage: ActorRef[Message])
post {
entity(as[PieceActionResponse]) { action =>
val playerId = action.playerId.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 Failure(exception) => throw exception
}

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,7 +8,7 @@
*/
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.server.Directives._
import akka.http.scaladsl.server._
@ -22,16 +22,13 @@ 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 me.arcanis.ffxivbis.models.PlayerId
import scala.util.{Failure, Success}
@Path("api/v1")
class PlayerEndpoint(override val storage: ActorRef[Message],
override val provider: ActorRef[BiSProviderMessage])
(implicit timeout: Timeout, scheduler: Scheduler)
extends PlayerHelper with Authorization with JsonSupport with HttpHandler {
class PlayerEndpoint(override val storage: ActorRef, ariyala: ActorRef)(implicit timeout: Timeout)
extends PlayerHelper(storage, ariyala) with Authorization with JsonSupport with HttpHandler {
def route: Route = getParty ~ modifyParty

View File

@ -8,31 +8,28 @@
*/
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.Route
import akka.util.Timeout
import com.typesafe.config.Config
import me.arcanis.ffxivbis.http.api.v1.json.JsonSupport
import me.arcanis.ffxivbis.messages.{BiSProviderMessage, Message}
class RootApiV1Endpoint(storage: ActorRef[Message],
provider: ActorRef[BiSProviderMessage],
config: Config)(implicit timeout: Timeout, scheduler: Scheduler)
class RootApiV1Endpoint(storage: ActorRef, ariyala: ActorRef, config: Config)
(implicit timeout: Timeout)
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 partyEndpoint = new PartyEndpoint(storage, provider)
private val playerEndpoint = new PlayerEndpoint(storage, provider)
private val playerEndpoint = new PlayerEndpoint(storage, ariyala)
private val typesEndpoint = new TypesEndpoint(config)
private val userEndpoint = new UserEndpoint(storage)
def route: Route =
handleExceptions(exceptionHandler) {
handleRejections(rejectionHandler) {
biSEndpoint.route ~ lootEndpoint.route ~ partyEndpoint.route ~
playerEndpoint.route ~ typesEndpoint.route ~ userEndpoint.route
biSEndpoint.route ~ lootEndpoint.route ~ playerEndpoint.route ~
typesEndpoint.route ~ userEndpoint.route
}
}
}

View File

@ -16,12 +16,12 @@ 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}
import me.arcanis.ffxivbis.models.{Job, Party, Permission, Piece}
@Path("api/v1")
class TypesEndpoint(config: Config) extends JsonSupport {
def route: Route = getJobs ~ getPermissions ~ getPieces ~ getPieceTypes ~ getPriority
def route: Route = getJobs ~ getPermissions ~ getPieces ~ getPriority
@GET
@Path("types/jobs")
@ -86,27 +86,6 @@ class TypesEndpoint(config: Config) extends JsonSupport {
}
}
@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"))

View File

@ -8,7 +8,7 @@
*/
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.server.Directives._
import akka.http.scaladsl.server._
@ -22,15 +22,13 @@ 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, UserHelper}
import me.arcanis.ffxivbis.messages.Message
import me.arcanis.ffxivbis.models.Permission
import scala.util.{Failure, Success}
@Path("api/v1")
class UserEndpoint(override val storage: ActorRef[Message])
(implicit timeout: Timeout, scheduler: Scheduler)
extends UserHelper with Authorization with JsonSupport {
class UserEndpoint(override val storage: ActorRef)(implicit timeout: Timeout)
extends UserHelper(storage) with Authorization with JsonSupport {
def route: Route = createParty ~ createUser ~ deleteUser ~ getUsers

View File

@ -8,8 +8,6 @@
*/
package me.arcanis.ffxivbis.http.api.v1.json
import java.time.Instant
import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport
import me.arcanis.ffxivbis.models.Permission
import spray.json._
@ -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 permissionFormat: RootJsonFormat[Permission.Value] = enumFormat(Permission)
implicit val errorFormat: RootJsonFormat[ErrorResponse] = jsonFormat1(ErrorResponse.apply)
implicit val partyIdFormat: RootJsonFormat[PartyIdResponse] = jsonFormat1(PartyIdResponse.apply)
implicit val pieceFormat: RootJsonFormat[PieceResponse] = jsonFormat3(PieceResponse.apply)
implicit val lootFormat: RootJsonFormat[LootResponse] = jsonFormat3(LootResponse.apply)
implicit val partyDescriptionFormat: RootJsonFormat[PartyDescriptionResponse] = jsonFormat2(PartyDescriptionResponse.apply)
implicit val playerFormat: RootJsonFormat[PlayerResponse] = jsonFormat7(PlayerResponse.apply)
implicit val playerActionFormat: RootJsonFormat[PlayerActionResponse] = jsonFormat2(PlayerActionResponse.apply)
implicit val playerIdFormat: RootJsonFormat[PlayerIdResponse] = jsonFormat3(PlayerIdResponse.apply)
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 playerIdWithCountersFormat: RootJsonFormat[PlayerIdWithCountersResponse] =
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(
@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 = "player description", required = true) playerId: PlayerIdResponse,
@Schema(description = "is piece free to roll or not") isFreeLoot: Option[Boolean])
@Schema(description = "player description", required = true) playerId: PlayerIdResponse)

View File

@ -9,18 +9,16 @@
package me.arcanis.ffxivbis.http.api.v1.json
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(
@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 = "piece name", required = true, example = "body") piece: String) {
def toPiece: Piece = Piece(piece, PieceType.withName(pieceType), Job.withName(job))
def toPiece: Piece = Piece(piece, isTome, Job.withName(job))
}
object 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

@ -15,13 +15,11 @@ case class PlayerIdResponse(
@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 = "player nick name", required = true, example = "Siuan Sanche") nick: String) {
def withPartyId(partyId: String): PlayerId =
PlayerId(partyId, Job.withName(job), nick)
}
object PlayerIdResponse {
def fromPlayerId(playerId: PlayerId): PlayerIdResponse =
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)
object PlayerIdWithCountersResponse {
def fromPlayerId(playerIdWithCounters: PlayerIdWithCounters): PlayerIdWithCountersResponse =
PlayerIdWithCountersResponse(
playerIdWithCounters.partyId,

View File

@ -16,22 +16,18 @@ case class PlayerResponse(
@Schema(description = "job name", required = true, example = "DNC") job: String,
@Schema(description = "player nick name", required = true, example = "Siuan Sanche") nick: String,
@Schema(description = "pieces in best in slot") bis: Option[Seq[PieceResponse]],
@Schema(description = "looted pieces") loot: Option[Seq[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 = "player loot priority", `type` = "number") priority: Option[Int]) {
def toPlayer: Player =
Player(-1, partyId, Job.withName(job), nick,
BiS(bis.getOrElse(Seq.empty).map(_.toPiece)),
loot.getOrElse(Seq.empty).map(_.toLoot),
Player(partyId, Job.withName(job), nick,
BiS(bis.getOrElse(Seq.empty).map(_.toPiece)), loot.getOrElse(Seq.empty).map(_.toPiece),
link, priority.getOrElse(0))
}
object PlayerResponse {
def fromPlayer(player: Player): PlayerResponse =
PlayerResponse(player.partyId, player.job.toString, player.nick,
Some(player.bis.pieces.map(PieceResponse.fromPiece)),
Some(player.loot.map(LootResponse.fromLoot)),
Some(player.bis.pieces.map(PieceResponse.fromPiece)), Some(player.loot.map(PieceResponse.fromPiece)),
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 = "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) {
def toUser: User =
User(partyId, username, password, permission.getOrElse(Permission.get))
}
object UserResponse {
def fromUser(user: User): UserResponse =
UserResponse(user.partyId, user.username, "", Some(user.permission))
}

View File

@ -8,20 +8,15 @@
*/
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.server.Directives._
import akka.http.scaladsl.server._
import akka.util.Timeout
import me.arcanis.ffxivbis.http.{Authorization, PlayerHelper}
import me.arcanis.ffxivbis.messages.{BiSProviderMessage, Message}
import me.arcanis.ffxivbis.http.Authorization
import scala.util.{Failure, Success}
class BasePartyView(override val storage: ActorRef[Message],
override val provider: ActorRef[BiSProviderMessage])
(implicit timeout: Timeout, scheduler: Scheduler)
extends PlayerHelper with Authorization {
class BasePartyView(override val storage: ActorRef)(implicit timeout: Timeout)
extends Authorization {
def route: Route = getIndex
@ -30,10 +25,8 @@ class BasePartyView(override val storage: ActorRef[Message],
extractExecutionContext { implicit executionContext =>
authenticateBasicBCrypt(s"party $partyId", authGet(partyId)) { _ =>
get {
onComplete(getPartyDescription(partyId)) {
case Success(description) =>
complete(StatusCodes.OK, RootView.toHtml(BasePartyView.template(partyId, description.alias)))
case Failure(exception) => throw exception
complete {
(StatusCodes.OK, RootView.toHtml(BasePartyView.template(partyId)))
}
}
}
@ -49,16 +42,16 @@ object BasePartyView {
def root(partyId: String): Text.TypedTag[String] =
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\">" +
html(lang:="en",
head(
titleTag(s"Party $alias"),
titleTag(s"Party $partyId"),
link(rel:="stylesheet", `type`:="text/css", href:="/static/styles.css")
),
body(
h2(s"Party $alias"),
h2(s"Party $partyId"),
br,
h2(a(href:=s"/party/$partyId/players", title:="party")("party")),
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
import akka.actor.typed.{ActorRef, Scheduler}
import akka.actor.ActorRef
import akka.http.scaladsl.model.StatusCodes
import akka.http.scaladsl.server.Directives._
import akka.http.scaladsl.server._
import akka.util.Timeout
import me.arcanis.ffxivbis.http.{Authorization, BiSHelper}
import me.arcanis.ffxivbis.messages.{BiSProviderMessage, Message}
import me.arcanis.ffxivbis.models.{Piece, PieceType, Player, PlayerId}
import me.arcanis.ffxivbis.models.{Piece, Player, PlayerId}
import scala.concurrent.{ExecutionContext, Future}
import scala.util.Try
class BiSView(override val storage: ActorRef[Message],
override val provider: ActorRef[BiSProviderMessage])
(implicit timeout: Timeout, scheduler: Scheduler)
extends BiSHelper with Authorization {
class BiSView(override val storage: ActorRef, ariyala: ActorRef)(implicit timeout: Timeout)
extends BiSHelper(storage, ariyala) with Authorization {
def route: Route = getBiS ~ modifyBiS
@ -49,10 +46,10 @@ class BiSView(override val storage: ActorRef[Message],
extractExecutionContext { implicit executionContext =>
authenticateBasicBCrypt(s"party $partyId", authPost(partyId)) { _ =>
post {
formFields("player".as[String], "piece".as[String].?, "piece_type".as[String].?, "link".as[String].?, "action".as[String]) {
(player, maybePiece, maybePieceType, maybeLink, action) =>
onComplete(modifyBiSCall(partyId, player, maybePiece, maybePieceType, maybeLink, action)) { _ =>
redirect(s"/party/$partyId/bis", StatusCodes.Found)
formFields("player".as[String], "piece".as[String].?, "is_tome".as[String].?, "link".as[String].?, "action".as[String]) {
(player, maybePiece, maybeIsTome, maybeLink, action) =>
onComplete(modifyBiSCall(partyId, player, maybePiece, maybeIsTome, maybeLink, action)) {
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,
maybePiece: Option[String], maybePieceType: Option[String],
maybePiece: Option[String], maybeIsTome: Option[String],
maybeLink: Option[String], action: String)
(implicit executionContext: ExecutionContext, timeout: Timeout): Future[Unit] = {
def getPiece(playerId: PlayerId, piece: String, pieceType: String) =
Try(Piece(piece, PieceType.withName(pieceType), playerId.job)).toOption
import me.arcanis.ffxivbis.utils.Implicits._
def bisAction(playerId: PlayerId, piece: String, pieceType: String)(fn: Piece => Future[Unit]) =
getPiece(playerId, piece, pieceType) match {
case Some(item) => fn(item)
case _ => Future.failed(new Error(s"Could not construct piece from `$piece ($pieceType)`"))
}
def getPiece(playerId: PlayerId, piece: String) =
Try(Piece(piece, maybeIsTome, playerId.job)).toOption
PlayerId(partyId, player) match {
case Some(playerId) => (maybePiece, maybePieceType, action, maybeLink) match {
case (Some(piece), Some(pieceType), "add", _) =>
bisAction(playerId, piece, pieceType)(addPieceBiS(playerId, _))
case (Some(piece), Some(pieceType), "remove", _) =>
bisAction(playerId, piece, pieceType)(removePieceBiS(playerId, _))
case (_, _, "create", Some(link)) => putBiS(playerId, link)
case Some(playerId) => (maybePiece, action, maybeLink) match {
case (Some(piece), "add", _) => getPiece(playerId, piece) match {
case Some(item) => addPieceBiS(playerId, item).map(_ => ())
case _ => Future.failed(new Error(s"Could not construct piece from `$piece`"))
}
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 construct player id from `$player`"))
@ -110,13 +107,13 @@ object BiSView {
(for (player <- party) yield option(player.playerId.toString)),
select(name:="piece", id:="piece", title:="piece")
(for (piece <- Piece.available) yield option(piece)),
select(name:="piece_type", id:="piece_type", title:="piece type")
(for (pieceType <- PieceType.available) yield option(pieceType.toString)),
input(name:="is_tome", id:="is_tome", title:="is tome", `type`:="checkbox"),
label(`for`:="is_tome")("is tome gear"),
input(name:="action", id:="action", `type`:="hidden", value:="add"),
input(name:="add", id:="add", `type`:="submit", value:="add")
),
form(action:=s"/party/$partyId/bis", method:="post")(
form(action:="/bis", method:="post")(
select(name:="player", id:="player", title:="player")
(for (player <- party) yield option(player.playerId.toString)),
input(name:="link", id:="link", placeholder:="player bis link", title:="link", `type`:="text"),
@ -128,18 +125,18 @@ object BiSView {
tr(
th("player"),
th("piece"),
th("piece type"),
th("is tome"),
th("")
),
for (player <- party; piece <- player.bis.pieces) yield tr(
td(`class`:="include_search")(player.playerId.toString),
td(`class`:="include_search")(piece.piece),
td(piece.pieceType.toString),
td(piece.isTomeToString),
td(
form(action:=s"/party/$partyId/bis", method:="post")(
input(name:="player", id:="player", `type`:="hidden", value:=player.playerId.toString),
input(name:="piece", id:="piece", `type`:="hidden", value:=piece.piece),
input(name:="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:="remove", id:="remove", `type`:="submit", value:="x")
)

View File

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

View File

@ -12,7 +12,6 @@ import scalatags.Text
import scalatags.Text.all._
object ExportToCSVView {
def template: Text.TypedTag[String] =
div(
button(onclick:="exportTableToCsv('result.csv')")("Export to CSV"),

View File

@ -8,22 +8,18 @@
*/
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.server.Directives._
import akka.http.scaladsl.server._
import akka.util.Timeout
import me.arcanis.ffxivbis.http.{PlayerHelper, UserHelper}
import me.arcanis.ffxivbis.messages.{BiSProviderMessage, Message}
import me.arcanis.ffxivbis.models.{PartyDescription, Permission, User}
import me.arcanis.ffxivbis.http.UserHelper
import me.arcanis.ffxivbis.models.{Permission, User}
import scala.concurrent.Future
import scala.util.{Failure, Success}
class IndexView(override val storage: ActorRef[Message],
override val provider: ActorRef[BiSProviderMessage])
(implicit timeout: Timeout, scheduler: Scheduler)
extends PlayerHelper with UserHelper {
class IndexView(storage: ActorRef)(implicit timeout: Timeout)
extends UserHelper(storage) {
def route: Route = createParty ~ getIndex
@ -31,17 +27,13 @@ class IndexView(override val storage: ActorRef[Message],
path("party") {
extractExecutionContext { implicit executionContext =>
post {
formFields("username".as[String], "password".as[String], "alias".as[String].?) { (username, password, maybeAlias) =>
onComplete {
newPartyId.flatMap { partyId =>
formFields("username".as[String], "password".as[String]) { (username, password) =>
onComplete(newPartyId) {
case Success(partyId) =>
val user = User(partyId, username, password, Permission.admin)
addUser(user, isHashedPassword = false).flatMap { _ =>
if (maybeAlias.getOrElse("").isEmpty) Future.successful(partyId)
else updateDescription(PartyDescription(partyId, maybeAlias)).map(_ => partyId)
onComplete(addUser(user, isHashedPassword = false)) {
case _ => redirect(s"/party/$partyId", StatusCodes.Found)
}
}
} {
case Success(partyId) => redirect(s"/party/$partyId", StatusCodes.Found)
case Failure(exception) => throw exception
}
}
@ -75,7 +67,6 @@ object IndexView {
body(
form(action:=s"party", method:="post")(
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:="password", id:="password", placeholder:="password", title:="password", `type`:="password"),
input(name:="add", id:="add", `type`:="submit", value:="add")

View File

@ -8,21 +8,19 @@
*/
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.server.Directives._
import akka.http.scaladsl.server.Route
import akka.util.Timeout
import me.arcanis.ffxivbis.http.{Authorization, LootHelper}
import me.arcanis.ffxivbis.messages.Message
import me.arcanis.ffxivbis.models.{Job, Piece, PieceType, PlayerIdWithCounters}
import me.arcanis.ffxivbis.models.{Job, Piece, PlayerIdWithCounters}
import scala.concurrent.{ExecutionContext, Future}
import scala.util.{Failure, Success, Try}
class LootSuggestView(override val storage: ActorRef[Message])
(implicit timeout: Timeout, scheduler: Scheduler)
extends LootHelper with Authorization {
class LootSuggestView(override val storage: ActorRef)(implicit timeout: Timeout)
extends LootHelper(storage) with Authorization {
def route: Route = getIndex ~ suggestLoot
@ -32,7 +30,7 @@ class LootSuggestView(override val storage: ActorRef[Message])
authenticateBasicBCrypt(s"party $partyId", authGet(partyId)) { _ =>
get {
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))
}
}
@ -45,20 +43,18 @@ class LootSuggestView(override val storage: ActorRef[Message])
extractExecutionContext { implicit executionContext =>
authenticateBasicBCrypt(s"party $partyId", authGet(partyId)) { _ =>
post {
formFields("piece".as[String], "job".as[String], "piece_type".as[String], "free_loot".as[String].?) {
(piece, job, pieceType, maybeFreeLoot) =>
import me.arcanis.ffxivbis.utils.Implicits._
formFields("piece".as[String], "job".as[String], "is_tome".as[String].?) { (piece, job, maybeTome) =>
import me.arcanis.ffxivbis.utils.Implicits._
val maybePiece = Try(Piece(piece, maybeTome, Job.withName(job))).toOption
val maybePiece = Try(Piece(piece, PieceType.withName(pieceType), Job.withName(job))).toOption
onComplete(suggestLootCall(partyId, maybePiece)) {
case Success(players) =>
val text = LootSuggestView.template(partyId, players, maybePiece, maybeFreeLoot, None)
complete(StatusCodes.OK, RootView.toHtml(text))
case Failure(exception) =>
val text = LootSuggestView.template(partyId, Seq.empty, None, false, Some(exception.getMessage))
complete(StatusCodes.OK, RootView.toHtml(text))
}
onComplete(suggestLootCall(partyId, maybePiece)) {
case Success(players) =>
val text = LootSuggestView.template(partyId, players, maybePiece, None)
complete(StatusCodes.OK, RootView.toHtml(text))
case Failure(exception) =>
val text = LootSuggestView.template(partyId, Seq.empty, maybePiece, Some(exception.getMessage))
complete(StatusCodes.OK, RootView.toHtml(text))
}
}
}
}
@ -77,8 +73,7 @@ object LootSuggestView {
import scalatags.Text.all._
import scalatags.Text.tags2.{title => titleTag}
def template(partyId: String, party: Seq[PlayerIdWithCounters], piece: Option[Piece],
isFreeLoot: Boolean, error: Option[String]): String =
def template(partyId: String, party: Seq[PlayerIdWithCounters], piece: Option[Piece], error: Option[String]): String =
"<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01//EN\" \"http://www.w3.org/TR/html4/strict.dtd\">" +
html(lang:="en",
head(
@ -97,10 +92,8 @@ object LootSuggestView {
(for (piece <- Piece.available) yield option(piece)),
select(name:="job", id:="job", title:="job")
(for (job <- Job.availableWithAnyJob) yield option(job.toString)),
select(name:="piece_type", id:="piece_type", title:="piece type")
(for (pieceType <- PieceType.available) yield option(pieceType.toString)),
input(name:="free_loot", id:="free_loot", title:="is free loot", `type`:="checkbox"),
label(`for`:="free_loot")("is free loot"),
input(name:="is_tome", id:="is_tome", title:="is tome", `type`:="checkbox"),
label(`for`:="is_tome")("is tome gear"),
input(name:="suggest", id:="suggest", `type`:="submit", value:="suggest")
),
@ -123,8 +116,7 @@ object LootSuggestView {
form(action:=s"/party/$partyId/loot", method:="post")(
input(name:="player", id:="player", `type`:="hidden", value:=player.playerId.toString),
input(name:="piece", id:="piece", `type`:="hidden", value:=piece.map(_.piece).getOrElse("")),
input(name:="piece_type", id:="piece_type", `type`:="hidden", value:=piece.map(_.pieceType.toString).getOrElse("")),
input(name:="free_loot", id:="free_loot", `type`:="hidden", value:=(if (isFreeLoot) "yes" else "no")),
input(name:="is_tome", id:="is_tome", `type`:="hidden", value:=piece.map(_.isTomeToString).getOrElse("")),
input(name:="action", id:="action", `type`:="hidden", value:="add"),
input(name:="add", id:="add", `type`:="submit", value:="add")
)

View File

@ -8,21 +8,19 @@
*/
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.server.Directives._
import akka.http.scaladsl.server.Route
import akka.util.Timeout
import me.arcanis.ffxivbis.http.{Authorization, LootHelper}
import me.arcanis.ffxivbis.messages.Message
import me.arcanis.ffxivbis.models.{Piece, PieceType, Player, PlayerId}
import me.arcanis.ffxivbis.models.{Piece, Player, PlayerId}
import scala.concurrent.{ExecutionContext, Future}
import scala.util.Try
class LootView(override val storage: ActorRef[Message])
(implicit timeout: Timeout, scheduler: Scheduler)
extends LootHelper with Authorization {
class LootView (override val storage: ActorRef)(implicit timeout: Timeout)
extends LootHelper(storage) with Authorization {
def route: Route = getLoot ~ modifyLoot
@ -48,10 +46,10 @@ class LootView(override val storage: ActorRef[Message])
extractExecutionContext { implicit executionContext =>
authenticateBasicBCrypt(s"party $partyId", authPost(partyId)) { _ =>
post {
formFields("player".as[String], "piece".as[String], "piece_type".as[String], "action".as[String], "free_loot".as[String].?) {
(player, piece, pieceType, action, isFreeLoot) =>
onComplete(modifyLootCall(partyId, player, piece, pieceType, isFreeLoot, action)) { _ =>
redirect(s"/party/$partyId/loot", StatusCodes.Found)
formFields("player".as[String], "piece".as[String], "is_tome".as[String].?, "action".as[String]) {
(player, maybePiece, maybeIsTome, action) =>
onComplete(modifyLootCall(partyId, player, maybePiece, maybeIsTome, action)) {
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,
maybePieceType: String, maybeFreeLoot: Option[String],
private def modifyLootCall(partyId: String, player: String,
maybePiece: String, maybeIsTome: Option[String],
action: String)
(implicit executionContext: ExecutionContext, timeout: Timeout): Future[Unit] = {
import me.arcanis.ffxivbis.utils.Implicits._
def getPiece(playerId: PlayerId) =
Try(Piece(maybePiece, PieceType.withName(maybePieceType), playerId.job)).toOption
Try(Piece(maybePiece, maybeIsTome, playerId.job)).toOption
PlayerId(partyId, player) match {
case Some(playerId) => (getPiece(playerId), action) match {
case (Some(piece), "add") => addPieceLoot(playerId, piece, maybeFreeLoot)
case (Some(piece), "remove") => removePieceLoot(playerId, piece)
case _ => Future.failed(new Error(s"Could not construct piece from `$maybePiece ($maybePieceType)`"))
case (Some(piece), "add") => addPieceLoot(playerId, piece).map(_ => ())
case (Some(piece), "remove") => removePieceLoot(playerId, piece).map(_ => ())
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`"))
}
@ -102,10 +100,8 @@ object LootView {
(for (player <- party) yield option(player.playerId.toString)),
select(name:="piece", id:="piece", title:="piece")
(for (piece <- Piece.available) yield option(piece)),
select(name:="piece_type", id:="piece_type", title:="piece type")
(for (pieceType <- PieceType.available) yield option(pieceType.toString)),
input(name:="free_loot", id:="free_loot", title:="is free loot", `type`:="checkbox"),
label(`for`:="free_loot")("is free loot"),
input(name:="is_tome", id:="is_tome", title:="is tome", `type`:="checkbox"),
label(`for`:="is_tome")("is tome gear"),
input(name:="action", id:="action", `type`:="hidden", value:="add"),
input(name:="add", id:="add", `type`:="submit", value:="add")
),
@ -114,23 +110,18 @@ object LootView {
tr(
th("player"),
th("piece"),
th("piece type"),
th("is free loot"),
th("timestamp"),
th("is tome"),
th("")
),
for (player <- party; loot <- player.loot) yield tr(
for (player <- party; piece <- player.loot) yield tr(
td(`class`:="include_search")(player.playerId.toString),
td(`class`:="include_search")(loot.piece.piece),
td(loot.piece.pieceType.toString),
td(loot.isFreeLootToString),
td(loot.timestamp.toString),
td(`class`:="include_search")(piece.piece),
td(piece.isTomeToString),
td(
form(action:=s"/party/$partyId/loot", method:="post")(
input(name:="player", id:="player", `type`:="hidden", value:=player.playerId.toString),
input(name:="piece", id:="piece", `type`:="hidden", value:=loot.piece.piece),
input(name:="piece_type", id:="piece_type", `type`:="hidden", value:=loot.piece.pieceType.toString),
input(name:="free_loot", id:="free_loot", `type`:="hidden", value:=loot.isFreeLootToString),
input(name:="piece", id:="piece", `type`:="hidden", value:=piece.piece),
input(name:="is_tome", id:="is_tome", `type`:="hidden", value:=piece.isTomeToString),
input(name:="action", id:="action", `type`:="hidden", value:="remove"),
input(name:="remove", id:="remove", `type`:="submit", value:="x")
)

View File

@ -8,21 +8,18 @@
*/
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.server.Directives._
import akka.http.scaladsl.server.Route
import akka.util.Timeout
import me.arcanis.ffxivbis.http.{Authorization, PlayerHelper}
import me.arcanis.ffxivbis.messages.{BiSProviderMessage, Message}
import me.arcanis.ffxivbis.models._
import scala.concurrent.{ExecutionContext, Future}
class PlayerView(override val storage: ActorRef[Message],
override val provider: ActorRef[BiSProviderMessage])
(implicit timeout: Timeout, scheduler: Scheduler)
extends PlayerHelper with Authorization {
class PlayerView(override val storage: ActorRef, ariyala: ActorRef)(implicit timeout: Timeout)
extends PlayerHelper(storage, ariyala) with Authorization {
def route: Route = getParty ~ modifyParty
@ -50,8 +47,8 @@ class PlayerView(override val storage: ActorRef[Message],
post {
formFields("nick".as[String], "job".as[String], "priority".as[Int].?, "link".as[String].?, "action".as[String]) {
(nick, job, maybePriority, maybeLink, action) =>
onComplete(modifyPartyCall(partyId, nick, job, maybePriority, maybeLink, action)) { _ =>
redirect(s"/party/$partyId/players", StatusCodes.Found)
onComplete(modifyPartyCall(partyId, nick, job, maybePriority, maybeLink, action)) {
case _ => redirect(s"/party/$partyId/players", StatusCodes.Found)
}
}
}
@ -65,11 +62,11 @@ class PlayerView(override val storage: ActorRef[Message],
(implicit executionContext: ExecutionContext, timeout: Timeout): Future[Unit] = {
def maybePlayerId = PlayerId(partyId, Some(nick), Some(job))
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 {
case ("add", Some(playerId)) => addPlayer(player(playerId))
case ("remove", Some(playerId)) => removePlayer(playerId)
case ("add", Some(playerId)) => addPlayer(player(playerId)).map(_ => ())
case ("remove", Some(playerId)) => removePlayer(playerId).map(_ => ())
case _ => Future.failed(new Error(s"Could not perform $action with $nick ($job)"))
}
}

View File

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

View File

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

View File

@ -8,21 +8,19 @@
*/
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.server.Directives._
import akka.http.scaladsl.server.Route
import akka.util.Timeout
import me.arcanis.ffxivbis.http.{Authorization, UserHelper}
import me.arcanis.ffxivbis.messages.Message
import me.arcanis.ffxivbis.models.{Permission, User}
import scala.concurrent.{ExecutionContext, Future}
import scala.util.Try
class UserView(override val storage: ActorRef[Message])
(implicit timeout: Timeout, scheduler: Scheduler)
extends UserHelper with Authorization {
class UserView(override val storage: ActorRef)(implicit timeout: Timeout)
extends UserHelper(storage) with Authorization {
def route: Route = getUsers ~ modifyUsers
@ -68,10 +66,10 @@ class UserView(override val storage: ActorRef[Message])
action match {
case "add" => (maybePassword, permission) match {
case (Some(password), Some(permission)) => addUser(User(partyId, username, password, permission), isHashedPassword = false)
case (Some(password), Some(permission)) => addUser(User(partyId, username, password, permission), isHashedPassword = false).map(_ => ())
case _ => Future.failed(new Error(s"Could not construct permission/password from `$maybePermission`/`$maybePassword`"))
}
case "remove" => removeUser(partyId, username)
case "remove" => removeUser(partyId, username).map(_ => ())
case _ => Future.failed(new Error(s"Could not perform $action"))
}
}

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
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 {
case upgrade: PieceUpgrade => upgrades.contains(upgrade)
@ -17,27 +31,50 @@ case class BiS(pieces: Seq[Piece]) {
def upgrades: Map[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
} withDefaultValue 0
def withPiece(piece: Piece): BiS = copy(pieces :+ piece)
def withoutPiece(piece: Piece): BiS = copy(pieces.filterNot(_.strictEqual(piece)))
def withPiece(piece: Piece): BiS = copyWithPiece(piece.piece, Some(piece))
def withoutPiece(piece: Piece): BiS = copyWithPiece(piece.piece, None)
override def equals(obj: Any): Boolean = {
def comparePieces(left: Seq[Piece], right: Seq[Piece]): Boolean =
left.groupBy(identity).view.mapValues(_.size).forall {
case (key, count) => right.count(_.strictEqual(key)) == count
}
obj match {
case left: BiS => comparePieces(left.pieces, pieces)
case _ => false
}
private def copyWithPiece(name: String, piece: Option[Piece]): BiS = {
val params = Map(
"weapon" -> weapon,
"head" -> head,
"body" -> body,
"hands" -> hands,
"waist" -> waist,
"legs" -> legs,
"feet" -> feet,
"ears" -> ears,
"neck" -> neck,
"wrist" -> wrist,
"left ring" -> leftRing,
"right ring" -> rightRing
) + (name -> piece)
BiS(params)
}
}
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("left ring").flatten,
data.get("right ring").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
object Job {
sealed trait RightSide
object AccessoriesDex extends RightSide
object AccessoriesInt extends RightSide
@ -26,15 +25,13 @@ object Job {
object BodyTanks extends LeftSide
object BodyRanges extends LeftSide
sealed trait Job extends Equals {
sealed trait Job {
def leftSide: LeftSide
def rightSide: RightSide
// conversion to string to avoid recursion
override def canEqual(that: Any): Boolean = that.isInstanceOf[Job]
override def equals(obj: Any): Boolean = {
def canEqual(obj: Any): Boolean = obj.isInstanceOf[Job]
def equality(objRepr: String): Boolean = objRepr match {
case _ if objRepr == AnyJob.toString => true
case _ if this.toString == AnyJob.toString => true
@ -62,10 +59,6 @@ object Job {
val leftSide: LeftSide = BodyMnks
val rightSide: RightSide = AccessoriesStr
}
trait Drgs extends Job {
val leftSide: LeftSide = BodyDrgs
val rightSide: RightSide = AccessoriesStr
}
trait Tanks extends Job {
val leftSide: LeftSide = BodyTanks
val rightSide: RightSide = AccessoriesVit
@ -83,11 +76,12 @@ object Job {
case object WHM extends Healers
case object SCH extends Healers
case object AST extends Healers
case object SGE extends Healers
case object MNK extends Mnks
case object DRG extends Drgs
case object RPR extends Drgs
case object DRG extends Job {
val leftSide: LeftSide = BodyDrgs
val rightSide: RightSide = AccessoriesStr
}
case object NIN extends Job {
val leftSide: LeftSide = BodyNins
val rightSide: RightSide = AccessoriesDex
@ -103,13 +97,13 @@ object Job {
case object RDM extends Casters
lazy val available: Seq[Job] =
Seq(PLD, WAR, DRK, GNB, WHM, SCH, AST, SGE, MNK, DRG, RPR, 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 =
availableWithAnyJob.find(_.toString.equalsIgnoreCase(job)) match {
availableWithAnyJob.find(_.toString.equalsIgnoreCase(job.toUpperCase)) match {
case Some(value) => value
case None if job.isEmpty => AnyJob
case _ => throw new IllegalArgumentException(s"Invalid or unknown job $job")
case _ => throw new IllegalArgumentException("Invalid or unknown job")
}
}

View File

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

View File

@ -15,15 +15,15 @@ import me.arcanis.ffxivbis.service.LootSelector
import scala.jdk.CollectionConverters._
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 {
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 player(playerId: PlayerId): Option[Player] = players.get(playerId)
def withPlayer(player: Player): Party =
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))
} catch {
case exception: Exception =>
@ -36,18 +36,20 @@ case class Party(partyDescription: PartyDescription, rules: Seq[String], players
}
object Party {
def apply(partyId: Option[String], config: Config): Party =
new Party(partyId.getOrElse(randomPartyId), getRules(config), Map.empty)
def apply(party: PartyDescription, config: Config,
def apply(partyId: String, config: Config,
players: Map[Long, Player], bis: Seq[Loot], loot: Seq[Loot]): Party = {
val bisByPlayer = bis.groupBy(_.playerId).view.mapValues(piece => BiS(piece.map(_.piece)))
val lootByPlayer = loot.groupBy(_.playerId).view
val lootByPlayer = loot.groupBy(_.playerId).view.mapValues(_.map(_.piece))
val playersWithItems = players.foldLeft(Map.empty[PlayerId, Player]) {
case (acc, (playerId, player)) =>
acc + (player.playerId -> player
.withBiS(bisByPlayer.get(playerId))
.withLoot(lootByPlayer.getOrElse(playerId, Seq.empty)))
}
Party(party, getRules(config), playersWithItems)
Party(partyId, getRules(config), playersWithItems)
}
def getRules(config: Config): Seq[String] =

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

View File

@ -1,20 +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
case object Artifact extends PieceType
lazy val available: Seq[PieceType] =
Seq(Crafted, Tome, Savage, Artifact)
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
case class Player(id: Long,
partyId: String,
case class Player(partyId: String,
job: Job.Job,
nick: String,
bis: BiS,
loot: Seq[Loot],
loot: Seq[Piece],
link: Option[String] = None,
priority: Int = 0) {
require(job ne Job.AnyJob, "AnyJob is not allowed")
@ -28,10 +27,10 @@ case class Player(id: Long,
partyId, job, nick, isRequired(piece), priority,
bisCountTotal(piece), lootCount(piece),
lootCountBiS(piece), lootCountTotal(piece))
def withLoot(piece: Loot): Player = withLoot(Seq(piece))
def withLoot(list: Seq[Loot]): Player = {
require(loot.forall(_.playerId == id), "player id must be same")
copy(loot = loot ++ list)
def withLoot(piece: Piece): Player = withLoot(Seq(piece))
def withLoot(list: Seq[Piece]): Player = list match {
case Nil => this
case _ => copy(loot = list)
}
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 {
case Some(p) => loot.count(item => !item.isFreeLoot && item.piece == p)
case Some(p) => loot.count(_ == p)
case None => lootCountTotal(piece)
}
def lootCountBiS(piece: Option[Piece]): Int = loot.map(_.piece).count(bis.hasPiece)
def lootCountTotal(piece: Option[Piece]): Int = loot.count(!_.isFreeLoot)
def lootCountBiS(piece: Option[Piece]): Int = loot.count(bis.hasPiece)
def lootCountTotal(piece: Option[Piece]): Int = loot.length
def lootPriority(piece: Piece): Int = priority
}

View File

@ -12,7 +12,6 @@ import scala.util.Try
import scala.util.matching.Regex
trait PlayerIdBase {
def job: Job.Job
def nick: String
@ -22,7 +21,6 @@ trait PlayerIdBase {
case class PlayerId(partyId: String, job: Job.Job, nick: String) extends PlayerIdBase
object PlayerId {
def apply(partyId: String, maybeNick: Option[String], maybeJob: Option[String]): Option[PlayerId] =
(maybeNick, maybeJob) match {
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 {
private case class PlayerCountersComparator(values: Int*) {
def >(that: PlayerCountersComparator): Boolean = {
@scala.annotation.tailrec

View File

@ -0,0 +1,142 @@
/*
* 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.Http
import akka.http.scaladsl.model._
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.system.dispatchers.lookup("me.arcanis.ffxivbis.default-dispatcher")
override def receive: Receive = {
case GetBiS(link, job) =>
val client = sender()
get(link, job).map(BiS(_)).pipeTo(client)
}
override def postStop(): Unit = {
http.shutdownAllConnectionPools()
super.postStop()
}
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("left ring")
case "ringRight" => Some("right ring")
case "head" | "hands" | "waist" | "legs" | "feet" | "ears" | "neck" | "wrist" => Some(key)
case _ => None
}
}

View File

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

View File

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

View File

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

View File

@ -0,0 +1,47 @@
package me.arcanis.ffxivbis.service
import java.time.Instant
import akka.actor.Actor
import scala.concurrent.ExecutionContext
import scala.concurrent.duration.FiniteDuration
class RateLimiter extends Actor {
import RateLimiter._
import me.arcanis.ffxivbis.utils.Implicits._
implicit private val executionContext: ExecutionContext = context.system.dispatcher
private val maxRequestCount: Int = context.system.settings.config.getInt("me.arcanis.ffxivbis.web.limits.max-count")
private val requestInterval: FiniteDuration = context.system.settings.config.getDuration("me.arcanis.ffxivbis.web.limits.interval")
override def receive: Receive = handle(Map.empty)
private def handle(cache: Map[String, Usage]): Receive = {
case username: String =>
val client = sender()
val usage = if (cache.contains(username)) {
cache(username)
} else {
context.system.scheduler.scheduleOnce(requestInterval, self, Reset(username))
Usage()
}
context become handle(cache + (username -> usage.increment))
val response = if (usage.count > maxRequestCount) Some(usage.left) else None
client ! response
case Reset(username) =>
context become handle(cache - username)
}
}
object RateLimiter {
private case class Usage(count: Int = 0, since: Instant = Instant.now) {
def increment: Usage = copy(count = count + 1)
def left: Long = (Instant.now.toEpochMilli - since.toEpochMilli) / 1000
}
case class Reset(username: String)
}

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" | "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 != 999) 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
import akka.actor.typed.scaladsl.Behaviors
import me.arcanis.ffxivbis.messages.{AddPieceToBis, DatabaseMessage, GetBiS, RemovePieceFromBiS, RemovePiecesFromBiS}
import akka.pattern.pipe
import me.arcanis.ffxivbis.models.{Piece, PlayerId}
import me.arcanis.ffxivbis.service.Database
trait DatabaseBiSHandler { this: Database =>
import DatabaseBiSHandler._
def bisHandler: DatabaseMessage.Handler = {
case AddPieceToBis(playerId, piece, client) =>
profile.insertPieceBiS(playerId, piece).foreach(_ => client ! ())
Behaviors.same
def bisHandler: Receive = {
case AddPieceToBis(playerId, piece) =>
val client = sender()
profile.insertPieceBiS(playerId, piece).pipeTo(client)
case GetBiS(partyId, maybePlayerId, client) =>
case GetBiS(partyId, maybePlayerId) =>
val client = sender()
getParty(partyId, withBiS = true, withLoot = false)
.map(filterParty(_, maybePlayerId))
.foreach(client ! _)
Behaviors.same
.pipeTo(client)
case RemovePieceFromBiS(playerId, piece, client) =>
profile.deletePieceBiS(playerId, piece).foreach(_ => client ! ())
Behaviors.same
case RemovePiecesFromBiS(playerId, client) =>
profile.deletePiecesBiS(playerId).foreach(_ => client ! ())
Behaviors.same
case RemovePieceFromBiS(playerId, piece) =>
val client = sender()
profile.deletePieceBiS(playerId, piece).pipeTo(client)
}
}
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,24 @@
*/
package me.arcanis.ffxivbis.service.impl
import akka.actor.typed.{Behavior, DispatcherSelector}
import akka.actor.typed.scaladsl.{AbstractBehavior, ActorContext}
import com.typesafe.config.Config
import me.arcanis.ffxivbis.messages.DatabaseMessage
import akka.actor.Props
import me.arcanis.ffxivbis.service.Database
import me.arcanis.ffxivbis.storage.DatabaseProfile
import scala.concurrent.ExecutionContext
class DatabaseImpl(context: ActorContext[DatabaseMessage])
extends AbstractBehavior[DatabaseMessage](context) with Database
class DatabaseImpl extends Database
with DatabaseBiSHandler with DatabaseLootHandler
with DatabasePartyHandler with DatabaseUserHandler {
override implicit val executionContext: ExecutionContext = {
val selector = DispatcherSelector.fromConfig("me.arcanis.ffxivbis.default-dispatcher")
context.system.dispatchers.lookup(selector)
}
override val config: Config = context.system.settings.config
override val profile: DatabaseProfile = new DatabaseProfile(executionContext, config)
implicit val executionContext: ExecutionContext =
context.system.dispatchers.lookup("me.arcanis.ffxivbis.default-dispatcher")
val profile = new DatabaseProfile(executionContext, context.system.settings.config)
override def onMessage(msg: DatabaseMessage): Behavior[DatabaseMessage] = handle(msg)
private def handle: DatabaseMessage.Handler =
override def receive: Receive =
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
import java.time.Instant
import akka.actor.typed.scaladsl.Behaviors
import me.arcanis.ffxivbis.messages.{AddPieceTo, DatabaseMessage, GetLoot, RemovePieceFrom, SuggestLoot}
import me.arcanis.ffxivbis.models.Loot
import akka.pattern.pipe
import me.arcanis.ffxivbis.models.{Piece, PlayerId}
import me.arcanis.ffxivbis.service.Database
trait DatabaseLootHandler { this: Database =>
import DatabaseLootHandler._
def lootHandler: DatabaseMessage.Handler = {
case AddPieceTo(playerId, piece, isFreeLoot, client) =>
val loot = Loot(-1, piece, Instant.now, isFreeLoot)
profile.insertPiece(playerId, loot).foreach(_ => client ! ())
Behaviors.same
def lootHandler: Receive = {
case AddPieceTo(playerId, piece) =>
val client = sender()
profile.insertPiece(playerId, piece).pipeTo(client)
case GetLoot(partyId, maybePlayerId, client) =>
case GetLoot(partyId, maybePlayerId) =>
val client = sender()
getParty(partyId, withBiS = false, withLoot = true)
.map(filterParty(_, maybePlayerId))
.foreach(client ! _)
Behaviors.same
.pipeTo(client)
case RemovePieceFrom(playerId, piece, client) =>
profile.deletePiece(playerId, piece).foreach(_ => client ! ())
Behaviors.same
case RemovePieceFrom(playerId, piece) =>
val client = sender()
profile.deletePiece(playerId, piece).pipeTo(client)
case SuggestLoot(partyId, piece, client) =>
getParty(partyId, withBiS = true, withLoot = true)
.map(_.suggestLoot(piece))
.foreach(client ! _)
Behaviors.same
case SuggestLoot(partyId, piece) =>
val client = sender()
getParty(partyId, withBiS = true, withLoot = true).map(_.suggestLoot(piece)).pipeTo(client)
}
}
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
import akka.actor.typed.scaladsl.Behaviors
import me.arcanis.ffxivbis.messages.{AddPlayer, DatabaseMessage, GetParty, GetPartyDescription, GetPlayer, RemovePlayer, UpdateParty}
import me.arcanis.ffxivbis.models.{BiS, Player}
import akka.pattern.pipe
import me.arcanis.ffxivbis.models.{BiS, Player, PlayerId}
import me.arcanis.ffxivbis.service.Database
import scala.concurrent.Future
trait DatabasePartyHandler { this: Database =>
import DatabasePartyHandler._
def partyHandler: DatabaseMessage.Handler = {
case AddPlayer(player, client) =>
profile.insertPlayer(player).foreach(_ => client ! ())
Behaviors.same
def partyHandler: Receive = {
case AddPlayer(player) =>
val client = sender()
profile.insertPlayer(player).pipeTo(client)
case GetParty(partyId, client) =>
getParty(partyId, withBiS = true, withLoot = true).foreach(client ! _)
Behaviors.same
case GetParty(partyId) =>
val client = sender()
getParty(partyId, withBiS = true, withLoot = true).pipeTo(client)
case GetPartyDescription(partyId, client) =>
profile.getPartyDescription(partyId).foreach(client ! _)
Behaviors.same
case GetPlayer(playerId, client) =>
case GetPlayer(playerId) =>
val client = sender()
val player = profile.getPlayerFull(playerId).flatMap { maybePlayerData =>
Future.traverse(maybePlayerData.toSeq) { playerData =>
for {
bis <- profile.getPiecesBiS(playerId)
loot <- profile.getPieces(playerId)
} yield Player(playerData.id, playerId.partyId, playerId.job,
playerId.nick, BiS(bis.map(_.piece)), loot,
} yield Player(playerId.partyId, playerId.job, playerId.nick,
BiS(bis.map(_.piece)), loot.map(_.piece),
playerData.link, playerData.priority)
}
}.map(_.headOption)
player.foreach(client ! _)
Behaviors.same
player.pipeTo(client)
case RemovePlayer(playerId, client) =>
profile.deletePlayer(playerId).foreach(_ => client ! ())
Behaviors.same
case UpdateParty(description, client) =>
profile.insertPartyDescription(description).foreach(_ => client ! ())
Behaviors.same
case RemovePlayer(playerId) =>
val client = sender()
profile.deletePlayer(playerId).pipeTo(client)
}
}
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
import akka.actor.typed.scaladsl.Behaviors
import me.arcanis.ffxivbis.messages.{AddUser, DatabaseMessage, DeleteUser, Exists, GetUser, GetUsers}
import akka.pattern.pipe
import me.arcanis.ffxivbis.models.User
import me.arcanis.ffxivbis.service.Database
trait DatabaseUserHandler { this: Database =>
import DatabaseUserHandler._
def userHandler: DatabaseMessage.Handler = {
case AddUser(user, isHashedPassword, client) =>
def userHandler: Receive = {
case AddUser(user, isHashedPassword) =>
val client = sender()
val toInsert = if (isHashedPassword) user else user.withHashedPassword
profile.insertUser(toInsert).foreach(_ => client ! ())
Behaviors.same
profile.insertUser(toInsert).pipeTo(client)
case DeleteUser(partyId, username, client) =>
profile.deleteUser(partyId, username).foreach(_ => client ! ())
Behaviors.same
case DeleteUser(partyId, username) =>
val client = sender()
profile.deleteUser(partyId, username).pipeTo(client)
case Exists(partyId, client) =>
profile.exists(partyId).foreach(client ! _)
Behaviors.same
case Exists(partyId) =>
val client = sender()
profile.exists(partyId).pipeTo(client)
case GetUser(partyId, username, client) =>
profile.getUser(partyId, username).foreach(client ! _)
Behaviors.same
case GetUser(partyId, username) =>
val client = sender()
profile.getUser(partyId, username).pipeTo(client)
case GetUsers(partyId, client) =>
profile.getUsers(partyId).foreach(client ! _)
Behaviors.same
case GetUsers(partyId) =>
val client = sender()
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,9 +8,7 @@
*/
package me.arcanis.ffxivbis.storage
import java.time.Instant
import me.arcanis.ffxivbis.models.{Job, Loot, Piece, PieceType}
import me.arcanis.ffxivbis.models.{Job, Loot, Piece}
import slick.lifted.ForeignKeyQuery
import scala.concurrent.Future
@ -18,27 +16,24 @@ import scala.concurrent.Future
trait BiSProfile { this: DatabaseProfile =>
import dbConfig.profile.api._
case class BiSRep(playerId: Long, created: Long, piece: String,
pieceType: String, job: String) {
def toLoot: Loot = Loot(
playerId, Piece(piece, PieceType.withName(pieceType), Job.withName(job)),
Instant.ofEpochMilli(created), isFreeLoot = false)
case class BiSRep(playerId: Long, created: Long, piece: String, isTome: Int, job: String) {
def toLoot: Loot = Loot(playerId, Piece(piece, isTome == 1, Job.withName(job)))
}
object BiSRep {
def fromPiece(playerId: Long, piece: Piece): BiSRep =
BiSRep(playerId, DatabaseProfile.now, piece.piece,
piece.pieceType.toString, piece.job.toString)
def fromPiece(playerId: Long, piece: Piece) =
BiSRep(playerId, DatabaseProfile.now, piece.piece, if (piece.isTome) 1 else 0,
piece.job.toString)
}
class BiSPieces(tag: Tag) extends Table[BiSRep](tag, "bis") {
def playerId: Rep[Long] = column[Long]("player_id", O.PrimaryKey)
def created: Rep[Long] = column[Long]("created")
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 * =
(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] =
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] =
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(playerIds: Seq[Long]): Future[Seq[Loot]] =
db.run(piecesBiS(playerIds).result).map(_.map(_.toLoot))
def insertPieceBiSById(piece: Piece)(playerId: Long): Future[Int] =
getPiecesBiSById(playerId).flatMap {
case pieces if pieces.exists(loot => loot.piece.strictEqual(piece)) => Future.successful(0)
case _ => db.run(bisTable.insertOrUpdate(BiSRep.fromPiece(playerId, piece)))
}
db.run(bisTable.insertOrUpdate(BiSRep.fromPiece(playerId, piece)))
private def pieceBiS(piece: BiSRep) =
piecesBiS(Seq(piece.playerId)).filter { stored =>
(stored.piece === piece.piece) && (stored.pieceType === piece.pieceType)
}
piecesBiS(Seq(piece.playerId)).filter(_.piece === piece.piece)
private def piecesBiS(playerIds: Seq[Long]) =
bisTable.filter(_.playerId.inSet(playerIds.toSet))
}

View File

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

View File

@ -8,9 +8,7 @@
*/
package me.arcanis.ffxivbis.storage
import java.time.Instant
import me.arcanis.ffxivbis.models.{Job, Loot, Piece, PieceType}
import me.arcanis.ffxivbis.models.{Job, Loot, Piece}
import slick.lifted.{ForeignKeyQuery, Index}
import scala.concurrent.Future
@ -18,19 +16,14 @@ import scala.concurrent.Future
trait LootProfile { this: DatabaseProfile =>
import dbConfig.profile.api._
case class LootRep(lootId: Option[Long], playerId: Long, created: Long,
piece: String, pieceType: String, job: String,
isFreeLoot: Int) {
def toLoot: Loot = Loot(
playerId,
Piece(piece, PieceType.withName(pieceType), Job.withName(job)),
Instant.ofEpochMilli(created), isFreeLoot == 1)
case class LootRep(lootId: Option[Long], playerId: Long, created: Long, piece: String,
isTome: Int, job: String) {
def toLoot: Loot = Loot(playerId, Piece(piece, isTome == 1, Job.withName(job)))
}
object LootRep {
def fromLoot(playerId: Long, loot: Loot): LootRep =
LootRep(None, playerId, loot.timestamp.toEpochMilli, loot.piece.piece,
loot.piece.pieceType.toString, loot.piece.job.toString,
if (loot.isFreeLoot) 1 else 0)
def fromPiece(playerId: Long, piece: Piece) =
LootRep(None, playerId, DatabaseProfile.now, piece.piece, if (piece.isTome) 1 else 0,
piece.job.toString)
}
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 created: Rep[Long] = column[Long]("created")
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 isFreeLoot: Rep[Int] = column[Int]("is_free_loot")
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] =
foreignKey("player_id", playerId, playersTable)(_.playerId, onDelete = ForeignKeyAction.Cascade)
@ -51,16 +43,16 @@ trait LootProfile { this: DatabaseProfile =>
index("loot_owner_idx", (playerId), unique = false)
}
def deletePieceById(loot: Loot)(playerId: Long): Future[Int] =
db.run(pieceLoot(LootRep.fromLoot(playerId, loot)).map(_.lootId).max.result).flatMap {
def deletePieceById(piece: Piece)(playerId: Long): Future[Int] =
db.run(pieceLoot(LootRep.fromPiece(playerId, piece)).map(_.lootId).max.result).flatMap {
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(playerIds: Seq[Long]): Future[Seq[Loot]] =
db.run(piecesLoot(playerIds).result).map(_.map(_.toLoot))
def insertPieceById(loot: Loot)(playerId: Long): Future[Int] =
db.run(lootTable.insertOrUpdate(LootRep.fromLoot(playerId, loot)))
def insertPieceById(piece: Piece)(playerId: Long): Future[Int] =
db.run(lootTable.insertOrUpdate(LootRep.fromPiece(playerId, piece)))
private def pieceLoot(piece: LootRep) =
piecesLoot(Seq(piece.playerId)).filter(_.piece === piece.piece)

View File

@ -13,11 +13,9 @@ import org.flywaydb.core.Flyway
import org.flywaydb.core.api.configuration.ClassicConfiguration
import scala.concurrent.Future
import scala.util.Try
class Migration(config: Config) {
def performMigration(): Try[Boolean] = {
def performMigration(): Future[Int] = {
val section = DatabaseProfile.getSection(config)
val url = section.getString("db.url")
@ -34,11 +32,10 @@ class Migration(config: Config) {
flywayConfiguration.setDataSource(url, username, password)
val flyway = new Flyway(flywayConfiguration)
Try(flyway.migrate().success)
Future.successful(flyway.migrate())
}
}
object Migration {
def apply(config: Config): Try[Boolean] = 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

@ -15,11 +15,10 @@ import scala.concurrent.Future
trait PlayersProfile { this: DatabaseProfile =>
import dbConfig.profile.api._
case class PlayerRep(partyId: String, playerId: Option[Long], created: Long,
nick: String, job: String, link: Option[String], priority: Int) {
case class PlayerRep(partyId: String, playerId: Option[Long], created: Long, nick: String,
job: String, link: Option[String], priority: Int) {
def toPlayer: Player =
Player(playerId.getOrElse(-1), partyId, Job.withName(job), nick,
BiS.empty, Seq.empty, link, priority)
Player(partyId, Job.withName(job), nick, BiS(Seq.empty), List.empty, link, priority)
}
object PlayerRep {
def fromPlayer(player: Player, id: Option[Long]): PlayerRep =
@ -40,6 +39,7 @@ trait PlayersProfile { this: DatabaseProfile =>
(partyId, playerId.?, created, nick, job, bisLink, priority) <> ((PlayerRep.apply _).tupled, PlayerRep.unapply)
}
def deletePlayer(playerId: PlayerId): Future[Int] = db.run(player(playerId).delete)
def getParty(partyId: String): Future[Map[Long, Player]] =
db.run(players(partyId).result).map(_.foldLeft(Map.empty[Long, Player]) {
@ -53,10 +53,10 @@ trait PlayersProfile { this: DatabaseProfile =>
def getPlayers(partyId: String): Future[Seq[Long]] =
db.run(players(partyId).map(_.playerId).result)
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 _ => db.run(playersTable.insertOrUpdate(PlayerRep.fromPlayer(playerObj, None)))
}
}.flatten
private def player(playerId: PlayerId) =
playersTable

View File

@ -16,8 +16,8 @@ import scala.concurrent.Future
trait UsersProfile { this: DatabaseProfile =>
import dbConfig.profile.api._
case class UserRep(partyId: String, userId: Option[Long], username: String,
password: String, permission: String) {
case class UserRep(partyId: String, userId: Option[Long], username: String, password: String,
permission: String) {
def toUser: User = User(partyId, username, password, Permission.withName(permission))
}
object UserRep {
@ -49,10 +49,10 @@ trait UsersProfile { this: DatabaseProfile =>
def getUsers(partyId: String): Future[Seq[User]] =
db.run(user(partyId, None).result).map(_.map(_.toUser))
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 _ => db.run(usersTable.insertOrUpdate(UserRep.fromUser(userObj, None)))
}
}.flatten
private def user(partyId: String, username: Option[String]) =
usersTable

View File

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

View File

@ -5,49 +5,33 @@ import me.arcanis.ffxivbis.models._
object Fixtures {
lazy val bis: BiS = BiS(
Seq(
Weapon(pieceType = PieceType.Savage ,Job.DNC),
Head(pieceType = PieceType.Savage, Job.DNC),
Body(pieceType = PieceType.Savage, Job.DNC),
Hands(pieceType = PieceType.Tome, Job.DNC),
Legs(pieceType = PieceType.Tome, Job.DNC),
Feet(pieceType = PieceType.Savage, Job.DNC),
Ears(pieceType = PieceType.Savage, Job.DNC),
Neck(pieceType = PieceType.Tome, Job.DNC),
Wrist(pieceType = PieceType.Savage, Job.DNC),
Ring(pieceType = PieceType.Tome, Job.DNC, "left ring"),
Ring(pieceType = PieceType.Tome, Job.DNC, "right ring")
)
)
lazy val bis2: BiS = BiS(
Seq(
Weapon(pieceType = PieceType.Savage ,Job.DNC),
Head(pieceType = PieceType.Tome, Job.DNC),
Body(pieceType = PieceType.Savage, Job.DNC),
Hands(pieceType = PieceType.Tome, Job.DNC),
Legs(pieceType = PieceType.Savage, Job.DNC),
Feet(pieceType = PieceType.Tome, Job.DNC),
Ears(pieceType = PieceType.Savage, Job.DNC),
Neck(pieceType = PieceType.Savage, Job.DNC),
Wrist(pieceType = PieceType.Savage, Job.DNC),
Ring(pieceType = PieceType.Tome, Job.DNC, "left ring"),
Ring(pieceType = PieceType.Savage, Job.DNC, "right ring")
Weapon(isTome = false ,Job.DNC),
Head(isTome = false, Job.DNC),
Body(isTome = false, Job.DNC),
Hands(isTome = true, Job.DNC),
Waist(isTome = true, Job.DNC),
Legs(isTome = true, Job.DNC),
Feet(isTome = false, Job.DNC),
Ears(isTome = false, Job.DNC),
Neck(isTome = true, Job.DNC),
Wrist(isTome = false, Job.DNC),
Ring(isTome = true, Job.DNC, "left ring"),
Ring(isTome = true, Job.DNC, "right ring")
)
)
lazy val link: String = "https://ffxiv.ariyala.com/19V5R"
lazy val link2: String = "https://ffxiv.ariyala.com/1A0WM"
lazy val link3: String = "https://etro.gg/gearset/26a67536-b4ce-4adc-a46a-f70e348bb138"
lazy val link4: String = "https://etro.gg/gearset/865fc886-994f-4c28-8fc1-4379f160a916"
lazy val lootWeapon: Piece = Weapon(pieceType = PieceType.Tome, Job.AnyJob)
lazy val lootBody: Piece = Body(pieceType = PieceType.Savage, Job.AnyJob)
lazy val lootBodyCrafted: Piece = Body(pieceType = PieceType.Crafted, Job.AnyJob)
lazy val lootHands: Piece = Hands(pieceType = PieceType.Tome, Job.AnyJob)
lazy val lootLegs: Piece = Legs(pieceType = PieceType.Savage, Job.AnyJob)
lazy val lootEars: Piece = Ears(pieceType = PieceType.Savage, Job.AnyJob)
lazy val lootRing: Piece = Ring(pieceType = PieceType.Tome, Job.AnyJob)
lazy val lootLeftRing: Piece = Ring(pieceType = PieceType.Tome, Job.AnyJob, "left ring")
lazy val lootRightRing: Piece = Ring(pieceType = PieceType.Tome, Job.AnyJob, "right ring")
lazy val lootWeapon: Piece = Weapon(isTome = true, Job.AnyJob)
lazy val lootBody: Piece = Body(isTome = false, Job.AnyJob)
lazy val lootHands: Piece = Hands(isTome = true, Job.AnyJob)
lazy val lootWaist: Piece = Waist(isTome = true, Job.AnyJob)
lazy val lootLegs: Piece = Legs(isTome = false, Job.AnyJob)
lazy val lootEars: Piece = Ears(isTome = false, Job.AnyJob)
lazy val lootRing: Piece = Ring(isTome = true, Job.AnyJob)
lazy val lootLeftRing: Piece = Ring(isTome = true, Job.AnyJob, "left ring")
lazy val lootRightRing: Piece = Ring(isTome = true, Job.AnyJob, "right ring")
lazy val lootUpgrade: Piece = BodyUpgrade
lazy val loot: Seq[Piece] = Seq(lootBody, lootHands, lootLegs, lootUpgrade)
@ -55,7 +39,7 @@ object Fixtures {
lazy val partyId2: String = Party.randomPartyId
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 userPassword: String = "password"

View File

@ -1,65 +1,50 @@
package me.arcanis.ffxivbis.http.api.v1
import akka.actor.testkit.typed.scaladsl.ActorTestKit
import akka.actor.typed.scaladsl.AskPattern.Askable
import akka.actor.ActorRef
import akka.http.scaladsl.model.{StatusCodes, Uri}
import akka.http.scaladsl.model.headers.{Authorization, BasicHttpCredentials}
import akka.http.scaladsl.testkit.{RouteTestTimeout, ScalatestRouteTest}
import akka.http.scaladsl.server._
import akka.pattern.ask
import akka.testkit.TestKit
import com.typesafe.config.Config
import me.arcanis.ffxivbis.{Fixtures, Settings}
import me.arcanis.ffxivbis.http.api.v1.json._
import me.arcanis.ffxivbis.messages.{AddPlayer, AddUser}
import me.arcanis.ffxivbis.models.{BiS, Job}
import me.arcanis.ffxivbis.service.bis.BisProvider
import me.arcanis.ffxivbis.service.{Database, PartyService}
import me.arcanis.ffxivbis.models.BiS
import me.arcanis.ffxivbis.service.{Ariyala, PartyService, impl}
import me.arcanis.ffxivbis.storage.Migration
import me.arcanis.ffxivbis.utils.Compare
import org.scalatest.matchers.should.Matchers
import org.scalatest.wordspec.AnyWordSpecLike
import org.scalatest.{Matchers, WordSpec}
import scala.concurrent.Await
import scala.concurrent.duration._
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)
override val testConfig: Config = testKit.system.settings.config
private val auth =
private val auth: Authorization =
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 askTimeout = 60 seconds
implicit private val routeTimeout: RouteTestTimeout = RouteTestTimeout(askTimeout)
private val timeout: FiniteDuration = 60 seconds
implicit private val routeTimeout: RouteTestTimeout = RouteTestTimeout(timeout)
private val storage = testKit.spawn(Database())
private val provider = testKit.spawn(BisProvider())
private val party = testKit.spawn(PartyService(storage))
private val route = new BiSEndpoint(party, provider)(askTimeout, testKit.scheduler).route
private val storage: ActorRef = system.actorOf(impl.DatabaseImpl.props)
private val ariyala: ActorRef = system.actorOf(Ariyala.props)
private val party: ActorRef = system.actorOf(PartyService.props(storage))
private val route: Route = new BiSEndpoint(party, ariyala)(timeout).route
override def beforeAll(): Unit = {
super.beforeAll()
Migration(testConfig)
Await.result(storage.ask(AddUser(Fixtures.userAdmin, isHashedPassword = true, _))(askTimeout, testKit.scheduler), askTimeout)
Await.result(storage.ask(AddPlayer(Fixtures.playerEmpty, _))(askTimeout, testKit.scheduler), askTimeout)
override def testConfig: Config = Settings.withRandomDatabase
override def beforeAll: Unit = {
Await.result(Migration(system.settings.config), timeout)
Await.result((storage ? impl.DatabaseUserHandler.AddUser(Fixtures.userAdmin, isHashedPassword = true))(timeout).mapTo[Int], timeout)
Await.result((storage ? impl.DatabasePartyHandler.AddPlayer(Fixtures.playerEmpty))(timeout).mapTo[Int], timeout)
}
override def afterAll(): Unit = {
Settings.clearDatabase(testConfig)
override def afterAll: Unit = {
TestKit.shutdownActorSystem(system)
testKit.shutdownTestKit()
super.afterAll()
}
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
Settings.clearDatabase(system.settings.config)
}
"api v1 bis endpoint" must {
@ -75,19 +60,17 @@ class BiSEndpointTest extends AnyWordSpecLike with Matchers with ScalatestRouteT
"return best in slot set" in {
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 {
status shouldEqual StatusCodes.OK
val actual = responseAs[Seq[PlayerResponse]]
actual.length shouldEqual 1
actual.foreach(compareBiSResponse(_, response))
responseAs[Seq[PlayerResponse]] shouldEqual response
}
}
"remove item from best in slot set" in {
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 {
status shouldEqual StatusCodes.Accepted
@ -96,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 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 {
status shouldEqual StatusCodes.OK
val actual = responseAs[Seq[PlayerResponse]]
actual.length shouldEqual 1
actual.foreach(compareBiSResponse(_, response))
responseAs[Seq[PlayerResponse]] shouldEqual response
}
}
"add item to best in slot set" in {
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 {
status shouldEqual StatusCodes.Accepted
@ -116,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 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 {
status shouldEqual StatusCodes.OK
val actual = responseAs[Seq[PlayerResponse]]
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))
responseAs[Seq[PlayerResponse]] shouldEqual response
}
}

View File

@ -1,61 +1,55 @@
package me.arcanis.ffxivbis.http.api.v1
import java.time.Instant
import akka.actor.testkit.typed.scaladsl.ActorTestKit
import akka.actor.typed.scaladsl.AskPattern.Askable
import akka.actor.ActorRef
import akka.http.scaladsl.model.{StatusCodes, Uri}
import akka.http.scaladsl.model.headers.{Authorization, BasicHttpCredentials}
import akka.http.scaladsl.testkit.{RouteTestTimeout, ScalatestRouteTest}
import akka.http.scaladsl.server._
import akka.pattern.ask
import akka.testkit.TestKit
import com.typesafe.config.Config
import me.arcanis.ffxivbis.{Fixtures, Settings}
import me.arcanis.ffxivbis.http.api.v1.json._
import me.arcanis.ffxivbis.messages.{AddPlayer, AddUser}
import me.arcanis.ffxivbis.service.{Database, PartyService}
import me.arcanis.ffxivbis.service.{PartyService, impl}
import me.arcanis.ffxivbis.storage.Migration
import org.scalatest.matchers.should.Matchers
import org.scalatest.wordspec.AnyWordSpecLike
import org.scalatest.{Matchers, WordSpec}
import scala.concurrent.Await
import scala.concurrent.duration._
import scala.language.postfixOps
class LootEndpointTest extends AnyWordSpecLike with Matchers with ScalatestRouteTest with JsonSupport {
class LootEndpointTest extends WordSpec
with Matchers with ScalatestRouteTest with JsonSupport {
private val testKit = ActorTestKit(Settings.withRandomDatabase)
override val testConfig: Config = testKit.system.settings.config
private val auth =
private val auth: Authorization =
Authorization(BasicHttpCredentials(Fixtures.userAdmin.username, Fixtures.userPassword))
private val endpoint = Uri(s"/party/${Fixtures.partyId}/loot")
private val endpoint: Uri = Uri(s"/party/${Fixtures.partyId}/loot")
private val playerId = PlayerIdResponse.fromPlayerId(Fixtures.playerEmpty.playerId)
private val askTimeout = 60 seconds
implicit private val routeTimeout: RouteTestTimeout = RouteTestTimeout(askTimeout)
private val timeout: FiniteDuration = 60 seconds
implicit private val routeTimeout: RouteTestTimeout = RouteTestTimeout(timeout)
private val storage = testKit.spawn(Database())
private val party = testKit.spawn(PartyService(storage))
private val route = new LootEndpoint(party)(askTimeout, testKit.scheduler).route
private val storage: ActorRef = system.actorOf(impl.DatabaseImpl.props)
private val party: ActorRef = system.actorOf(PartyService.props(storage))
private val route: Route = new LootEndpoint(party)(timeout).route
override def beforeAll(): Unit = {
super.beforeAll()
Migration(testConfig)
Await.result(storage.ask(AddUser(Fixtures.userAdmin, isHashedPassword = true, _))(askTimeout, testKit.scheduler), askTimeout)
Await.result(storage.ask(AddPlayer(Fixtures.playerEmpty, _))(askTimeout, testKit.scheduler), askTimeout)
override def testConfig: Config = Settings.withRandomDatabase
override def beforeAll: Unit = {
Await.result(Migration(system.settings.config), timeout)
Await.result((storage ? impl.DatabaseUserHandler.AddUser(Fixtures.userAdmin, isHashedPassword = true))(timeout).mapTo[Int], timeout)
Await.result((storage ? impl.DatabasePartyHandler.AddPlayer(Fixtures.playerEmpty))(timeout).mapTo[Int], timeout)
}
override def afterAll(): Unit = {
Settings.clearDatabase(testConfig)
override def afterAll: Unit = {
TestKit.shutdownActorSystem(system)
testKit.shutdownTestKit()
super.afterAll()
Settings.clearDatabase(system.settings.config)
}
"api v1 loot endpoint" must {
"add item to loot" in {
val piece = PieceResponse.fromPiece(Fixtures.lootBody)
val entity = PieceActionResponse(ApiAction.add, piece, playerId, Some(false))
val entity = PieceActionResponse(ApiAction.add, piece, playerId)
Post(endpoint, entity).withHeaders(auth) ~> route ~> check {
status shouldEqual StatusCodes.Accepted
@ -64,23 +58,18 @@ class LootEndpointTest extends AnyWordSpecLike with Matchers with ScalatestRoute
}
"return looted items" in {
import me.arcanis.ffxivbis.utils.Converters._
val uri = endpoint.withQuery(Uri.Query(Map("nick" -> playerId.nick, "job" -> playerId.job)))
val response = Seq(PlayerResponse.fromPlayer(Fixtures.playerEmpty.withLoot(Fixtures.lootBody)))
Get(uri).withHeaders(auth) ~> route ~> check {
status shouldEqual StatusCodes.OK
val withEmptyTimestamp = responseAs[Seq[PlayerResponse]].map { player =>
player.copy(loot = player.loot.map(_.map(_.copy(timestamp = Instant.ofEpochMilli(0)))))
}
withEmptyTimestamp shouldEqual response
responseAs[Seq[PlayerResponse]] shouldEqual response
}
}
"remove item from loot" in {
val piece = PieceResponse.fromPiece(Fixtures.lootBody)
val entity = PieceActionResponse(ApiAction.remove, piece, playerId, Some(false))
val entity = PieceActionResponse(ApiAction.remove, piece, playerId)
Post(endpoint, entity).withHeaders(auth) ~> route ~> check {
status shouldEqual StatusCodes.Accepted

View File

@ -1,74 +1,60 @@
package me.arcanis.ffxivbis.http.api.v1
import akka.actor.testkit.typed.scaladsl.ActorTestKit
import akka.actor.typed.scaladsl.AskPattern.Askable
import akka.actor.ActorRef
import akka.http.scaladsl.model.{StatusCodes, Uri}
import akka.http.scaladsl.model.headers.{Authorization, BasicHttpCredentials}
import akka.http.scaladsl.testkit.{RouteTestTimeout, ScalatestRouteTest}
import akka.http.scaladsl.server._
import akka.pattern.ask
import akka.testkit.TestKit
import com.typesafe.config.Config
import me.arcanis.ffxivbis.{Fixtures, Settings}
import me.arcanis.ffxivbis.http.api.v1.json._
import me.arcanis.ffxivbis.messages.AddUser
import me.arcanis.ffxivbis.models.PartyDescription
import me.arcanis.ffxivbis.service.bis.BisProvider
import me.arcanis.ffxivbis.service.{Database, PartyService}
import me.arcanis.ffxivbis.service.{Ariyala, PartyService, impl}
import me.arcanis.ffxivbis.storage.Migration
import org.scalatest.matchers.should.Matchers
import org.scalatest.wordspec.AnyWordSpecLike
import org.scalatest.{Matchers, WordSpec}
import scala.concurrent.Await
import scala.concurrent.duration._
import scala.language.postfixOps
class PartyEndpointTest extends AnyWordSpecLike with Matchers with ScalatestRouteTest with JsonSupport {
class PartyEndpointTest extends WordSpec
with Matchers with ScalatestRouteTest with JsonSupport {
private val testKit = ActorTestKit(Settings.withRandomDatabase)
override val testConfig: Config = testKit.system.settings.config
private val auth =
private val auth: Authorization =
Authorization(BasicHttpCredentials(Fixtures.userAdmin.username, Fixtures.userPassword))
private val endpoint = Uri(s"/party/${Fixtures.partyId}/description")
private val askTimeout = 60 seconds
implicit private val routeTimeout: RouteTestTimeout = RouteTestTimeout(askTimeout)
private val endpoint: Uri = Uri(s"/party/${Fixtures.partyId}")
private val playerId = PlayerIdResponse.fromPlayerId(Fixtures.playerEmpty.playerId)
private val timeout: FiniteDuration = 60 seconds
implicit private val routeTimeout: RouteTestTimeout = RouteTestTimeout(timeout)
private val storage = testKit.spawn(Database())
private val provider = testKit.spawn(BisProvider())
private val party = testKit.spawn(PartyService(storage))
private val route = new PartyEndpoint(party, provider)(askTimeout, testKit.scheduler).route
private val storage: ActorRef = system.actorOf(impl.DatabaseImpl.props)
private val ariyala: ActorRef = system.actorOf(Ariyala.props)
private val party: ActorRef = system.actorOf(PartyService.props(storage))
private val route: Route = new PlayerEndpoint(party, ariyala)(timeout).route
override def beforeAll(): Unit = {
super.beforeAll()
Migration(testConfig)
Await.result(storage.ask(AddUser(Fixtures.userAdmin, isHashedPassword = true, _))(askTimeout, testKit.scheduler), askTimeout)
override def testConfig: Config = Settings.withRandomDatabase
override def beforeAll: Unit = {
Await.result(Migration(system.settings.config), timeout)
Await.result((storage ? impl.DatabaseUserHandler.AddUser(Fixtures.userAdmin, isHashedPassword = true))(timeout).mapTo[Int], timeout)
Await.result((storage ? impl.DatabasePartyHandler.AddPlayer(Fixtures.playerEmpty))(timeout).mapTo[Int], timeout)
}
override def afterAll(): Unit = {
Settings.clearDatabase(testConfig)
override def afterAll: Unit = {
TestKit.shutdownActorSystem(system)
testKit.shutdownTestKit()
super.afterAll()
Settings.clearDatabase(system.settings.config)
}
"api v1 party endpoint" must {
"get empty party description" in {
Get(endpoint).withHeaders(auth) ~> route ~> check {
status shouldEqual StatusCodes.OK
responseAs[PartyDescriptionResponse].toDescription shouldEqual PartyDescription.empty(Fixtures.partyId)
}
}
"update party description" in {
val entity = PartyDescriptionResponse(Fixtures.partyId, Some("random party name"))
Post(endpoint, entity).withHeaders(auth) ~> route ~> check {
status shouldEqual StatusCodes.Accepted
}
"get users" in {
val uri = endpoint.withQuery(Uri.Query(Map("nick" -> playerId.nick, "job" -> playerId.job)))
val response = Seq(PlayerResponse.fromPlayer(Fixtures.playerEmpty))
Get(endpoint).withHeaders(auth) ~> route ~> check {
status shouldEqual StatusCodes.OK
responseAs[PartyDescriptionResponse].toDescription shouldEqual entity.toDescription
responseAs[Seq[PlayerResponse]] shouldEqual response
}
}

View File

@ -1,65 +0,0 @@
package me.arcanis.ffxivbis.http.api.v1
import akka.actor.testkit.typed.scaladsl.ActorTestKit
import akka.actor.typed.scaladsl.AskPattern.Askable
import akka.http.scaladsl.model.{StatusCodes, Uri}
import akka.http.scaladsl.model.headers.{Authorization, BasicHttpCredentials}
import akka.http.scaladsl.testkit.{RouteTestTimeout, ScalatestRouteTest}
import akka.testkit.TestKit
import com.typesafe.config.Config
import me.arcanis.ffxivbis.{Fixtures, Settings}
import me.arcanis.ffxivbis.http.api.v1.json._
import me.arcanis.ffxivbis.messages.{AddPlayer, AddUser}
import me.arcanis.ffxivbis.service.bis.BisProvider
import me.arcanis.ffxivbis.service.{Database, PartyService}
import me.arcanis.ffxivbis.storage.Migration
import org.scalatest.matchers.should.Matchers
import org.scalatest.wordspec.AnyWordSpecLike
import scala.concurrent.Await
import scala.concurrent.duration._
import scala.language.postfixOps
class PlayerEndpointTest extends AnyWordSpecLike with Matchers with ScalatestRouteTest with JsonSupport {
private val testKit = ActorTestKit(Settings.withRandomDatabase)
override val testConfig: Config = testKit.system.settings.config
private val auth =
Authorization(BasicHttpCredentials(Fixtures.userAdmin.username, Fixtures.userPassword))
private val endpoint = Uri(s"/party/${Fixtures.partyId}")
private val askTimeout = 60 seconds
implicit private val routeTimeout: RouteTestTimeout = RouteTestTimeout(askTimeout)
private val storage = testKit.spawn(Database())
private val provider = testKit.spawn(BisProvider())
private val party = testKit.spawn(PartyService(storage))
private val route = new PlayerEndpoint(party, provider)(askTimeout, testKit.scheduler).route
override def beforeAll(): Unit = {
super.beforeAll()
Migration(testConfig)
Await.result(storage.ask(AddUser(Fixtures.userAdmin, isHashedPassword = true, _))(askTimeout, testKit.scheduler), askTimeout)
Await.result(storage.ask(AddPlayer(Fixtures.playerEmpty, _))(askTimeout, testKit.scheduler), askTimeout)
}
override def afterAll(): Unit = {
Settings.clearDatabase(testConfig)
TestKit.shutdownActorSystem(system)
testKit.shutdownTestKit()
super.afterAll()
}
"api v1 player endpoint" must {
"get users" in {
val response = Seq(PlayerResponse.fromPlayer(Fixtures.playerEmpty))
Get(endpoint).withHeaders(auth) ~> route ~> check {
status shouldEqual StatusCodes.OK
responseAs[Seq[PlayerResponse]] shouldEqual response
}
}
}
}

View File

@ -2,21 +2,21 @@ package me.arcanis.ffxivbis.http.api.v1
import akka.http.scaladsl.model.StatusCodes
import akka.http.scaladsl.testkit.ScalatestRouteTest
import akka.http.scaladsl.server._
import com.typesafe.config.Config
import me.arcanis.ffxivbis.Settings
import me.arcanis.ffxivbis.http.api.v1.json._
import me.arcanis.ffxivbis.models.{Job, Party, Permission, Piece, PieceType}
import org.scalatest.matchers.should.Matchers
import org.scalatest.wordspec.AnyWordSpecLike
import me.arcanis.ffxivbis.models.{Job, Party, Permission, Piece}
import org.scalatest.{Matchers, WordSpec}
import scala.language.postfixOps
class TypesEndpointTest extends AnyWordSpecLike
class TypesEndpointTest extends WordSpec
with Matchers with ScalatestRouteTest with JsonSupport {
override val testConfig: Config = Settings.withRandomDatabase
private val route: Route = new TypesEndpoint(testConfig).route
private val route = new TypesEndpoint(testConfig).route
override def testConfig: Config = Settings.withRandomDatabase
"api v1 types endpoint" must {
@ -41,13 +41,6 @@ class TypesEndpointTest extends AnyWordSpecLike
}
}
"return all available piece types" in {
Get("/types/pieces/types") ~> route ~> check {
status shouldEqual StatusCodes.OK
responseAs[Seq[String]] shouldEqual PieceType.available.map(_.toString)
}
}
"return current priority" in {
Get("/types/priority") ~> route ~> check {
status shouldEqual StatusCodes.OK

View File

@ -1,48 +1,45 @@
package me.arcanis.ffxivbis.http.api.v1
import akka.actor.testkit.typed.scaladsl.ActorTestKit
import akka.actor.ActorRef
import akka.http.scaladsl.model.{StatusCodes, Uri}
import akka.http.scaladsl.model.headers.{Authorization, BasicHttpCredentials}
import akka.http.scaladsl.testkit.{RouteTestTimeout, ScalatestRouteTest}
import akka.http.scaladsl.server._
import akka.testkit.TestKit
import com.typesafe.config.Config
import me.arcanis.ffxivbis.{Fixtures, Settings}
import me.arcanis.ffxivbis.http.api.v1.json._
import me.arcanis.ffxivbis.service.{Database, PartyService}
import me.arcanis.ffxivbis.service.{PartyService, impl}
import me.arcanis.ffxivbis.storage.Migration
import org.scalatest.matchers.should.Matchers
import org.scalatest.wordspec.AnyWordSpecLike
import org.scalatest.{Matchers, WordSpec}
import scala.concurrent.Await
import scala.concurrent.duration._
import scala.language.postfixOps
class UserEndpointTest extends AnyWordSpecLike with Matchers with ScalatestRouteTest with JsonSupport {
class UserEndpointTest extends WordSpec
with Matchers with ScalatestRouteTest with JsonSupport {
private val testKit = ActorTestKit(Settings.withRandomDatabase)
override val testConfig: Config = testKit.system.settings.config
private val auth =
private val auth: Authorization =
Authorization(BasicHttpCredentials(Fixtures.userAdmin.username, Fixtures.userPassword))
private def endpoint = Uri(s"/party/$partyId/users")
private val askTimeout = 60 seconds
implicit private val routeTimeout: RouteTestTimeout = RouteTestTimeout(askTimeout)
private def endpoint: Uri = Uri(s"/party/$partyId/users")
private val timeout: FiniteDuration = 60 seconds
implicit private val routeTimeout: RouteTestTimeout = RouteTestTimeout(timeout)
private var partyId = Fixtures.partyId
private val storage = testKit.spawn(Database())
private val party = testKit.spawn(PartyService(storage))
private val route = new UserEndpoint(party)(askTimeout, testKit.scheduler).route
private var partyId: String = Fixtures.partyId
private val storage: ActorRef = system.actorOf(impl.DatabaseImpl.props)
private val party: ActorRef = system.actorOf(PartyService.props(storage))
private val route: Route = new UserEndpoint(party)(timeout).route
override def beforeAll(): Unit = {
super.beforeAll()
Migration(testConfig)
override def testConfig: Config = Settings.withRandomDatabase
override def beforeAll: Unit = {
Await.result(Migration(system.settings.config), timeout)
}
override def afterAll(): Unit = {
Settings.clearDatabase(testConfig)
override def afterAll: Unit = {
TestKit.shutdownActorSystem(system)
testKit.shutdownTestKit()
super.afterAll()
Settings.clearDatabase(system.settings.config)
}
"api v1 users endpoint" must {

View File

@ -2,10 +2,9 @@ package me.arcanis.ffxivbis.models
import me.arcanis.ffxivbis.Fixtures
import me.arcanis.ffxivbis.utils.Compare
import org.scalatest.matchers.should.Matchers
import org.scalatest.wordspec.AnyWordSpecLike
import org.scalatest.{BeforeAndAfterAll, Matchers, WordSpecLike}
class BiSTest extends AnyWordSpecLike with Matchers {
class BiSTest extends WordSpecLike with Matchers with BeforeAndAfterAll {
"bis model" must {
@ -30,14 +29,18 @@ class BiSTest extends AnyWordSpecLike with Matchers {
val bis = BiS(Seq(Fixtures.lootLegs))
val newBis = bis.withPiece(Fixtures.lootHands)
newBis shouldEqual BiS(Seq(Fixtures.lootLegs, Fixtures.lootHands))
newBis.legs shouldEqual Some(Fixtures.lootLegs)
newBis.hands shouldEqual Some(Fixtures.lootHands)
newBis.pieces.length shouldEqual 2
}
"create copy without piece" in {
val bis = BiS(Seq(Fixtures.lootHands, Fixtures.lootLegs))
val newBis = bis.withoutPiece(Fixtures.lootHands)
newBis shouldEqual BiS(Seq(Fixtures.lootLegs))
newBis.legs shouldEqual Some(Fixtures.lootLegs)
newBis.hands shouldEqual None
newBis.pieces.length shouldEqual 1
}
"ignore upgrade on modification" in {
@ -46,7 +49,7 @@ class BiSTest extends AnyWordSpecLike with Matchers {
}
"return upgrade list" in {
Compare.mapEquals(Fixtures.bis.upgrades, Map[PieceUpgrade, Int](BodyUpgrade -> 2, AccessoryUpgrade -> 3)) shouldEqual true
Compare.mapEquals(Fixtures.bis.upgrades, Map[PieceUpgrade, Int](BodyUpgrade -> 2, AccessoryUpgrade -> 4)) shouldEqual true
}
}

View File

@ -1,9 +1,8 @@
package me.arcanis.ffxivbis.models
import org.scalatest.matchers.should.Matchers
import org.scalatest.wordspec.AnyWordSpecLike
import org.scalatest.{BeforeAndAfterAll, Matchers, WordSpecLike}
class JobTest extends AnyWordSpecLike with Matchers {
class JobTest extends WordSpecLike with Matchers with BeforeAndAfterAll {
"job model" must {
@ -23,7 +22,7 @@ class JobTest extends AnyWordSpecLike with Matchers {
"equal AnyJob to others" in {
Job.available.foreach { job =>
Job.AnyJob shouldEqual job
Job.AnyJob shouldBe job
}
}

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