20 Commits

Author SHA1 Message Date
8ddb999988 Release 0.15.7 2026-01-08 14:26:16 +02:00
cd3379561a fix: restore button visibility 2026-01-08 14:26:01 +02:00
727e6c134a chore: license update 2026-01-08 14:24:22 +02:00
f7f112330b feat: multi loot selector preview 2026-01-08 14:24:09 +02:00
8e52be728e test: add test for 7.4 gear 2026-01-08 13:54:40 +02:00
5025e760c1 Release 0.15.6 2026-01-08 13:46:20 +02:00
35c784bf89 feat: try to migrate to xivapi v2 2026-01-08 13:45:59 +02:00
757c4cc6df feat: allow suggest from interface for ro users 2026-01-08 12:49:34 +02:00
ca24ee298e build: install sbt explicitly 2026-01-08 12:44:51 +02:00
af7a92af35 Release 0.15.5 2026-01-07 14:56:37 +02:00
4c05ceefcd fix: add | to allowed chars 2026-01-07 14:56:01 +02:00
fa43517b16 Release 0.15.4 2024-07-31 15:47:09 +03:00
77e99439e7 chore: fix file formatting 2024-07-31 15:46:56 +03:00
c9eb311cfe bug: remove unreachable code 2024-07-31 15:46:56 +03:00
d662e303c8 feat: xivgear demo support 2024-07-31 15:41:10 +03:00
1c8aaea712 Release 0.15.1 2024-07-22 14:00:59 +03:00
3c8e5f8da8 fix: read itemcost correctly 2024-07-22 13:58:26 +03:00
0bcda3233e feat: dependencies bump- 2024-07-03 13:36:36 +03:00
c4be6f12f1 feat: VPR and PCT support 2024-07-03 13:33:44 +03:00
f3535f6e16 bump libraries 2023-10-20 13:32:55 +03:00
94 changed files with 495 additions and 210 deletions

View File

