This commit is contained in:
Evgenii Alekseev 2022-01-31 03:28:07 +03:00
parent 88617eccdf
commit ed3cdd62bd
56 changed files with 573 additions and 390 deletions

View File

@ -10,8 +10,7 @@ package me.arcanis.ffxivbis
import akka.actor.typed.ActorSystem import akka.actor.typed.ActorSystem
object ffxivbis { object ffxivbis extends App {
def main(args: Array[String]): Unit = ActorSystem[Nothing](Application(), "ffxivbis", Configuration.load())
ActorSystem[Nothing](Application(), "ffxivbis", Configuration.load())
} }

View File

@ -13,7 +13,8 @@ import akka.actor.typed.{ActorRef, Scheduler}
import akka.util.Timeout import akka.util.Timeout
import com.google.common.cache.{CacheBuilder, CacheLoader, LoadingCache} import com.google.common.cache.{CacheBuilder, CacheLoader, LoadingCache}
import com.typesafe.config.Config import com.typesafe.config.Config
import me.arcanis.ffxivbis.messages.{GetUser, Message} import me.arcanis.ffxivbis.messages.DatabaseMessage.GetUser
import me.arcanis.ffxivbis.messages.Message
import me.arcanis.ffxivbis.models.{Permission, User} import me.arcanis.ffxivbis.models.{Permission, User}
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit

View File

@ -16,13 +16,15 @@ import com.typesafe.scalalogging.StrictLogging
import me.arcanis.ffxivbis.http.api.v1.json._ import me.arcanis.ffxivbis.http.api.v1.json._
import spray.json._ import spray.json._
import scala.util.control.NonFatal
trait HttpHandler extends StrictLogging { this: JsonSupport => trait HttpHandler extends StrictLogging { this: JsonSupport =>
def exceptionHandler: ExceptionHandler = ExceptionHandler { def exceptionHandler: ExceptionHandler = ExceptionHandler {
case ex: IllegalArgumentException => case exception: IllegalArgumentException =>
complete(StatusCodes.BadRequest, ErrorModel(ex.getMessage)) complete(StatusCodes.BadRequest, ErrorModel(exception.getMessage))
case other: Exception => case NonFatal(other) =>
logger.error("exception during request completion", other) logger.error("exception during request completion", other)
complete(StatusCodes.InternalServerError, ErrorModel("unknown server error")) complete(StatusCodes.InternalServerError, ErrorModel("unknown server error"))
} }

View File

@ -9,5 +9,6 @@
package me.arcanis.ffxivbis.http.api.v1.json package me.arcanis.ffxivbis.http.api.v1.json
object ApiAction extends Enumeration { object ApiAction extends Enumeration {
val add, remove = Value val add, remove = Value
} }

View File

@ -12,7 +12,8 @@ import akka.actor.typed.scaladsl.AskPattern.Askable
import akka.actor.typed.{ActorRef, Scheduler} import akka.actor.typed.{ActorRef, Scheduler}
import akka.util.Timeout import akka.util.Timeout
import me.arcanis.ffxivbis.http.api.v1.json.ApiAction import me.arcanis.ffxivbis.http.api.v1.json.ApiAction
import me.arcanis.ffxivbis.messages._ import me.arcanis.ffxivbis.messages.DatabaseMessage._
import me.arcanis.ffxivbis.messages.Message
import me.arcanis.ffxivbis.models.{Piece, Player, PlayerId} import me.arcanis.ffxivbis.models.{Piece, Player, PlayerId}
import scala.concurrent.{ExecutionContext, Future} import scala.concurrent.{ExecutionContext, Future}

View File

@ -11,7 +11,8 @@ package me.arcanis.ffxivbis.http.helpers
import akka.actor.typed.scaladsl.AskPattern.Askable import akka.actor.typed.scaladsl.AskPattern.Askable
import akka.actor.typed.{ActorRef, Scheduler} import akka.actor.typed.{ActorRef, Scheduler}
import akka.util.Timeout import akka.util.Timeout
import me.arcanis.ffxivbis.messages.{BiSProviderMessage, DownloadBiS} import me.arcanis.ffxivbis.messages.BiSProviderMessage
import me.arcanis.ffxivbis.messages.BiSProviderMessage._
import me.arcanis.ffxivbis.models.{BiS, Job} import me.arcanis.ffxivbis.models.{BiS, Job}
import scala.concurrent.Future import scala.concurrent.Future
@ -20,6 +21,6 @@ trait BisProviderHelper {
def provider: ActorRef[BiSProviderMessage] def provider: ActorRef[BiSProviderMessage]
def downloadBiS(link: String, job: Job.Job)(implicit timeout: Timeout, scheduler: Scheduler): Future[BiS] = def downloadBiS(link: String, job: Job)(implicit timeout: Timeout, scheduler: Scheduler): Future[BiS] =
provider.ask(DownloadBiS(link, job, _)) provider.ask(DownloadBiS(link, job, _))
} }

View File

@ -12,7 +12,8 @@ import akka.actor.typed.scaladsl.AskPattern.Askable
import akka.actor.typed.{ActorRef, Scheduler} import akka.actor.typed.{ActorRef, Scheduler}
import akka.util.Timeout import akka.util.Timeout
import me.arcanis.ffxivbis.http.api.v1.json.ApiAction import me.arcanis.ffxivbis.http.api.v1.json.ApiAction
import me.arcanis.ffxivbis.messages._ import me.arcanis.ffxivbis.messages.DatabaseMessage._
import me.arcanis.ffxivbis.messages.Message
import me.arcanis.ffxivbis.models.{Piece, Player, PlayerId, PlayerIdWithCounters} import me.arcanis.ffxivbis.models.{Piece, Player, PlayerId, PlayerIdWithCounters}
import scala.concurrent.{ExecutionContext, Future} import scala.concurrent.{ExecutionContext, Future}

View File

@ -12,7 +12,8 @@ import akka.actor.typed.scaladsl.AskPattern.Askable
import akka.actor.typed.{ActorRef, Scheduler} import akka.actor.typed.{ActorRef, Scheduler}
import akka.util.Timeout import akka.util.Timeout
import me.arcanis.ffxivbis.http.api.v1.json.ApiAction import me.arcanis.ffxivbis.http.api.v1.json.ApiAction
import me.arcanis.ffxivbis.messages._ import me.arcanis.ffxivbis.messages.DatabaseMessage._
import me.arcanis.ffxivbis.messages.Message
import me.arcanis.ffxivbis.models.{PartyDescription, Player, PlayerId} import me.arcanis.ffxivbis.models.{PartyDescription, Player, PlayerId}
import scala.concurrent.{ExecutionContext, Future} import scala.concurrent.{ExecutionContext, Future}

View File

@ -11,7 +11,9 @@ package me.arcanis.ffxivbis.http.helpers
import akka.actor.typed.scaladsl.AskPattern.Askable import akka.actor.typed.scaladsl.AskPattern.Askable
import akka.actor.typed.{ActorRef, Scheduler} import akka.actor.typed.{ActorRef, Scheduler}
import akka.util.Timeout import akka.util.Timeout
import me.arcanis.ffxivbis.messages._ import me.arcanis.ffxivbis.messages.ControlMessage.GetNewPartyId
import me.arcanis.ffxivbis.messages.DatabaseMessage._
import me.arcanis.ffxivbis.messages.Message
import me.arcanis.ffxivbis.models.User import me.arcanis.ffxivbis.models.User
import scala.concurrent.Future import scala.concurrent.Future

View File

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

View File

@ -11,8 +11,13 @@ package me.arcanis.ffxivbis.messages
import akka.actor.typed.ActorRef import akka.actor.typed.ActorRef
import me.arcanis.ffxivbis.models.Party import me.arcanis.ffxivbis.models.Party
case class ForgetParty(partyId: String) extends Message sealed trait ControlMessage extends Message
case class GetNewPartyId(replyTo: ActorRef[String]) extends Message object ControlMessage {
case class StoreParty(partyId: String, party: Party) extends Message case class ForgetParty(partyId: String) extends ControlMessage
case class GetNewPartyId(replyTo: ActorRef[String]) extends ControlMessage
case class StoreParty(partyId: String, party: Party) extends ControlMessage
}

View File

@ -19,105 +19,108 @@ sealed trait DatabaseMessage extends Message {
def isReadOnly: Boolean def isReadOnly: Boolean
} }
// bis handler object DatabaseMessage {
trait BisDatabaseMessage extends DatabaseMessage
case class AddPieceToBis(playerId: PlayerId, piece: Piece, replyTo: ActorRef[Unit]) extends BisDatabaseMessage { // bis handler
override val partyId: String = playerId.partyId trait BisDatabaseMessage extends DatabaseMessage
override val isReadOnly: Boolean = false
} case class AddPieceToBis(playerId: PlayerId, piece: Piece, replyTo: ActorRef[Unit]) extends BisDatabaseMessage {
override val partyId: String = playerId.partyId
case class GetBiS(partyId: String, playerId: Option[PlayerId], replyTo: ActorRef[Seq[Player]]) override val isReadOnly: Boolean = false
extends BisDatabaseMessage { }
override val isReadOnly: Boolean = true
} case class GetBiS(partyId: String, playerId: Option[PlayerId], replyTo: ActorRef[Seq[Player]])
extends BisDatabaseMessage {
case class RemovePieceFromBiS(playerId: PlayerId, piece: Piece, replyTo: ActorRef[Unit]) extends BisDatabaseMessage { override val isReadOnly: Boolean = true
override val partyId: String = playerId.partyId }
override val isReadOnly: Boolean = false
} case class RemovePieceFromBiS(playerId: PlayerId, piece: Piece, replyTo: ActorRef[Unit]) extends BisDatabaseMessage {
override val partyId: String = playerId.partyId
case class RemovePiecesFromBiS(playerId: PlayerId, replyTo: ActorRef[Unit]) extends BisDatabaseMessage { override val isReadOnly: Boolean = false
override val partyId: String = playerId.partyId }
override val isReadOnly: Boolean = false
} case class RemovePiecesFromBiS(playerId: PlayerId, replyTo: ActorRef[Unit]) extends BisDatabaseMessage {
override val partyId: String = playerId.partyId
// loot handler override val isReadOnly: Boolean = false
trait LootDatabaseMessage extends DatabaseMessage }
case class AddPieceTo(playerId: PlayerId, piece: Piece, isFreeLoot: Boolean, replyTo: ActorRef[Unit]) // loot handler
extends LootDatabaseMessage { trait LootDatabaseMessage extends DatabaseMessage
override val partyId: String = playerId.partyId
override val isReadOnly: Boolean = false case class AddPieceTo(playerId: PlayerId, piece: Piece, isFreeLoot: Boolean, replyTo: ActorRef[Unit])
} extends LootDatabaseMessage {
override val partyId: String = playerId.partyId
case class GetLoot(partyId: String, playerId: Option[PlayerId], replyTo: ActorRef[Seq[Player]]) override val isReadOnly: Boolean = false
extends LootDatabaseMessage { }
override val isReadOnly: Boolean = true
} case class GetLoot(partyId: String, playerId: Option[PlayerId], replyTo: ActorRef[Seq[Player]])
extends LootDatabaseMessage {
case class RemovePieceFrom(playerId: PlayerId, piece: Piece, isFreeLoot: Boolean, replyTo: ActorRef[Unit]) override val isReadOnly: Boolean = true
extends LootDatabaseMessage { }
override val partyId: String = playerId.partyId
override val isReadOnly: Boolean = false case class RemovePieceFrom(playerId: PlayerId, piece: Piece, isFreeLoot: Boolean, replyTo: ActorRef[Unit])
} extends LootDatabaseMessage {
override val partyId: String = playerId.partyId
case class SuggestLoot(partyId: String, piece: Piece, replyTo: ActorRef[LootSelector.LootSelectorResult]) override val isReadOnly: Boolean = false
extends LootDatabaseMessage { }
override val isReadOnly: Boolean = true
} case class SuggestLoot(partyId: String, piece: Piece, replyTo: ActorRef[LootSelector.LootSelectorResult])
extends LootDatabaseMessage {
// party handler override val isReadOnly: Boolean = true
trait PartyDatabaseMessage extends DatabaseMessage }
case class AddPlayer(player: Player, replyTo: ActorRef[Unit]) extends PartyDatabaseMessage { // party handler
override val partyId: String = player.partyId trait PartyDatabaseMessage extends DatabaseMessage
override val isReadOnly: Boolean = false
} case class AddPlayer(player: Player, replyTo: ActorRef[Unit]) extends PartyDatabaseMessage {
override val partyId: String = player.partyId
case class GetParty(partyId: String, replyTo: ActorRef[Party]) extends PartyDatabaseMessage { override val isReadOnly: Boolean = false
override val isReadOnly: Boolean = true }
}
case class GetParty(partyId: String, replyTo: ActorRef[Party]) extends PartyDatabaseMessage {
case class GetPartyDescription(partyId: String, replyTo: ActorRef[PartyDescription]) extends PartyDatabaseMessage { override val isReadOnly: Boolean = true
override val isReadOnly: Boolean = true }
}
case class GetPartyDescription(partyId: String, replyTo: ActorRef[PartyDescription]) extends PartyDatabaseMessage {
case class GetPlayer(playerId: PlayerId, replyTo: ActorRef[Option[Player]]) extends PartyDatabaseMessage { override val isReadOnly: Boolean = true
override val partyId: String = playerId.partyId }
override val isReadOnly: Boolean = true
} case class GetPlayer(playerId: PlayerId, replyTo: ActorRef[Option[Player]]) extends PartyDatabaseMessage {
override val partyId: String = playerId.partyId
case class RemovePlayer(playerId: PlayerId, replyTo: ActorRef[Unit]) extends PartyDatabaseMessage { override val isReadOnly: Boolean = true
override val partyId: String = playerId.partyId }
override val isReadOnly: Boolean = false
} case class RemovePlayer(playerId: PlayerId, replyTo: ActorRef[Unit]) extends PartyDatabaseMessage {
override val partyId: String = playerId.partyId
case class UpdateParty(partyDescription: PartyDescription, replyTo: ActorRef[Unit]) extends PartyDatabaseMessage { override val isReadOnly: Boolean = false
override val partyId: String = partyDescription.partyId }
override val isReadOnly: Boolean = false
} case class UpdateParty(partyDescription: PartyDescription, replyTo: ActorRef[Unit]) extends PartyDatabaseMessage {
override val partyId: String = partyDescription.partyId
// user handler override val isReadOnly: Boolean = false
trait UserDatabaseMessage extends DatabaseMessage }
case class AddUser(user: User, isHashedPassword: Boolean, replyTo: ActorRef[Unit]) extends UserDatabaseMessage { // user handler
override val partyId: String = user.partyId trait UserDatabaseMessage extends DatabaseMessage
override val isReadOnly: Boolean = false
} case class AddUser(user: User, isHashedPassword: Boolean, replyTo: ActorRef[Unit]) extends UserDatabaseMessage {
override val partyId: String = user.partyId
case class DeleteUser(partyId: String, username: String, replyTo: ActorRef[Unit]) extends UserDatabaseMessage { override val isReadOnly: Boolean = false
override val isReadOnly: Boolean = true }
}
case class DeleteUser(partyId: String, username: String, replyTo: ActorRef[Unit]) extends UserDatabaseMessage {
case class Exists(partyId: String, replyTo: ActorRef[Boolean]) extends UserDatabaseMessage { override val isReadOnly: Boolean = true
override val isReadOnly: Boolean = true }
}
case class Exists(partyId: String, replyTo: ActorRef[Boolean]) extends UserDatabaseMessage {
case class GetUser(partyId: String, username: String, replyTo: ActorRef[Option[User]]) extends UserDatabaseMessage { override val isReadOnly: Boolean = true
override val isReadOnly: Boolean = true }
}
case class GetUser(partyId: String, username: String, replyTo: ActorRef[Option[User]]) extends UserDatabaseMessage {
case class GetUsers(partyId: String, replyTo: ActorRef[Seq[User]]) extends UserDatabaseMessage { override val isReadOnly: Boolean = true
override val isReadOnly: Boolean = true }
case class GetUsers(partyId: String, replyTo: ActorRef[Seq[User]]) extends UserDatabaseMessage {
override val isReadOnly: Boolean = true
}
} }

