mirror of
https://github.com/arcan1s/ffxivbis.git
synced 2025-04-25 09:47:18 +00:00
etro support
This commit is contained in:
parent
0171b229a1
commit
534ed98459
@ -1,7 +1,7 @@
|
|||||||
me.arcanis.ffxivbis {
|
me.arcanis.ffxivbis {
|
||||||
ariyala {
|
bis-provider {
|
||||||
# ariyala base url, string, required
|
# either use ariyala or etro
|
||||||
ariyala-url = "https://ffxiv.ariyala.com"
|
use-etro = no
|
||||||
# xivapi base url, string, required
|
# xivapi base url, string, required
|
||||||
xivapi-url = "https://xivapi.com"
|
xivapi-url = "https://xivapi.com"
|
||||||
# xivapi developer key, string, optional
|
# xivapi developer key, string, optional
|
||||||
|
@ -13,8 +13,9 @@ import akka.http.scaladsl.Http
|
|||||||
import akka.stream.ActorMaterializer
|
import akka.stream.ActorMaterializer
|
||||||
import com.typesafe.scalalogging.StrictLogging
|
import com.typesafe.scalalogging.StrictLogging
|
||||||
import me.arcanis.ffxivbis.http.RootEndpoint
|
import me.arcanis.ffxivbis.http.RootEndpoint
|
||||||
|
import me.arcanis.ffxivbis.service.bis.BisProvider
|
||||||
import me.arcanis.ffxivbis.service.impl.DatabaseImpl
|
import me.arcanis.ffxivbis.service.impl.DatabaseImpl
|
||||||
import me.arcanis.ffxivbis.service.{Ariyala, PartyService}
|
import me.arcanis.ffxivbis.service.PartyService
|
||||||
import me.arcanis.ffxivbis.storage.Migration
|
import me.arcanis.ffxivbis.storage.Migration
|
||||||
|
|
||||||
import scala.concurrent.duration.Duration
|
import scala.concurrent.duration.Duration
|
||||||
@ -33,10 +34,12 @@ class Application extends Actor with StrictLogging {
|
|||||||
|
|
||||||
Migration(config).onComplete {
|
Migration(config).onComplete {
|
||||||
case Success(_) =>
|
case Success(_) =>
|
||||||
val ariyala = context.system.actorOf(Ariyala.props, "ariyala")
|
val useEtro = config.getBoolean("me.arcanis.ffxivbis.bis-provider.use-etro")
|
||||||
|
|
||||||
|
val bisProvider = context.system.actorOf(BisProvider.props(useEtro), "bis-provider")
|
||||||
val storage = context.system.actorOf(DatabaseImpl.props, "storage")
|
val storage = context.system.actorOf(DatabaseImpl.props, "storage")
|
||||||
val party = context.system.actorOf(PartyService.props(storage), "party")
|
val party = context.system.actorOf(PartyService.props(storage), "party")
|
||||||
val http = new RootEndpoint(context.system, party, ariyala)
|
val http = new RootEndpoint(context.system, party, bisProvider)
|
||||||
|
|
||||||
logger.info(s"start server at $host:$port")
|
logger.info(s"start server at $host:$port")
|
||||||
val bind = Http()(context.system).bindAndHandle(http.route, host, port)
|
val bind = Http()(context.system).bindAndHandle(http.route, host, port)
|
||||||
|
@ -17,7 +17,7 @@ import me.arcanis.ffxivbis.service.impl.DatabaseBiSHandler
|
|||||||
|
|
||||||
import scala.concurrent.{ExecutionContext, Future}
|
import scala.concurrent.{ExecutionContext, Future}
|
||||||
|
|
||||||
trait BiSHelper extends AriyalaHelper {
|
trait BiSHelper extends BisProviderHelper {
|
||||||
|
|
||||||
def storage: ActorRef
|
def storage: ActorRef
|
||||||
|
|
||||||
|
@ -12,15 +12,15 @@ import akka.actor.ActorRef
|
|||||||
import akka.pattern.ask
|
import akka.pattern.ask
|
||||||
import akka.util.Timeout
|
import akka.util.Timeout
|
||||||
import me.arcanis.ffxivbis.models.{BiS, Job}
|
import me.arcanis.ffxivbis.models.{BiS, Job}
|
||||||
import me.arcanis.ffxivbis.service.Ariyala
|
import me.arcanis.ffxivbis.service.bis.BisProvider
|
||||||
|
|
||||||
import scala.concurrent.{ExecutionContext, Future}
|
import scala.concurrent.{ExecutionContext, Future}
|
||||||
|
|
||||||
trait AriyalaHelper {
|
trait BisProviderHelper {
|
||||||
|
|
||||||
def ariyala: ActorRef
|
def ariyala: ActorRef
|
||||||
|
|
||||||
def downloadBiS(link: String, job: Job.Job)
|
def downloadBiS(link: String, job: Job.Job)
|
||||||
(implicit executionContext: ExecutionContext, timeout: Timeout): Future[BiS] =
|
(implicit executionContext: ExecutionContext, timeout: Timeout): Future[BiS] =
|
||||||
(ariyala ? Ariyala.GetBiS(link, job)).mapTo[BiS]
|
(ariyala ? BisProvider.GetBiS(link, job)).mapTo[BiS]
|
||||||
}
|
}
|
@ -17,7 +17,7 @@ import me.arcanis.ffxivbis.service.impl.{DatabaseBiSHandler, DatabasePartyHandle
|
|||||||
|
|
||||||
import scala.concurrent.{ExecutionContext, Future}
|
import scala.concurrent.{ExecutionContext, Future}
|
||||||
|
|
||||||
trait PlayerHelper extends AriyalaHelper {
|
trait PlayerHelper extends BisProviderHelper {
|
||||||
|
|
||||||
def storage: ActorRef
|
def storage: ActorRef
|
||||||
|
|
||||||
|
@ -106,7 +106,7 @@ object Piece {
|
|||||||
case "feet" => Feet(pieceType, job)
|
case "feet" => Feet(pieceType, job)
|
||||||
case "ears" => Ears(pieceType, job)
|
case "ears" => Ears(pieceType, job)
|
||||||
case "neck" => Neck(pieceType, job)
|
case "neck" => Neck(pieceType, job)
|
||||||
case "wrist" => Wrist(pieceType, job)
|
case "wrist" | "wrists" => Wrist(pieceType, job)
|
||||||
case ring @ ("ring" | "left ring" | "right ring") => Ring(pieceType, job, ring)
|
case ring @ ("ring" | "left ring" | "right ring") => Ring(pieceType, job, ring)
|
||||||
case "accessory upgrade" => AccessoryUpgrade
|
case "accessory upgrade" => AccessoryUpgrade
|
||||||
case "body upgrade" => BodyUpgrade
|
case "body upgrade" => BodyUpgrade
|
||||||
|
@ -1,204 +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
|
|
||||||
|
|
||||||
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, PieceType}
|
|
||||||
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, getPieceType))
|
|
||||||
}
|
|
||||||
|
|
||||||
private 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, Ariyala.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, Ariyala.parseXivapiJsonToType(shops))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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, pieceTypes: Seq[Long] => Future[Map[Long, PieceType.PieceType]])
|
|
||||||
(js: JsObject)
|
|
||||||
(implicit executionContext: ExecutionContext): Future[Seq[Piece]] =
|
|
||||||
parseAriyalaJson(job)(js).flatMap { pieces =>
|
|
||||||
pieceTypes(pieces.values.toSeq).map { types =>
|
|
||||||
pieces.view.mapValues(types).map {
|
|
||||||
case (piece, pieceType) => Piece(piece, pieceType, job)
|
|
||||||
}.toSeq
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private def parseXivapiJsonToShop(js: JsObject)
|
|
||||||
(implicit executionContext: ExecutionContext): Future[Map[Long, (String, Long)]] = {
|
|
||||||
def extractTraderId(js: JsObject) = {
|
|
||||||
js.fields
|
|
||||||
.get("Recipe").map(_ => "crafted" -> -1L) // you can craft this item
|
|
||||||
.orElse { // lets try shop items
|
|
||||||
js.fields("SpecialShop").asJsObject
|
|
||||||
.fields.collectFirst {
|
|
||||||
case (shopName, JsArray(array)) if shopName.startsWith("ItemReceive") =>
|
|
||||||
val shopId = array.head match {
|
|
||||||
case JsNumber(id) => id.toLong
|
|
||||||
case other => throw deserializationError(s"Could not parse $other")
|
|
||||||
}
|
|
||||||
shopName.replace("ItemReceive", "") -> shopId
|
|
||||||
}
|
|
||||||
}.getOrElse(throw deserializationError(s"Could not parse $js"))
|
|
||||||
}
|
|
||||||
|
|
||||||
Future {
|
|
||||||
js.fields("Results") match {
|
|
||||||
case array: JsArray =>
|
|
||||||
array.elements.map(_.asJsObject.getFields("ID", "GameContentLinks") match {
|
|
||||||
case Seq(JsNumber(id), shop) => id.toLong -> extractTraderId(shop.asJsObject)
|
|
||||||
case other => throw deserializationError(s"Could not parse $other")
|
|
||||||
}).toMap
|
|
||||||
case other => throw deserializationError(s"Could not parse $other")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private def parseXivapiJsonToType(shops: Map[Long, (String, Long)])(js: JsObject)
|
|
||||||
(implicit executionContext: ExecutionContext): Future[Map[Long, PieceType.PieceType]] =
|
|
||||||
Future {
|
|
||||||
val shopMap = js.fields("Results") match {
|
|
||||||
case array: JsArray =>
|
|
||||||
array.elements.map { shop =>
|
|
||||||
shop.asJsObject.fields("ID") match {
|
|
||||||
case JsNumber(id) => id.toLong -> shop.asJsObject
|
|
||||||
case other => throw deserializationError(s"Could not parse $other")
|
|
||||||
}
|
|
||||||
}.toMap
|
|
||||||
case other => throw deserializationError(s"Could not parse $other")
|
|
||||||
}
|
|
||||||
|
|
||||||
shops.map { case (itemId, (index, shopId)) =>
|
|
||||||
val pieceType =
|
|
||||||
if (index == "crafted" && shopId == -1) PieceType.Crafted
|
|
||||||
else {
|
|
||||||
Try(shopMap(shopId).fields(s"ItemCost$index").asJsObject)
|
|
||||||
.toOption
|
|
||||||
.getOrElse(throw new Exception(s"${shopMap(shopId).fields(s"ItemCost$index")}, $index"))
|
|
||||||
.getFields("IsUnique", "StackSize") match {
|
|
||||||
case Seq(JsNumber(isUnique), JsNumber(stackSize)) =>
|
|
||||||
if (isUnique == 1 || stackSize.toLong == 2000) PieceType.Tome // either upgraded gear or tomes found
|
|
||||||
else PieceType.Savage
|
|
||||||
case other => throw deserializationError(s"Could not parse $other")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
itemId -> pieceType
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
44
src/main/scala/me/arcanis/ffxivbis/service/bis/Ariyala.scala
Normal file
44
src/main/scala/me/arcanis/ffxivbis/service/bis/Ariyala.scala
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
/*
|
||||||
|
* 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}
|
||||||
|
|
||||||
|
trait 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)))
|
||||||
|
}
|
@ -0,0 +1,76 @@
|
|||||||
|
/*
|
||||||
|
* 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.{Actor, Props}
|
||||||
|
import akka.http.scaladsl.model._
|
||||||
|
import akka.pattern.pipe
|
||||||
|
import com.typesafe.scalalogging.StrictLogging
|
||||||
|
import me.arcanis.ffxivbis.models.{BiS, Job, Piece, PieceType}
|
||||||
|
import spray.json._
|
||||||
|
|
||||||
|
import scala.concurrent.{ExecutionContext, Future}
|
||||||
|
|
||||||
|
abstract class BisProvider extends Actor with XivApi with StrictLogging {
|
||||||
|
|
||||||
|
def idParser(job: Job.Job, js: JsObject)
|
||||||
|
(implicit executionContext: ExecutionContext): Future[Map[String, Long]]
|
||||||
|
def uri(root: Uri, id: String): Uri
|
||||||
|
|
||||||
|
override def receive: Receive = {
|
||||||
|
case BisProvider.GetBiS(link, job) =>
|
||||||
|
val client = sender()
|
||||||
|
get(link, job).map(BiS(_)).pipeTo(client)
|
||||||
|
}
|
||||||
|
|
||||||
|
override def postStop(): Unit = {
|
||||||
|
shutdown()
|
||||||
|
super.postStop()
|
||||||
|
}
|
||||||
|
|
||||||
|
private def get(link: String, job: Job.Job): Future[Seq[Piece]] = {
|
||||||
|
val url = Uri(link)
|
||||||
|
val id = Paths.get(link).normalize.getFileName.toString
|
||||||
|
|
||||||
|
sendRequest(uri(url, id), BisProvider.parseBisJsonToPieces(job, idParser, getPieceType))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
object BisProvider {
|
||||||
|
|
||||||
|
def props(useEtro: Boolean): Props =
|
||||||
|
if (useEtro) Props(new BisProvider with Etro)
|
||||||
|
else Props(new BisProvider with Ariyala)
|
||||||
|
|
||||||
|
case class GetBiS(link: String, job: Job.Job)
|
||||||
|
|
||||||
|
private def parseBisJsonToPieces(job: Job.Job,
|
||||||
|
idParser: (Job.Job, JsObject) => Future[Map[String, Long]],
|
||||||
|
pieceTypes: Seq[Long] => Future[Map[Long, PieceType.PieceType]])
|
||||||
|
(js: JsObject)
|
||||||
|
(implicit executionContext: ExecutionContext): Future[Seq[Piece]] =
|
||||||
|
idParser(job, js).flatMap { pieces =>
|
||||||
|
pieceTypes(pieces.values.toSeq).map { types =>
|
||||||
|
pieces.view.mapValues(types).map {
|
||||||
|
case (piece, pieceType) => Piece(piece, pieceType, job)
|
||||||
|
}.toSeq
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def remapKey(key: String): Option[String] = key match {
|
||||||
|
case "mainhand" => Some("weapon")
|
||||||
|
case "chest" => Some("body")
|
||||||
|
case "ringLeft" | "fingerL" => Some("left ring")
|
||||||
|
case "ringRight" | "fingerR" => Some("right ring")
|
||||||
|
case "weapon" | "head" | "body" | "hands" | "waist" | "legs" | "feet" | "ears" | "neck" | "wrist" | "wrists" => Some(key)
|
||||||
|
case _ => None
|
||||||
|
}
|
||||||
|
}
|
30
src/main/scala/me/arcanis/ffxivbis/service/bis/Etro.scala
Normal file
30
src/main/scala/me/arcanis/ffxivbis/service/bis/Etro.scala
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
/*
|
||||||
|
* 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}
|
||||||
|
|
||||||
|
trait 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)
|
||||||
|
}
|
@ -0,0 +1,47 @@
|
|||||||
|
/*
|
||||||
|
* 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.ActorContext
|
||||||
|
import akka.http.scaladsl.Http
|
||||||
|
import akka.http.scaladsl.model.headers.Location
|
||||||
|
import akka.http.scaladsl.model.{HttpRequest, HttpResponse, Uri}
|
||||||
|
import akka.stream.ActorMaterializer
|
||||||
|
import akka.stream.scaladsl.{Keep, Sink}
|
||||||
|
import akka.util.ByteString
|
||||||
|
import spray.json._
|
||||||
|
|
||||||
|
import scala.concurrent.{ExecutionContext, Future}
|
||||||
|
|
||||||
|
trait RequestExecutor {
|
||||||
|
|
||||||
|
def context: ActorContext
|
||||||
|
|
||||||
|
private val http = Http()(context.system)
|
||||||
|
implicit val materializer: ActorMaterializer = ActorMaterializer()(context)
|
||||||
|
implicit val executionContext: ExecutionContext =
|
||||||
|
context.system.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()
|
||||||
|
}
|
112
src/main/scala/me/arcanis/ffxivbis/service/bis/XivApi.scala
Normal file
112
src/main/scala/me/arcanis/ffxivbis/service/bis/XivApi.scala
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
/*
|
||||||
|
* 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 = context.system.settings.config
|
||||||
|
private val xivapiUrl = config.getString("me.arcanis.ffxivbis.bis-provider.xivapi-url")
|
||||||
|
private val xivapiKey = Try(config.getString("me.arcanis.ffxivbis.bis-provider.xivapi-key")).toOption
|
||||||
|
|
||||||
|
def getPieceType(itemIds: Seq[Long]): Future[Map[Long, PieceType.PieceType]] = {
|
||||||
|
val uriForItems = Uri(xivapiUrl)
|
||||||
|
.withPath(Uri.Path / "item")
|
||||||
|
.withQuery(Uri.Query(Map(
|
||||||
|
"columns" -> Seq("ID", "GameContentLinks").mkString(","),
|
||||||
|
"ids" -> itemIds.mkString(","),
|
||||||
|
"private_key" -> xivapiKey.getOrElse("")
|
||||||
|
)))
|
||||||
|
|
||||||
|
sendRequest(uriForItems, XivApi.parseXivapiJsonToShop).flatMap { shops =>
|
||||||
|
val shopIds = shops.values.map(_._2).toSet
|
||||||
|
val columns = shops.values.map(pair => s"ItemCost${pair._1}").toSet
|
||||||
|
val uriForShops = Uri(xivapiUrl)
|
||||||
|
.withPath(Uri.Path / "specialshop")
|
||||||
|
.withQuery(Uri.Query(Map(
|
||||||
|
"columns" -> (columns + "ID").mkString(","),
|
||||||
|
"ids" -> shopIds.mkString(","),
|
||||||
|
"private_key" -> xivapiKey.getOrElse("")
|
||||||
|
)))
|
||||||
|
|
||||||
|
sendRequest(uriForShops, XivApi.parseXivapiJsonToType(shops))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
object XivApi {
|
||||||
|
|
||||||
|
private def parseXivapiJsonToShop(js: JsObject)
|
||||||
|
(implicit executionContext: ExecutionContext): Future[Map[Long, (String, Long)]] = {
|
||||||
|
def extractTraderId(js: JsObject) = {
|
||||||
|
js.fields
|
||||||
|
.get("Recipe").map(_ => "crafted" -> -1L) // you can craft this item
|
||||||
|
.orElse { // lets try shop items
|
||||||
|
js.fields("SpecialShop").asJsObject
|
||||||
|
.fields.collectFirst {
|
||||||
|
case (shopName, JsArray(array)) if shopName.startsWith("ItemReceive") =>
|
||||||
|
val shopId = array.head match {
|
||||||
|
case JsNumber(id) => id.toLong
|
||||||
|
case other => throw deserializationError(s"Could not parse $other")
|
||||||
|
}
|
||||||
|
shopName.replace("ItemReceive", "") -> shopId
|
||||||
|
}
|
||||||
|
}.getOrElse(throw deserializationError(s"Could not parse $js"))
|
||||||
|
}
|
||||||
|
|
||||||
|
Future {
|
||||||
|
js.fields("Results") match {
|
||||||
|
case array: JsArray =>
|
||||||
|
array.elements.map(_.asJsObject.getFields("ID", "GameContentLinks") match {
|
||||||
|
case Seq(JsNumber(id), shop) => id.toLong -> extractTraderId(shop.asJsObject)
|
||||||
|
case other => throw deserializationError(s"Could not parse $other")
|
||||||
|
}).toMap
|
||||||
|
case other => throw deserializationError(s"Could not parse $other")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private def parseXivapiJsonToType(shops: Map[Long, (String, Long)])(js: JsObject)
|
||||||
|
(implicit executionContext: ExecutionContext): Future[Map[Long, PieceType.PieceType]] =
|
||||||
|
Future {
|
||||||
|
val shopMap = js.fields("Results") match {
|
||||||
|
case array: JsArray =>
|
||||||
|
array.elements.map { shop =>
|
||||||
|
shop.asJsObject.fields("ID") match {
|
||||||
|
case JsNumber(id) => id.toLong -> shop.asJsObject
|
||||||
|
case other => throw deserializationError(s"Could not parse $other")
|
||||||
|
}
|
||||||
|
}.toMap
|
||||||
|
case other => throw deserializationError(s"Could not parse $other")
|
||||||
|
}
|
||||||
|
|
||||||
|
shops.map { case (itemId, (index, shopId)) =>
|
||||||
|
val pieceType =
|
||||||
|
if (index == "crafted" && shopId == -1) PieceType.Crafted
|
||||||
|
else {
|
||||||
|
Try(shopMap(shopId).fields(s"ItemCost$index").asJsObject)
|
||||||
|
.toOption
|
||||||
|
.getOrElse(throw new Exception(s"${shopMap(shopId).fields(s"ItemCost$index")}, $index"))
|
||||||
|
.getFields("IsUnique", "StackSize") match {
|
||||||
|
case Seq(JsNumber(isUnique), JsNumber(stackSize)) =>
|
||||||
|
if (isUnique == 1 || stackSize.toLong == 2000) PieceType.Tome // either upgraded gear or tomes found
|
||||||
|
else PieceType.Savage
|
||||||
|
case other => throw deserializationError(s"Could not parse $other")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
itemId -> pieceType
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -22,6 +22,7 @@ object Fixtures {
|
|||||||
|
|
||||||
lazy val link: String = "https://ffxiv.ariyala.com/19V5R"
|
lazy val link: String = "https://ffxiv.ariyala.com/19V5R"
|
||||||
lazy val link2: String = "https://ffxiv.ariyala.com/1A0WM"
|
lazy val link2: String = "https://ffxiv.ariyala.com/1A0WM"
|
||||||
|
lazy val link3: String = "https://etro.gg/gearset/26a67536-b4ce-4adc-a46a-f70e348bb138"
|
||||||
|
|
||||||
lazy val lootWeapon: Piece = Weapon(pieceType = PieceType.Tome, Job.AnyJob)
|
lazy val lootWeapon: Piece = Weapon(pieceType = PieceType.Tome, Job.AnyJob)
|
||||||
lazy val lootBody: Piece = Body(pieceType = PieceType.Savage, Job.AnyJob)
|
lazy val lootBody: Piece = Body(pieceType = PieceType.Savage, Job.AnyJob)
|
||||||
|
@ -11,7 +11,8 @@ import com.typesafe.config.Config
|
|||||||
import me.arcanis.ffxivbis.{Fixtures, Settings}
|
import me.arcanis.ffxivbis.{Fixtures, Settings}
|
||||||
import me.arcanis.ffxivbis.http.api.v1.json._
|
import me.arcanis.ffxivbis.http.api.v1.json._
|
||||||
import me.arcanis.ffxivbis.models.BiS
|
import me.arcanis.ffxivbis.models.BiS
|
||||||
import me.arcanis.ffxivbis.service.{Ariyala, PartyService, impl}
|
import me.arcanis.ffxivbis.service.bis.BisProvider
|
||||||
|
import me.arcanis.ffxivbis.service.{PartyService, impl}
|
||||||
import me.arcanis.ffxivbis.storage.Migration
|
import me.arcanis.ffxivbis.storage.Migration
|
||||||
import org.scalatest.{Matchers, WordSpec}
|
import org.scalatest.{Matchers, WordSpec}
|
||||||
|
|
||||||
@ -30,9 +31,9 @@ class BiSEndpointTest extends WordSpec
|
|||||||
implicit private val routeTimeout: RouteTestTimeout = RouteTestTimeout(timeout)
|
implicit private val routeTimeout: RouteTestTimeout = RouteTestTimeout(timeout)
|
||||||
|
|
||||||
private val storage: ActorRef = system.actorOf(impl.DatabaseImpl.props)
|
private val storage: ActorRef = system.actorOf(impl.DatabaseImpl.props)
|
||||||
private val ariyala: ActorRef = system.actorOf(Ariyala.props)
|
private val provider: ActorRef = system.actorOf(BisProvider.props(false))
|
||||||
private val party: ActorRef = system.actorOf(PartyService.props(storage))
|
private val party: ActorRef = system.actorOf(PartyService.props(storage))
|
||||||
private val route: Route = new BiSEndpoint(party, ariyala)(timeout).route
|
private val route: Route = new BiSEndpoint(party, provider)(timeout).route
|
||||||
|
|
||||||
override def testConfig: Config = Settings.withRandomDatabase
|
override def testConfig: Config = Settings.withRandomDatabase
|
||||||
|
|
||||||
|
@ -11,7 +11,8 @@ import com.typesafe.config.Config
|
|||||||
import me.arcanis.ffxivbis.{Fixtures, Settings}
|
import me.arcanis.ffxivbis.{Fixtures, Settings}
|
||||||
import me.arcanis.ffxivbis.http.api.v1.json._
|
import me.arcanis.ffxivbis.http.api.v1.json._
|
||||||
import me.arcanis.ffxivbis.models.PartyDescription
|
import me.arcanis.ffxivbis.models.PartyDescription
|
||||||
import me.arcanis.ffxivbis.service.{Ariyala, impl}
|
import me.arcanis.ffxivbis.service.bis.BisProvider
|
||||||
|
import me.arcanis.ffxivbis.service.impl
|
||||||
import me.arcanis.ffxivbis.storage.Migration
|
import me.arcanis.ffxivbis.storage.Migration
|
||||||
import org.scalatest.{Matchers, WordSpec}
|
import org.scalatest.{Matchers, WordSpec}
|
||||||
|
|
||||||
@ -29,8 +30,8 @@ class PartyEndpointTest extends WordSpec
|
|||||||
implicit private val routeTimeout: RouteTestTimeout = RouteTestTimeout(timeout)
|
implicit private val routeTimeout: RouteTestTimeout = RouteTestTimeout(timeout)
|
||||||
|
|
||||||
private val storage: ActorRef = system.actorOf(impl.DatabaseImpl.props)
|
private val storage: ActorRef = system.actorOf(impl.DatabaseImpl.props)
|
||||||
private val ariyala: ActorRef = system.actorOf(Ariyala.props)
|
private val provider: ActorRef = system.actorOf(BisProvider.props(false))
|
||||||
private val route: Route = new PartyEndpoint(storage, ariyala)(timeout).route
|
private val route: Route = new PartyEndpoint(storage, provider)(timeout).route
|
||||||
|
|
||||||
override def testConfig: Config = Settings.withRandomDatabase
|
override def testConfig: Config = Settings.withRandomDatabase
|
||||||
|
|
||||||
|
@ -10,7 +10,8 @@ import akka.testkit.TestKit
|
|||||||
import com.typesafe.config.Config
|
import com.typesafe.config.Config
|
||||||
import me.arcanis.ffxivbis.{Fixtures, Settings}
|
import me.arcanis.ffxivbis.{Fixtures, Settings}
|
||||||
import me.arcanis.ffxivbis.http.api.v1.json._
|
import me.arcanis.ffxivbis.http.api.v1.json._
|
||||||
import me.arcanis.ffxivbis.service.{Ariyala, PartyService, impl}
|
import me.arcanis.ffxivbis.service.bis.BisProvider
|
||||||
|
import me.arcanis.ffxivbis.service.{PartyService, impl}
|
||||||
import me.arcanis.ffxivbis.storage.Migration
|
import me.arcanis.ffxivbis.storage.Migration
|
||||||
import org.scalatest.{Matchers, WordSpec}
|
import org.scalatest.{Matchers, WordSpec}
|
||||||
|
|
||||||
@ -29,9 +30,9 @@ class PlayerEndpointTest extends WordSpec
|
|||||||
implicit private val routeTimeout: RouteTestTimeout = RouteTestTimeout(timeout)
|
implicit private val routeTimeout: RouteTestTimeout = RouteTestTimeout(timeout)
|
||||||
|
|
||||||
private val storage: ActorRef = system.actorOf(impl.DatabaseImpl.props)
|
private val storage: ActorRef = system.actorOf(impl.DatabaseImpl.props)
|
||||||
private val ariyala: ActorRef = system.actorOf(Ariyala.props)
|
private val provider: ActorRef = system.actorOf(BisProvider.props(false))
|
||||||
private val party: ActorRef = system.actorOf(PartyService.props(storage))
|
private val party: ActorRef = system.actorOf(PartyService.props(storage))
|
||||||
private val route: Route = new PlayerEndpoint(party, ariyala)(timeout).route
|
private val route: Route = new PlayerEndpoint(party, provider)(timeout).route
|
||||||
|
|
||||||
override def testConfig: Config = Settings.withRandomDatabase
|
override def testConfig: Config = Settings.withRandomDatabase
|
||||||
|
|
||||||
|
@ -5,6 +5,7 @@ import akka.pattern.ask
|
|||||||
import akka.testkit.{ImplicitSender, TestKit}
|
import akka.testkit.{ImplicitSender, TestKit}
|
||||||
import me.arcanis.ffxivbis.{Fixtures, Settings}
|
import me.arcanis.ffxivbis.{Fixtures, Settings}
|
||||||
import me.arcanis.ffxivbis.models._
|
import me.arcanis.ffxivbis.models._
|
||||||
|
import me.arcanis.ffxivbis.service.bis.BisProvider
|
||||||
import org.scalatest.{BeforeAndAfterAll, Matchers, WordSpecLike}
|
import org.scalatest.{BeforeAndAfterAll, Matchers, WordSpecLike}
|
||||||
|
|
||||||
import scala.concurrent.Await
|
import scala.concurrent.Await
|
||||||
@ -22,16 +23,16 @@ class LootSelectorTest extends TestKit(ActorSystem("lootselector"))
|
|||||||
private val timeout: FiniteDuration = 60 seconds
|
private val timeout: FiniteDuration = 60 seconds
|
||||||
|
|
||||||
override def beforeAll(): Unit = {
|
override def beforeAll(): Unit = {
|
||||||
val ariyala = system.actorOf(Ariyala.props)
|
val provider = system.actorOf(BisProvider.props(false))
|
||||||
|
|
||||||
val dncSet = Await.result((ariyala ? Ariyala.GetBiS(Fixtures.link, Job.DNC) )(timeout).mapTo[BiS], timeout)
|
val dncSet = Await.result((provider ? BisProvider.GetBiS(Fixtures.link, Job.DNC) )(timeout).mapTo[BiS], timeout)
|
||||||
dnc = dnc.withBiS(Some(dncSet))
|
dnc = dnc.withBiS(Some(dncSet))
|
||||||
|
|
||||||
val drgSet = Await.result((ariyala ? Ariyala.GetBiS(Fixtures.link2, Job.DRG) )(timeout).mapTo[BiS], timeout)
|
val drgSet = Await.result((provider ? BisProvider.GetBiS(Fixtures.link2, Job.DRG) )(timeout).mapTo[BiS], timeout)
|
||||||
drg = drg.withBiS(Some(drgSet))
|
drg = drg.withBiS(Some(drgSet))
|
||||||
|
|
||||||
default = default.withPlayer(dnc).withPlayer(drg)
|
default = default.withPlayer(dnc).withPlayer(drg)
|
||||||
system.stop(ariyala)
|
system.stop(provider)
|
||||||
}
|
}
|
||||||
|
|
||||||
"loot selector" must {
|
"loot selector" must {
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
package me.arcanis.ffxivbis.service
|
package me.arcanis.ffxivbis.service.bis
|
||||||
|
|
||||||
import akka.actor.ActorSystem
|
import akka.actor.ActorSystem
|
||||||
import akka.testkit.{ImplicitSender, TestKit}
|
import akka.testkit.{ImplicitSender, TestKit}
|
||||||
@ -9,7 +9,7 @@ import org.scalatest.{BeforeAndAfterAll, Matchers, WordSpecLike}
|
|||||||
import scala.concurrent.duration._
|
import scala.concurrent.duration._
|
||||||
import scala.language.postfixOps
|
import scala.language.postfixOps
|
||||||
|
|
||||||
class AriyalaTest extends TestKit(ActorSystem("ariyala"))
|
class BisProviderTest extends TestKit(ActorSystem("bis-provider"))
|
||||||
with ImplicitSender with WordSpecLike with Matchers with BeforeAndAfterAll {
|
with ImplicitSender with WordSpecLike with Matchers with BeforeAndAfterAll {
|
||||||
|
|
||||||
private val timeout: FiniteDuration = 60 seconds
|
private val timeout: FiniteDuration = 60 seconds
|
||||||
@ -18,9 +18,15 @@ class AriyalaTest extends TestKit(ActorSystem("ariyala"))
|
|||||||
|
|
||||||
"ariyala actor" must {
|
"ariyala actor" must {
|
||||||
|
|
||||||
"get best in slot set" in {
|
"get best in slot set (ariyala)" in {
|
||||||
val ariyala = system.actorOf(Ariyala.props)
|
val provider = system.actorOf(BisProvider.props(false))
|
||||||
ariyala ! Ariyala.GetBiS(Fixtures.link, Job.DNC)
|
provider ! BisProvider.GetBiS(Fixtures.link, Job.DNC)
|
||||||
|
expectMsg(timeout, Fixtures.bis)
|
||||||
|
}
|
||||||
|
|
||||||
|
"get best in slot set (etro)" in {
|
||||||
|
val provider = system.actorOf(BisProvider.props(true))
|
||||||
|
provider ! BisProvider.GetBiS(Fixtures.link3, Job.DNC)
|
||||||
expectMsg(timeout, Fixtures.bis)
|
expectMsg(timeout, Fixtures.bis)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user