@ -12,6 +12,7 @@ jobs:
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: extract version - name: extract version
id: version id: version
run: echo ::set-output name=VERSION::${GITHUB_REF#refs/tags/} run: echo ::set-output name=VERSION::${GITHUB_REF#refs/tags/}
@ -21,13 +22,24 @@ jobs:
with: with:
token: ${{ secrets.GITHUB_TOKEN }} token: ${{ secrets.GITHUB_TOKEN }}
filter: 'Release \d+\.\d+\.\d+' filter: 'Release \d+\.\d+\.\d+'
- name: setup JDK - name: setup JDK
uses: actions/setup-java@v2 uses: actions/setup-java@v2
with: with:
distribution: temurin distribution: temurin
java-version: 18 java-version: 18
- name: install sbt
uses: eclipse-score/apt-install@main
with:
packages: wget
- run: |
wget https://scala.jfrog.io/artifactory/debian/sbt-1.11.7.deb
sudo dpkg -i sbt-1.11.7.deb
- name: create dist - name: create dist
run: make dist run: make dist
- name: release - name: release
uses: softprops/action-gh-release@v1 uses: softprops/action-gh-release@v1
with: with:

View File

@ -13,10 +13,20 @@ jobs:
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: setup JDK - name: setup JDK
uses: actions/setup-java@v2 uses: actions/setup-java@v2
with: with:
distribution: temurin distribution: temurin
java-version: 18 java-version: 18
- name: install sbt
uses: eclipse-score/apt-install@main
with:
packages: wget
- run: |
wget https://scala.jfrog.io/artifactory/debian/sbt-1.11.7.deb
sudo dpkg -i sbt-1.11.7.deb
- name: run tests - name: run tests
run: make tests run: make tests

View File

@ -2,7 +2,7 @@ organization := "me.arcanis"
name := "ffxivbis" name := "ffxivbis"
scalaVersion := "2.13.8" scalaVersion := "2.13.12"
scalacOptions ++= Seq("-deprecation", "-feature") scalacOptions ++= Seq("-deprecation", "-feature")

View File

@ -1,26 +1,25 @@
val AkkaVersion = "2.6.20" val AkkaVersion = "2.8.6"
val AkkaHttpVersion = "10.2.10" val AkkaHttpVersion = "10.5.3"
val ScalaTestVersion = "3.2.12" val ScalaTestVersion = "3.2.19"
val SlickVersion = "3.3.3"
libraryDependencies += "ch.qos.logback" % "logback-classic" % "1.2.11" libraryDependencies += "ch.qos.logback" % "logback-classic" % "1.5.6"
libraryDependencies += "com.typesafe.scala-logging" %% "scala-logging" % "3.9.5" libraryDependencies += "com.typesafe.scala-logging" %% "scala-logging" % "3.9.5"
libraryDependencies += "com.typesafe.akka" %% "akka-http" % AkkaHttpVersion libraryDependencies += "com.typesafe.akka" %% "akka-http" % AkkaHttpVersion
libraryDependencies += "com.typesafe.akka" %% "akka-http-spray-json" % AkkaHttpVersion libraryDependencies += "com.typesafe.akka" %% "akka-http-spray-json" % AkkaHttpVersion
libraryDependencies += "com.typesafe.akka" %% "akka-actor-typed" % AkkaVersion libraryDependencies += "com.typesafe.akka" %% "akka-actor-typed" % AkkaVersion
libraryDependencies += "com.typesafe.akka" %% "akka-stream" % AkkaVersion libraryDependencies += "com.typesafe.akka" %% "akka-stream" % AkkaVersion
libraryDependencies += "com.github.swagger-akka-http" %% "swagger-akka-http" % "2.8.0" libraryDependencies += "com.github.swagger-akka-http" %% "swagger-akka-http" % "2.11.0"
libraryDependencies += "jakarta.platform" % "jakarta.jakartaee-web-api" % "9.1.0" libraryDependencies += "jakarta.platform" % "jakarta.jakartaee-web-api" % "10.0.0"
libraryDependencies += "ch.megard" %% "akka-http-cors" % "1.1.3" libraryDependencies += "ch.megard" %% "akka-http-cors" % "1.2.0"
libraryDependencies += "io.spray" %% "spray-json" % "1.3.6" libraryDependencies += "io.spray" %% "spray-json" % "1.3.6"
libraryDependencies += "org.playframework.anorm" %% "anorm" % "2.6.10" libraryDependencies += "org.playframework.anorm" %% "anorm" % "2.7.0"
libraryDependencies += "com.zaxxer" % "HikariCP" % "5.0.1" exclude("org.slf4j", "slf4j-api") libraryDependencies += "com.zaxxer" % "HikariCP" % "5.1.0" exclude("org.slf4j", "slf4j-api")
libraryDependencies += "org.flywaydb" % "flyway-core" % "9.2.2" libraryDependencies += "org.flywaydb" % "flyway-core" % "9.16.0"
libraryDependencies += "org.xerial" % "sqlite-jdbc" % "3.39.2.1" libraryDependencies += "org.xerial" % "sqlite-jdbc" % "3.46.0.0"
libraryDependencies += "org.postgresql" % "postgresql" % "42.5.0" libraryDependencies += "org.postgresql" % "postgresql" % "42.7.3"
libraryDependencies += "org.mindrot" % "jbcrypt" % "0.4" libraryDependencies += "org.mindrot" % "jbcrypt" % "0.4"
libraryDependencies += "com.google.guava" % "guava" % "31.0.1-jre" libraryDependencies += "com.google.guava" % "guava" % "31.0.1-jre"

View File

@ -41,7 +41,7 @@
<div class="container"> <div class="container">
<div id="toolbar"> <div id="toolbar">
<button id="add-btn" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#add-loot-dialog" hidden> <button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#add-loot-dialog">
<i class="bi bi-plus"></i> add <i class="bi bi-plus"></i> add
</button> </button>
<button class="btn btn-secondary" onclick="reload()"> <button class="btn btn-secondary" onclick="reload()">
@ -147,7 +147,7 @@
<div class="modal-footer"> <div class="modal-footer">
<button type="button" class="btn btn-danger" data-bs-dismiss="modal">close</button> <button type="button" class="btn btn-danger" data-bs-dismiss="modal">close</button>
<button type="button" class="btn btn-secondary" onclick="suggestLoot()">suggest</button> <button type="button" class="btn btn-secondary" onclick="suggestLoot()">suggest</button>
<button type="submit" class="btn btn-primary">add</button> <button id="add-btn" type="submit" class="btn btn-primary">add</button>
</div> </div>
</form> </form>
</div> </div>

View File

@ -4,7 +4,7 @@ me.arcanis.ffxivbis {
include "item_data.json" include "item_data.json"
# xivapi base url, string, required # xivapi base url, string, required
xivapi-url = "https://xivapi.com" xivapi-url = "https://v2.xivapi.com"
# xivapi developer key, string, optional # xivapi developer key, string, optional
#xivapi-key = "abcdef" #xivapi-key = "abcdef"
} }

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2019-2022 Evgeniy Alekseev. * Copyright (c) 2021-2026 Evgeniy Alekseev.
* *
* This file is part of ffxivbis * This file is part of ffxivbis
* (see https://github.com/arcan1s/ffxivbis). * (see https://github.com/arcan1s/ffxivbis).

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2019-2022 Evgeniy Alekseev. * Copyright (c) 2021-2026 Evgeniy Alekseev.
* *
* This file is part of ffxivbis * This file is part of ffxivbis
* (see https://github.com/arcan1s/ffxivbis). * (see https://github.com/arcan1s/ffxivbis).

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2019-2022 Evgeniy Alekseev. * Copyright (c) 2021-2026 Evgeniy Alekseev.
* *
* This file is part of ffxivbis * This file is part of ffxivbis
* (see https://github.com/arcan1s/ffxivbis). * (see https://github.com/arcan1s/ffxivbis).

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2019-2022 Evgeniy Alekseev. * Copyright (c) 2021-2026 Evgeniy Alekseev.
* *
* This file is part of ffxivbis * This file is part of ffxivbis
* (see https://github.com/arcan1s/ffxivbis). * (see https://github.com/arcan1s/ffxivbis).

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2019-2022 Evgeniy Alekseev. * Copyright (c) 2021-2026 Evgeniy Alekseev.
* *
* This file is part of ffxivbis * This file is part of ffxivbis
* (see https://github.com/arcan1s/ffxivbis). * (see https://github.com/arcan1s/ffxivbis).

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2019-2022 Evgeniy Alekseev. * Copyright (c) 2021-2026 Evgeniy Alekseev.
* *
* This file is part of ffxivbis * This file is part of ffxivbis
* (see https://github.com/arcan1s/ffxivbis). * (see https://github.com/arcan1s/ffxivbis).

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2019-2022 Evgeniy Alekseev. * Copyright (c) 2021-2026 Evgeniy Alekseev.
* *
* This file is part of ffxivbis * This file is part of ffxivbis
* (see https://github.com/arcan1s/ffxivbis). * (see https://github.com/arcan1s/ffxivbis).
@ -15,6 +15,7 @@ import akka.util.Timeout
import ch.megard.akka.http.cors.scaladsl.CorsDirectives.cors import ch.megard.akka.http.cors.scaladsl.CorsDirectives.cors
import com.typesafe.scalalogging.StrictLogging import com.typesafe.scalalogging.StrictLogging
import me.arcanis.ffxivbis.http.api.v1.RootApiV1Endpoint import me.arcanis.ffxivbis.http.api.v1.RootApiV1Endpoint
import me.arcanis.ffxivbis.http.api.v2.RootApiV2Endpoint
import me.arcanis.ffxivbis.http.view.RootView import me.arcanis.ffxivbis.http.view.RootView
import me.arcanis.ffxivbis.messages.{BiSProviderMessage, Message} import me.arcanis.ffxivbis.messages.{BiSProviderMessage, Message}
@ -31,6 +32,7 @@ class RootEndpoint(system: ActorSystem[Nothing], storage: ActorRef[Message], pro
private val auth = AuthorizationProvider(config, storage) private val auth = AuthorizationProvider(config, storage)
private val rootApiV1Endpoint = new RootApiV1Endpoint(storage, auth, provider, config) private val rootApiV1Endpoint = new RootApiV1Endpoint(storage, auth, provider, config)
private val rootApiV2Endpoint = new RootApiV2Endpoint(storage, auth)
private val rootView = new RootView(auth) private val rootView = new RootView(auth)
private val swagger = new Swagger(config) private val swagger = new Swagger(config)
@ -47,6 +49,7 @@ class RootEndpoint(system: ActorSystem[Nothing], storage: ActorRef[Message], pro
pathPrefix("api") { pathPrefix("api") {
pathPrefix(Segment) { pathPrefix(Segment) {
case "v1" => rootApiV1Endpoint.routes case "v1" => rootApiV1Endpoint.routes
case "v2" => rootApiV2Endpoint.routes
case _ => reject case _ => reject
} }
} }

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2019-2022 Evgeniy Alekseev. * Copyright (c) 2021-2026 Evgeniy Alekseev.
* *
* This file is part of ffxivbis * This file is part of ffxivbis
* (see https://github.com/arcan1s/ffxivbis). * (see https://github.com/arcan1s/ffxivbis).

View File

@ -1,3 +1,11 @@
/*
* Copyright (c) 2021-2026 Evgeniy Alekseev.
*
* This file is part of ffxivbis
* (see https://github.com/arcan1s/ffxivbis).
*
* License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause
*/
package me.arcanis.ffxivbis.http package me.arcanis.ffxivbis.http
import scala.collection.immutable.HashSet import scala.collection.immutable.HashSet
@ -12,5 +20,5 @@ trait ValidatorHelper {
object ValidatorHelper { object ValidatorHelper {
final val VALID_CHARACTERS = HashSet.from("!@#$%^&*()-_=+;:',./? ") final val VALID_CHARACTERS = HashSet.from("!@#$%^&*()-_=+;:',./?| ")
} }

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2019-2022 Evgeniy Alekseev. * Copyright (c) 2021-2026 Evgeniy Alekseev.
* *
* This file is part of ffxivbis * This file is part of ffxivbis
* (see https://github.com/arcan1s/ffxivbis). * (see https://github.com/arcan1s/ffxivbis).

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2019-2022 Evgeniy Alekseev. * Copyright (c) 2021-2026 Evgeniy Alekseev.
* *
* This file is part of ffxivbis * This file is part of ffxivbis
* (see https://github.com/arcan1s/ffxivbis). * (see https://github.com/arcan1s/ffxivbis).

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2019-2022 Evgeniy Alekseev. * Copyright (c) 2021-2026 Evgeniy Alekseev.
* *
* This file is part of ffxivbis * This file is part of ffxivbis
* (see https://github.com/arcan1s/ffxivbis). * (see https://github.com/arcan1s/ffxivbis).

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2019-2022 Evgeniy Alekseev. * Copyright (c) 2021-2026 Evgeniy Alekseev.
* *
* This file is part of ffxivbis * This file is part of ffxivbis
* (see https://github.com/arcan1s/ffxivbis). * (see https://github.com/arcan1s/ffxivbis).

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2019-2022 Evgeniy Alekseev. * Copyright (c) 2021-2026 Evgeniy Alekseev.
* *
* This file is part of ffxivbis * This file is part of ffxivbis
* (see https://github.com/arcan1s/ffxivbis). * (see https://github.com/arcan1s/ffxivbis).

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2019-2022 Evgeniy Alekseev. * Copyright (c) 2021-2026 Evgeniy Alekseev.
* *
* This file is part of ffxivbis * This file is part of ffxivbis
* (see https://github.com/arcan1s/ffxivbis). * (see https://github.com/arcan1s/ffxivbis).

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2019-2022 Evgeniy Alekseev. * Copyright (c) 2021-2026 Evgeniy Alekseev.
* *
* This file is part of ffxivbis * This file is part of ffxivbis
* (see https://github.com/arcan1s/ffxivbis). * (see https://github.com/arcan1s/ffxivbis).

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2019-2022 Evgeniy Alekseev. * Copyright (c) 2021-2026 Evgeniy Alekseev.
* *
* This file is part of ffxivbis * This file is part of ffxivbis
* (see https://github.com/arcan1s/ffxivbis). * (see https://github.com/arcan1s/ffxivbis).

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2019-2022 Evgeniy Alekseev. * Copyright (c) 2021-2026 Evgeniy Alekseev.
* *
* This file is part of ffxivbis * This file is part of ffxivbis
* (see https://github.com/arcan1s/ffxivbis). * (see https://github.com/arcan1s/ffxivbis).

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2019-2022 Evgeniy Alekseev. * Copyright (c) 2021-2026 Evgeniy Alekseev.
* *
* This file is part of ffxivbis * This file is part of ffxivbis
* (see https://github.com/arcan1s/ffxivbis). * (see https://github.com/arcan1s/ffxivbis).

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2019-2022 Evgeniy Alekseev. * Copyright (c) 2021-2026 Evgeniy Alekseev.
* *
* This file is part of ffxivbis * This file is part of ffxivbis
* (see https://github.com/arcan1s/ffxivbis). * (see https://github.com/arcan1s/ffxivbis).

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2019-2022 Evgeniy Alekseev. * Copyright (c) 2021-2026 Evgeniy Alekseev.
* *
* This file is part of ffxivbis * This file is part of ffxivbis
* (see https://github.com/arcan1s/ffxivbis). * (see https://github.com/arcan1s/ffxivbis).

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2019-2022 Evgeniy Alekseev. * Copyright (c) 2021-2026 Evgeniy Alekseev.
* *
* This file is part of ffxivbis * This file is part of ffxivbis
* (see https://github.com/arcan1s/ffxivbis). * (see https://github.com/arcan1s/ffxivbis).

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2019-2022 Evgeniy Alekseev. * Copyright (c) 2021-2026 Evgeniy Alekseev.
* *
* This file is part of ffxivbis * This file is part of ffxivbis
* (see https://github.com/arcan1s/ffxivbis). * (see https://github.com/arcan1s/ffxivbis).

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2019-2022 Evgeniy Alekseev. * Copyright (c) 2021-2026 Evgeniy Alekseev.
* *
* This file is part of ffxivbis * This file is part of ffxivbis
* (see https://github.com/arcan1s/ffxivbis). * (see https://github.com/arcan1s/ffxivbis).

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2019-2022 Evgeniy Alekseev. * Copyright (c) 2021-2026 Evgeniy Alekseev.
* *
* This file is part of ffxivbis * This file is part of ffxivbis
* (see https://github.com/arcan1s/ffxivbis). * (see https://github.com/arcan1s/ffxivbis).

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2019-2022 Evgeniy Alekseev. * Copyright (c) 2021-2026 Evgeniy Alekseev.
* *
* This file is part of ffxivbis * This file is part of ffxivbis
* (see https://github.com/arcan1s/ffxivbis). * (see https://github.com/arcan1s/ffxivbis).

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2019-2022 Evgeniy Alekseev. * Copyright (c) 2021-2026 Evgeniy Alekseev.
* *
* This file is part of ffxivbis * This file is part of ffxivbis
* (see https://github.com/arcan1s/ffxivbis). * (see https://github.com/arcan1s/ffxivbis).

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2019-2022 Evgeniy Alekseev. * Copyright (c) 2021-2026 Evgeniy Alekseev.
* *
* This file is part of ffxivbis * This file is part of ffxivbis
* (see https://github.com/arcan1s/ffxivbis). * (see https://github.com/arcan1s/ffxivbis).

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2019-2022 Evgeniy Alekseev. * Copyright (c) 2021-2026 Evgeniy Alekseev.
* *
* This file is part of ffxivbis * This file is part of ffxivbis
* (see https://github.com/arcan1s/ffxivbis). * (see https://github.com/arcan1s/ffxivbis).

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2019-2022 Evgeniy Alekseev. * Copyright (c) 2021-2026 Evgeniy Alekseev.
* *
* This file is part of ffxivbis * This file is part of ffxivbis
* (see https://github.com/arcan1s/ffxivbis). * (see https://github.com/arcan1s/ffxivbis).

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2019-2022 Evgeniy Alekseev. * Copyright (c) 2021-2026 Evgeniy Alekseev.
* *
* This file is part of ffxivbis * This file is part of ffxivbis
* (see https://github.com/arcan1s/ffxivbis). * (see https://github.com/arcan1s/ffxivbis).

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2019-2022 Evgeniy Alekseev. * Copyright (c) 2021-2026 Evgeniy Alekseev.
* *
* This file is part of ffxivbis * This file is part of ffxivbis
* (see https://github.com/arcan1s/ffxivbis). * (see https://github.com/arcan1s/ffxivbis).

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2019-2022 Evgeniy Alekseev. * Copyright (c) 2021-2026 Evgeniy Alekseev.
* *
* This file is part of ffxivbis * This file is part of ffxivbis
* (see https://github.com/arcan1s/ffxivbis). * (see https://github.com/arcan1s/ffxivbis).

View File

@ -1,3 +1,11 @@
/*
* Copyright (c) 2021-2026 Evgeniy Alekseev.
*
* This file is part of ffxivbis
* (see https://github.com/arcan1s/ffxivbis).
*
* License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause
*/
package me.arcanis.ffxivbis.http.api.v1.json package me.arcanis.ffxivbis.http.api.v1.json
import me.arcanis.ffxivbis.http.ValidatorHelper import me.arcanis.ffxivbis.http.ValidatorHelper

View File

@ -0,0 +1,14 @@
/*
* Copyright (c) 2021-2026 Evgeniy Alekseev.
*
* This file is part of ffxivbis
* (see https://github.com/arcan1s/ffxivbis).
*
* License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause
*/
package me.arcanis.ffxivbis.http.api.v2
import me.arcanis.ffxivbis.http.api.v1.{HttpHandler => HttpHandlerV1}
import me.arcanis.ffxivbis.http.api.v2.json.JsonSupport
trait HttpHandler extends HttpHandlerV1 { this: JsonSupport => }

View File

@ -0,0 +1,107 @@
/*
* Copyright (c) 2021-2026 Evgeniy Alekseev.
*
* This file is part of ffxivbis
* (see https://github.com/arcan1s/ffxivbis).
*
* License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause
*/
package me.arcanis.ffxivbis.http.api.v2
import akka.actor.typed.{ActorRef, Scheduler}
import akka.http.scaladsl.server.Directives._
import akka.http.scaladsl.server._
import akka.util.Timeout
import io.swagger.v3.oas.annotations.enums.ParameterIn
import io.swagger.v3.oas.annotations.media.{ArraySchema, Content, Schema}
import io.swagger.v3.oas.annotations.parameters.RequestBody
import io.swagger.v3.oas.annotations.responses.ApiResponse
import io.swagger.v3.oas.annotations.security.SecurityRequirement
import io.swagger.v3.oas.annotations.{Operation, Parameter}
import jakarta.ws.rs._
import me.arcanis.ffxivbis.http.api.v1.json.{ErrorModel, PieceModel, PlayerIdWithCountersModel}
import me.arcanis.ffxivbis.http.api.v2.json._
import me.arcanis.ffxivbis.http.helpers.LootHelper
import me.arcanis.ffxivbis.http.{Authorization, AuthorizationProvider}
import me.arcanis.ffxivbis.messages.Message
@Path("/api/v2")
class LootEndpoint(override val storage: ActorRef[Message], override val auth: AuthorizationProvider)(implicit
timeout: Timeout,
scheduler: Scheduler
) extends LootHelper
with Authorization
with JsonSupport
with HttpHandler {
def routes: Route = suggestLoot
@PUT
@Path("party/{partyId}/loot")
@Consumes(value = Array("application/json"))
@Produces(value = Array("application/json"))
@Operation(
summary = "suggest loot",
description = "Suggest loot pieces to party",
parameters = Array(
new Parameter(
name = "partyId",
in = ParameterIn.PATH,
description = "unique party ID",
example = "o3KicHQPW5b0JcOm5yI3"
),
),
requestBody = new RequestBody(
description = "piece description",
required = true,
content = Array(new Content(schema = new Schema(implementation = classOf[PieceModel])))
),
responses = Array(
new ApiResponse(
responseCode = "200",
description = "Players with counters ordered by priority to get this item",
content = Array(
new Content(
array = new ArraySchema(schema = new Schema(implementation = classOf[PlayerIdWithCountersModel])),
)
)
),
new ApiResponse(
responseCode = "400",
description = "Invalid parameters were supplied",
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel])))
),
new ApiResponse(
responseCode = "401",
description = "Supplied authorization is invalid",
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel])))
),
new ApiResponse(
responseCode = "403",
description = "Access is forbidden",
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel])))
),
new ApiResponse(
responseCode = "500",
description = "Internal server error",
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel])))
),
),
security = Array(new SecurityRequirement(name = "basic", scopes = Array("get"))),
tags = Array("loot"),
)
def suggestLoot: Route =
path("party" / Segment / "loot") { partyId =>
extractExecutionContext { implicit executionContext =>
authenticateBasicBCrypt(s"party $partyId", authGet(partyId)) { _ =>
put {
entity(as[PiecesModel]) { piece =>
onSuccess(suggestPiece(partyId, piece.toPiece)) { response =>
complete(response.map(PlayerIdWithCountersModel.fromPlayerId))
}
}
}
}
}
}
}

View File

@ -0,0 +1,36 @@
/*
* Copyright (c) 2021-2026 Evgeniy Alekseev.
*
* This file is part of ffxivbis
* (see https://github.com/arcan1s/ffxivbis).
*
* License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause
*/
package me.arcanis.ffxivbis.http.api.v2
import akka.actor.typed.{ActorRef, Scheduler}
import akka.http.scaladsl.server.Directives._
import akka.http.scaladsl.server.Route
import akka.util.Timeout
import me.arcanis.ffxivbis.http.AuthorizationProvider
import me.arcanis.ffxivbis.http.api.v2.json.JsonSupport
import me.arcanis.ffxivbis.messages.Message
class RootApiV2Endpoint(
storage: ActorRef[Message],
auth: AuthorizationProvider,
)(implicit
timeout: Timeout,
scheduler: Scheduler
) extends JsonSupport
with HttpHandler {
private val lootEndpoint = new LootEndpoint(storage, auth)
def routes: Route =
handleExceptions(exceptionHandler) {
handleRejections(rejectionHandler) {
lootEndpoint.routes
}
}
}

View File

@ -0,0 +1,17 @@
/*
* Copyright (c) 2021-2026 Evgeniy Alekseev.
*
* This file is part of ffxivbis
* (see https://github.com/arcan1s/ffxivbis).
*
* License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause
*/
package me.arcanis.ffxivbis.http.api.v2.json
import me.arcanis.ffxivbis.http.api.v1.json.{JsonSupport => JsonSupportV1}
import spray.json._
trait JsonSupport extends JsonSupportV1 {
implicit val piecesFormat: RootJsonFormat[PiecesModel] = jsonFormat1(PiecesModel.apply)
}

View File

@ -0,0 +1,25 @@
/*
* Copyright (c) 2021-2026 Evgeniy Alekseev.
*
* This file is part of ffxivbis
* (see https://github.com/arcan1s/ffxivbis).
*
* License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause
*/
package me.arcanis.ffxivbis.http.api.v2.json
import io.swagger.v3.oas.annotations.media.Schema
import me.arcanis.ffxivbis.http.api.v1.json.PieceModel
import me.arcanis.ffxivbis.models.Piece
case class PiecesModel(
@Schema(description = "pieces list", required = true) pieces: Seq[PieceModel],
) {
def toPiece: Seq[Piece] = pieces.map(_.toPiece)
}
object PiecesModel {
def fromPiece(pieces: Seq[Piece]): PiecesModel = PiecesModel(pieces.map(PieceModel.fromPiece))
}

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2019-2022 Evgeniy Alekseev. * Copyright (c) 2021-2026 Evgeniy Alekseev.
* *
* This file is part of ffxivbis * This file is part of ffxivbis
* (see https://github.com/arcan1s/ffxivbis). * (see https://github.com/arcan1s/ffxivbis).

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2019-2022 Evgeniy Alekseev. * Copyright (c) 2021-2026 Evgeniy Alekseev.
* *
* This file is part of ffxivbis * This file is part of ffxivbis
* (see https://github.com/arcan1s/ffxivbis). * (see https://github.com/arcan1s/ffxivbis).

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2019-2022 Evgeniy Alekseev. * Copyright (c) 2021-2026 Evgeniy Alekseev.
* *
* This file is part of ffxivbis * This file is part of ffxivbis
* (see https://github.com/arcan1s/ffxivbis). * (see https://github.com/arcan1s/ffxivbis).
@ -56,4 +56,11 @@ trait LootHelper {
scheduler: Scheduler scheduler: Scheduler
): Future[Seq[PlayerIdWithCounters]] = ): Future[Seq[PlayerIdWithCounters]] =
storage.ask(SuggestLoot(partyId, piece, _)).map(_.result) storage.ask(SuggestLoot(partyId, piece, _)).map(_.result)
def suggestPiece(partyId: String, pieces: Seq[Piece])(implicit
executionContext: ExecutionContext,
timeout: Timeout,
scheduler: Scheduler
): Future[Seq[PlayerIdWithCounters]] =
storage.ask(SuggestMultiLoot(partyId, pieces, _)).map(_.result)
} }

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2019-2022 Evgeniy Alekseev. * Copyright (c) 2021-2026 Evgeniy Alekseev.
* *
* This file is part of ffxivbis * This file is part of ffxivbis
* (see https://github.com/arcan1s/ffxivbis). * (see https://github.com/arcan1s/ffxivbis).

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2019-2022 Evgeniy Alekseev. * Copyright (c) 2021-2026 Evgeniy Alekseev.
* *
* This file is part of ffxivbis * This file is part of ffxivbis
* (see https://github.com/arcan1s/ffxivbis). * (see https://github.com/arcan1s/ffxivbis).

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2019-2022 Evgeniy Alekseev. * Copyright (c) 2021-2026 Evgeniy Alekseev.
* *
* This file is part of ffxivbis * This file is part of ffxivbis
* (see https://github.com/arcan1s/ffxivbis). * (see https://github.com/arcan1s/ffxivbis).

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2019-2022 Evgeniy Alekseev. * Copyright (c) 2021-2026 Evgeniy Alekseev.
* *
* This file is part of ffxivbis * This file is part of ffxivbis
* (see https://github.com/arcan1s/ffxivbis). * (see https://github.com/arcan1s/ffxivbis).

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2019-2022 Evgeniy Alekseev. * Copyright (c) 2021-2026 Evgeniy Alekseev.
* *
* This file is part of ffxivbis * This file is part of ffxivbis
* (see https://github.com/arcan1s/ffxivbis). * (see https://github.com/arcan1s/ffxivbis).

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2019-2022 Evgeniy Alekseev. * Copyright (c) 2021-2026 Evgeniy Alekseev.
* *
* This file is part of ffxivbis * This file is part of ffxivbis
* (see https://github.com/arcan1s/ffxivbis). * (see https://github.com/arcan1s/ffxivbis).
@ -69,6 +69,11 @@ object DatabaseMessage {
override val isReadOnly: Boolean = true override val isReadOnly: Boolean = true
} }
case class SuggestMultiLoot(partyId: String, pieces: Seq[Piece], replyTo: ActorRef[LootSelector.LootSelectorResult])
extends LootDatabaseMessage {
override val isReadOnly: Boolean = true
}
// party handler // party handler
trait PartyDatabaseMessage extends DatabaseMessage trait PartyDatabaseMessage extends DatabaseMessage

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2019-2022 Evgeniy Alekseev. * Copyright (c) 2021-2026 Evgeniy Alekseev.
* *
* This file is part of ffxivbis * This file is part of ffxivbis
* (see https://github.com/arcan1s/ffxivbis). * (see https://github.com/arcan1s/ffxivbis).

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2019-2022 Evgeniy Alekseev. * Copyright (c) 2021-2026 Evgeniy Alekseev.
* *
* This file is part of ffxivbis * This file is part of ffxivbis
* (see https://github.com/arcan1s/ffxivbis). * (see https://github.com/arcan1s/ffxivbis).

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2019-2022 Evgeniy Alekseev. * Copyright (c) 2021-2026 Evgeniy Alekseev.
* *
* This file is part of ffxivbis * This file is part of ffxivbis
* (see https://github.com/arcan1s/ffxivbis). * (see https://github.com/arcan1s/ffxivbis).
@ -95,6 +95,7 @@ object Job {
case object RPR extends Drgs case object RPR extends Drgs
case object NIN extends Nins case object NIN extends Nins
case object SAM extends Mnks case object SAM extends Mnks
case object VPR extends Mnks
case object BRD extends Ranges case object BRD extends Ranges
case object MCH extends Ranges case object MCH extends Ranges
@ -103,9 +104,10 @@ object Job {
case object BLM extends Casters case object BLM extends Casters
case object SMN extends Casters case object SMN extends Casters
case object RDM extends Casters case object RDM extends Casters
case object PCT extends Casters
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, VPR, BRD, MCH, DNC, BLM, SMN, RDM, PCT)
val availableWithAnyJob: Seq[Job] = available.prepended(AnyJob) val availableWithAnyJob: Seq[Job] = available.prepended(AnyJob)
def withName(job: String): Job = def withName(job: String): Job =

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2019-2022 Evgeniy Alekseev. * Copyright (c) 2021-2026 Evgeniy Alekseev.
* *
* This file is part of ffxivbis * This file is part of ffxivbis
* (see https://github.com/arcan1s/ffxivbis). * (see https://github.com/arcan1s/ffxivbis).

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2019-2022 Evgeniy Alekseev. * Copyright (c) 2021-2026 Evgeniy Alekseev.
* *
* This file is part of ffxivbis * This file is part of ffxivbis
* (see https://github.com/arcan1s/ffxivbis). * (see https://github.com/arcan1s/ffxivbis).
@ -35,8 +35,8 @@ case class Party(partyDescription: PartyDescription, rules: Seq[String], players
this this
} }
def suggestLoot(piece: Piece): LootSelector.LootSelectorResult = def suggestLoot(pieces: Seq[Piece]): LootSelector.LootSelectorResult =
LootSelector(getPlayers, piece, rules) LootSelector(getPlayers, pieces, rules)
} }
object Party { object Party {

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2019-2022 Evgeniy Alekseev. * Copyright (c) 2021-2026 Evgeniy Alekseev.
* *
* This file is part of ffxivbis * This file is part of ffxivbis
* (see https://github.com/arcan1s/ffxivbis). * (see https://github.com/arcan1s/ffxivbis).

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2019-2022 Evgeniy Alekseev. * Copyright (c) 2021-2026 Evgeniy Alekseev.
* *
* This file is part of ffxivbis * This file is part of ffxivbis
* (see https://github.com/arcan1s/ffxivbis). * (see https://github.com/arcan1s/ffxivbis).
@ -113,7 +113,7 @@ object Piece {
case "weapon" => Weapon(pieceType, job) case "weapon" => Weapon(pieceType, job)
case "head" => Head(pieceType, job) case "head" => Head(pieceType, job)
case "body" => Body(pieceType, job) case "body" => Body(pieceType, job)
case "hands" => Hands(pieceType, job) case "hand" | "hands" => Hands(pieceType, job)
case "legs" => Legs(pieceType, job) case "legs" => Legs(pieceType, job)
case "feet" => Feet(pieceType, job) case "feet" => Feet(pieceType, job)
case "ears" => Ears(pieceType, job) case "ears" => Ears(pieceType, job)

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2019-2022 Evgeniy Alekseev. * Copyright (c) 2021-2026 Evgeniy Alekseev.
* *
* This file is part of ffxivbis * This file is part of ffxivbis
* (see https://github.com/arcan1s/ffxivbis). * (see https://github.com/arcan1s/ffxivbis).

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2019-2022 Evgeniy Alekseev. * Copyright (c) 2021-2026 Evgeniy Alekseev.
* *
* This file is part of ffxivbis * This file is part of ffxivbis
* (see https://github.com/arcan1s/ffxivbis). * (see https://github.com/arcan1s/ffxivbis).

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2019-2022 Evgeniy Alekseev. * Copyright (c) 2021-2026 Evgeniy Alekseev.
* *
* This file is part of ffxivbis * This file is part of ffxivbis
* (see https://github.com/arcan1s/ffxivbis). * (see https://github.com/arcan1s/ffxivbis).

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2019-2022 Evgeniy Alekseev. * Copyright (c) 2021-2026 Evgeniy Alekseev.
* *
* This file is part of ffxivbis * This file is part of ffxivbis
* (see https://github.com/arcan1s/ffxivbis). * (see https://github.com/arcan1s/ffxivbis).

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2019-2022 Evgeniy Alekseev. * Copyright (c) 2021-2026 Evgeniy Alekseev.
* *
* This file is part of ffxivbis * This file is part of ffxivbis
* (see https://github.com/arcan1s/ffxivbis). * (see https://github.com/arcan1s/ffxivbis).

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2019-2022 Evgeniy Alekseev. * Copyright (c) 2021-2026 Evgeniy Alekseev.
* *
* This file is part of ffxivbis * This file is part of ffxivbis
* (see https://github.com/arcan1s/ffxivbis). * (see https://github.com/arcan1s/ffxivbis).
@ -10,9 +10,9 @@ package me.arcanis.ffxivbis.service
import me.arcanis.ffxivbis.models.{Piece, Player, PlayerIdWithCounters} import me.arcanis.ffxivbis.models.{Piece, Player, PlayerIdWithCounters}
class LootSelector(players: Seq[Player], piece: Piece, orderBy: Seq[String]) { class LootSelector(players: Seq[Player], pieces: Seq[Piece], orderBy: Seq[String]) {
val counters: Seq[PlayerIdWithCounters] = players.map(_.withCounters(Some(piece))) val counters: Seq[PlayerIdWithCounters] = pieces.flatMap(piece => players.map(_.withCounters(Some(piece))))
def suggest: LootSelector.LootSelectorResult = def suggest: LootSelector.LootSelectorResult =
LootSelector.LootSelectorResult { LootSelector.LootSelectorResult {
@ -22,8 +22,8 @@ class LootSelector(players: Seq[Player], piece: Piece, orderBy: Seq[String]) {
object LootSelector { object LootSelector {
def apply(players: Seq[Player], piece: Piece, orderBy: Seq[String]): LootSelectorResult = def apply(players: Seq[Player], pieces: Seq[Piece], orderBy: Seq[String]): LootSelectorResult =
new LootSelector(players, piece, orderBy).suggest new LootSelector(players, pieces, orderBy).suggest
case class LootSelectorResult(result: Seq[PlayerIdWithCounters]) case class LootSelectorResult(result: Seq[PlayerIdWithCounters])
} }

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2019-2022 Evgeniy Alekseev. * Copyright (c) 2021-2026 Evgeniy Alekseev.
* *
* This file is part of ffxivbis * This file is part of ffxivbis
* (see https://github.com/arcan1s/ffxivbis). * (see https://github.com/arcan1s/ffxivbis).

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2019-2022 Evgeniy Alekseev. * Copyright (c) 2021-2026 Evgeniy Alekseev.
* *
* This file is part of ffxivbis * This file is part of ffxivbis
* (see https://github.com/arcan1s/ffxivbis). * (see https://github.com/arcan1s/ffxivbis).
@ -16,7 +16,7 @@ import com.typesafe.scalalogging.StrictLogging
import me.arcanis.ffxivbis.messages.BiSProviderMessage 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, XIVGear}
import spray.json._ import spray.json._
import java.nio.file.Paths import java.nio.file.Paths
@ -52,7 +52,10 @@ class BisProvider(context: ActorContext[BiSProviderMessage])
val url = Uri(link) val url = Uri(link)
val id = Paths.get(link).normalize.getFileName.toString val id = Paths.get(link).normalize.getFileName.toString
val parser = if (url.authority.host.address().contains("etro")) Etro else Ariyala val parser =
if (url.authority.host.address().contains("etro")) Etro
else if (url.authority.host.address().contains("xivgear.app")) XIVGear
else Ariyala
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 {
@ -81,12 +84,11 @@ object BisProvider {
} }
} }
def remapKey(key: String): Option[String] = key match { def remapKey(key: String): Option[String] = Some(key.toLowerCase).collect {
case "mainhand" => Some("weapon") case "mainhand" => "weapon"
case "chest" => Some("body") case "chest" => "body"
case "ringLeft" | "fingerL" => Some("left ring") case "ringleft" | "fingerl" => "left ring"
case "ringRight" | "fingerR" => Some("right ring") case "ringright" | "fingerr" => "right ring"
case "weapon" | "head" | "body" | "hands" | "legs" | "feet" | "ears" | "neck" | "wrist" | "wrists" => Some(key) case "weapon" | "head" | "body" | "hand" | "hands" | "legs" | "feet" | "ears" | "neck" | "wrist" | "wrists" => key
case _ => None
} }
} }

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2019-2022 Evgeniy Alekseev. * Copyright (c) 2021-2026 Evgeniy Alekseev.
* *
* This file is part of ffxivbis * This file is part of ffxivbis
* (see https://github.com/arcan1s/ffxivbis). * (see https://github.com/arcan1s/ffxivbis).

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2019-2022 Evgeniy Alekseev. * Copyright (c) 2021-2026 Evgeniy Alekseev.
* *
* This file is part of ffxivbis * This file is part of ffxivbis
* (see https://github.com/arcan1s/ffxivbis). * (see https://github.com/arcan1s/ffxivbis).
@ -40,104 +40,39 @@ trait XivApi extends RequestExecutor {
else remotePieceType(remote).map(_ ++ local) else remotePieceType(remote).map(_ ++ local)
} }
private def remotePieceType(itemIds: Seq[Long]): Future[Map[Long, PieceType]] = { private def remotePieceType(itemIds: Seq[Long]): Future[Map[Long, PieceType]] =
val uriForItems = Uri(xivapiUrl) Future
.withPath(Uri.Path / "item") .traverse(itemIds) { id =>
val uriForItem = Uri(xivapiUrl)
.withPath(Uri.Path / "api" / "sheet" / "Item" / id.toString)
.withQuery( .withQuery(
Uri.Query( Uri.Query(
Map( Map(
"columns" -> Seq("ID", "GameContentLinks").mkString(","), "fields" -> Seq("Lot").mkString(","),
"ids" -> itemIds.mkString(","),
"private_key" -> xivapiKey.getOrElse("") "private_key" -> xivapiKey.getOrElse("")
) )
) )
) )
sendRequest(uriForItems, XivApi.parseXivapiJsonToShop).flatMap { shops => sendRequest(uriForItem, XivApi.parseXivapiJsonToLot).map(id -> _)
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))
}
} }
.map(_.toMap)
} }
object XivApi { object XivApi {
private val defaultShop = JsObject("IsUnique" -> JsNumber(1), "StackSize" -> JsNumber(999)) private def parseXivapiJsonToLot(js: JsObject)(implicit executionContext: ExecutionContext): Future[PieceType] =
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 { Future {
js.fields("Results") match { js.fields("fields") match {
case array: JsArray => case JsObject(fields) =>
array.elements fields
.map(_.asJsObject.getFields("ID", "GameContentLinks") match { .get("Lot")
case Seq(JsNumber(id), shop: JsObject) => id.toLong -> extractTraderId(shop.asJsObject) .collect {
case other => throw deserializationError(s"Could not parse $other") case JsBoolean(true) => PieceType.Savage
}) case JsBoolean(false) => PieceType.Tome
.toMap }
case other => throw deserializationError(s"Could not parse $other") .getOrElse(throw deserializationError(s"Could not find lot field in $fields"))
} case other => throw deserializationError(s"Could not read fields as object from $other")
}
}
private def parseXivapiJsonToType(
shops: Map[Long, (String, Long)]
)(js: JsObject)(implicit executionContext: ExecutionContext): Future[Map[Long, PieceType]] =
Future {
val shopMap = js.fields("Results") match {
case array: JsArray =>
array.elements.collect { case shop: JsObject =>
shop.fields("ID") match {
case JsNumber(id) => id.toLong -> shop
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 == -1L) PieceType.Crafted
else
Try(shopMap(shopId).fields(s"ItemCost$index").asJsObject)
.getOrElse(defaultShop)
.getFields("IsUnique", "StackSize") match {
case Seq(JsNumber(isUnique), JsNumber(stackSize)) =>
if (isUnique == 1 || stackSize.toLong != 999) PieceType.Tome // either upgraded gear or tomes found
else PieceType.Savage
case other => throw deserializationError(s"Could not parse $other")
}
itemId -> pieceType
} }
} }
} }

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2019-2022 Evgeniy Alekseev. * Copyright (c) 2021-2026 Evgeniy Alekseev.
* *
* This file is part of ffxivbis * This file is part of ffxivbis
* (see https://github.com/arcan1s/ffxivbis). * (see https://github.com/arcan1s/ffxivbis).

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2019-2022 Evgeniy Alekseev. * Copyright (c) 2021-2026 Evgeniy Alekseev.
* *
* This file is part of ffxivbis * This file is part of ffxivbis
* (see https://github.com/arcan1s/ffxivbis). * (see https://github.com/arcan1s/ffxivbis).

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2019-2022 Evgeniy Alekseev. * Copyright (c) 2021-2026 Evgeniy Alekseev.
* *
* This file is part of ffxivbis * This file is part of ffxivbis
* (see https://github.com/arcan1s/ffxivbis). * (see https://github.com/arcan1s/ffxivbis).

View File

@ -0,0 +1,44 @@
/*
* Copyright (c) 2021-2026 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.parser.impl
import akka.http.scaladsl.model.Uri
import me.arcanis.ffxivbis.models.Job
import me.arcanis.ffxivbis.service.bis.BisProvider
import me.arcanis.ffxivbis.service.bis.parser.Parser
import spray.json.{deserializationError, JsNumber, JsObject}
import scala.concurrent.{ExecutionContext, Future}
object XIVGear extends Parser {
override def parse(job: Job, js: JsObject)(implicit executionContext: ExecutionContext): Future[Map[String, Long]] =
Future {
val set = js.fields.get("items") match {
case Some(JsObject(items)) => items
case other => throw deserializationError(s"Invalid job name $other")
}
set.foldLeft(Map.empty[String, Long]) {
case (acc, (key, JsObject(properties))) =>
val pieceId = properties.get("id").collect { case JsNumber(id) =>
id.toLong
}
(for (
piece <- BisProvider.remapKey(key);
id <- pieceId
) yield (piece, id)).map(acc + _).getOrElse(acc)
case (acc, _) => acc
}
}
override def uri(root: Uri, id: String): Uri = {
val gearSet = Uri(id).query().get("page").map(_.replace("sl|", "")).getOrElse(id)
root.withHost(s"api.${root.authority.host.address()}").withPath(Uri.Path / "shortlink" / gearSet)
}
}

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2019-2022 Evgeniy Alekseev. * Copyright (c) 2021-2026 Evgeniy Alekseev.
* *
* This file is part of ffxivbis * This file is part of ffxivbis
* (see https://github.com/arcan1s/ffxivbis). * (see https://github.com/arcan1s/ffxivbis).

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2019-2022 Evgeniy Alekseev. * Copyright (c) 2021-2026 Evgeniy Alekseev.
* *
* This file is part of ffxivbis * This file is part of ffxivbis
* (see https://github.com/arcan1s/ffxivbis). * (see https://github.com/arcan1s/ffxivbis).

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2019-2022 Evgeniy Alekseev. * Copyright (c) 2021-2026 Evgeniy Alekseev.
* *
* This file is part of ffxivbis * This file is part of ffxivbis
* (see https://github.com/arcan1s/ffxivbis). * (see https://github.com/arcan1s/ffxivbis).

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2019-2022 Evgeniy Alekseev. * Copyright (c) 2021-2026 Evgeniy Alekseev.
* *
* This file is part of ffxivbis * This file is part of ffxivbis
* (see https://github.com/arcan1s/ffxivbis). * (see https://github.com/arcan1s/ffxivbis).

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2019-2022 Evgeniy Alekseev. * Copyright (c) 2021-2026 Evgeniy Alekseev.
* *
* This file is part of ffxivbis * This file is part of ffxivbis
* (see https://github.com/arcan1s/ffxivbis). * (see https://github.com/arcan1s/ffxivbis).
@ -40,7 +40,14 @@ trait DatabaseLootHandler { this: Database =>
case SuggestLoot(partyId, piece, client) => case SuggestLoot(partyId, piece, client) =>
run { run {
getParty(partyId, withBiS = true, withLoot = true) getParty(partyId, withBiS = true, withLoot = true)
.map(_.suggestLoot(piece)) .map(_.suggestLoot(Seq(piece)))
}(client ! _)
Behaviors.same
case SuggestMultiLoot(partyId, pieces, client) =>
run {
getParty(partyId, withBiS = true, withLoot = true)
.map(_.suggestLoot(pieces))
}(client ! _) }(client ! _)
Behaviors.same Behaviors.same
} }

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2019-2022 Evgeniy Alekseev. * Copyright (c) 2021-2026 Evgeniy Alekseev.
* *
* This file is part of ffxivbis * This file is part of ffxivbis
* (see https://github.com/arcan1s/ffxivbis). * (see https://github.com/arcan1s/ffxivbis).

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2019-2022 Evgeniy Alekseev. * Copyright (c) 2021-2026 Evgeniy Alekseev.
* *
* This file is part of ffxivbis * This file is part of ffxivbis
* (see https://github.com/arcan1s/ffxivbis). * (see https://github.com/arcan1s/ffxivbis).

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2019-2022 Evgeniy Alekseev. * Copyright (c) 2021-2026 Evgeniy Alekseev.
* *
* This file is part of ffxivbis * This file is part of ffxivbis
* (see https://github.com/arcan1s/ffxivbis). * (see https://github.com/arcan1s/ffxivbis).

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2019-2022 Evgeniy Alekseev. * Copyright (c) 2021-2026 Evgeniy Alekseev.
* *
* This file is part of ffxivbis * This file is part of ffxivbis
* (see https://github.com/arcan1s/ffxivbis). * (see https://github.com/arcan1s/ffxivbis).

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2019-2022 Evgeniy Alekseev. * Copyright (c) 2021-2026 Evgeniy Alekseev.
* *
* This file is part of ffxivbis * This file is part of ffxivbis
* (see https://github.com/arcan1s/ffxivbis). * (see https://github.com/arcan1s/ffxivbis).

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2019-2022 Evgeniy Alekseev. * Copyright (c) 2021-2026 Evgeniy Alekseev.
* *
* This file is part of ffxivbis * This file is part of ffxivbis
* (see https://github.com/arcan1s/ffxivbis). * (see https://github.com/arcan1s/ffxivbis).

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2019-2022 Evgeniy Alekseev. * Copyright (c) 2021-2026 Evgeniy Alekseev.
* *
* This file is part of ffxivbis * This file is part of ffxivbis
* (see https://github.com/arcan1s/ffxivbis). * (see https://github.com/arcan1s/ffxivbis).

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2019-2022 Evgeniy Alekseev. * Copyright (c) 2021-2026 Evgeniy Alekseev.
* *
* This file is part of ffxivbis * This file is part of ffxivbis
* (see https://github.com/arcan1s/ffxivbis). * (see https://github.com/arcan1s/ffxivbis).

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2019-2022 Evgeniy Alekseev. * Copyright (c) 2021-2026 Evgeniy Alekseev.
* *
* This file is part of ffxivbis * This file is part of ffxivbis
* (see https://github.com/arcan1s/ffxivbis). * (see https://github.com/arcan1s/ffxivbis).

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2019-2022 Evgeniy Alekseev. * Copyright (c) 2021-2026 Evgeniy Alekseev.
* *
* This file is part of ffxivbis * This file is part of ffxivbis
* (see https://github.com/arcan1s/ffxivbis). * (see https://github.com/arcan1s/ffxivbis).

View File

@ -52,12 +52,44 @@ object Fixtures {
Piece.Ring(pieceType = PieceType.Tome, Job.SGE, "right ring") Piece.Ring(pieceType = PieceType.Tome, Job.SGE, "right ring")
) )
) )
lazy val bis4: BiS = BiS(
Seq(
Piece.Weapon(pieceType = PieceType.Savage ,Job.VPR),
Piece.Head(pieceType = PieceType.Savage, Job.VPR),
Piece.Body(pieceType = PieceType.Savage, Job.VPR),
Piece.Hands(pieceType = PieceType.Tome, Job.VPR),
Piece.Legs(pieceType = PieceType.Tome, Job.VPR),
Piece.Feet(pieceType = PieceType.Savage, Job.VPR),
Piece.Ears(pieceType = PieceType.Tome, Job.VPR),
Piece.Neck(pieceType = PieceType.Savage, Job.VPR),
Piece.Wrist(pieceType = PieceType.Tome, Job.VPR),
Piece.Ring(pieceType = PieceType.Savage, Job.VPR, "left ring"),
Piece.Ring(pieceType = PieceType.Tome, Job.VPR, "right ring")
)
)
lazy val bis5: BiS = BiS(
Seq(
Piece.Weapon(pieceType = PieceType.Savage ,Job.BRD),
Piece.Head(pieceType = PieceType.Tome, Job.BRD),
Piece.Body(pieceType = PieceType.Savage, Job.BRD),
Piece.Hands(pieceType = PieceType.Savage, Job.BRD),
Piece.Legs(pieceType = PieceType.Tome, Job.BRD),
Piece.Feet(pieceType = PieceType.Tome, Job.BRD),
Piece.Ears(pieceType = PieceType.Tome, Job.BRD),
Piece.Neck(pieceType = PieceType.Savage, Job.BRD),
Piece.Wrist(pieceType = PieceType.Tome, Job.BRD),
Piece.Ring(pieceType = PieceType.Tome, Job.BRD, "left ring"),
Piece.Ring(pieceType = PieceType.Savage, Job.BRD, "right ring")
)
)
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 link3: String = "https://etro.gg/gearset/26a67536-b4ce-4adc-a46a-f70e348bb138"
lazy val link4: String = "https://etro.gg/gearset/865fc886-994f-4c28-8fc1-4379f160a916" lazy val 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 link6: String = "https://xivgear.app/?page=sl%7Cd65b4776-01e1-4269-af74-0bc6e01ca2ec"
lazy val link7: String = "https://xivgear.app/?page=sl|22777835-b7c8-457e-bf21-6221d0d122ea"
lazy val lootWeapon: Piece = Piece.Weapon(pieceType = PieceType.Tome, Job.AnyJob) lazy val lootWeapon: Piece = Piece.Weapon(pieceType = PieceType.Tome, Job.AnyJob)
lazy val lootBody: Piece = Piece.Body(pieceType = PieceType.Savage, Job.AnyJob) lazy val lootBody: Piece = Piece.Body(pieceType = PieceType.Savage, Job.AnyJob)

View File

@ -43,14 +43,14 @@ 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(Piece.Head(pieceType = PieceType.Savage, Job.AnyJob))) shouldEqual Seq(dnc.playerId, drg.playerId) toPlayerId(default.suggestLoot(Seq(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 = 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(Seq(piece))) shouldEqual Seq(drg.playerId, dnc.playerId)
} }
"suggest upgrade" in { "suggest upgrade" in {
@ -60,26 +60,26 @@ class LootSelectorTest extends AnyWordSpecLike with Matchers with BeforeAndAfter
) )
) )
toPlayerId(party.suggestLoot(Piece.WeaponUpgrade)) shouldEqual Seq(dnc.playerId, drg.playerId) toPlayerId(party.suggestLoot(Seq(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(Piece.Body(pieceType = PieceType.Savage, Job.AnyJob))) shouldEqual Seq(drg.playerId, dnc.playerId) toPlayerId(party.suggestLoot(Seq(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(Piece.Head(pieceType = PieceType.Savage, Job.AnyJob))) val party = default.withPlayer(dnc.withLoot(Piece.Head(pieceType = PieceType.Savage, Job.AnyJob)))
toPlayerId(party.suggestLoot(Piece.Body(pieceType = PieceType.Savage, Job.AnyJob))) shouldEqual Seq(drg.playerId, dnc.playerId) toPlayerId(party.suggestLoot(Seq(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 = 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(Seq(piece))) shouldEqual Seq(drg.playerId, dnc.playerId)
} }
"suggest loot by total piece got" in { "suggest loot by total piece got" in {
@ -88,7 +88,7 @@ class LootSelectorTest extends AnyWordSpecLike with Matchers with BeforeAndAfter
.withPlayer(dnc.withLoot(Seq(piece, piece).map(pieceToLoot))) .withPlayer(dnc.withLoot(Seq(piece, piece).map(pieceToLoot)))
.withPlayer(drg.withLoot(piece)) .withPlayer(drg.withLoot(piece))
toPlayerId(party.suggestLoot(piece)) shouldEqual Seq(drg.playerId, dnc.playerId) toPlayerId(party.suggestLoot(Seq(piece))) shouldEqual Seq(drg.playerId, dnc.playerId)
} }
} }

View File

@ -35,11 +35,23 @@ class BisProviderTest extends ScalaTestWithActorTestKit(Settings.withRandomDatab
probe.expectMessage(askTimeout, Fixtures.bis) probe.expectMessage(askTimeout, Fixtures.bis)
} }
"get best in slot set (etro 2)" in { "get best in slot set (etro 2)" ignore {
val probe = testKit.createTestProbe[BiS]() val probe = testKit.createTestProbe[BiS]()
provider ! DownloadBiS(Fixtures.link4, Job.DNC, probe.ref) provider ! DownloadBiS(Fixtures.link4, Job.DNC, probe.ref)
probe.expectMessage(askTimeout, Fixtures.bis2) probe.expectMessage(askTimeout, Fixtures.bis2)
} }
"get best in slot set (xivgear)" in {
val probe = testKit.createTestProbe[BiS]()
provider ! DownloadBiS(Fixtures.link6, Job.VPR, probe.ref)
probe.expectMessage(askTimeout, Fixtures.bis4)
}
"get best in slot set (xivgear 2)" in {
val probe = testKit.createTestProbe[BiS]()
provider ! DownloadBiS(Fixtures.link7, Job.BRD, probe.ref)
probe.expectMessage(askTimeout, Fixtures.bis5)
}
} }
} }

View File

@ -1 +1 @@
version := "0.14.0" version := "0.15.7"