mirror of
https://github.com/arcan1s/ffxivbis.git
synced 2025-07-08 19:35:52 +00:00
Compare commits
7 Commits
Author | SHA1 | Date | |
---|---|---|---|
5eae1d46a2 | |||
eb24019965 | |||
173ea9079f | |||
12c99bd52c | |||
bdfb5aedeb | |||
666a1b8b7a | |||
65a4a25b3a |
@ -28,4 +28,4 @@ REST API documentation is available at `http://0.0.0.0:8000/swagger`. HTML repre
|
|||||||
|
|
||||||
## Public service
|
## Public service
|
||||||
|
|
||||||
There is also public service which is available at https://ffxivbis.arcanis.me.
|
There is also public service which is available at http://ffxivbis.arcanis.me.
|
||||||
|
@ -5,7 +5,7 @@ me.arcanis.ffxivbis {
|
|||||||
# xivapi base url, string, required
|
# xivapi base url, string, required
|
||||||
xivapi-url = "https://xivapi.com"
|
xivapi-url = "https://xivapi.com"
|
||||||
# xivapi developer key, string, optional
|
# xivapi developer key, string, optional
|
||||||
# xivapi-key = "abcdef"
|
#xivapi-key = "abcdef"
|
||||||
}
|
}
|
||||||
|
|
||||||
database {
|
database {
|
||||||
@ -54,6 +54,8 @@ me.arcanis.ffxivbis {
|
|||||||
host = "127.0.0.1"
|
host = "127.0.0.1"
|
||||||
# port to bind, int, required
|
# port to bind, int, required
|
||||||
port = 8000
|
port = 8000
|
||||||
|
# hostname to use in docs, if not set host:port will be used
|
||||||
|
#hostname = "127.0.0.1:8000"
|
||||||
|
|
||||||
# rate limits
|
# rate limits
|
||||||
limits {
|
limits {
|
||||||
|
@ -29,6 +29,7 @@ class RootEndpoint(system: ActorSystem, storage: ActorRef, ariyala: ActorRef)
|
|||||||
|
|
||||||
private val rootApiV1Endpoint: RootApiV1Endpoint = new RootApiV1Endpoint(storage, ariyala, config)
|
private val rootApiV1Endpoint: RootApiV1Endpoint = new RootApiV1Endpoint(storage, ariyala, config)
|
||||||
private val rootView: RootView = new RootView(storage, ariyala)
|
private val rootView: RootView = new RootView(storage, ariyala)
|
||||||
|
private val swagger: Swagger = new Swagger(config)
|
||||||
private val httpLogger = Logger("http")
|
private val httpLogger = Logger("http")
|
||||||
|
|
||||||
private val withHttpLog: Directive0 =
|
private val withHttpLog: Directive0 =
|
||||||
@ -43,7 +44,7 @@ class RootEndpoint(system: ActorSystem, storage: ActorRef, ariyala: ActorRef)
|
|||||||
|
|
||||||
def route: Route =
|
def route: Route =
|
||||||
withHttpLog {
|
withHttpLog {
|
||||||
apiRoute ~ htmlRoute ~ Swagger.routes ~ swaggerUIRoute
|
apiRoute ~ htmlRoute ~ swagger.routes ~ swaggerUIRoute
|
||||||
}
|
}
|
||||||
|
|
||||||
private def apiRoute: Route =
|
private def apiRoute: Route =
|
||||||
|
@ -10,11 +10,12 @@ package me.arcanis.ffxivbis.http
|
|||||||
|
|
||||||
import com.github.swagger.akka.SwaggerHttpService
|
import com.github.swagger.akka.SwaggerHttpService
|
||||||
import com.github.swagger.akka.model.{Info, License}
|
import com.github.swagger.akka.model.{Info, License}
|
||||||
|
import com.typesafe.config.Config
|
||||||
import io.swagger.v3.oas.models.security.SecurityScheme
|
import io.swagger.v3.oas.models.security.SecurityScheme
|
||||||
|
|
||||||
import scala.io.Source
|
import scala.io.Source
|
||||||
|
|
||||||
object Swagger extends SwaggerHttpService {
|
class Swagger(config: Config) extends SwaggerHttpService {
|
||||||
override val apiClasses: Set[Class[_]] = Set(
|
override val apiClasses: Set[Class[_]] = Set(
|
||||||
classOf[api.v1.BiSEndpoint], classOf[api.v1.LootEndpoint],
|
classOf[api.v1.BiSEndpoint], classOf[api.v1.LootEndpoint],
|
||||||
classOf[api.v1.PlayerEndpoint], classOf[api.v1.TypesEndpoint],
|
classOf[api.v1.PlayerEndpoint], classOf[api.v1.TypesEndpoint],
|
||||||
@ -28,12 +29,16 @@ object Swagger extends SwaggerHttpService {
|
|||||||
license = Some(License("BSD", "https://raw.githubusercontent.com/arcan1s/ffxivbis/master/LICENSE"))
|
license = Some(License("BSD", "https://raw.githubusercontent.com/arcan1s/ffxivbis/master/LICENSE"))
|
||||||
)
|
)
|
||||||
|
|
||||||
|
override val host: String =
|
||||||
|
if (config.hasPath("me.arcanis.ffxivbis.web.hostname")) config.getString("me.arcanis.ffxivbis.web.hostname")
|
||||||
|
else s"${config.getString("me.arcanis.ffxivbis.web.host")}:${config.getString("me.arcanis.ffxivbis.web.port")}"
|
||||||
|
|
||||||
private val basicAuth = new SecurityScheme()
|
private val basicAuth = new SecurityScheme()
|
||||||
.description("basic http auth")
|
.description("basic http auth")
|
||||||
.`type`(SecurityScheme.Type.HTTP)
|
.`type`(SecurityScheme.Type.HTTP)
|
||||||
.in(SecurityScheme.In.HEADER)
|
.in(SecurityScheme.In.HEADER)
|
||||||
.scheme("bearer")
|
.scheme("bearer")
|
||||||
override def securitySchemes: Map[String, SecurityScheme] = Map("basic auth" -> basicAuth)
|
override val securitySchemes: Map[String, SecurityScheme] = Map("basic auth" -> basicAuth)
|
||||||
|
|
||||||
override val unwantedDefinitions: Seq[String] =
|
override val unwantedDefinitions: Seq[String] =
|
||||||
Seq("Function1", "Function1RequestContextFutureRouteResult")
|
Seq("Function1", "Function1RequestContextFutureRouteResult")
|
||||||
|
@ -18,7 +18,7 @@ case class PlayerResponse(
|
|||||||
@Schema(description = "pieces in best in slot") bis: Option[Seq[PieceResponse]],
|
@Schema(description = "pieces in best in slot") bis: Option[Seq[PieceResponse]],
|
||||||
@Schema(description = "looted pieces") loot: Option[Seq[PieceResponse]],
|
@Schema(description = "looted pieces") loot: Option[Seq[PieceResponse]],
|
||||||
@Schema(description = "link to best in slot", example = "https://ffxiv.ariyala.com/19V5R") link: Option[String],
|
@Schema(description = "link to best in slot", example = "https://ffxiv.ariyala.com/19V5R") link: Option[String],
|
||||||
@Schema(description = "player loot priority") priority: Option[Int]) {
|
@Schema(description = "player loot priority", `type` = "number") priority: Option[Int]) {
|
||||||
def toPlayer: Player =
|
def toPlayer: Player =
|
||||||
Player(partyId, Job.withName(job), nick,
|
Player(partyId, Job.withName(job), nick,
|
||||||
BiS(bis.getOrElse(Seq.empty).map(_.toPiece)), loot.getOrElse(Seq.empty).map(_.toPiece),
|
BiS(bis.getOrElse(Seq.empty).map(_.toPiece)), loot.getOrElse(Seq.empty).map(_.toPiece),
|
||||||
|
@ -58,15 +58,27 @@ class Ariyala extends Actor with StrictLogging {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private def getIsTome(itemIds: Seq[Long]): Future[Map[Long, Boolean]] = {
|
private def getIsTome(itemIds: Seq[Long]): Future[Map[Long, Boolean]] = {
|
||||||
val uri = Uri(xivapiUrl)
|
val uriForItems = Uri(xivapiUrl)
|
||||||
.withPath(Uri.Path / "item")
|
.withPath(Uri.Path / "item")
|
||||||
.withQuery(Uri.Query(Map(
|
.withQuery(Uri.Query(Map(
|
||||||
"columns" -> Seq("ID", "IsEquippable").mkString(","),
|
"columns" -> Seq("ID", "GameContentLinks").mkString(","),
|
||||||
"ids" -> itemIds.mkString(","),
|
"ids" -> itemIds.mkString(","),
|
||||||
"private_key" -> xivapiKey.getOrElse("")
|
"private_key" -> xivapiKey.getOrElse("")
|
||||||
)))
|
)))
|
||||||
|
|
||||||
sendRequest(uri, Ariyala.parseXivapiJson)
|
sendRequest(uriForItems, Ariyala.parseXivapiJsonToShop).flatMap { shops =>
|
||||||
|
val shopIds = shops.values.map(_._2).toSet
|
||||||
|
val columns = shops.values.map(pair => s"ItemCost${pair._1}").toSet.toSeq
|
||||||
|
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.parseXivapiJsonToTome(shops))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private def sendRequest[T](uri: Uri, parser: JsObject => Future[T]): Future[T] =
|
private def sendRequest[T](uri: Uri, parser: JsObject => Future[T]): Future[T] =
|
||||||
@ -118,18 +130,56 @@ object Ariyala {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private def parseXivapiJson(js: JsObject)
|
private def parseXivapiJsonToShop(js: JsObject)
|
||||||
(implicit executionContext: ExecutionContext): Future[Map[Long, Boolean]] =
|
(implicit executionContext: ExecutionContext): Future[Map[Long, (String, Long)]] = {
|
||||||
|
def extractTraderId(js: JsObject) =
|
||||||
|
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 {
|
Future {
|
||||||
js.fields("Results") match {
|
js.fields("Results") match {
|
||||||
case array: JsArray =>
|
case array: JsArray =>
|
||||||
array.elements.map(_.asJsObject.getFields("ID", "IsEquippable") match {
|
array.elements.map(_.asJsObject.getFields("ID", "GameContentLinks") match {
|
||||||
case Seq(JsNumber(id), JsNumber(isTome)) => id.toLong -> (isTome == 0)
|
case Seq(JsNumber(id), shop) => id.toLong -> extractTraderId(shop.asJsObject)
|
||||||
case other => throw deserializationError(s"Could not parse $other")
|
case other => throw deserializationError(s"Could not parse $other")
|
||||||
}).toMap
|
}).toMap
|
||||||
case other => throw deserializationError(s"Could not parse $other")
|
case other => throw deserializationError(s"Could not parse $other")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private def parseXivapiJsonToTome(shops: Map[Long, (String, Long)])(js: JsObject)
|
||||||
|
(implicit executionContext: ExecutionContext): Future[Map[Long, Boolean]] =
|
||||||
|
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 isTome = 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)) =>
|
||||||
|
// either upgraded gear or tomes found
|
||||||
|
isUnique == 1 || stackSize.toLong == 2000
|
||||||
|
case other => throw deserializationError(s"Could not parse $other")
|
||||||
|
}
|
||||||
|
itemId -> isTome
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private def remapKey(key: String): Option[String] = key match {
|
private def remapKey(key: String): Option[String] = key match {
|
||||||
case "mainhand" => Some("weapon")
|
case "mainhand" => Some("weapon")
|
||||||
|
@ -1 +1 @@
|
|||||||
version := "0.9.5"
|
version := "0.9.8"
|
||||||
|
Reference in New Issue
Block a user