View File

@ -11,14 +11,14 @@ package me.arcanis.ffxivbis.models
case class BiS(pieces: Seq[Piece]) { case class BiS(pieces: Seq[Piece]) {
def hasPiece(piece: Piece): Boolean = piece match { def hasPiece(piece: Piece): Boolean = piece match {
case upgrade: PieceUpgrade => upgrades.contains(upgrade) case upgrade: Piece.PieceUpgrade => upgrades.contains(upgrade)
case _ => pieces.contains(piece) case _ => pieces.contains(piece)
} }
def upgrades: Map[PieceUpgrade, Int] = def upgrades: Map[Piece.PieceUpgrade, Int] =
pieces pieces
.groupBy(_.upgrade) .groupBy(_.upgrade)
.foldLeft(Map.empty[PieceUpgrade, Int]) { .foldLeft(Map.empty[Piece.PieceUpgrade, Int]) {
case (acc, (Some(k), v)) => acc + (k -> v.size) case (acc, (Some(k), v)) => acc + (k -> v.size)
case (acc, _) => acc case (acc, _) => acc
} }
@ -43,5 +43,5 @@ case class BiS(pieces: Seq[Piece]) {
object BiS { object BiS {
def empty: BiS = BiS(Seq.empty) val empty: BiS = BiS(Seq.empty)
} }

View File

@ -8,6 +8,26 @@
*/ */
package me.arcanis.ffxivbis.models package me.arcanis.ffxivbis.models
sealed trait Job extends Equals {
def leftSide: Job.LeftSide
def rightSide: Job.RightSide
// conversion to string to avoid recursion
override def canEqual(that: Any): Boolean = that.isInstanceOf[Job]
override def equals(obj: Any): Boolean = {
def equality(objRepr: String): Boolean = objRepr match {
case _ if objRepr == Job.AnyJob.toString => true
case _ if this.toString == Job.AnyJob.toString => true
case _ => this.toString == objRepr
}
canEqual(obj) && equality(obj.toString)
}
}
object Job { object Job {
sealed trait RightSide sealed trait RightSide
@ -26,54 +46,38 @@ object Job {
object BodyTanks extends LeftSide object BodyTanks extends LeftSide
object BodyRanges extends LeftSide object BodyRanges extends LeftSide
sealed trait Job extends Equals {
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 equality(objRepr: String): Boolean = objRepr match {
case _ if objRepr == AnyJob.toString => true
case _ if this.toString == AnyJob.toString => true
case _ => this.toString == objRepr
}
canEqual(obj) && equality(obj.toString)
}
}
case object AnyJob extends Job { case object AnyJob extends Job {
val leftSide: LeftSide = null override val leftSide: LeftSide = null
val rightSide: RightSide = null override val rightSide: RightSide = null
} }
trait Casters extends Job { trait Casters extends Job {
val leftSide: LeftSide = BodyCasters override val leftSide: LeftSide = BodyCasters
val rightSide: RightSide = AccessoriesInt override val rightSide: RightSide = AccessoriesInt
} }
trait Healers extends Job { trait Healers extends Job {
val leftSide: LeftSide = BodyHealers override val leftSide: LeftSide = BodyHealers
val rightSide: RightSide = AccessoriesMnd override val rightSide: RightSide = AccessoriesMnd
} }
trait Mnks extends Job { trait Mnks extends Job {
val leftSide: LeftSide = BodyMnks override val leftSide: LeftSide = BodyMnks
val rightSide: RightSide = AccessoriesStr override val rightSide: RightSide = AccessoriesStr
} }
trait Drgs extends Job { trait Drgs extends Job {
val leftSide: LeftSide = BodyDrgs override val leftSide: LeftSide = BodyDrgs
val rightSide: RightSide = AccessoriesStr override val rightSide: RightSide = AccessoriesStr
}
trait Nins extends Job {
override val leftSide: LeftSide = BodyNins
override val rightSide: RightSide = AccessoriesDex
} }
trait Tanks extends Job { trait Tanks extends Job {
val leftSide: LeftSide = BodyTanks override val leftSide: LeftSide = BodyTanks
val rightSide: RightSide = AccessoriesVit override val rightSide: RightSide = AccessoriesVit
} }
trait Ranges extends Job { trait Ranges extends Job {
val leftSide: LeftSide = BodyRanges override val leftSide: LeftSide = BodyRanges
val rightSide: RightSide = AccessoriesDex override val rightSide: RightSide = AccessoriesDex
} }
case object PLD extends Tanks case object PLD extends Tanks
@ -89,10 +93,7 @@ object Job {
case object MNK extends Mnks case object MNK extends Mnks
case object DRG extends Drgs case object DRG extends Drgs
case object RPR extends Drgs case object RPR extends Drgs
case object NIN extends Job { case object NIN extends Nins
val leftSide: LeftSide = BodyNins
val rightSide: RightSide = AccessoriesDex
}
case object SAM extends Mnks case object SAM extends Mnks
case object BRD extends Ranges case object BRD extends Ranges
@ -103,11 +104,11 @@ object Job {
case object SMN extends Casters case object SMN extends Casters
case object RDM extends Casters case object RDM extends Casters
lazy val available: Seq[Job] = 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, SGE, MNK, DRG, RPR, NIN, SAM, BRD, MCH, DNC, BLM, SMN, RDM)
lazy val availableWithAnyJob: Seq[Job] = available.prepended(AnyJob) val availableWithAnyJob: Seq[Job] = available.prepended(AnyJob)
def withName(job: String): Job.Job = def withName(job: String): Job =
availableWithAnyJob.find(_.toString.equalsIgnoreCase(job)) match { availableWithAnyJob.find(_.toString.equalsIgnoreCase(job)) match {
case Some(value) => value case Some(value) => value
case None if job.isEmpty => AnyJob case None if job.isEmpty => AnyJob

View File

@ -12,7 +12,5 @@ import java.time.Instant
case class Loot(playerId: Long, piece: Piece, timestamp: Instant, isFreeLoot: Boolean) { case class Loot(playerId: Long, piece: Piece, timestamp: Instant, isFreeLoot: Boolean) {
def isFreeLootToString: String = if (isFreeLoot) "yes" else "no" lazy val isFreeLootToInt: Int = if (isFreeLoot) 1 else 0
def isFreeLootToInt: Int = if (isFreeLoot) 1 else 0
} }

View File

@ -14,6 +14,7 @@ import me.arcanis.ffxivbis.service.LootSelector
import scala.jdk.CollectionConverters._ import scala.jdk.CollectionConverters._
import scala.util.Random import scala.util.Random
import scala.util.control.NonFatal
case class Party(partyDescription: PartyDescription, rules: Seq[String], players: Map[PlayerId, Player]) case class Party(partyDescription: PartyDescription, rules: Seq[String], players: Map[PlayerId, Player])
extends StrictLogging { extends StrictLogging {
@ -29,7 +30,7 @@ case class Party(partyDescription: PartyDescription, rules: Seq[String], players
require(player.partyId == partyDescription.partyId, "player must belong to this party") require(player.partyId == partyDescription.partyId, "player must belong to this party")
copy(players = players + (player.playerId -> player)) copy(players = players + (player.playerId -> player))
} catch { } catch {
case exception: Exception => case NonFatal(exception) =>
logger.error("cannot add player", exception) logger.error("cannot add player", exception)
this this
} }

View File

@ -8,10 +8,7 @@
*/ */
package me.arcanis.ffxivbis.models package me.arcanis.ffxivbis.models
case class PartyDescription(partyId: String, partyAlias: Option[String]) { case class PartyDescription(partyId: String, partyAlias: Option[String])
def alias: String = partyAlias.getOrElse(partyId)
}
object PartyDescription { object PartyDescription {

View File

@ -10,20 +10,20 @@ package me.arcanis.ffxivbis.models
sealed trait Piece extends Equals { sealed trait Piece extends Equals {
def pieceType: PieceType.PieceType def pieceType: PieceType
def job: Job.Job def job: Job
def piece: String def piece: String
def withJob(other: Job.Job): Piece def withJob(other: Job): Piece
def upgrade: Option[PieceUpgrade] = { def upgrade: Option[Piece.PieceUpgrade] = {
val isTome = pieceType == PieceType.Tome val isTome = pieceType == PieceType.Tome
Some(this).collect { Some(this).collect {
case _: PieceAccessory if isTome => AccessoryUpgrade case _: Piece.PieceAccessory if isTome => Piece.AccessoryUpgrade
case _: PieceBody if isTome => BodyUpgrade case _: Piece.PieceBody if isTome => Piece.BodyUpgrade
case _: PieceWeapon if isTome => WeaponUpgrade case _: Piece.PieceWeapon if isTome => Piece.WeaponUpgrade
} }
} }
@ -31,83 +31,84 @@ sealed trait Piece extends Equals {
def strictEqual(obj: Any): Boolean = equals(obj) 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 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 {
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 {
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 {
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 {
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 {
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 {
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 {
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 {
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 {
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"
) 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 _ => false
}
}
case object AccessoryUpgrade extends PieceUpgrade {
val piece: String = "accessory upgrade"
}
case object BodyUpgrade extends PieceUpgrade {
val piece: String = "body upgrade"
}
case object WeaponUpgrade extends PieceUpgrade {
val piece: String = "weapon upgrade"
}
object Piece { object Piece {
def apply(piece: String, pieceType: PieceType.PieceType, job: Job.Job = Job.AnyJob): Piece =
trait PieceAccessory extends Piece
trait PieceBody extends Piece
trait PieceUpgrade extends Piece {
override val pieceType: PieceType = PieceType.Tome
override val job: Job = Job.AnyJob
override def withJob(other: Job): Piece = this
}
trait PieceWeapon extends Piece
case class Weapon(override val pieceType: PieceType, override val job: Job) extends PieceWeapon {
override val piece: String = "weapon"
override def withJob(other: Job): Piece = copy(job = other)
}
case class Head(override val pieceType: PieceType, override val job: Job) extends PieceBody {
override val piece: String = "head"
override def withJob(other: Job): Piece = copy(job = other)
}
case class Body(override val pieceType: PieceType, override val job: Job) extends PieceBody {
override val piece: String = "body"
override def withJob(other: Job): Piece = copy(job = other)
}
case class Hands(override val pieceType: PieceType, override val job: Job) extends PieceBody {
override val piece: String = "hands"
override def withJob(other: Job): Piece = copy(job = other)
}
case class Legs(override val pieceType: PieceType, override val job: Job) extends PieceBody {
override val piece: String = "legs"
override def withJob(other: Job): Piece = copy(job = other)
}
case class Feet(override val pieceType: PieceType, override val job: Job) extends PieceBody {
override val piece: String = "feet"
override def withJob(other: Job): Piece = copy(job = other)
}
case class Ears(override val pieceType: PieceType, override val job: Job) extends PieceAccessory {
override val piece: String = "ears"
override def withJob(other: Job): Piece = copy(job = other)
}
case class Neck(override val pieceType: PieceType, override val job: Job) extends PieceAccessory {
override val piece: String = "neck"
override def withJob(other: Job): Piece = copy(job = other)
}
case class Wrist(override val pieceType: PieceType, override val job: Job) extends PieceAccessory {
override val piece: String = "wrist"
override def withJob(other: Job): Piece = copy(job = other)
}
case class Ring(
override val pieceType: PieceType,
override val job: Job,
override val piece: String = "ring"
) extends PieceAccessory {
override def withJob(other: 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 _ => false
}
}
case object AccessoryUpgrade extends PieceUpgrade {
override val piece: String = "accessory upgrade"
}
case object BodyUpgrade extends PieceUpgrade {
override val piece: String = "body upgrade"
}
case object WeaponUpgrade extends PieceUpgrade {
override val piece: String = "weapon upgrade"
}
def apply(piece: String, pieceType: PieceType, job: Job = Job.AnyJob): Piece =
piece.toLowerCase match { piece.toLowerCase match {
case "weapon" => Weapon(pieceType, job) case "weapon" => Weapon(pieceType, job)
case "head" => Head(pieceType, job) case "head" => Head(pieceType, job)
@ -125,7 +126,7 @@ object Piece {
case other => throw new Error(s"Unknown item type $other") case other => throw new Error(s"Unknown item type $other")
} }
lazy val available: Seq[String] = Seq( val available: Seq[String] = Seq(
"weapon", "weapon",
"head", "head",
"body", "body",

View File

@ -8,17 +8,16 @@
*/ */
package me.arcanis.ffxivbis.models package me.arcanis.ffxivbis.models
sealed trait PieceType
object PieceType { object PieceType {
sealed trait PieceType
case object Crafted extends PieceType
case object Tome extends PieceType
case object Savage extends PieceType case object Savage extends PieceType
case object Tome extends PieceType
case object Crafted extends PieceType
case object Artifact extends PieceType case object Artifact extends PieceType
lazy val available: Seq[PieceType] = val available: Seq[PieceType] = Seq(Savage, Tome, Crafted, Artifact)
Seq(Crafted, Tome, Savage, Artifact)
def withName(pieceType: String): PieceType = def withName(pieceType: String): PieceType =
available.find(_.toString.equalsIgnoreCase(pieceType)) match { available.find(_.toString.equalsIgnoreCase(pieceType)) match {

View File

@ -11,7 +11,7 @@ package me.arcanis.ffxivbis.models
case class Player( case class Player(
id: Long, id: Long,
partyId: String, partyId: String,
job: Job.Job, job: Job,
nick: String, nick: String,
bis: BiS, bis: BiS,
loot: Seq[Loot], loot: Seq[Loot],
@ -51,7 +51,7 @@ case class Player(
piece match { piece match {
case None => false case None => false
case Some(p) if !bis.hasPiece(p) => false case Some(p) if !bis.hasPiece(p) => false
case Some(p: PieceUpgrade) => bis.upgrades(p) > lootCount(piece) case Some(p: Piece.PieceUpgrade) => bis.upgrades(p) > lootCount(piece)
case Some(_) => lootCount(piece) == 0 case Some(_) => lootCount(piece) == 0
} }

View File

@ -13,14 +13,14 @@ import scala.util.matching.Regex
trait PlayerIdBase { trait PlayerIdBase {
def job: Job.Job def job: Job
def nick: String def nick: String
override def toString: String = s"$nick ($job)" override def toString: String = s"$nick ($job)"
} }
case class PlayerId(partyId: String, job: Job.Job, nick: String) extends PlayerIdBase case class PlayerId(partyId: String, job: Job, nick: String) extends PlayerIdBase
object PlayerId { object PlayerId {

View File

@ -10,7 +10,7 @@ package me.arcanis.ffxivbis.models
case class PlayerIdWithCounters( case class PlayerIdWithCounters(
partyId: String, partyId: String,
job: Job.Job, job: Job,
nick: String, nick: String,
isRequired: Boolean, isRequired: Boolean,
priority: Int, priority: Int,
@ -24,8 +24,6 @@ case class PlayerIdWithCounters(
def gt(that: PlayerIdWithCounters, orderBy: Seq[String]): Boolean = def gt(that: PlayerIdWithCounters, orderBy: Seq[String]): Boolean =
withCounters(orderBy) > that.withCounters(orderBy) withCounters(orderBy) > that.withCounters(orderBy)
def isRequiredToString: String = if (isRequired) "yes" else "no"
def playerId: PlayerId = PlayerId(partyId, job, nick) def playerId: PlayerId = PlayerId(partyId, job, nick)
private val counters: Map[String, Int] = Map( private val counters: Map[String, Int] = Map(
@ -47,13 +45,13 @@ object PlayerIdWithCounters {
def >(that: PlayerCountersComparator): Boolean = { def >(that: PlayerCountersComparator): Boolean = {
@scala.annotation.tailrec @scala.annotation.tailrec
def compareLists(left: List[Int], right: List[Int]): Boolean = def compare(left: Seq[Int], right: Seq[Int]): Boolean =
(left, right) match { (left, right) match {
case (hl :: tl, hr :: tr) => if (hl == hr) compareLists(tl, tr) else hl > hr case (hl :: tl, hr :: tr) => if (hl == hr) compare(tl, tr) else hl > hr
case (_ :: _, Nil) => true case (_ :: _, Nil) => true
case (_, _) => false case (_, _) => false
} }
compareLists(values.toList, that.values.toList) compare(values, that.values)
} }
} }
} }

View File

@ -11,6 +11,7 @@ package me.arcanis.ffxivbis.models
import org.mindrot.jbcrypt.BCrypt import org.mindrot.jbcrypt.BCrypt
object Permission extends Enumeration { object Permission extends Enumeration {
val get, post, admin = Value val get, post, admin = Value
} }

View File

@ -36,23 +36,24 @@ class PartyService(context: ActorContext[Message], storage: ActorRef[DatabaseMes
override def onMessage(msg: Message): Behavior[Message] = handle(Map.empty)(msg) override def onMessage(msg: Message): Behavior[Message] = handle(Map.empty)(msg)
private def handle(cache: Map[String, Party]): Message.Handler = { private def handle(cache: Map[String, Party]): Message.Handler = {
case ForgetParty(partyId) => case ControlMessage.ForgetParty(partyId) =>
Behaviors.receiveMessage(handle(cache - partyId)) Behaviors.receiveMessage(handle(cache - partyId))
case GetNewPartyId(client) => case ControlMessage.GetNewPartyId(client) =>
getPartyId.foreach(client ! _) getPartyId.foreach(client ! _)
Behaviors.same Behaviors.same
case StoreParty(partyId, party) => case ControlMessage.StoreParty(partyId, party) =>
Behaviors.receiveMessage(handle(cache.updated(partyId, party))) Behaviors.receiveMessage(handle(cache.updated(partyId, party)))
case GetParty(partyId, client) => case DatabaseMessage.GetParty(partyId, client) =>
val party = cache.get(partyId) match { val party = cache.get(partyId) match {
case Some(party) => Future.successful(party) case Some(party) => Future.successful(party)
case None => case None =>
storage.ask(ref => GetParty(partyId, ref)).map { party => storage.ask(ref => DatabaseMessage.GetParty(partyId, ref)).map { party =>
context.self ! StoreParty(partyId, party) context.self ! ControlMessage.StoreParty(partyId, party)
context.system.scheduler.scheduleOnce(cacheTimeout, () => context.self ! ForgetParty(partyId)) context.system.scheduler
.scheduleOnce(cacheTimeout, () => context.self ! ControlMessage.ForgetParty(partyId))
party party
} }
} }
@ -67,7 +68,7 @@ class PartyService(context: ActorContext[Message], storage: ActorRef[DatabaseMes
private def getPartyId: Future[String] = { private def getPartyId: Future[String] = {
val partyId = Party.randomPartyId val partyId = Party.randomPartyId
storage.ask(ref => Exists(partyId, ref)).flatMap { storage.ask(ref => DatabaseMessage.Exists(partyId, ref)).flatMap {
case true => getPartyId case true => getPartyId
case false => Future.successful(partyId) case false => Future.successful(partyId)
} }

View File

@ -13,7 +13,7 @@ import akka.actor.typed.scaladsl.{AbstractBehavior, ActorContext, Behaviors}
import akka.actor.typed.{Behavior, PostStop, Signal} import akka.actor.typed.{Behavior, PostStop, Signal}
import akka.http.scaladsl.model._ import akka.http.scaladsl.model._
import com.typesafe.scalalogging.StrictLogging import com.typesafe.scalalogging.StrictLogging
import me.arcanis.ffxivbis.messages.{BiSProviderMessage, DownloadBiS} import me.arcanis.ffxivbis.messages.BiSProviderMessage
import me.arcanis.ffxivbis.models.{BiS, Job, Piece, PieceType} import me.arcanis.ffxivbis.models.{BiS, Job, Piece, PieceType}
import me.arcanis.ffxivbis.service.bis.parser.Parser import me.arcanis.ffxivbis.service.bis.parser.Parser
import me.arcanis.ffxivbis.service.bis.parser.impl.{Ariyala, Etro} import me.arcanis.ffxivbis.service.bis.parser.impl.{Ariyala, Etro}
@ -21,6 +21,7 @@ import spray.json._
import java.nio.file.Paths import java.nio.file.Paths
import scala.concurrent.{ExecutionContext, Future} import scala.concurrent.{ExecutionContext, Future}
import scala.util.control.NonFatal
import scala.util.{Failure, Success} import scala.util.{Failure, Success}
class BisProvider(context: ActorContext[BiSProviderMessage]) class BisProvider(context: ActorContext[BiSProviderMessage])
@ -32,7 +33,7 @@ class BisProvider(context: ActorContext[BiSProviderMessage])
override def onMessage(msg: BiSProviderMessage): Behavior[BiSProviderMessage] = override def onMessage(msg: BiSProviderMessage): Behavior[BiSProviderMessage] =
msg match { msg match {
case DownloadBiS(link, job, client) => case BiSProviderMessage.DownloadBiS(link, job, client) =>
get(link, job).onComplete { get(link, job).onComplete {
case Success(items) => client ! BiS(items) case Success(items) => client ! BiS(items)
case Failure(exception) => case Failure(exception) =>
@ -46,7 +47,7 @@ class BisProvider(context: ActorContext[BiSProviderMessage])
Behaviors.same Behaviors.same
} }
private def get(link: String, job: Job.Job): Future[Seq[Piece]] = private def get(link: String, job: Job): Future[Seq[Piece]] =
try { try {
val url = Uri(link) val url = Uri(link)
val id = Paths.get(link).normalize.getFileName.toString val id = Paths.get(link).normalize.getFileName.toString
@ -55,7 +56,7 @@ class BisProvider(context: ActorContext[BiSProviderMessage])
val uri = parser.uri(url, id) val uri = parser.uri(url, id)
sendRequest(uri, BisProvider.parseBisJsonToPieces(job, parser, getPieceType)) sendRequest(uri, BisProvider.parseBisJsonToPieces(job, parser, getPieceType))
} catch { } catch {
case exception: Exception => Future.failed(exception) case NonFatal(exception) => Future.failed(exception)
} }
} }
@ -65,9 +66,9 @@ object BisProvider {
Behaviors.setup[BiSProviderMessage](context => new BisProvider(context)) Behaviors.setup[BiSProviderMessage](context => new BisProvider(context))
private def parseBisJsonToPieces( private def parseBisJsonToPieces(
job: Job.Job, job: Job,
idParser: Parser, idParser: Parser,
pieceTypes: Seq[Long] => Future[Map[Long, PieceType.PieceType]] pieceTypes: Seq[Long] => Future[Map[Long, PieceType]]
)(js: JsObject)(implicit executionContext: ExecutionContext): Future[Seq[Piece]] = )(js: JsObject)(implicit executionContext: ExecutionContext): Future[Seq[Piece]] =
idParser.parse(job, js).flatMap { pieces => idParser.parse(job, js).flatMap { pieces =>
pieceTypes(pieces.values.toSeq).map { types => pieceTypes(pieces.values.toSeq).map { types =>

View File

@ -22,7 +22,7 @@ trait XivApi extends RequestExecutor {
private val xivapiUrl = config.getString("me.arcanis.ffxivbis.bis-provider.xivapi-url") private val xivapiUrl = config.getString("me.arcanis.ffxivbis.bis-provider.xivapi-url")
private val xivapiKey = Try(config.getString("me.arcanis.ffxivbis.bis-provider.xivapi-key")).toOption private val xivapiKey = Try(config.getString("me.arcanis.ffxivbis.bis-provider.xivapi-key")).toOption
private val preloadedItems: Map[Long, PieceType.PieceType] = private val preloadedItems: Map[Long, PieceType] =
config config
.getConfigList("me.arcanis.ffxivbis.bis-provider.cached-items") .getConfigList("me.arcanis.ffxivbis.bis-provider.cached-items")
.asScala .asScala
@ -31,17 +31,16 @@ trait XivApi extends RequestExecutor {
} }
.toMap .toMap
def getPieceType(itemIds: Seq[Long]): Future[Map[Long, PieceType.PieceType]] = { def getPieceType(itemIds: Seq[Long]): Future[Map[Long, PieceType]] = {
val (local, remote) = itemIds.foldLeft((Map.empty[Long, PieceType.PieceType], Seq.empty[Long])) { val (local, remote) = itemIds.foldLeft((Map.empty[Long, PieceType], Seq.empty[Long])) { case ((l, r), id) =>
case ((l, r), id) => if (preloadedItems.contains(id)) (l.updated(id, preloadedItems(id)), r)
if (preloadedItems.contains(id)) (l.updated(id, preloadedItems(id)), r) else (l, r :+ id)
else (l, r :+ id)
} }
if (remote.isEmpty) Future.successful(local) if (remote.isEmpty) Future.successful(local)
else remotePieceType(remote).map(_ ++ local) else remotePieceType(remote).map(_ ++ local)
} }
private def remotePieceType(itemIds: Seq[Long]): Future[Map[Long, PieceType.PieceType]] = { private def remotePieceType(itemIds: Seq[Long]): Future[Map[Long, PieceType]] = {
val uriForItems = Uri(xivapiUrl) val uriForItems = Uri(xivapiUrl)
.withPath(Uri.Path / "item") .withPath(Uri.Path / "item")
.withQuery( .withQuery(
@ -111,7 +110,7 @@ object XivApi {
private def parseXivapiJsonToType( private def parseXivapiJsonToType(
shops: Map[Long, (String, Long)] shops: Map[Long, (String, Long)]
)(js: JsObject)(implicit executionContext: ExecutionContext): Future[Map[Long, PieceType.PieceType]] = )(js: JsObject)(implicit executionContext: ExecutionContext): Future[Map[Long, PieceType]] =
Future { Future {
val shopMap = js.fields("Results") match { val shopMap = js.fields("Results") match {
case array: JsArray => case array: JsArray =>

View File

@ -17,7 +17,7 @@ import scala.concurrent.{ExecutionContext, Future}
trait Parser extends StrictLogging { trait Parser extends StrictLogging {
def parse(job: Job.Job, js: JsObject)(implicit executionContext: ExecutionContext): Future[Map[String, Long]] def parse(job: Job, js: JsObject)(implicit executionContext: ExecutionContext): Future[Map[String, Long]]
def uri(root: Uri, id: String): Uri def uri(root: Uri, id: String): Uri
} }

View File

@ -18,7 +18,7 @@ import scala.concurrent.{ExecutionContext, Future}
object Ariyala extends Parser { object Ariyala extends Parser {
override def parse(job: Job.Job, js: JsObject)(implicit override def parse(job: Job, js: JsObject)(implicit
executionContext: ExecutionContext executionContext: ExecutionContext
): Future[Map[String, Long]] = ): Future[Map[String, Long]] =
Future { Future {

View File

@ -18,7 +18,7 @@ import scala.concurrent.{ExecutionContext, Future}
object Etro extends Parser { object Etro extends Parser {
override def parse(job: Job.Job, js: JsObject)(implicit override def parse(job: Job, js: JsObject)(implicit
executionContext: ExecutionContext executionContext: ExecutionContext
): Future[Map[String, Long]] = ): Future[Map[String, Long]] =
Future { Future {

View File

@ -10,7 +10,8 @@ package me.arcanis.ffxivbis.service.database.impl
import akka.actor.typed.Behavior import akka.actor.typed.Behavior
import akka.actor.typed.scaladsl.Behaviors import akka.actor.typed.scaladsl.Behaviors
import me.arcanis.ffxivbis.messages._ import me.arcanis.ffxivbis.messages.DatabaseMessage
import me.arcanis.ffxivbis.messages.DatabaseMessage._
import me.arcanis.ffxivbis.service.database.Database import me.arcanis.ffxivbis.service.database.Database
trait DatabaseBiSHandler { this: Database => trait DatabaseBiSHandler { this: Database =>

View File

@ -11,7 +11,8 @@ package me.arcanis.ffxivbis.service.database.impl
import akka.actor.typed.scaladsl.{AbstractBehavior, ActorContext} import akka.actor.typed.scaladsl.{AbstractBehavior, ActorContext}
import akka.actor.typed.{Behavior, DispatcherSelector} import akka.actor.typed.{Behavior, DispatcherSelector}
import com.typesafe.config.Config import com.typesafe.config.Config
import me.arcanis.ffxivbis.messages.{BisDatabaseMessage, DatabaseMessage, LootDatabaseMessage, PartyDatabaseMessage, UserDatabaseMessage} import me.arcanis.ffxivbis.messages.DatabaseMessage
import me.arcanis.ffxivbis.messages.DatabaseMessage.{BisDatabaseMessage, LootDatabaseMessage, PartyDatabaseMessage, UserDatabaseMessage}
import me.arcanis.ffxivbis.service.database.Database import me.arcanis.ffxivbis.service.database.Database
import me.arcanis.ffxivbis.storage.DatabaseProfile import me.arcanis.ffxivbis.storage.DatabaseProfile

View File

@ -10,7 +10,8 @@ package me.arcanis.ffxivbis.service.database.impl
import akka.actor.typed.Behavior import akka.actor.typed.Behavior
import akka.actor.typed.scaladsl.Behaviors import akka.actor.typed.scaladsl.Behaviors
import me.arcanis.ffxivbis.messages._ import me.arcanis.ffxivbis.messages.DatabaseMessage
import me.arcanis.ffxivbis.messages.DatabaseMessage._
import me.arcanis.ffxivbis.models.Loot import me.arcanis.ffxivbis.models.Loot
import me.arcanis.ffxivbis.service.database.Database import me.arcanis.ffxivbis.service.database.Database

View File

@ -10,7 +10,8 @@ package me.arcanis.ffxivbis.service.database.impl
import akka.actor.typed.Behavior import akka.actor.typed.Behavior
import akka.actor.typed.scaladsl.Behaviors import akka.actor.typed.scaladsl.Behaviors
import me.arcanis.ffxivbis.messages._ import me.arcanis.ffxivbis.messages.DatabaseMessage
import me.arcanis.ffxivbis.messages.DatabaseMessage._
import me.arcanis.ffxivbis.models.{BiS, Player} import me.arcanis.ffxivbis.models.{BiS, Player}
import me.arcanis.ffxivbis.service.database.Database import me.arcanis.ffxivbis.service.database.Database

View File

@ -10,7 +10,8 @@ package me.arcanis.ffxivbis.service.database.impl
import akka.actor.typed.Behavior import akka.actor.typed.Behavior
import akka.actor.typed.scaladsl.Behaviors import akka.actor.typed.scaladsl.Behaviors
import me.arcanis.ffxivbis.messages._ import me.arcanis.ffxivbis.messages.DatabaseMessage
import me.arcanis.ffxivbis.messages.DatabaseMessage._
import me.arcanis.ffxivbis.service.database.Database import me.arcanis.ffxivbis.service.database.Database
trait DatabaseUserHandler { this: Database => trait DatabaseUserHandler { this: Database =>

View File

@ -16,6 +16,7 @@ import me.arcanis.ffxivbis.models.{Loot, Piece, PlayerId}
import java.time.Instant import java.time.Instant
import javax.sql.DataSource import javax.sql.DataSource
import scala.concurrent.{ExecutionContext, Future} import scala.concurrent.{ExecutionContext, Future}
import scala.util.control.NonFatal
class DatabaseProfile(override val executionContext: ExecutionContext, config: Config) class DatabaseProfile(override val executionContext: ExecutionContext, config: Config)
extends StrictLogging extends StrictLogging
@ -31,7 +32,7 @@ class DatabaseProfile(override val executionContext: ExecutionContext, config: C
val dataSourceConfig = DatabaseConnection.getDataSourceConfig(profile) val dataSourceConfig = DatabaseConnection.getDataSourceConfig(profile)
new HikariDataSource(dataSourceConfig) new HikariDataSource(dataSourceConfig)
} catch { } catch {
case exception: Exception => case NonFatal(exception) =>
logger.error("exception during storage initialization", exception) logger.error("exception during storage initialization", exception)
throw exception throw exception
} }

View File

@ -0,0 +1,16 @@
package me.arcanis.ffxivbis
import akka.actor.testkit.typed.scaladsl.ScalaTestWithActorTestKit
import org.scalatest.wordspec.AnyWordSpecLike
class ApplicationTest extends ScalaTestWithActorTestKit(Settings.withRandomDatabase)
with AnyWordSpecLike {
"application" must {
"load" in {
testKit.spawn[Nothing](Application())
}
}
}

View File

@ -0,0 +1,20 @@
package me.arcanis.ffxivbis
import org.scalatest.matchers.should.Matchers
import org.scalatest.wordspec.AnyWordSpecLike
class ConfigurationTest extends AnyWordSpecLike with Matchers {
private val requiredPaths =
Seq("akka.http.server.transparent-head-requests")
"configuration helper" must {
requiredPaths.foreach { path =>
s"has $path propery" in {
Configuration.load().hasPath(path) shouldBe true
}
}
}
}

View File

@ -6,49 +6,50 @@ import me.arcanis.ffxivbis.models._
import scala.concurrent.Future import scala.concurrent.Future
object Fixtures { object Fixtures {
lazy val bis: BiS = BiS( lazy val bis: BiS = BiS(
Seq( Seq(
Weapon(pieceType = PieceType.Savage ,Job.DNC), Piece.Weapon(pieceType = PieceType.Savage ,Job.DNC),
Head(pieceType = PieceType.Savage, Job.DNC), Piece.Head(pieceType = PieceType.Savage, Job.DNC),
Body(pieceType = PieceType.Savage, Job.DNC), Piece.Body(pieceType = PieceType.Savage, Job.DNC),
Hands(pieceType = PieceType.Tome, Job.DNC), Piece.Hands(pieceType = PieceType.Tome, Job.DNC),
Legs(pieceType = PieceType.Tome, Job.DNC), Piece.Legs(pieceType = PieceType.Tome, Job.DNC),
Feet(pieceType = PieceType.Savage, Job.DNC), Piece.Feet(pieceType = PieceType.Savage, Job.DNC),
Ears(pieceType = PieceType.Savage, Job.DNC), Piece.Ears(pieceType = PieceType.Savage, Job.DNC),
Neck(pieceType = PieceType.Tome, Job.DNC), Piece.Neck(pieceType = PieceType.Tome, Job.DNC),
Wrist(pieceType = PieceType.Savage, Job.DNC), Piece.Wrist(pieceType = PieceType.Savage, Job.DNC),
Ring(pieceType = PieceType.Tome, Job.DNC, "left ring"), Piece.Ring(pieceType = PieceType.Tome, Job.DNC, "left ring"),
Ring(pieceType = PieceType.Tome, Job.DNC, "right ring") Piece.Ring(pieceType = PieceType.Tome, Job.DNC, "right ring")
) )
) )
lazy val bis2: BiS = BiS( lazy val bis2: BiS = BiS(
Seq( Seq(
Weapon(pieceType = PieceType.Savage ,Job.DNC), Piece.Weapon(pieceType = PieceType.Savage ,Job.DNC),
Head(pieceType = PieceType.Tome, Job.DNC), Piece.Head(pieceType = PieceType.Tome, Job.DNC),
Body(pieceType = PieceType.Savage, Job.DNC), Piece.Body(pieceType = PieceType.Savage, Job.DNC),
Hands(pieceType = PieceType.Tome, Job.DNC), Piece.Hands(pieceType = PieceType.Tome, Job.DNC),
Legs(pieceType = PieceType.Savage, Job.DNC), Piece.Legs(pieceType = PieceType.Savage, Job.DNC),
Feet(pieceType = PieceType.Tome, Job.DNC), Piece.Feet(pieceType = PieceType.Tome, Job.DNC),
Ears(pieceType = PieceType.Savage, Job.DNC), Piece.Ears(pieceType = PieceType.Savage, Job.DNC),
Neck(pieceType = PieceType.Savage, Job.DNC), Piece.Neck(pieceType = PieceType.Savage, Job.DNC),
Wrist(pieceType = PieceType.Savage, Job.DNC), Piece.Wrist(pieceType = PieceType.Savage, Job.DNC),
Ring(pieceType = PieceType.Tome, Job.DNC, "left ring"), Piece.Ring(pieceType = PieceType.Tome, Job.DNC, "left ring"),
Ring(pieceType = PieceType.Savage, Job.DNC, "right ring") Piece.Ring(pieceType = PieceType.Savage, Job.DNC, "right ring")
) )
) )
lazy val bis3: BiS = BiS( lazy val bis3: BiS = BiS(
Seq( Seq(
Weapon(pieceType = PieceType.Savage ,Job.SGE), Piece.Weapon(pieceType = PieceType.Savage ,Job.SGE),
Head(pieceType = PieceType.Tome, Job.SGE), Piece.Head(pieceType = PieceType.Tome, Job.SGE),
Body(pieceType = PieceType.Savage, Job.SGE), Piece.Body(pieceType = PieceType.Savage, Job.SGE),
Hands(pieceType = PieceType.Tome, Job.SGE), Piece.Hands(pieceType = PieceType.Tome, Job.SGE),
Legs(pieceType = PieceType.Tome, Job.SGE), Piece.Legs(pieceType = PieceType.Tome, Job.SGE),
Feet(pieceType = PieceType.Savage, Job.SGE), Piece.Feet(pieceType = PieceType.Savage, Job.SGE),
Ears(pieceType = PieceType.Savage, Job.SGE), Piece.Ears(pieceType = PieceType.Savage, Job.SGE),
Neck(pieceType = PieceType.Tome, Job.SGE), Piece.Neck(pieceType = PieceType.Tome, Job.SGE),
Wrist(pieceType = PieceType.Savage, Job.SGE), Piece.Wrist(pieceType = PieceType.Savage, Job.SGE),
Ring(pieceType = PieceType.Savage, Job.SGE, "left ring"), Piece.Ring(pieceType = PieceType.Savage, Job.SGE, "left ring"),
Ring(pieceType = PieceType.Tome, Job.SGE, "right ring") Piece.Ring(pieceType = PieceType.Tome, Job.SGE, "right ring")
) )
) )
@ -58,16 +59,16 @@ object Fixtures {
lazy val link4: String = "https://etro.gg/gearset/865fc886-994f-4c28-8fc1-4379f160a916" lazy val link4: String = "https://etro.gg/gearset/865fc886-994f-4c28-8fc1-4379f160a916"
lazy val link5: String = "https://ffxiv.ariyala.com/1FGU0" lazy val link5: String = "https://ffxiv.ariyala.com/1FGU0"
lazy val lootWeapon: Piece = Weapon(pieceType = PieceType.Tome, Job.AnyJob) lazy val lootWeapon: Piece = Piece.Weapon(pieceType = PieceType.Tome, Job.AnyJob)
lazy val lootBody: Piece = Body(pieceType = PieceType.Savage, Job.AnyJob) lazy val lootBody: Piece = Piece.Body(pieceType = PieceType.Savage, Job.AnyJob)
lazy val lootBodyCrafted: Piece = Body(pieceType = PieceType.Crafted, Job.AnyJob) lazy val lootBodyCrafted: Piece = Piece.Body(pieceType = PieceType.Crafted, Job.AnyJob)
lazy val lootHands: Piece = Hands(pieceType = PieceType.Tome, Job.AnyJob) lazy val lootHands: Piece = Piece.Hands(pieceType = PieceType.Tome, Job.AnyJob)
lazy val lootLegs: Piece = Legs(pieceType = PieceType.Savage, Job.AnyJob) lazy val lootLegs: Piece = Piece.Legs(pieceType = PieceType.Savage, Job.AnyJob)
lazy val lootEars: Piece = Ears(pieceType = PieceType.Savage, Job.AnyJob) lazy val lootEars: Piece = Piece.Ears(pieceType = PieceType.Savage, Job.AnyJob)
lazy val lootRing: Piece = Ring(pieceType = PieceType.Tome, Job.AnyJob) lazy val lootRing: Piece = Piece.Ring(pieceType = PieceType.Tome, Job.AnyJob)
lazy val lootLeftRing: Piece = Ring(pieceType = PieceType.Tome, Job.AnyJob, "left ring") lazy val lootLeftRing: Piece = Piece.Ring(pieceType = PieceType.Tome, Job.AnyJob, "left ring")
lazy val lootRightRing: Piece = Ring(pieceType = PieceType.Tome, Job.AnyJob, "right ring") lazy val lootRightRing: Piece = Piece.Ring(pieceType = PieceType.Tome, Job.AnyJob, "right ring")
lazy val lootUpgrade: Piece = BodyUpgrade lazy val lootUpgrade: Piece = Piece.BodyUpgrade
lazy val loot: Seq[Piece] = Seq(lootBody, lootHands, lootLegs, lootUpgrade) lazy val loot: Seq[Piece] = Seq(lootBody, lootHands, lootLegs, lootUpgrade)
lazy val partyId: String = Party.randomPartyId lazy val partyId: String = Party.randomPartyId
@ -84,5 +85,5 @@ object Fixtures {
lazy val users: Seq[User] = Seq(userAdmin, userGet) lazy val users: Seq[User] = Seq(userAdmin, userGet)
lazy val authProvider: AuthorizationProvider = (_: String, _: String) => Future.successful(Some(userAdmin)) lazy val authProvider: AuthorizationProvider = (_: String, _: String) => Future.successful(Some(userAdmin))
lazy val rejectingProvider: AuthorizationProvider = (_: String, _: String) => Future.successful(None) lazy val rejectingAuthProvider: AuthorizationProvider = (_: String, _: String) => Future.successful(None)
} }

View File

@ -5,11 +5,12 @@ import com.typesafe.config.{Config, ConfigFactory, ConfigValueFactory}
import java.io.File import java.io.File
object Settings { object Settings {
def config(values: Map[String, AnyRef]): Config = { def config(values: Map[String, AnyRef]): Config = {
@scala.annotation.tailrec @scala.annotation.tailrec
def replace(acc: Config, iter: List[(String, AnyRef)]): Config = iter match { def replace(config: Config, iter: List[(String, AnyRef)]): Config = iter match {
case Nil => acc case Nil => config
case (key -> value) :: tail => replace(acc.withValue(key, ConfigValueFactory.fromAnyRef(value)), tail) case (key -> value) :: tail => replace(config.withValue(key, ConfigValueFactory.fromAnyRef(value)), tail)
} }
val default = ConfigFactory.load() val default = ConfigFactory.load()
@ -23,7 +24,9 @@ object Settings {
if (databaseFile.exists) if (databaseFile.exists)
databaseFile.delete() databaseFile.delete()
} }
def randomDatabasePath: String = File.createTempFile("ffxivdb-",".db").toPath.toString def randomDatabasePath: String = File.createTempFile("ffxivdb-",".db").toPath.toString
def withRandomDatabase: Config = def withRandomDatabase: Config =
config(Map("me.arcanis.ffxivbis.database.sqlite.jdbcUrl" -> s"jdbc:sqlite:$randomDatabasePath")) config(Map("me.arcanis.ffxivbis.database.sqlite.jdbcUrl" -> s"jdbc:sqlite:$randomDatabasePath"))
} }

View File

@ -1,7 +1,7 @@
package me.arcanis.ffxivbis.http package me.arcanis.ffxivbis.http
import akka.http.scaladsl.model.{StatusCodes, Uri}
import akka.http.scaladsl.model.headers.{Authorization, BasicHttpCredentials} import akka.http.scaladsl.model.headers.{Authorization, BasicHttpCredentials}
import akka.http.scaladsl.model.{StatusCodes, Uri}
import akka.http.scaladsl.server.Route import akka.http.scaladsl.server.Route
import akka.http.scaladsl.testkit.ScalatestRouteTest import akka.http.scaladsl.testkit.ScalatestRouteTest
import me.arcanis.ffxivbis.Fixtures import me.arcanis.ffxivbis.Fixtures
@ -25,7 +25,7 @@ class AuthorizationTest extends AnyWordSpecLike with Matchers with ScalatestRout
} }
"reject credentials" in { "reject credentials" in {
val route = new RootView(Fixtures.rejectingProvider).routes val route = new RootView(Fixtures.rejectingAuthProvider).routes
Get(Uri(s"/party/${Fixtures.partyId}")).withHeaders(auth) ~> Route.seal(route) ~> check { Get(Uri(s"/party/${Fixtures.partyId}")).withHeaders(auth) ~> Route.seal(route) ~> check {
status shouldEqual StatusCodes.Unauthorized status shouldEqual StatusCodes.Unauthorized

View File

@ -8,7 +8,7 @@ import akka.http.scaladsl.testkit.{RouteTestTimeout, ScalatestRouteTest}
import akka.testkit.TestKit import akka.testkit.TestKit
import com.typesafe.config.Config import com.typesafe.config.Config
import me.arcanis.ffxivbis.http.api.v1.json._ import me.arcanis.ffxivbis.http.api.v1.json._
import me.arcanis.ffxivbis.messages.{AddPlayer, AddUser} import me.arcanis.ffxivbis.messages.DatabaseMessage.{AddPlayer, AddUser}
import me.arcanis.ffxivbis.models.{BiS, Job} import me.arcanis.ffxivbis.models.{BiS, Job}
import me.arcanis.ffxivbis.service.PartyService import me.arcanis.ffxivbis.service.PartyService
import me.arcanis.ffxivbis.service.bis.BisProvider import me.arcanis.ffxivbis.service.bis.BisProvider
@ -53,15 +53,6 @@ class BiSEndpointTest extends AnyWordSpecLike with Matchers with ScalatestRouteT
super.afterAll() super.afterAll()
} }
private def compareBiSResponse(actual: PlayerModel, expected: PlayerModel): Unit = {
actual.partyId shouldEqual expected.partyId
actual.nick shouldEqual expected.nick
actual.job shouldEqual expected.job
Compare.seqEquals(actual.bis.get, expected.bis.get) shouldEqual true
actual.link shouldEqual expected.link
actual.priority shouldEqual expected.priority
}
"api v1 bis endpoint" must { "api v1 bis endpoint" must {
"create best in slot set from ariyala" in { "create best in slot set from ariyala" in {
@ -216,4 +207,13 @@ class BiSEndpointTest extends AnyWordSpecLike with Matchers with ScalatestRouteT
} }
} }
private def compareBiSResponse(actual: PlayerModel, expected: PlayerModel): 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
}
} }

View File

@ -0,0 +1,78 @@
package me.arcanis.ffxivbis.http.api.v1
import akka.http.scaladsl.model.StatusCodes
import akka.http.scaladsl.model.headers.Allow
import akka.http.scaladsl.server.Directives.{path, _}
import akka.http.scaladsl.server.Route
import akka.http.scaladsl.testkit.ScalatestRouteTest
import me.arcanis.ffxivbis.http.api.v1.json._
import org.scalatest.matchers.should.Matchers
import org.scalatest.wordspec.AnyWordSpecLike
class HttpHandlerTest extends AnyWordSpecLike with Matchers with ScalatestRouteTest with JsonSupport with HttpHandler {
"http handler" must {
"convert IllegalArgumentException into 400 response" in {
Get("/400") ~> withExceptionHandler("400", failWith(new IllegalArgumentException(""))) ~> check {
status shouldEqual StatusCodes.BadRequest
responseAs[ErrorModel] shouldEqual ErrorModel("")
}
}
"convert IllegalArgumentException into 400 response with details" in {
Get("/400") ~> withExceptionHandler("400", failWith(new IllegalArgumentException("message"))) ~> check {
status shouldEqual StatusCodes.BadRequest
responseAs[ErrorModel] shouldEqual ErrorModel("message")
}
}
"convert exception message to error response" in {
Get("/500") ~> withExceptionHandler("500", failWith(new ArithmeticException)) ~> check {
status shouldEqual StatusCodes.InternalServerError
responseAs[ErrorModel] shouldEqual ErrorModel("unknown server error")
}
}
"process OPTIONS request" in {
Options("/200") ~> withRejectionHandler() ~> check {
status shouldEqual StatusCodes.OK
headers.collectFirst { case header: Allow => header } should not be empty
responseAs[String] shouldBe empty
}
}
"reject unknown request" in {
Post("/200") ~> withRejectionHandler() ~> check {
status shouldEqual StatusCodes.MethodNotAllowed
headers.collectFirst { case header: Allow => header } should not be empty
responseAs[ErrorModel].message should not be empty
}
}
"handle 404 response" in {
Get("/404") ~> withRejectionHandler() ~> check {
status shouldEqual StatusCodes.NotFound
responseAs[ErrorModel] shouldEqual ErrorModel("The requested resource could not be found.")
}
}
}
private def single(uri: String, completeWith: Route) =
path(uri)(get(completeWith))
private def withExceptionHandler(uri: String = "200", completeWith: Route = complete(StatusCodes.OK)) =
Route.seal {
handleExceptions(exceptionHandler) {
single(uri, completeWith)
}
}
private def withRejectionHandler(uri: String = "200", completeWith: Route = complete(StatusCodes.OK)) =
Route.seal {
handleRejections(rejectionHandler) {
single(uri, completeWith)
}
}
}

View File

@ -8,7 +8,7 @@ import akka.http.scaladsl.testkit.{RouteTestTimeout, ScalatestRouteTest}
import akka.testkit.TestKit import akka.testkit.TestKit
import com.typesafe.config.Config import com.typesafe.config.Config
import me.arcanis.ffxivbis.http.api.v1.json._ import me.arcanis.ffxivbis.http.api.v1.json._
import me.arcanis.ffxivbis.messages.{AddPlayer, AddUser} import me.arcanis.ffxivbis.messages.DatabaseMessage.{AddPlayer, AddUser}
import me.arcanis.ffxivbis.service.PartyService import me.arcanis.ffxivbis.service.PartyService
import me.arcanis.ffxivbis.service.database.{Database, Migration} import me.arcanis.ffxivbis.service.database.{Database, Migration}
import me.arcanis.ffxivbis.{Fixtures, Settings} import me.arcanis.ffxivbis.{Fixtures, Settings}

View File

@ -8,7 +8,7 @@ import akka.http.scaladsl.testkit.{RouteTestTimeout, ScalatestRouteTest}
import akka.testkit.TestKit import akka.testkit.TestKit
import com.typesafe.config.Config import com.typesafe.config.Config
import me.arcanis.ffxivbis.http.api.v1.json._ import me.arcanis.ffxivbis.http.api.v1.json._
import me.arcanis.ffxivbis.messages.AddUser import me.arcanis.ffxivbis.messages.DatabaseMessage.AddUser
import me.arcanis.ffxivbis.models.PartyDescription import me.arcanis.ffxivbis.models.PartyDescription
import me.arcanis.ffxivbis.service.PartyService import me.arcanis.ffxivbis.service.PartyService
import me.arcanis.ffxivbis.service.bis.BisProvider import me.arcanis.ffxivbis.service.bis.BisProvider

View File

@ -7,9 +7,8 @@ import akka.http.scaladsl.model.{StatusCodes, Uri}
import akka.http.scaladsl.testkit.{RouteTestTimeout, ScalatestRouteTest} import akka.http.scaladsl.testkit.{RouteTestTimeout, ScalatestRouteTest}
import akka.testkit.TestKit import akka.testkit.TestKit
import com.typesafe.config.Config import com.typesafe.config.Config
import me.arcanis.ffxivbis
import me.arcanis.ffxivbis.http.api.v1.json._ import me.arcanis.ffxivbis.http.api.v1.json._
import me.arcanis.ffxivbis.messages.{AddPlayer, AddUser} import me.arcanis.ffxivbis.messages.DatabaseMessage.{AddPlayer, AddUser}
import me.arcanis.ffxivbis.service.PartyService import me.arcanis.ffxivbis.service.PartyService
import me.arcanis.ffxivbis.service.bis.BisProvider import me.arcanis.ffxivbis.service.bis.BisProvider
import me.arcanis.ffxivbis.service.database.{Database, Migration} import me.arcanis.ffxivbis.service.database.{Database, Migration}

View File

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

View File

@ -9,13 +9,13 @@ class PieceTest extends AnyWordSpecLike with Matchers {
"piece model" must { "piece model" must {
"return upgrade" in { "return upgrade" in {
Fixtures.lootWeapon.upgrade shouldEqual Some(WeaponUpgrade) Fixtures.lootWeapon.upgrade shouldEqual Some(Piece.WeaponUpgrade)
Fixtures.lootBody.upgrade shouldEqual None Fixtures.lootBody.upgrade shouldEqual None
Fixtures.lootHands.upgrade shouldEqual Some(BodyUpgrade) Fixtures.lootHands.upgrade shouldEqual Some(Piece.BodyUpgrade)
Fixtures.lootLegs.upgrade shouldEqual None Fixtures.lootLegs.upgrade shouldEqual None
Fixtures.lootEars.upgrade shouldEqual None Fixtures.lootEars.upgrade shouldEqual None
Fixtures.lootLeftRing.upgrade shouldEqual Some(AccessoryUpgrade) Fixtures.lootLeftRing.upgrade shouldEqual Some(Piece.AccessoryUpgrade)
Fixtures.lootRightRing.upgrade shouldEqual Some(AccessoryUpgrade) Fixtures.lootRightRing.upgrade shouldEqual Some(Piece.AccessoryUpgrade)
} }
"build piece from string" in { "build piece from string" in {

View File

@ -2,7 +2,7 @@ package me.arcanis.ffxivbis.service
import akka.actor.testkit.typed.scaladsl.ActorTestKit import akka.actor.testkit.typed.scaladsl.ActorTestKit
import akka.actor.typed.scaladsl.AskPattern.Askable import akka.actor.typed.scaladsl.AskPattern.Askable
import me.arcanis.ffxivbis.messages.DownloadBiS import me.arcanis.ffxivbis.messages.BiSProviderMessage.DownloadBiS
import me.arcanis.ffxivbis.models._ import me.arcanis.ffxivbis.models._
import me.arcanis.ffxivbis.service.bis.BisProvider import me.arcanis.ffxivbis.service.bis.BisProvider
import me.arcanis.ffxivbis.{Fixtures, Settings} import me.arcanis.ffxivbis.{Fixtures, Settings}
@ -28,10 +28,10 @@ class LootSelectorTest extends AnyWordSpecLike with Matchers with BeforeAndAfter
val testKit = ActorTestKit(Settings.withRandomDatabase) val testKit = ActorTestKit(Settings.withRandomDatabase)
val provider = testKit.spawn(BisProvider()) val provider = testKit.spawn(BisProvider())
val dncSet = Await.result(provider.ask(DownloadBiS(Fixtures.link, Job.DNC, _) )(timeout, testKit.scheduler), timeout) val dncSet = Await.result(provider.ask(DownloadBiS(Fixtures.link, Job.DNC, _))(timeout, testKit.scheduler), timeout)
dnc = dnc.withBiS(Some(dncSet)) dnc = dnc.withBiS(Some(dncSet))
val drgSet = Await.result(provider.ask(DownloadBiS(Fixtures.link2, Job.DRG, _) )(timeout, testKit.scheduler), timeout) val drgSet = Await.result(provider.ask(DownloadBiS(Fixtures.link2, Job.DRG, _))(timeout, testKit.scheduler), timeout)
drg = drg.withBiS(Some(drgSet)) drg = drg.withBiS(Some(drgSet))
default = default.withPlayer(dnc).withPlayer(drg) default = default.withPlayer(dnc).withPlayer(drg)
@ -43,11 +43,11 @@ class LootSelectorTest extends AnyWordSpecLike with Matchers with BeforeAndAfter
"loot selector" must { "loot selector" must {
"suggest loot by isRequired" in { "suggest loot by isRequired" in {
toPlayerId(default.suggestLoot(Head(pieceType = PieceType.Savage, Job.AnyJob))) shouldEqual Seq(dnc.playerId, drg.playerId) toPlayerId(default.suggestLoot(Piece.Head(pieceType = PieceType.Savage, Job.AnyJob))) shouldEqual Seq(dnc.playerId, drg.playerId)
} }
"suggest loot if a player already have it" in { "suggest loot if a player already have it" in {
val piece = Body(pieceType = PieceType.Savage, Job.AnyJob) val piece = Piece.Body(pieceType = PieceType.Savage, Job.AnyJob)
val party = default.withPlayer(dnc.withLoot(piece)) val party = default.withPlayer(dnc.withLoot(piece))
toPlayerId(party.suggestLoot(piece)) shouldEqual Seq(drg.playerId, dnc.playerId) toPlayerId(party.suggestLoot(piece)) shouldEqual Seq(drg.playerId, dnc.playerId)
@ -56,34 +56,34 @@ class LootSelectorTest extends AnyWordSpecLike with Matchers with BeforeAndAfter
"suggest upgrade" in { "suggest upgrade" in {
val party = default.withPlayer( val party = default.withPlayer(
dnc.withBiS( dnc.withBiS(
Some(dnc.bis.withPiece(Weapon(pieceType = PieceType.Tome, Job.DNC))) Some(dnc.bis.withPiece(Piece.Weapon(pieceType = PieceType.Tome, Job.DNC)))
) )
) )
toPlayerId(party.suggestLoot(WeaponUpgrade)) shouldEqual Seq(dnc.playerId, drg.playerId) toPlayerId(party.suggestLoot(Piece.WeaponUpgrade)) shouldEqual Seq(dnc.playerId, drg.playerId)
} }
"suggest loot by priority" in { "suggest loot by priority" in {
val party = default.withPlayer(dnc.copy(priority = 2)) val party = default.withPlayer(dnc.copy(priority = 2))
toPlayerId(party.suggestLoot(Body(pieceType = PieceType.Savage, Job.AnyJob))) shouldEqual Seq(drg.playerId, dnc.playerId) toPlayerId(party.suggestLoot(Piece.Body(pieceType = PieceType.Savage, Job.AnyJob))) shouldEqual Seq(drg.playerId, dnc.playerId)
} }
"suggest loot by bis pieces got" in { "suggest loot by bis pieces got" in {
val party = default.withPlayer(dnc.withLoot(Head(pieceType = PieceType.Savage, Job.AnyJob))) val party = default.withPlayer(dnc.withLoot(Piece.Head(pieceType = PieceType.Savage, Job.AnyJob)))
toPlayerId(party.suggestLoot(Body(pieceType = PieceType.Savage, Job.AnyJob))) shouldEqual Seq(drg.playerId, dnc.playerId) toPlayerId(party.suggestLoot(Piece.Body(pieceType = PieceType.Savage, Job.AnyJob))) shouldEqual Seq(drg.playerId, dnc.playerId)
} }
"suggest loot by this piece got" in { "suggest loot by this piece got" in {
val piece = Body(pieceType = PieceType.Tome, Job.AnyJob) val piece = Piece.Body(pieceType = PieceType.Tome, Job.AnyJob)
val party = default.withPlayer(dnc.withLoot(piece)) val party = default.withPlayer(dnc.withLoot(piece))
toPlayerId(party.suggestLoot(piece)) shouldEqual Seq(drg.playerId, dnc.playerId) toPlayerId(party.suggestLoot(piece)) shouldEqual Seq(drg.playerId, dnc.playerId)
} }
"suggest loot by total piece got" in { "suggest loot by total piece got" in {
val piece = Body(pieceType = PieceType.Tome, Job.AnyJob) val piece = Piece.Body(pieceType = PieceType.Tome, Job.AnyJob)
val party = default val party = default
.withPlayer(dnc.withLoot(Seq(piece, piece).map(pieceToLoot))) .withPlayer(dnc.withLoot(Seq(piece, piece).map(pieceToLoot)))
.withPlayer(drg.withLoot(piece)) .withPlayer(drg.withLoot(piece))

View File

@ -1,7 +1,7 @@
package me.arcanis.ffxivbis.service.bis package me.arcanis.ffxivbis.service.bis
import akka.actor.testkit.typed.scaladsl.ScalaTestWithActorTestKit import akka.actor.testkit.typed.scaladsl.ScalaTestWithActorTestKit
import me.arcanis.ffxivbis.messages.DownloadBiS import me.arcanis.ffxivbis.messages.BiSProviderMessage.DownloadBiS
import me.arcanis.ffxivbis.models._ import me.arcanis.ffxivbis.models._
import me.arcanis.ffxivbis.{Fixtures, Settings} import me.arcanis.ffxivbis.{Fixtures, Settings}
import org.scalatest.wordspec.AnyWordSpecLike import org.scalatest.wordspec.AnyWordSpecLike

View File

@ -2,7 +2,7 @@ package me.arcanis.ffxivbis.service.database
import akka.actor.testkit.typed.scaladsl.ScalaTestWithActorTestKit import akka.actor.testkit.typed.scaladsl.ScalaTestWithActorTestKit
import akka.actor.typed.scaladsl.AskPattern.Askable import akka.actor.typed.scaladsl.AskPattern.Askable
import me.arcanis.ffxivbis.messages.{AddPieceToBis, AddPlayer, GetBiS, RemovePieceFromBiS} import me.arcanis.ffxivbis.messages.DatabaseMessage._
import me.arcanis.ffxivbis.models._ import me.arcanis.ffxivbis.models._
import me.arcanis.ffxivbis.utils.Compare import me.arcanis.ffxivbis.utils.Compare
import me.arcanis.ffxivbis.{Fixtures, Settings} import me.arcanis.ffxivbis.{Fixtures, Settings}
@ -70,7 +70,7 @@ class DatabaseBiSHandlerTest extends ScalaTestWithActorTestKit(Settings.withRand
"update piece in bis set" in { "update piece in bis set" in {
val updateProbe = testKit.createTestProbe[Unit]() val updateProbe = testKit.createTestProbe[Unit]()
val newPiece = Hands(pieceType = PieceType.Savage, Job.DNC) val newPiece = Piece.Hands(pieceType = PieceType.Savage, Job.DNC)
database ! AddPieceToBis(Fixtures.playerEmpty.playerId, newPiece, updateProbe.ref) database ! AddPieceToBis(Fixtures.playerEmpty.playerId, newPiece, updateProbe.ref)
updateProbe.expectMessage(askTimeout, ()) updateProbe.expectMessage(askTimeout, ())

View File

@ -2,8 +2,7 @@ package me.arcanis.ffxivbis.service.database
import akka.actor.testkit.typed.scaladsl.ScalaTestWithActorTestKit import akka.actor.testkit.typed.scaladsl.ScalaTestWithActorTestKit
import akka.actor.typed.scaladsl.AskPattern.Askable import akka.actor.typed.scaladsl.AskPattern.Askable
import ch.qos.logback.core.util.FixedDelay import me.arcanis.ffxivbis.messages.DatabaseMessage._
import me.arcanis.ffxivbis.messages.{AddPieceTo, AddPlayer, GetLoot, RemovePieceFrom}
import me.arcanis.ffxivbis.models._ import me.arcanis.ffxivbis.models._
import me.arcanis.ffxivbis.utils.Compare import me.arcanis.ffxivbis.utils.Compare
import me.arcanis.ffxivbis.{Fixtures, Settings} import me.arcanis.ffxivbis.{Fixtures, Settings}

View File

@ -1,7 +1,7 @@
package me.arcanis.ffxivbis.service.database package me.arcanis.ffxivbis.service.database
import akka.actor.testkit.typed.scaladsl.ScalaTestWithActorTestKit import akka.actor.testkit.typed.scaladsl.ScalaTestWithActorTestKit
import me.arcanis.ffxivbis.messages.{AddPlayer, GetParty, GetPlayer, RemovePlayer} import me.arcanis.ffxivbis.messages.DatabaseMessage._
import me.arcanis.ffxivbis.models._ import me.arcanis.ffxivbis.models._
import me.arcanis.ffxivbis.utils.Compare import me.arcanis.ffxivbis.utils.Compare
import me.arcanis.ffxivbis.{Fixtures, Settings} import me.arcanis.ffxivbis.{Fixtures, Settings}

View File

@ -1,7 +1,7 @@
package me.arcanis.ffxivbis.service.database package me.arcanis.ffxivbis.service.database
import akka.actor.testkit.typed.scaladsl.ScalaTestWithActorTestKit import akka.actor.testkit.typed.scaladsl.ScalaTestWithActorTestKit
import me.arcanis.ffxivbis.messages.{AddUser, DeleteUser, GetUser, GetUsers} import me.arcanis.ffxivbis.messages.DatabaseMessage._
import me.arcanis.ffxivbis.models.User import me.arcanis.ffxivbis.models.User
import me.arcanis.ffxivbis.utils.Compare import me.arcanis.ffxivbis.utils.Compare
import me.arcanis.ffxivbis.{Fixtures, Settings} import me.arcanis.ffxivbis.{Fixtures, Settings}
@ -80,5 +80,6 @@ class DatabaseUserHandlerTest extends ScalaTestWithActorTestKit(Settings.withRan
database ! GetUser(Fixtures.partyId, Fixtures.userGet.username, probe.ref) database ! GetUser(Fixtures.partyId, Fixtures.userGet.username, probe.ref)
probe.expectMessage(askTimeout, None) probe.expectMessage(askTimeout, None)
} }
} }
} }

View File

@ -1,6 +1,7 @@
package me.arcanis.ffxivbis.utils package me.arcanis.ffxivbis.utils
object Compare { object Compare {
def mapEquals[K, T](left: Map[K, T], right: Map[K, T]): Boolean = def mapEquals[K, T](left: Map[K, T], right: Map[K, T]): Boolean =
left.forall { left.forall {
case (key, value) => right.contains(key) && right(key) == value case (key, value) => right.contains(key) && right(key) == value

View File

@ -8,5 +8,4 @@ import scala.language.implicitConversions
object Converters { object Converters {
implicit def pieceToLoot(piece: Piece): Loot = Loot(-1, piece, Instant.ofEpochMilli(0), isFreeLoot = false) implicit def pieceToLoot(piece: Piece): Loot = Loot(-1, piece, Instant.ofEpochMilli(0), isFreeLoot = false)
} }

View File

@ -0,0 +1,43 @@
package me.arcanis.ffxivbis.utils
import akka.util.Timeout
import me.arcanis.ffxivbis.Settings
import org.scalatest.matchers.should.Matchers
import org.scalatest.wordspec.AnyWordSpecLike
import java.util.concurrent.TimeUnit
import scala.concurrent.duration.FiniteDuration
class ImplicitsTest extends AnyWordSpecLike with Matchers {
import me.arcanis.ffxivbis.utils.Implicits._
"configuration extension" must {
"return finite duration" in {
val config = Settings.config(Map("value" -> "1m"))
config.getFiniteDuration("value") shouldBe FiniteDuration(1, TimeUnit.MINUTES)
}
"return optional string" in {
val config = Settings.config(Map("value" -> "string"))
config.getOptString("value") shouldBe Some("string")
}
"return None for missing optional string" in {
val config = Settings.config(Map.empty)
config.getOptString("value") shouldBe None
}
"return None for empty optional string" in {
val config = Settings.config(Map("value" -> ""))
config.getOptString("value") shouldBe None
}
"return timeout" in {
val config = Settings.config(Map("value" -> "1m"))
config.getTimeout("value") shouldBe Timeout(1, TimeUnit.MINUTES)
}
}
}