mirror of
https://github.com/arcan1s/ffxivbis.git
synced 2026-01-11 12:03:42 +00:00
Compare commits
27 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8ddb999988 | |||
| cd3379561a | |||
| 727e6c134a | |||
| f7f112330b | |||
| 8e52be728e | |||
| 5025e760c1 | |||
| 35c784bf89 | |||
| 757c4cc6df | |||
| ca24ee298e | |||
| af7a92af35 | |||
| 4c05ceefcd | |||
| fa43517b16 | |||
| 77e99439e7 | |||
| c9eb311cfe | |||
| d662e303c8 | |||
| 1c8aaea712 | |||
| 3c8e5f8da8 | |||
| 0bcda3233e | |||
| c4be6f12f1 | |||
| f3535f6e16 | |||
| bdf413d494 | |||
| 7a1a73592e | |||
| b1ac894ccf | |||
| 6023e86570 | |||
| a4ab1e49be | |||
| cb99486f8a | |||
| 0e8b95d0dd |
14
.github/workflows/release.yml
vendored
14
.github/workflows/release.yml
vendored
@ -12,6 +12,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: extract version
|
||||
id: version
|
||||
run: echo ::set-output name=VERSION::${GITHUB_REF#refs/tags/}
|
||||
@ -21,13 +22,24 @@ jobs:
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
filter: 'Release \d+\.\d+\.\d+'
|
||||
|
||||
- name: setup JDK
|
||||
uses: actions/setup-java@v2
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: 17
|
||||
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
|
||||
run: make dist
|
||||
|
||||
- name: release
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
|
||||
14
.github/workflows/run-tests.yml
vendored
14
.github/workflows/run-tests.yml
vendored
@ -13,10 +13,20 @@ jobs:
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: setup JDK
|
||||
uses: actions/setup-java@v2
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: 17
|
||||
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
|
||||
run: make tests
|
||||
run: make tests
|
||||
|
||||
3
TODO.md
3
TODO.md
@ -1,3 +0,0 @@
|
||||
* [x] items improvements
|
||||
* [x] multiple parties support
|
||||
* [ ] pretty UI
|
||||
@ -2,7 +2,7 @@ organization := "me.arcanis"
|
||||
|
||||
name := "ffxivbis"
|
||||
|
||||
scalaVersion := "2.13.6"
|
||||
scalaVersion := "2.13.12"
|
||||
|
||||
scalacOptions ++= Seq("-deprecation", "-feature")
|
||||
|
||||
|
||||
@ -1,26 +1,25 @@
|
||||
val AkkaVersion = "2.6.18"
|
||||
val AkkaHttpVersion = "10.2.7"
|
||||
val ScalaTestVersion = "3.2.10"
|
||||
val SlickVersion = "3.3.3"
|
||||
val AkkaVersion = "2.8.6"
|
||||
val AkkaHttpVersion = "10.5.3"
|
||||
val ScalaTestVersion = "3.2.19"
|
||||
|
||||
libraryDependencies += "ch.qos.logback" % "logback-classic" % "1.2.10"
|
||||
libraryDependencies += "com.typesafe.scala-logging" %% "scala-logging" % "3.9.4"
|
||||
libraryDependencies += "ch.qos.logback" % "logback-classic" % "1.5.6"
|
||||
libraryDependencies += "com.typesafe.scala-logging" %% "scala-logging" % "3.9.5"
|
||||
|
||||
libraryDependencies += "com.typesafe.akka" %% "akka-http" % AkkaHttpVersion
|
||||
libraryDependencies += "com.typesafe.akka" %% "akka-http-spray-json" % AkkaHttpVersion
|
||||
libraryDependencies += "com.typesafe.akka" %% "akka-actor-typed" % AkkaVersion
|
||||
libraryDependencies += "com.typesafe.akka" %% "akka-stream" % AkkaVersion
|
||||
libraryDependencies += "com.github.swagger-akka-http" %% "swagger-akka-http" % "2.6.0"
|
||||
libraryDependencies += "jakarta.platform" % "jakarta.jakartaee-web-api" % "9.1.0"
|
||||
libraryDependencies += "ch.megard" %% "akka-http-cors" % "1.1.2"
|
||||
libraryDependencies += "com.github.swagger-akka-http" %% "swagger-akka-http" % "2.11.0"
|
||||
libraryDependencies += "jakarta.platform" % "jakarta.jakartaee-web-api" % "10.0.0"
|
||||
libraryDependencies += "ch.megard" %% "akka-http-cors" % "1.2.0"
|
||||
|
||||
libraryDependencies += "io.spray" %% "spray-json" % "1.3.6"
|
||||
|
||||
libraryDependencies += "org.playframework.anorm" %% "anorm" % "2.6.10"
|
||||
libraryDependencies += "com.zaxxer" % "HikariCP" % "5.0.1" exclude("org.slf4j", "slf4j-api")
|
||||
libraryDependencies += "org.flywaydb" % "flyway-core" % "8.4.1"
|
||||
libraryDependencies += "org.xerial" % "sqlite-jdbc" % "3.36.0.3"
|
||||
libraryDependencies += "org.postgresql" % "postgresql" % "42.3.1"
|
||||
libraryDependencies += "org.playframework.anorm" %% "anorm" % "2.7.0"
|
||||
libraryDependencies += "com.zaxxer" % "HikariCP" % "5.1.0" exclude("org.slf4j", "slf4j-api")
|
||||
libraryDependencies += "org.flywaydb" % "flyway-core" % "9.16.0"
|
||||
libraryDependencies += "org.xerial" % "sqlite-jdbc" % "3.46.0.0"
|
||||
libraryDependencies += "org.postgresql" % "postgresql" % "42.7.3"
|
||||
|
||||
libraryDependencies += "org.mindrot" % "jbcrypt" % "0.4"
|
||||
libraryDependencies += "com.google.guava" % "guava" % "31.0.1-jre"
|
||||
|
||||
@ -1 +1 @@
|
||||
sbt.version = 1.5.8
|
||||
sbt.version = 1.7.1
|
||||
|
||||
@ -0,0 +1,3 @@
|
||||
update parties set party_alias = regexp_replace(party_alias, '[^A-Za-z0-9!@#$%^&*()\-_=+;:'',./? ]', '', 'g');
|
||||
update players set nick = regexp_replace(nick, '[^A-Za-z0-9!@#$%^&*()\-_=+;:'',./? ]', '', 'g');
|
||||
update users set username = regexp_replace(username, '[^A-Za-z0-9!@#$%^&*()\-_=+;:'',./? ]', '', 'g');
|
||||
@ -0,0 +1 @@
|
||||
update players set bis_link = null where bis_link = '';
|
||||
@ -9,9 +9,9 @@
|
||||
<link rel="shortcut icon" href="/static/favicon.ico">
|
||||
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous" type="text/css">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.7.2/font/bootstrap-icons.css" type="text/css">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.8.3/font/bootstrap-icons.css" type="text/css">
|
||||
|
||||
<link rel="stylesheet" href="https://unpkg.com/bootstrap-table@1.19.1/dist/bootstrap-table.min.css" type="text/css">
|
||||
<link rel="stylesheet" href="https://unpkg.com/bootstrap-table@1.20.2/dist/bootstrap-table.min.css" type="text/css">
|
||||
|
||||
|
||||
<link rel="stylesheet" href="https://unpkg.com/jquery-resizable-columns@0.2.3/dist/jquery.resizableColumns.css" type="text/css">
|
||||
@ -157,11 +157,11 @@
|
||||
<script src="https://unpkg.com/jquery-resizable-columns@0.2.3/dist/jquery.resizableColumns.min.js"></script>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p" crossorigin="anonymous"></script>
|
||||
<script src="https://unpkg.com/bootstrap-table@1.19.1/dist/bootstrap-table.min.js"></script>
|
||||
<script src="https://unpkg.com/bootstrap-table@1.20.2/dist/bootstrap-table.min.js"></script>
|
||||
|
||||
<script src="https://unpkg.com/bootstrap-table@1.19.1/dist/extensions/export/bootstrap-table-export.min.js"></script>
|
||||
<script src="https://unpkg.com/bootstrap-table@1.20.2/dist/extensions/export/bootstrap-table-export.min.js"></script>
|
||||
|
||||
<script src="https://unpkg.com/bootstrap-table@1.19.1/dist/extensions/resizable/bootstrap-table-resizable.js"></script>
|
||||
<script src="https://unpkg.com/bootstrap-table@1.20.2/dist/extensions/resizable/bootstrap-table-resizable.js"></script>
|
||||
|
||||
<script src="/static/utils.js"></script>
|
||||
<script src="/static/load.js"></script>
|
||||
@ -207,8 +207,8 @@
|
||||
}),
|
||||
type: "POST",
|
||||
contentType: "application/json",
|
||||
success: function (_) { reload(); },
|
||||
error: function (jqXHR, _, errorThrown) { requestAlert(jqXHR, errorThrown); },
|
||||
success: _ => { reload(); },
|
||||
error: (jqXHR, _, errorThrown) => { requestAlert(jqXHR, errorThrown); },
|
||||
});
|
||||
updateBisDialog.modal("hide");
|
||||
return true; // action expects boolean result
|
||||
@ -247,9 +247,9 @@
|
||||
url: `/api/v1/party/${partyId}`,
|
||||
type: "GET",
|
||||
dataType: "json",
|
||||
success: function (data) {
|
||||
const items = data.map(function (player) {
|
||||
return player.bis.map(function (loot) {
|
||||
success: response => {
|
||||
const items = response.map(player => {
|
||||
return player.bis.map(loot => {
|
||||
return {
|
||||
nick: player.nick,
|
||||
job: player.job,
|
||||
@ -258,12 +258,12 @@
|
||||
};
|
||||
});
|
||||
});
|
||||
const payload = items.reduce(function (left, right) { return left.concat(right); }, []);
|
||||
const payload = items.reduce((left, right) => { return left.concat(right); }, []);
|
||||
table.bootstrapTable("load", payload);
|
||||
table.bootstrapTable("uncheckAll");
|
||||
table.bootstrapTable("hideLoading");
|
||||
|
||||
const options = data.map(function (player) {
|
||||
const options = response.map(player => {
|
||||
const option = document.createElement("option");
|
||||
option.innerText = formatPlayerId(player);
|
||||
option.dataset.nick = player.nick;
|
||||
@ -272,13 +272,13 @@
|
||||
});
|
||||
playerInput.empty().append(options);
|
||||
},
|
||||
error: function (jqXHR, _, errorThrown) { requestAlert(jqXHR, errorThrown); },
|
||||
error: (jqXHR, _, errorThrown) => { requestAlert(jqXHR, errorThrown); },
|
||||
});
|
||||
}
|
||||
|
||||
function removePiece() {
|
||||
const pieces = table.bootstrapTable("getSelections");
|
||||
pieces.map(function (loot) {
|
||||
pieces.map(loot => {
|
||||
$.ajax({
|
||||
url: `/api/v1/party/${partyId}/bis`,
|
||||
data: JSON.stringify({
|
||||
@ -296,8 +296,8 @@
|
||||
}),
|
||||
type: "POST",
|
||||
contentType: "application/json",
|
||||
success: function (_) { reload(); },
|
||||
error: function (jqXHR, _, errorThrown) { requestAlert(jqXHR, errorThrown); },
|
||||
success: _ => { reload(); },
|
||||
error: (jqXHR, _, errorThrown) => { requestAlert(jqXHR, errorThrown); },
|
||||
});
|
||||
});
|
||||
}
|
||||
@ -325,8 +325,8 @@
|
||||
}),
|
||||
type: "PUT",
|
||||
contentType: "application/json",
|
||||
success: function (_) { reload(); },
|
||||
error: function (jqXHR, _, errorThrown) { requestAlert(jqXHR, errorThrown); },
|
||||
success: _ => { reload(); },
|
||||
error: (jqXHR, _, errorThrown) => { requestAlert(jqXHR, errorThrown); },
|
||||
});
|
||||
updateBisDialog.modal("hide");
|
||||
return true; // action expects boolean result
|
||||
@ -342,7 +342,7 @@
|
||||
return false; // should not happen
|
||||
}
|
||||
|
||||
$(function () {
|
||||
$(() => {
|
||||
setupFormClear(updateBisDialog, reset);
|
||||
setupRemoveButton(table, removeButton);
|
||||
|
||||
@ -353,8 +353,8 @@
|
||||
|
||||
hideControls();
|
||||
|
||||
updateBisButton.click(function () { reset(); });
|
||||
addPieceButton.click(function () { reset(); });
|
||||
updateBisButton.click(() => { reset(); });
|
||||
addPieceButton.click(() => { reset(); });
|
||||
|
||||
table.bootstrapTable({});
|
||||
reload();
|
||||
|
||||
@ -87,6 +87,7 @@
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p" crossorigin="anonymous"></script>
|
||||
|
||||
<script src="/static/utils.js"></script>
|
||||
<script src="/static/load.js"></script>
|
||||
|
||||
<script>
|
||||
|
||||
@ -9,9 +9,9 @@
|
||||
<link rel="shortcut icon" href="/static/favicon.ico">
|
||||
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous" type="text/css">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.7.2/font/bootstrap-icons.css" type="text/css">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.8.3/font/bootstrap-icons.css" type="text/css">
|
||||
|
||||
<link rel="stylesheet" href="https://unpkg.com/bootstrap-table@1.19.1/dist/bootstrap-table.min.css" type="text/css">
|
||||
<link rel="stylesheet" href="https://unpkg.com/bootstrap-table@1.20.2/dist/bootstrap-table.min.css" type="text/css">
|
||||
|
||||
|
||||
<link rel="stylesheet" href="https://unpkg.com/jquery-resizable-columns@0.2.3/dist/jquery.resizableColumns.css" type="text/css">
|
||||
@ -41,7 +41,7 @@
|
||||
|
||||
<div class="container">
|
||||
<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
|
||||
</button>
|
||||
<button class="btn btn-secondary" onclick="reload()">
|
||||
@ -88,7 +88,7 @@
|
||||
|
||||
<div id="add-loot-dialog" tabindex="-1" role="dialog" class="modal fade">
|
||||
<div class="modal-dialog modal-lg" role="document">
|
||||
<form class="modal-content" action="javascript:" onsubmit="addLoot()">
|
||||
<form class="modal-content" action="javascript:" onsubmit="addLootModal()">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">add looted piece</h4>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="close"></button>
|
||||
@ -132,6 +132,7 @@
|
||||
<table id="stats" class="table table-striped table-hover">
|
||||
<thead class="table-primary">
|
||||
<tr>
|
||||
<th data-formatter="addLootFormatter"></th>
|
||||
<th data-field="nick">nick</th>
|
||||
<th data-field="job">job</th>
|
||||
<th data-field="isRequired">required</th>
|
||||
@ -146,7 +147,7 @@
|
||||
<div class="modal-footer">
|
||||
<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="submit" class="btn btn-primary">add</button>
|
||||
<button id="add-btn" type="submit" class="btn btn-primary">add</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@ -174,11 +175,11 @@
|
||||
<script src="https://unpkg.com/jquery-resizable-columns@0.2.3/dist/jquery.resizableColumns.min.js"></script>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p" crossorigin="anonymous"></script>
|
||||
<script src="https://unpkg.com/bootstrap-table@1.19.1/dist/bootstrap-table.min.js"></script>
|
||||
<script src="https://unpkg.com/bootstrap-table@1.20.2/dist/bootstrap-table.min.js"></script>
|
||||
|
||||
<script src="https://unpkg.com/bootstrap-table@1.19.1/dist/extensions/export/bootstrap-table-export.min.js"></script>
|
||||
<script src="https://unpkg.com/bootstrap-table@1.20.2/dist/extensions/export/bootstrap-table-export.min.js"></script>
|
||||
|
||||
<script src="https://unpkg.com/bootstrap-table@1.19.1/dist/extensions/resizable/bootstrap-table-resizable.js"></script>
|
||||
<script src="https://unpkg.com/bootstrap-table@1.20.2/dist/extensions/resizable/bootstrap-table-resizable.js"></script>
|
||||
|
||||
<script src="/static/utils.js"></script>
|
||||
<script src="/static/load.js"></script>
|
||||
@ -198,30 +199,40 @@
|
||||
const pieceTypeInput = $("#piece-type");
|
||||
const playerInput = $("#player");
|
||||
|
||||
function addLoot() {
|
||||
const player = getCurrentOption(playerInput);
|
||||
function addLoot(nick, job) {
|
||||
$.ajax({
|
||||
url: `/api/v1/party/${partyId}/loot`,
|
||||
data: JSON.stringify({
|
||||
action: "add",
|
||||
piece: {
|
||||
pieceType: pieceTypeInput.val(),
|
||||
job: player.dataset.job,
|
||||
job: job,
|
||||
piece: pieceInput.val(),
|
||||
},
|
||||
playerId: {
|
||||
partyId: partyId,
|
||||
nick: player.dataset.nick,
|
||||
job: player.dataset.job,
|
||||
nick: nick,
|
||||
job: job,
|
||||
},
|
||||
isFreeLoot: freeLootInput.is(":checked"),
|
||||
}),
|
||||
type: "POST",
|
||||
contentType: "application/json",
|
||||
success: function (_) { reload(); },
|
||||
error: function (jqXHR, _, errorThrown) { requestAlert(jqXHR, errorThrown); },
|
||||
success: _ => {
|
||||
addLootDialog.modal("hide");
|
||||
reload();
|
||||
},
|
||||
error: (jqXHR, _, errorThrown) => { requestAlert(jqXHR, errorThrown); },
|
||||
});
|
||||
addLootDialog.modal("hide");
|
||||
}
|
||||
|
||||
function addLootFormatter(value, row, index) {
|
||||
return `<button type="button" class="btn btn-primary" onclick="addLoot('${row.nick}', '${row.job}')"><i class="bi bi-plus"></i></button>`;
|
||||
}
|
||||
|
||||
function addLootModal() {
|
||||
const player = getCurrentOption(playerInput);
|
||||
addLoot(player.dataset.nick, player.dataset.job);
|
||||
return true; // action expects boolean result
|
||||
}
|
||||
|
||||
@ -236,9 +247,9 @@
|
||||
url: `/api/v1/party/${partyId}`,
|
||||
type: "GET",
|
||||
dataType: "json",
|
||||
success: function (data) {
|
||||
const items = data.map(function (player) {
|
||||
return player.loot.map(function (loot) {
|
||||
success: response => {
|
||||
const items = response.map(player => {
|
||||
return player.loot.map(loot => {
|
||||
return {
|
||||
nick: player.nick,
|
||||
job: player.job,
|
||||
@ -249,12 +260,12 @@
|
||||
};
|
||||
});
|
||||
});
|
||||
const payload = items.reduce(function (left, right) { return left.concat(right); }, []);
|
||||
const payload = items.reduce((left, right) => { return left.concat(right); }, []);
|
||||
table.bootstrapTable("load", payload);
|
||||
table.bootstrapTable("uncheckAll");
|
||||
table.bootstrapTable("hideLoading");
|
||||
|
||||
const options = data.map(function (player) {
|
||||
const options = response.map(player => {
|
||||
const option = document.createElement("option");
|
||||
option.innerText = formatPlayerId(player);
|
||||
option.dataset.nick = player.nick;
|
||||
@ -263,13 +274,13 @@
|
||||
});
|
||||
playerInput.empty().append(options);
|
||||
},
|
||||
error: function (jqXHR, _, errorThrown) { requestAlert(jqXHR, errorThrown); },
|
||||
error: (jqXHR, _, errorThrown) => { requestAlert(jqXHR, errorThrown); },
|
||||
});
|
||||
}
|
||||
|
||||
function removeLoot() {
|
||||
const pieces = table.bootstrapTable("getSelections");
|
||||
pieces.map(function (loot) {
|
||||
pieces.map(loot => {
|
||||
$.ajax({
|
||||
url: `/api/v1/party/${partyId}/loot`,
|
||||
data: JSON.stringify({
|
||||
@ -288,8 +299,8 @@
|
||||
}),
|
||||
type: "POST",
|
||||
contentType: "application/json",
|
||||
success: function (_) { reload(); },
|
||||
error: function (jqXHR, _, errorThrown) { requestAlert(jqXHR, errorThrown); },
|
||||
success: _ => { reload(); },
|
||||
error: (jqXHR, _, errorThrown) => { requestAlert(jqXHR, errorThrown); },
|
||||
});
|
||||
});
|
||||
}
|
||||
@ -306,8 +317,8 @@
|
||||
type: "PUT",
|
||||
contentType: "application/json",
|
||||
dataType: "json",
|
||||
success: function (data) {
|
||||
const payload = data.map(function (stat) {
|
||||
success: response => {
|
||||
const payload = response.map(stat => {
|
||||
return {
|
||||
nick: stat.nick,
|
||||
job: stat.job,
|
||||
@ -321,11 +332,11 @@
|
||||
stats.bootstrapTable("uncheckAll");
|
||||
stats.bootstrapTable("hideLoading");
|
||||
},
|
||||
error: function (jqXHR, _, errorThrown) { requestAlert(jqXHR, errorThrown); },
|
||||
error: (jqXHR, _, errorThrown) => { requestAlert(jqXHR, errorThrown); },
|
||||
});
|
||||
}
|
||||
|
||||
$(function () {
|
||||
$(() => {
|
||||
setupFormClear(addLootDialog);
|
||||
setupRemoveButton(table, removeButton);
|
||||
|
||||
|
||||
@ -9,9 +9,9 @@
|
||||
<link rel="shortcut icon" href="/static/favicon.ico">
|
||||
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous" type="text/css">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.7.2/font/bootstrap-icons.css" type="text/css">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.8.3/font/bootstrap-icons.css" type="text/css">
|
||||
|
||||
<link rel="stylesheet" href="https://unpkg.com/bootstrap-table@1.19.1/dist/bootstrap-table.min.css" type="text/css">
|
||||
<link rel="stylesheet" href="https://unpkg.com/bootstrap-table@1.20.2/dist/bootstrap-table.min.css" type="text/css">
|
||||
|
||||
|
||||
<link rel="stylesheet" href="https://unpkg.com/jquery-resizable-columns@0.2.3/dist/jquery.resizableColumns.css" type="text/css">
|
||||
@ -147,11 +147,11 @@
|
||||
<script src="https://unpkg.com/jquery-resizable-columns@0.2.3/dist/jquery.resizableColumns.min.js"></script>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p" crossorigin="anonymous"></script>
|
||||
<script src="https://unpkg.com/bootstrap-table@1.19.1/dist/bootstrap-table.min.js"></script>
|
||||
<script src="https://unpkg.com/bootstrap-table@1.20.2/dist/bootstrap-table.min.js"></script>
|
||||
|
||||
<script src="https://unpkg.com/bootstrap-table@1.19.1/dist/extensions/export/bootstrap-table-export.min.js"></script>
|
||||
<script src="https://unpkg.com/bootstrap-table@1.20.2/dist/extensions/export/bootstrap-table-export.min.js"></script>
|
||||
|
||||
<script src="https://unpkg.com/bootstrap-table@1.19.1/dist/extensions/resizable/bootstrap-table-resizable.js"></script>
|
||||
<script src="https://unpkg.com/bootstrap-table@1.20.2/dist/extensions/resizable/bootstrap-table-resizable.js"></script>
|
||||
|
||||
<script src="/static/utils.js"></script>
|
||||
<script src="/static/load.js"></script>
|
||||
@ -184,8 +184,8 @@
|
||||
}),
|
||||
type: "POST",
|
||||
contentType: "application/json",
|
||||
success: function (_) { reload(); },
|
||||
error: function (jqXHR, _, errorThrown) { requestAlert(jqXHR, errorThrown); },
|
||||
success: _ => { reload(); },
|
||||
error: (jqXHR, _, errorThrown) => { requestAlert(jqXHR, errorThrown); },
|
||||
});
|
||||
addPlayerDialog.modal("hide");
|
||||
return true; // action expects boolean result
|
||||
@ -210,18 +210,18 @@
|
||||
url: `/api/v1/party/${partyId}`,
|
||||
type: "GET",
|
||||
dataType: "json",
|
||||
success: function (data) {
|
||||
table.bootstrapTable("load", data);
|
||||
success: response => {
|
||||
table.bootstrapTable("load", response);
|
||||
table.bootstrapTable("uncheckAll");
|
||||
table.bootstrapTable("hideLoading");
|
||||
},
|
||||
error: function (jqXHR, _, errorThrown) { requestAlert(jqXHR, errorThrown); },
|
||||
error: (jqXHR, _, errorThrown) => { requestAlert(jqXHR, errorThrown); },
|
||||
});
|
||||
}
|
||||
|
||||
function removePlayers() {
|
||||
const players = table.bootstrapTable("getSelections");
|
||||
players.map(function (player) {
|
||||
players.map(player => {
|
||||
$.ajax({
|
||||
url: `/api/v1/party/${partyId}`,
|
||||
data: JSON.stringify({
|
||||
@ -234,13 +234,13 @@
|
||||
}),
|
||||
type: "POST",
|
||||
contentType: "application/json",
|
||||
success: function (_) { reload(); },
|
||||
error: function (jqXHR, _, errorThrown) { requestAlert(jqXHR, errorThrown); },
|
||||
success: _ => { reload(); },
|
||||
error: (jqXHR, _, errorThrown) => { requestAlert(jqXHR, errorThrown); },
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
$(function () {
|
||||
$(() => {
|
||||
setupFormClear(addPlayerDialog);
|
||||
setupRemoveButton(table, removeButton);
|
||||
|
||||
|
||||
@ -9,9 +9,9 @@
|
||||
<link rel="shortcut icon" href="/static/favicon.ico">
|
||||
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous" type="text/css">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.7.2/font/bootstrap-icons.css" type="text/css">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.8.3/font/bootstrap-icons.css" type="text/css">
|
||||
|
||||
<link rel="stylesheet" href="https://unpkg.com/bootstrap-table@1.19.1/dist/bootstrap-table.min.css" type="text/css">
|
||||
<link rel="stylesheet" href="https://unpkg.com/bootstrap-table@1.20.2/dist/bootstrap-table.min.css" type="text/css">
|
||||
|
||||
|
||||
<link rel="stylesheet" href="https://unpkg.com/jquery-resizable-columns@0.2.3/dist/jquery.resizableColumns.css" type="text/css">
|
||||
@ -141,11 +141,11 @@
|
||||
<script src="https://unpkg.com/jquery-resizable-columns@0.2.3/dist/jquery.resizableColumns.min.js"></script>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p" crossorigin="anonymous"></script>
|
||||
<script src="https://unpkg.com/bootstrap-table@1.19.1/dist/bootstrap-table.min.js"></script>
|
||||
<script src="https://unpkg.com/bootstrap-table@1.20.2/dist/bootstrap-table.min.js"></script>
|
||||
|
||||
<script src="https://unpkg.com/bootstrap-table@1.19.1/dist/extensions/export/bootstrap-table-export.min.js"></script>
|
||||
<script src="https://unpkg.com/bootstrap-table@1.20.2/dist/extensions/export/bootstrap-table-export.min.js"></script>
|
||||
|
||||
<script src="https://unpkg.com/bootstrap-table@1.19.1/dist/extensions/resizable/bootstrap-table-resizable.js"></script>
|
||||
<script src="https://unpkg.com/bootstrap-table@1.20.2/dist/extensions/resizable/bootstrap-table-resizable.js"></script>
|
||||
|
||||
<script src="/static/utils.js"></script>
|
||||
<script src="/static/load.js"></script>
|
||||
@ -173,8 +173,8 @@
|
||||
}),
|
||||
type: "POST",
|
||||
contentType: "application/json",
|
||||
success: function (_) { reload(); },
|
||||
error: function (jqXHR, _, errorThrown) { requestAlert(jqXHR, errorThrown); },
|
||||
success: _ => { reload(); },
|
||||
error: (jqXHR, _, errorThrown) => { requestAlert(jqXHR, errorThrown); },
|
||||
});
|
||||
addUserDialog.modal("hide");
|
||||
return true; // action expects boolean result
|
||||
@ -191,28 +191,28 @@
|
||||
url: `/api/v1/party/${partyId}/users`,
|
||||
type: "GET",
|
||||
dataType: "json",
|
||||
success: function (data) {
|
||||
table.bootstrapTable("load", data);
|
||||
success: response => {
|
||||
table.bootstrapTable("load", response);
|
||||
table.bootstrapTable("uncheckAll");
|
||||
table.bootstrapTable("hideLoading");
|
||||
},
|
||||
error: function (jqXHR, _, errorThrown) { requestAlert(jqXHR, errorThrown); },
|
||||
error: (jqXHR, _, errorThrown) => { requestAlert(jqXHR, errorThrown); },
|
||||
});
|
||||
}
|
||||
|
||||
function removeUsers() {
|
||||
const users = table.bootstrapTable("getSelections");
|
||||
users.map(function (user) {
|
||||
users.map(user => {
|
||||
$.ajax({
|
||||
url: `/api/v1/party/${partyId}/users/${user.username}`,
|
||||
type: "DELETE",
|
||||
success: function (_) { reload(); },
|
||||
error: function (jqXHR, _, errorThrown) { requestAlert(jqXHR, errorThrown); },
|
||||
success: _ => { reload(); },
|
||||
error: (jqXHR, _, errorThrown) => { requestAlert(jqXHR, errorThrown); },
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
$(function () {
|
||||
$(() => {
|
||||
setupFormClear(addUserDialog);
|
||||
setupRemoveButton(table, removeButton);
|
||||
|
||||
|
||||
@ -12,5 +12,6 @@
|
||||
</logger>
|
||||
<logger name="org.flywaydb.core.internal" level="INFO" />
|
||||
<logger name="com.zaxxer.hikari.pool" level="INFO" />
|
||||
<logger name="io.swagger" level="INFO" />
|
||||
|
||||
</configuration>
|
||||
|
||||
@ -4,7 +4,7 @@ me.arcanis.ffxivbis {
|
||||
include "item_data.json"
|
||||
|
||||
# xivapi base url, string, required
|
||||
xivapi-url = "https://xivapi.com"
|
||||
xivapi-url = "https://v2.xivapi.com"
|
||||
# xivapi developer key, string, optional
|
||||
#xivapi-key = "abcdef"
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2019-2022 Evgeniy Alekseev.
|
||||
* Copyright (c) 2021-2026 Evgeniy Alekseev.
|
||||
*
|
||||
* This file is part of ffxivbis
|
||||
* (see https://github.com/arcan1s/ffxivbis).
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2019-2022 Evgeniy Alekseev.
|
||||
* Copyright (c) 2021-2026 Evgeniy Alekseev.
|
||||
*
|
||||
* This file is part of ffxivbis
|
||||
* (see https://github.com/arcan1s/ffxivbis).
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2019-2022 Evgeniy Alekseev.
|
||||
* Copyright (c) 2021-2026 Evgeniy Alekseev.
|
||||
*
|
||||
* This file is part of ffxivbis
|
||||
* (see https://github.com/arcan1s/ffxivbis).
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2019-2022 Evgeniy Alekseev.
|
||||
* Copyright (c) 2021-2026 Evgeniy Alekseev.
|
||||
*
|
||||
* This file is part of ffxivbis
|
||||
* (see https://github.com/arcan1s/ffxivbis).
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2019-2022 Evgeniy Alekseev.
|
||||
* Copyright (c) 2021-2026 Evgeniy Alekseev.
|
||||
*
|
||||
* This file is part of ffxivbis
|
||||
* (see https://github.com/arcan1s/ffxivbis).
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2019-2022 Evgeniy Alekseev.
|
||||
* Copyright (c) 2021-2026 Evgeniy Alekseev.
|
||||
*
|
||||
* This file is part of ffxivbis
|
||||
* (see https://github.com/arcan1s/ffxivbis).
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2019-2022 Evgeniy Alekseev.
|
||||
* Copyright (c) 2021-2026 Evgeniy Alekseev.
|
||||
*
|
||||
* This file is part of 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 com.typesafe.scalalogging.StrictLogging
|
||||
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.messages.{BiSProviderMessage, Message}
|
||||
|
||||
@ -31,6 +32,7 @@ class RootEndpoint(system: ActorSystem[Nothing], storage: ActorRef[Message], pro
|
||||
private val auth = AuthorizationProvider(config, storage)
|
||||
|
||||
private val rootApiV1Endpoint = new RootApiV1Endpoint(storage, auth, provider, config)
|
||||
private val rootApiV2Endpoint = new RootApiV2Endpoint(storage, auth)
|
||||
private val rootView = new RootView(auth)
|
||||
private val swagger = new Swagger(config)
|
||||
|
||||
@ -47,6 +49,7 @@ class RootEndpoint(system: ActorSystem[Nothing], storage: ActorRef[Message], pro
|
||||
pathPrefix("api") {
|
||||
pathPrefix(Segment) {
|
||||
case "v1" => rootApiV1Endpoint.routes
|
||||
case "v2" => rootApiV2Endpoint.routes
|
||||
case _ => reject
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2019-2022 Evgeniy Alekseev.
|
||||
* Copyright (c) 2021-2026 Evgeniy Alekseev.
|
||||
*
|
||||
* This file is part of ffxivbis
|
||||
* (see https://github.com/arcan1s/ffxivbis).
|
||||
|
||||
@ -0,0 +1,24 @@
|
||||
/*
|
||||
* 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
|
||||
|
||||
import scala.collection.immutable.HashSet
|
||||
|
||||
trait ValidatorHelper {
|
||||
|
||||
def isValidString(string: String): Boolean = string.nonEmpty && string.forall(isValidSymbol)
|
||||
|
||||
def isValidSymbol(char: Char): Boolean =
|
||||
char.isLetterOrDigit || ValidatorHelper.VALID_CHARACTERS.contains(char)
|
||||
}
|
||||
|
||||
object ValidatorHelper {
|
||||
|
||||
final val VALID_CHARACTERS = HashSet.from("!@#$%^&*()-_=+;:',./?| ")
|
||||
}
|
||||
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2019-2022 Evgeniy Alekseev.
|
||||
* Copyright (c) 2021-2026 Evgeniy Alekseev.
|
||||
*
|
||||
* This file is part of ffxivbis
|
||||
* (see https://github.com/arcan1s/ffxivbis).
|
||||
@ -49,7 +49,12 @@ class BiSEndpoint(
|
||||
summary = "create best in slot",
|
||||
description = "Create the best in slot set",
|
||||
parameters = Array(
|
||||
new Parameter(name = "partyId", in = ParameterIn.PATH, description = "unique party ID", example = "abcdefgh"),
|
||||
new Parameter(
|
||||
name = "partyId",
|
||||
in = ParameterIn.PATH,
|
||||
description = "unique party ID",
|
||||
example = "o3KicHQPW5b0JcOm5yI3"
|
||||
),
|
||||
),
|
||||
requestBody = new RequestBody(
|
||||
description = "player best in slot description",
|
||||
@ -105,7 +110,12 @@ class BiSEndpoint(
|
||||
summary = "get best in slot",
|
||||
description = "Return the best in slot items",
|
||||
parameters = Array(
|
||||
new Parameter(name = "partyId", in = ParameterIn.PATH, description = "unique party ID", example = "abcdefgh"),
|
||||
new Parameter(
|
||||
name = "partyId",
|
||||
in = ParameterIn.PATH,
|
||||
description = "unique party ID",
|
||||
example = "o3KicHQPW5b0JcOm5yI3"
|
||||
),
|
||||
new Parameter(
|
||||
name = "nick",
|
||||
in = ParameterIn.QUERY,
|
||||
@ -167,7 +177,12 @@ class BiSEndpoint(
|
||||
summary = "modify best in slot",
|
||||
description = "Add or remove an item from the best in slot",
|
||||
parameters = Array(
|
||||
new Parameter(name = "partyId", in = ParameterIn.PATH, description = "unique party ID", example = "abcdefgh"),
|
||||
new Parameter(
|
||||
name = "partyId",
|
||||
in = ParameterIn.PATH,
|
||||
description = "unique party ID",
|
||||
example = "o3KicHQPW5b0JcOm5yI3"
|
||||
),
|
||||
),
|
||||
requestBody = new RequestBody(
|
||||
description = "action and piece description",
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2019-2022 Evgeniy Alekseev.
|
||||
* Copyright (c) 2021-2026 Evgeniy Alekseev.
|
||||
*
|
||||
* This file is part of ffxivbis
|
||||
* (see https://github.com/arcan1s/ffxivbis).
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2019-2022 Evgeniy Alekseev.
|
||||
* Copyright (c) 2021-2026 Evgeniy Alekseev.
|
||||
*
|
||||
* This file is part of ffxivbis
|
||||
* (see https://github.com/arcan1s/ffxivbis).
|
||||
@ -46,7 +46,12 @@ class LootEndpoint(override val storage: ActorRef[Message], override val auth: A
|
||||
summary = "get loot list",
|
||||
description = "Return the looted items",
|
||||
parameters = Array(
|
||||
new Parameter(name = "partyId", in = ParameterIn.PATH, description = "unique party ID", example = "abcdefgh"),
|
||||
new Parameter(
|
||||
name = "partyId",
|
||||
in = ParameterIn.PATH,
|
||||
description = "unique party ID",
|
||||
example = "o3KicHQPW5b0JcOm5yI3"
|
||||
),
|
||||
new Parameter(
|
||||
name = "nick",
|
||||
in = ParameterIn.QUERY,
|
||||
@ -107,7 +112,12 @@ class LootEndpoint(override val storage: ActorRef[Message], override val auth: A
|
||||
summary = "modify loot list",
|
||||
description = "Add or remove an item from the loot list",
|
||||
parameters = Array(
|
||||
new Parameter(name = "partyId", in = ParameterIn.PATH, description = "unique party ID", example = "abcdefgh"),
|
||||
new Parameter(
|
||||
name = "partyId",
|
||||
in = ParameterIn.PATH,
|
||||
description = "unique party ID",
|
||||
example = "o3KicHQPW5b0JcOm5yI3"
|
||||
),
|
||||
),
|
||||
requestBody = new RequestBody(
|
||||
description = "action and piece description",
|
||||
@ -164,7 +174,12 @@ class LootEndpoint(override val storage: ActorRef[Message], override val auth: A
|
||||
summary = "suggest loot",
|
||||
description = "Suggest loot piece to party",
|
||||
parameters = Array(
|
||||
new Parameter(name = "partyId", in = ParameterIn.PATH, description = "unique party ID", example = "abcdefgh"),
|
||||
new Parameter(
|
||||
name = "partyId",
|
||||
in = ParameterIn.PATH,
|
||||
description = "unique party ID",
|
||||
example = "o3KicHQPW5b0JcOm5yI3"
|
||||
),
|
||||
),
|
||||
requestBody = new RequestBody(
|
||||
description = "piece description",
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2019-2022 Evgeniy Alekseev.
|
||||
* Copyright (c) 2021-2026 Evgeniy Alekseev.
|
||||
*
|
||||
* This file is part of ffxivbis
|
||||
* (see https://github.com/arcan1s/ffxivbis).
|
||||
@ -49,7 +49,12 @@ class PartyEndpoint(
|
||||
summary = "get party description",
|
||||
description = "Return the party description",
|
||||
parameters = Array(
|
||||
new Parameter(name = "partyId", in = ParameterIn.PATH, description = "unique party ID", example = "abcdefgh"),
|
||||
new Parameter(
|
||||
name = "partyId",
|
||||
in = ParameterIn.PATH,
|
||||
description = "unique party ID",
|
||||
example = "o3KicHQPW5b0JcOm5yI3"
|
||||
),
|
||||
),
|
||||
responses = Array(
|
||||
new ApiResponse(
|
||||
@ -96,7 +101,12 @@ class PartyEndpoint(
|
||||
summary = "modify party description",
|
||||
description = "Edit party description",
|
||||
parameters = Array(
|
||||
new Parameter(name = "partyId", in = ParameterIn.PATH, description = "unique party ID", example = "abcdefgh"),
|
||||
new Parameter(
|
||||
name = "partyId",
|
||||
in = ParameterIn.PATH,
|
||||
description = "unique party ID",
|
||||
example = "o3KicHQPW5b0JcOm5yI3"
|
||||
),
|
||||
),
|
||||
requestBody = new RequestBody(
|
||||
description = "new party description",
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2019-2022 Evgeniy Alekseev.
|
||||
* Copyright (c) 2021-2026 Evgeniy Alekseev.
|
||||
*
|
||||
* This file is part of ffxivbis
|
||||
* (see https://github.com/arcan1s/ffxivbis).
|
||||
@ -50,7 +50,12 @@ class PlayerEndpoint(
|
||||
summary = "get party",
|
||||
description = "Return the players who belong to the party",
|
||||
parameters = Array(
|
||||
new Parameter(name = "partyId", in = ParameterIn.PATH, description = "unique party ID", example = "abcdefgh"),
|
||||
new Parameter(
|
||||
name = "partyId",
|
||||
in = ParameterIn.PATH,
|
||||
description = "unique party ID",
|
||||
example = "o3KicHQPW5b0JcOm5yI3"
|
||||
),
|
||||
new Parameter(
|
||||
name = "nick",
|
||||
in = ParameterIn.QUERY,
|
||||
@ -111,7 +116,12 @@ class PlayerEndpoint(
|
||||
summary = "get party statistics",
|
||||
description = "Return the party statistics",
|
||||
parameters = Array(
|
||||
new Parameter(name = "partyId", in = ParameterIn.PATH, description = "unique party ID", example = "abcdefgh"),
|
||||
new Parameter(
|
||||
name = "partyId",
|
||||
in = ParameterIn.PATH,
|
||||
description = "unique party ID",
|
||||
example = "o3KicHQPW5b0JcOm5yI3"
|
||||
),
|
||||
new Parameter(
|
||||
name = "nick",
|
||||
in = ParameterIn.QUERY,
|
||||
@ -172,7 +182,12 @@ class PlayerEndpoint(
|
||||
summary = "modify party",
|
||||
description = "Add or remove a player from party list",
|
||||
parameters = Array(
|
||||
new Parameter(name = "partyId", in = ParameterIn.PATH, description = "unique party ID", example = "abcdefgh"),
|
||||
new Parameter(
|
||||
name = "partyId",
|
||||
in = ParameterIn.PATH,
|
||||
description = "unique party ID",
|
||||
example = "o3KicHQPW5b0JcOm5yI3"
|
||||
),
|
||||
),
|
||||
requestBody = new RequestBody(
|
||||
description = "player description",
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2019-2022 Evgeniy Alekseev.
|
||||
* Copyright (c) 2021-2026 Evgeniy Alekseev.
|
||||
*
|
||||
* This file is part of ffxivbis
|
||||
* (see https://github.com/arcan1s/ffxivbis).
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2019-2022 Evgeniy Alekseev.
|
||||
* Copyright (c) 2021-2026 Evgeniy Alekseev.
|
||||
*
|
||||
* This file is part of ffxivbis
|
||||
* (see https://github.com/arcan1s/ffxivbis).
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2019-2022 Evgeniy Alekseev.
|
||||
* Copyright (c) 2021-2026 Evgeniy Alekseev.
|
||||
*
|
||||
* This file is part of ffxivbis
|
||||
* (see https://github.com/arcan1s/ffxivbis).
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2019-2022 Evgeniy Alekseev.
|
||||
* Copyright (c) 2021-2026 Evgeniy Alekseev.
|
||||
*
|
||||
* This file is part of ffxivbis
|
||||
* (see https://github.com/arcan1s/ffxivbis).
|
||||
@ -96,7 +96,12 @@ class UserEndpoint(override val storage: ActorRef[Message], override val auth: A
|
||||
summary = "create new user",
|
||||
description = "Add an user to the specified party",
|
||||
parameters = Array(
|
||||
new Parameter(name = "partyId", in = ParameterIn.PATH, description = "unique party ID", example = "abcdefgh"),
|
||||
new Parameter(
|
||||
name = "partyId",
|
||||
in = ParameterIn.PATH,
|
||||
description = "unique party ID",
|
||||
example = "o3KicHQPW5b0JcOm5yI3"
|
||||
),
|
||||
),
|
||||
requestBody = new RequestBody(
|
||||
description = "user description",
|
||||
@ -151,7 +156,12 @@ class UserEndpoint(override val storage: ActorRef[Message], override val auth: A
|
||||
summary = "remove user",
|
||||
description = "Remove an user from the specified party",
|
||||
parameters = Array(
|
||||
new Parameter(name = "partyId", in = ParameterIn.PATH, description = "unique party ID", example = "abcdefgh"),
|
||||
new Parameter(
|
||||
name = "partyId",
|
||||
in = ParameterIn.PATH,
|
||||
description = "unique party ID",
|
||||
example = "o3KicHQPW5b0JcOm5yI3"
|
||||
),
|
||||
new Parameter(name = "username", in = ParameterIn.PATH, description = "username to remove", example = "siuan"),
|
||||
),
|
||||
responses = Array(
|
||||
@ -195,7 +205,12 @@ class UserEndpoint(override val storage: ActorRef[Message], override val auth: A
|
||||
summary = "get users",
|
||||
description = "Return the list of users belong to party",
|
||||
parameters = Array(
|
||||
new Parameter(name = "partyId", in = ParameterIn.PATH, description = "unique party ID", example = "abcdefgh"),
|
||||
new Parameter(
|
||||
name = "partyId",
|
||||
in = ParameterIn.PATH,
|
||||
description = "unique party ID",
|
||||
example = "o3KicHQPW5b0JcOm5yI3"
|
||||
),
|
||||
),
|
||||
responses = Array(
|
||||
new ApiResponse(
|
||||
@ -246,7 +261,12 @@ class UserEndpoint(override val storage: ActorRef[Message], override val auth: A
|
||||
summary = "get current user",
|
||||
description = "Return the current user descriptor",
|
||||
parameters = Array(
|
||||
new Parameter(name = "partyId", in = ParameterIn.PATH, description = "unique party ID", example = "abcdefgh"),
|
||||
new Parameter(
|
||||
name = "partyId",
|
||||
in = ParameterIn.PATH,
|
||||
description = "unique party ID",
|
||||
example = "o3KicHQPW5b0JcOm5yI3"
|
||||
),
|
||||
),
|
||||
responses = Array(
|
||||
new ApiResponse(
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2019-2022 Evgeniy Alekseev.
|
||||
* Copyright (c) 2021-2026 Evgeniy Alekseev.
|
||||
*
|
||||
* This file is part of ffxivbis
|
||||
* (see https://github.com/arcan1s/ffxivbis).
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2019-2022 Evgeniy Alekseev.
|
||||
* Copyright (c) 2021-2026 Evgeniy Alekseev.
|
||||
*
|
||||
* This file is part of ffxivbis
|
||||
* (see https://github.com/arcan1s/ffxivbis).
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2019-2022 Evgeniy Alekseev.
|
||||
* Copyright (c) 2021-2026 Evgeniy Alekseev.
|
||||
*
|
||||
* This file is part of ffxivbis
|
||||
* (see https://github.com/arcan1s/ffxivbis).
|
||||
@ -16,12 +16,12 @@ import java.time.Instant
|
||||
|
||||
trait JsonSupport extends SprayJsonSupport with DefaultJsonProtocol {
|
||||
|
||||
private def enumFormat[E <: Enumeration](enum: E): RootJsonFormat[E#Value] =
|
||||
private def enumFormat[E <: Enumeration](enumeration: E): RootJsonFormat[E#Value] =
|
||||
new RootJsonFormat[E#Value] {
|
||||
override def write(obj: E#Value): JsValue = obj.toString.toJson
|
||||
override def read(json: JsValue): E#Value = json match {
|
||||
case JsNumber(value) => enum(value.toInt)
|
||||
case JsString(name) => enum.withName(name)
|
||||
case JsNumber(value) => enumeration(value.toInt)
|
||||
case JsString(name) => enumeration.withName(name)
|
||||
case other => deserializationError(s"String or number expected, got $other")
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2019-2022 Evgeniy Alekseev.
|
||||
* Copyright (c) 2021-2026 Evgeniy Alekseev.
|
||||
*
|
||||
* This file is part of ffxivbis
|
||||
* (see https://github.com/arcan1s/ffxivbis).
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2019-2022 Evgeniy Alekseev.
|
||||
* Copyright (c) 2021-2026 Evgeniy Alekseev.
|
||||
*
|
||||
* This file is part of ffxivbis
|
||||
* (see https://github.com/arcan1s/ffxivbis).
|
||||
@ -12,9 +12,11 @@ import io.swagger.v3.oas.annotations.media.Schema
|
||||
import me.arcanis.ffxivbis.models.PartyDescription
|
||||
|
||||
case class PartyDescriptionModel(
|
||||
@Schema(description = "party id", required = true, example = "abcdefgh") partyId: String,
|
||||
@Schema(description = "party id", required = true, example = "o3KicHQPW5b0JcOm5yI3") partyId: String,
|
||||
@Schema(description = "party name") partyAlias: Option[String]
|
||||
) {
|
||||
) extends Validator {
|
||||
|
||||
require(partyAlias.forall(isValidString), stringMatchError("Party alias"))
|
||||
|
||||
def toDescription: PartyDescription = PartyDescription(partyId, partyAlias)
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2019-2022 Evgeniy Alekseev.
|
||||
* Copyright (c) 2021-2026 Evgeniy Alekseev.
|
||||
*
|
||||
* This file is part of ffxivbis
|
||||
* (see https://github.com/arcan1s/ffxivbis).
|
||||
@ -10,4 +10,6 @@ package me.arcanis.ffxivbis.http.api.v1.json
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema
|
||||
|
||||
case class PartyIdModel(@Schema(description = "party id", required = true, example = "abcdefgh") partyId: String)
|
||||
case class PartyIdModel(
|
||||
@Schema(description = "party id", required = true, example = "o3KicHQPW5b0JcOm5yI3") partyId: String
|
||||
)
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2019-2022 Evgeniy Alekseev.
|
||||
* Copyright (c) 2021-2026 Evgeniy Alekseev.
|
||||
*
|
||||
* This file is part of ffxivbis
|
||||
* (see https://github.com/arcan1s/ffxivbis).
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2019-2022 Evgeniy Alekseev.
|
||||
* Copyright (c) 2021-2026 Evgeniy Alekseev.
|
||||
*
|
||||
* This file is part of ffxivbis
|
||||
* (see https://github.com/arcan1s/ffxivbis).
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2019-2022 Evgeniy Alekseev.
|
||||
* Copyright (c) 2021-2026 Evgeniy Alekseev.
|
||||
*
|
||||
* This file is part of ffxivbis
|
||||
* (see https://github.com/arcan1s/ffxivbis).
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2019-2022 Evgeniy Alekseev.
|
||||
* Copyright (c) 2021-2026 Evgeniy Alekseev.
|
||||
*
|
||||
* This file is part of ffxivbis
|
||||
* (see https://github.com/arcan1s/ffxivbis).
|
||||
@ -17,4 +17,7 @@ case class PlayerBiSLinkModel(
|
||||
example = "https://ffxiv.ariyala.com/19V5R"
|
||||
) link: String,
|
||||
@Schema(description = "player description", required = true) playerId: PlayerIdModel
|
||||
)
|
||||
) extends Validator {
|
||||
|
||||
require(isValidString(link), stringMatchError("BiS link"))
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2019-2022 Evgeniy Alekseev.
|
||||
* Copyright (c) 2021-2026 Evgeniy Alekseev.
|
||||
*
|
||||
* This file is part of ffxivbis
|
||||
* (see https://github.com/arcan1s/ffxivbis).
|
||||
@ -12,10 +12,14 @@ import io.swagger.v3.oas.annotations.media.Schema
|
||||
import me.arcanis.ffxivbis.models.{Job, PlayerId}
|
||||
|
||||
case class PlayerIdModel(
|
||||
@Schema(description = "unique party ID. Required in responses", example = "abcdefgh") partyId: Option[String],
|
||||
@Schema(description = "unique party ID. Required in responses", example = "o3KicHQPW5b0JcOm5yI3") partyId: Option[
|
||||
String
|
||||
],
|
||||
@Schema(description = "job name", required = true, example = "DNC") job: String,
|
||||
@Schema(description = "player nick name", required = true, example = "Siuan Sanche") nick: String
|
||||
) {
|
||||
) extends Validator {
|
||||
|
||||
require(isValidString(nick), stringMatchError("Player name"))
|
||||
|
||||
def withPartyId(partyId: String): PlayerId =
|
||||
PlayerId(partyId, Job.withName(job), nick)
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2019-2022 Evgeniy Alekseev.
|
||||
* Copyright (c) 2021-2026 Evgeniy Alekseev.
|
||||
*
|
||||
* This file is part of ffxivbis
|
||||
* (see https://github.com/arcan1s/ffxivbis).
|
||||
@ -12,7 +12,7 @@ import io.swagger.v3.oas.annotations.media.Schema
|
||||
import me.arcanis.ffxivbis.models.PlayerIdWithCounters
|
||||
|
||||
case class PlayerIdWithCountersModel(
|
||||
@Schema(description = "unique party ID", required = true, example = "abcdefgh") partyId: String,
|
||||
@Schema(description = "unique party ID", required = true, example = "o3KicHQPW5b0JcOm5yI3") partyId: String,
|
||||
@Schema(description = "job name", required = true, example = "DNC") job: String,
|
||||
@Schema(description = "player nick name", required = true, example = "Siuan Sanche") nick: String,
|
||||
@Schema(description = "is piece required by player or not", required = true) isRequired: Boolean,
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2019-2022 Evgeniy Alekseev.
|
||||
* Copyright (c) 2021-2026 Evgeniy Alekseev.
|
||||
*
|
||||
* This file is part of ffxivbis
|
||||
* (see https://github.com/arcan1s/ffxivbis).
|
||||
@ -12,7 +12,7 @@ import io.swagger.v3.oas.annotations.media.Schema
|
||||
import me.arcanis.ffxivbis.models.{BiS, Job, Player}
|
||||
|
||||
case class PlayerModel(
|
||||
@Schema(description = "unique party ID", required = true, example = "abcdefgh") partyId: String,
|
||||
@Schema(description = "unique party ID", required = true, example = "o3KicHQPW5b0JcOm5yI3") partyId: String,
|
||||
@Schema(description = "job name", required = true, example = "DNC") job: String,
|
||||
@Schema(description = "player nick name", required = true, example = "Siuan Sanche") nick: String,
|
||||
@Schema(description = "pieces in best in slot") bis: Option[Seq[PieceModel]],
|
||||
@ -24,7 +24,10 @@ case class PlayerModel(
|
||||
`type` = "number"
|
||||
) lootCountBiS: Option[Int],
|
||||
@Schema(description = "total count of looted pieces", `type` = "number") lootCountTotal: Option[Int],
|
||||
) {
|
||||
) extends Validator {
|
||||
|
||||
require(isValidString(nick), stringMatchError("Player name"))
|
||||
require(link.forall(isValidString), stringMatchError("BiS link"))
|
||||
|
||||
def toPlayer: Player =
|
||||
Player(
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2019-2022 Evgeniy Alekseev.
|
||||
* Copyright (c) 2021-2026 Evgeniy Alekseev.
|
||||
*
|
||||
* This file is part of ffxivbis
|
||||
* (see https://github.com/arcan1s/ffxivbis).
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2019-2022 Evgeniy Alekseev.
|
||||
* Copyright (c) 2021-2026 Evgeniy Alekseev.
|
||||
*
|
||||
* This file is part of ffxivbis
|
||||
* (see https://github.com/arcan1s/ffxivbis).
|
||||
@ -12,23 +12,30 @@ import io.swagger.v3.oas.annotations.media.Schema
|
||||
import me.arcanis.ffxivbis.models.{Permission, User}
|
||||
|
||||
case class UserModel(
|
||||
@Schema(description = "unique party ID", required = true, example = "abcdefgh") partyId: String,
|
||||
@Schema(description = "unique party ID", required = true, example = "o3KicHQPW5b0JcOm5yI3") partyId: String,
|
||||
@Schema(description = "username to login to party", required = true, example = "siuan") username: String,
|
||||
@Schema(description = "password to login to party", required = true, example = "pa55w0rd") password: String,
|
||||
@Schema(description = "password to login to party, required for user editing", example = "pa55w0rd") password: Option[
|
||||
String
|
||||
],
|
||||
@Schema(
|
||||
description = "user permission",
|
||||
defaultValue = "get",
|
||||
`type` = "string",
|
||||
allowableValues = Array("get", "post", "admin")
|
||||
) permission: Option[Permission.Value] = None
|
||||
) {
|
||||
) extends Validator {
|
||||
|
||||
require(isValidString(username), stringMatchError("Username"))
|
||||
require(password.forall(_.nonEmpty), "Password must not be empty")
|
||||
|
||||
def toUser: User =
|
||||
User(partyId, username, password, permission.getOrElse(Permission.get))
|
||||
password.fold(throw new IllegalArgumentException("Password must noot be empty"))(
|
||||
User(partyId, username, _, permission.getOrElse(Permission.get))
|
||||
)
|
||||
}
|
||||
|
||||
object UserModel {
|
||||
|
||||
def fromUser(user: User): UserModel =
|
||||
UserModel(user.partyId, user.username, "", Some(user.permission))
|
||||
UserModel(user.partyId, user.username, None, Some(user.permission))
|
||||
}
|
||||
|
||||
@ -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.v1.json
|
||||
|
||||
import me.arcanis.ffxivbis.http.ValidatorHelper
|
||||
|
||||
trait Validator extends ValidatorHelper {
|
||||
|
||||
def stringMatchError(what: String): String =
|
||||
s"$what must contain only letters or digits or one of (${ValidatorHelper.VALID_CHARACTERS.mkString(", ")})"
|
||||
}
|
||||
@ -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 => }
|
||||
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
@ -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))
|
||||
}
|
||||
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2019-2022 Evgeniy Alekseev.
|
||||
* Copyright (c) 2021-2026 Evgeniy Alekseev.
|
||||
*
|
||||
* This file is part of ffxivbis
|
||||
* (see https://github.com/arcan1s/ffxivbis).
|
||||
@ -45,13 +45,15 @@ trait BiSHelper extends BisProviderHelper {
|
||||
timeout: Timeout,
|
||||
scheduler: Scheduler
|
||||
): Future[Unit] =
|
||||
storage.ask(RemovePiecesFromBiS(playerId, _)).flatMap { _ =>
|
||||
downloadBiS(link, playerId.job)
|
||||
.flatMap { bis =>
|
||||
Future.traverse(bis.pieces)(addPieceBiS(playerId, _))
|
||||
}
|
||||
.map(_ => ())
|
||||
}
|
||||
storage
|
||||
.ask(RemovePiecesFromBiS(playerId, _))
|
||||
.flatMap { _ =>
|
||||
downloadBiS(link, playerId.job)
|
||||
.flatMap { bis =>
|
||||
Future.traverse(bis.pieces)(addPieceBiS(playerId, _))
|
||||
}
|
||||
}
|
||||
.flatMap(_ => storage.ask(UpdateBiSLink(playerId, link, _)))
|
||||
|
||||
def removePieceBiS(playerId: PlayerId, piece: Piece)(implicit timeout: Timeout, scheduler: Scheduler): Future[Unit] =
|
||||
storage.ask(RemovePieceFromBiS(playerId, piece, _))
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2019-2022 Evgeniy Alekseev.
|
||||
* Copyright (c) 2021-2026 Evgeniy Alekseev.
|
||||
*
|
||||
* This file is part of ffxivbis
|
||||
* (see https://github.com/arcan1s/ffxivbis).
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2019-2022 Evgeniy Alekseev.
|
||||
* Copyright (c) 2021-2026 Evgeniy Alekseev.
|
||||
*
|
||||
* This file is part of ffxivbis
|
||||
* (see https://github.com/arcan1s/ffxivbis).
|
||||
@ -56,4 +56,11 @@ trait LootHelper {
|
||||
scheduler: Scheduler
|
||||
): Future[Seq[PlayerIdWithCounters]] =
|
||||
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)
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2019-2022 Evgeniy Alekseev.
|
||||
* Copyright (c) 2021-2026 Evgeniy Alekseev.
|
||||
*
|
||||
* This file is part of ffxivbis
|
||||
* (see https://github.com/arcan1s/ffxivbis).
|
||||
@ -27,15 +27,15 @@ trait PlayerHelper extends BisProviderHelper {
|
||||
)(implicit executionContext: ExecutionContext, timeout: Timeout, scheduler: Scheduler): Future[Unit] =
|
||||
storage
|
||||
.ask(ref => AddPlayer(player, ref))
|
||||
.map { res =>
|
||||
.map { _ =>
|
||||
player.link.map(_.trim).filter(_.nonEmpty) match {
|
||||
case Some(link) =>
|
||||
downloadBiS(link, player.job)
|
||||
.map { bis =>
|
||||
bis.pieces.map(piece => storage.ask(AddPieceToBis(player.playerId, piece, _)))
|
||||
}
|
||||
.map(_ => res)
|
||||
case None => Future.successful(res)
|
||||
.flatMap(_ => storage.ask(UpdateBiSLink(player.playerId, link, _)))
|
||||
case None => Future.successful(())
|
||||
}
|
||||
}
|
||||
.flatten
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2019-2022 Evgeniy Alekseev.
|
||||
* Copyright (c) 2021-2026 Evgeniy Alekseev.
|
||||
*
|
||||
* This file is part of ffxivbis
|
||||
* (see https://github.com/arcan1s/ffxivbis).
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2019-2022 Evgeniy Alekseev.
|
||||
* Copyright (c) 2021-2026 Evgeniy Alekseev.
|
||||
*
|
||||
* This file is part of ffxivbis
|
||||
* (see https://github.com/arcan1s/ffxivbis).
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2019-2022 Evgeniy Alekseev.
|
||||
* Copyright (c) 2021-2026 Evgeniy Alekseev.
|
||||
*
|
||||
* This file is part of ffxivbis
|
||||
* (see https://github.com/arcan1s/ffxivbis).
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2019-2022 Evgeniy Alekseev.
|
||||
* Copyright (c) 2021-2026 Evgeniy Alekseev.
|
||||
*
|
||||
* This file is part of ffxivbis
|
||||
* (see https://github.com/arcan1s/ffxivbis).
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2019-2022 Evgeniy Alekseev.
|
||||
* Copyright (c) 2021-2026 Evgeniy Alekseev.
|
||||
*
|
||||
* This file is part of ffxivbis
|
||||
* (see https://github.com/arcan1s/ffxivbis).
|
||||
@ -69,6 +69,11 @@ object DatabaseMessage {
|
||||
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
|
||||
trait PartyDatabaseMessage extends DatabaseMessage
|
||||
|
||||
@ -95,6 +100,11 @@ object DatabaseMessage {
|
||||
override val isReadOnly: Boolean = false
|
||||
}
|
||||
|
||||
case class UpdateBiSLink(playerId: PlayerId, link: String, actorRef: ActorRef[Unit]) extends PartyDatabaseMessage {
|
||||
override val partyId: String = playerId.partyId
|
||||
override val isReadOnly: Boolean = false
|
||||
}
|
||||
|
||||
case class UpdateParty(partyDescription: PartyDescription, replyTo: ActorRef[Unit]) extends PartyDatabaseMessage {
|
||||
override val partyId: String = partyDescription.partyId
|
||||
override val isReadOnly: Boolean = false
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2019-2022 Evgeniy Alekseev.
|
||||
* Copyright (c) 2021-2026 Evgeniy Alekseev.
|
||||
*
|
||||
* This file is part of ffxivbis
|
||||
* (see https://github.com/arcan1s/ffxivbis).
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2019-2022 Evgeniy Alekseev.
|
||||
* Copyright (c) 2021-2026 Evgeniy Alekseev.
|
||||
*
|
||||
* This file is part of ffxivbis
|
||||
* (see https://github.com/arcan1s/ffxivbis).
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2019-2022 Evgeniy Alekseev.
|
||||
* Copyright (c) 2021-2026 Evgeniy Alekseev.
|
||||
*
|
||||
* This file is part of ffxivbis
|
||||
* (see https://github.com/arcan1s/ffxivbis).
|
||||
@ -95,6 +95,7 @@ object Job {
|
||||
case object RPR extends Drgs
|
||||
case object NIN extends Nins
|
||||
case object SAM extends Mnks
|
||||
case object VPR extends Mnks
|
||||
|
||||
case object BRD extends Ranges
|
||||
case object MCH extends Ranges
|
||||
@ -103,9 +104,10 @@ object Job {
|
||||
case object BLM extends Casters
|
||||
case object SMN extends Casters
|
||||
case object RDM extends Casters
|
||||
case object PCT extends Casters
|
||||
|
||||
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)
|
||||
|
||||
def withName(job: String): Job =
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2019-2022 Evgeniy Alekseev.
|
||||
* Copyright (c) 2021-2026 Evgeniy Alekseev.
|
||||
*
|
||||
* This file is part of ffxivbis
|
||||
* (see https://github.com/arcan1s/ffxivbis).
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2019-2022 Evgeniy Alekseev.
|
||||
* Copyright (c) 2021-2026 Evgeniy Alekseev.
|
||||
*
|
||||
* This file is part of ffxivbis
|
||||
* (see https://github.com/arcan1s/ffxivbis).
|
||||
@ -35,8 +35,8 @@ case class Party(partyDescription: PartyDescription, rules: Seq[String], players
|
||||
this
|
||||
}
|
||||
|
||||
def suggestLoot(piece: Piece): LootSelector.LootSelectorResult =
|
||||
LootSelector(getPlayers, piece, rules)
|
||||
def suggestLoot(pieces: Seq[Piece]): LootSelector.LootSelectorResult =
|
||||
LootSelector(getPlayers, pieces, rules)
|
||||
}
|
||||
|
||||
object Party {
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2019-2022 Evgeniy Alekseev.
|
||||
* Copyright (c) 2021-2026 Evgeniy Alekseev.
|
||||
*
|
||||
* This file is part of ffxivbis
|
||||
* (see https://github.com/arcan1s/ffxivbis).
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2019-2022 Evgeniy Alekseev.
|
||||
* Copyright (c) 2021-2026 Evgeniy Alekseev.
|
||||
*
|
||||
* This file is part of ffxivbis
|
||||
* (see https://github.com/arcan1s/ffxivbis).
|
||||
@ -113,7 +113,7 @@ object Piece {
|
||||
case "weapon" => Weapon(pieceType, job)
|
||||
case "head" => Head(pieceType, job)
|
||||
case "body" => Body(pieceType, job)
|
||||
case "hands" => Hands(pieceType, job)
|
||||
case "hand" | "hands" => Hands(pieceType, job)
|
||||
case "legs" => Legs(pieceType, job)
|
||||
case "feet" => Feet(pieceType, job)
|
||||
case "ears" => Ears(pieceType, job)
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2019-2022 Evgeniy Alekseev.
|
||||
* Copyright (c) 2021-2026 Evgeniy Alekseev.
|
||||
*
|
||||
* This file is part of ffxivbis
|
||||
* (see https://github.com/arcan1s/ffxivbis).
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2019-2022 Evgeniy Alekseev.
|
||||
* Copyright (c) 2021-2026 Evgeniy Alekseev.
|
||||
*
|
||||
* This file is part of ffxivbis
|
||||
* (see https://github.com/arcan1s/ffxivbis).
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2019-2022 Evgeniy Alekseev.
|
||||
* Copyright (c) 2021-2026 Evgeniy Alekseev.
|
||||
*
|
||||
* This file is part of ffxivbis
|
||||
* (see https://github.com/arcan1s/ffxivbis).
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2019-2022 Evgeniy Alekseev.
|
||||
* Copyright (c) 2021-2026 Evgeniy Alekseev.
|
||||
*
|
||||
* This file is part of ffxivbis
|
||||
* (see https://github.com/arcan1s/ffxivbis).
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2019-2022 Evgeniy Alekseev.
|
||||
* Copyright (c) 2021-2026 Evgeniy Alekseev.
|
||||
*
|
||||
* This file is part of ffxivbis
|
||||
* (see https://github.com/arcan1s/ffxivbis).
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2019-2022 Evgeniy Alekseev.
|
||||
* Copyright (c) 2021-2026 Evgeniy Alekseev.
|
||||
*
|
||||
* This file is part of ffxivbis
|
||||
* (see https://github.com/arcan1s/ffxivbis).
|
||||
@ -10,9 +10,9 @@ package me.arcanis.ffxivbis.service
|
||||
|
||||
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 =
|
||||
LootSelector.LootSelectorResult {
|
||||
@ -22,8 +22,8 @@ class LootSelector(players: Seq[Player], piece: Piece, orderBy: Seq[String]) {
|
||||
|
||||
object LootSelector {
|
||||
|
||||
def apply(players: Seq[Player], piece: Piece, orderBy: Seq[String]): LootSelectorResult =
|
||||
new LootSelector(players, piece, orderBy).suggest
|
||||
def apply(players: Seq[Player], pieces: Seq[Piece], orderBy: Seq[String]): LootSelectorResult =
|
||||
new LootSelector(players, pieces, orderBy).suggest
|
||||
|
||||
case class LootSelectorResult(result: Seq[PlayerIdWithCounters])
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2019-2022 Evgeniy Alekseev.
|
||||
* Copyright (c) 2021-2026 Evgeniy Alekseev.
|
||||
*
|
||||
* This file is part of ffxivbis
|
||||
* (see https://github.com/arcan1s/ffxivbis).
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2019-2022 Evgeniy Alekseev.
|
||||
* Copyright (c) 2021-2026 Evgeniy Alekseev.
|
||||
*
|
||||
* This file is part of 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.models.{BiS, Job, Piece, PieceType}
|
||||
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 java.nio.file.Paths
|
||||
@ -52,7 +52,10 @@ class BisProvider(context: ActorContext[BiSProviderMessage])
|
||||
val url = Uri(link)
|
||||
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)
|
||||
sendRequest(uri, BisProvider.parseBisJsonToPieces(job, parser, getPieceType))
|
||||
} catch {
|
||||
@ -81,12 +84,11 @@ object BisProvider {
|
||||
}
|
||||
}
|
||||
|
||||
def remapKey(key: String): Option[String] = key match {
|
||||
case "mainhand" => Some("weapon")
|
||||
case "chest" => Some("body")
|
||||
case "ringLeft" | "fingerL" => Some("left ring")
|
||||
case "ringRight" | "fingerR" => Some("right ring")
|
||||
case "weapon" | "head" | "body" | "hands" | "legs" | "feet" | "ears" | "neck" | "wrist" | "wrists" => Some(key)
|
||||
case _ => None
|
||||
def remapKey(key: String): Option[String] = Some(key.toLowerCase).collect {
|
||||
case "mainhand" => "weapon"
|
||||
case "chest" => "body"
|
||||
case "ringleft" | "fingerl" => "left ring"
|
||||
case "ringright" | "fingerr" => "right ring"
|
||||
case "weapon" | "head" | "body" | "hand" | "hands" | "legs" | "feet" | "ears" | "neck" | "wrist" | "wrists" => key
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2019-2022 Evgeniy Alekseev.
|
||||
* Copyright (c) 2021-2026 Evgeniy Alekseev.
|
||||
*
|
||||
* This file is part of ffxivbis
|
||||
* (see https://github.com/arcan1s/ffxivbis).
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2019-2022 Evgeniy Alekseev.
|
||||
* Copyright (c) 2021-2026 Evgeniy Alekseev.
|
||||
*
|
||||
* This file is part of ffxivbis
|
||||
* (see https://github.com/arcan1s/ffxivbis).
|
||||
@ -40,102 +40,39 @@ trait XivApi extends RequestExecutor {
|
||||
else remotePieceType(remote).map(_ ++ local)
|
||||
}
|
||||
|
||||
private def remotePieceType(itemIds: Seq[Long]): Future[Map[Long, 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("")
|
||||
private def remotePieceType(itemIds: Seq[Long]): Future[Map[Long, PieceType]] =
|
||||
Future
|
||||
.traverse(itemIds) { id =>
|
||||
val uriForItem = Uri(xivapiUrl)
|
||||
.withPath(Uri.Path / "api" / "sheet" / "Item" / id.toString)
|
||||
.withQuery(
|
||||
Uri.Query(
|
||||
Map(
|
||||
"fields" -> Seq("Lot").mkString(","),
|
||||
"private_key" -> xivapiKey.getOrElse("")
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
sendRequest(uriForShops, XivApi.parseXivapiJsonToType(shops))
|
||||
}
|
||||
}
|
||||
sendRequest(uriForItem, XivApi.parseXivapiJsonToLot).map(id -> _)
|
||||
}
|
||||
.map(_.toMap)
|
||||
}
|
||||
|
||||
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"))
|
||||
|
||||
private def parseXivapiJsonToLot(js: JsObject)(implicit executionContext: ExecutionContext): Future[PieceType] =
|
||||
Future {
|
||||
js.fields("Results") match {
|
||||
case array: JsArray =>
|
||||
array.elements
|
||||
.map(_.asJsObject.getFields("ID", "GameContentLinks") match {
|
||||
case Seq(JsNumber(id), shop: JsObject) => 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]] =
|
||||
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")
|
||||
js.fields("fields") match {
|
||||
case JsObject(fields) =>
|
||||
fields
|
||||
.get("Lot")
|
||||
.collect {
|
||||
case JsBoolean(true) => PieceType.Savage
|
||||
case JsBoolean(false) => PieceType.Tome
|
||||
}
|
||||
}.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(throw new Exception(s"${shopMap(shopId).fields(s"ItemCost$index")}, $index"))
|
||||
.getFields("IsUnique", "StackSize") match {
|
||||
case Seq(JsNumber(isUnique), JsNumber(stackSize)) =>
|
||||
if (isUnique == 1 || stackSize.toLong != 999) PieceType.Tome // either upgraded gear or tomes found
|
||||
else PieceType.Savage
|
||||
case other => throw deserializationError(s"Could not parse $other")
|
||||
}
|
||||
itemId -> pieceType
|
||||
.getOrElse(throw deserializationError(s"Could not find lot field in $fields"))
|
||||
case other => throw deserializationError(s"Could not read fields as object from $other")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2019-2022 Evgeniy Alekseev.
|
||||
* Copyright (c) 2021-2026 Evgeniy Alekseev.
|
||||
*
|
||||
* This file is part of ffxivbis
|
||||
* (see https://github.com/arcan1s/ffxivbis).
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2019-2022 Evgeniy Alekseev.
|
||||
* Copyright (c) 2021-2026 Evgeniy Alekseev.
|
||||
*
|
||||
* This file is part of ffxivbis
|
||||
* (see https://github.com/arcan1s/ffxivbis).
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2019-2022 Evgeniy Alekseev.
|
||||
* Copyright (c) 2021-2026 Evgeniy Alekseev.
|
||||
*
|
||||
* This file is part of ffxivbis
|
||||
* (see https://github.com/arcan1s/ffxivbis).
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2019-2022 Evgeniy Alekseev.
|
||||
* Copyright (c) 2021-2026 Evgeniy Alekseev.
|
||||
*
|
||||
* This file is part of ffxivbis
|
||||
* (see https://github.com/arcan1s/ffxivbis).
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2019-2022 Evgeniy Alekseev.
|
||||
* Copyright (c) 2021-2026 Evgeniy Alekseev.
|
||||
*
|
||||
* This file is part of ffxivbis
|
||||
* (see https://github.com/arcan1s/ffxivbis).
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2019-2022 Evgeniy Alekseev.
|
||||
* Copyright (c) 2021-2026 Evgeniy Alekseev.
|
||||
*
|
||||
* This file is part of ffxivbis
|
||||
* (see https://github.com/arcan1s/ffxivbis).
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2019-2022 Evgeniy Alekseev.
|
||||
* Copyright (c) 2021-2026 Evgeniy Alekseev.
|
||||
*
|
||||
* This file is part of ffxivbis
|
||||
* (see https://github.com/arcan1s/ffxivbis).
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2019-2022 Evgeniy Alekseev.
|
||||
* Copyright (c) 2021-2026 Evgeniy Alekseev.
|
||||
*
|
||||
* This file is part of ffxivbis
|
||||
* (see https://github.com/arcan1s/ffxivbis).
|
||||
@ -40,7 +40,14 @@ trait DatabaseLootHandler { this: Database =>
|
||||
case SuggestLoot(partyId, piece, client) =>
|
||||
run {
|
||||
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 ! _)
|
||||
Behaviors.same
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2019-2022 Evgeniy Alekseev.
|
||||
* Copyright (c) 2021-2026 Evgeniy Alekseev.
|
||||
*
|
||||
* This file is part of ffxivbis
|
||||
* (see https://github.com/arcan1s/ffxivbis).
|
||||
@ -62,6 +62,10 @@ trait DatabasePartyHandler { this: Database =>
|
||||
run(profile.deletePlayer(playerId))(_ => client ! ())
|
||||
Behaviors.same
|
||||
|
||||
case UpdateBiSLink(playerId, link, client) =>
|
||||
run(profile.updateBiSLink(playerId, link))(_ => client ! ())
|
||||
Behaviors.same
|
||||
|
||||
case UpdateParty(description, client) =>
|
||||
run(profile.insertPartyDescription(description))(_ => client ! ())
|
||||
Behaviors.same
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2019-2022 Evgeniy Alekseev.
|
||||
* Copyright (c) 2021-2026 Evgeniy Alekseev.
|
||||
*
|
||||
* This file is part of ffxivbis
|
||||
* (see https://github.com/arcan1s/ffxivbis).
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2019-2022 Evgeniy Alekseev.
|
||||
* Copyright (c) 2021-2026 Evgeniy Alekseev.
|
||||
*
|
||||
* This file is part of ffxivbis
|
||||
* (see https://github.com/arcan1s/ffxivbis).
|
||||
@ -53,12 +53,14 @@ trait BiSProfile extends DatabaseConnection {
|
||||
def getPiecesBiSById(playerId: Long): Future[Seq[Loot]] = getPiecesBiSById(Seq(playerId))
|
||||
|
||||
def getPiecesBiSById(playerIds: Seq[Long]): Future[Seq[Loot]] =
|
||||
withConnection { implicit conn =>
|
||||
SQL("""select * from bis where player_id in ({player_ids})""")
|
||||
.on("player_ids" -> playerIds)
|
||||
.executeQuery()
|
||||
.as(loot.*)
|
||||
}
|
||||
if (playerIds.isEmpty) Future.successful(Seq.empty)
|
||||
else
|
||||
withConnection { implicit conn =>
|
||||
SQL("""select * from bis where player_id in ({player_ids})""")
|
||||
.on("player_ids" -> playerIds)
|
||||
.executeQuery()
|
||||
.as(loot.*)
|
||||
}
|
||||
|
||||
def insertPieceBiSById(piece: Piece)(playerId: Long): Future[Int] =
|
||||
withConnection { implicit conn =>
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2019-2022 Evgeniy Alekseev.
|
||||
* Copyright (c) 2021-2026 Evgeniy Alekseev.
|
||||
*
|
||||
* This file is part of ffxivbis
|
||||
* (see https://github.com/arcan1s/ffxivbis).
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2019-2022 Evgeniy Alekseev.
|
||||
* Copyright (c) 2021-2026 Evgeniy Alekseev.
|
||||
*
|
||||
* This file is part of ffxivbis
|
||||
* (see https://github.com/arcan1s/ffxivbis).
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2019-2022 Evgeniy Alekseev.
|
||||
* Copyright (c) 2021-2026 Evgeniy Alekseev.
|
||||
*
|
||||
* This file is part of ffxivbis
|
||||
* (see https://github.com/arcan1s/ffxivbis).
|
||||
@ -59,12 +59,14 @@ trait LootProfile extends DatabaseConnection {
|
||||
def getPiecesById(playerId: Long): Future[Seq[Loot]] = getPiecesById(Seq(playerId))
|
||||
|
||||
def getPiecesById(playerIds: Seq[Long]): Future[Seq[Loot]] =
|
||||
withConnection { implicit conn =>
|
||||
SQL("""select * from loot where player_id in ({player_ids})""")
|
||||
.on("player_ids" -> playerIds)
|
||||
.executeQuery()
|
||||
.as(loot.*)
|
||||
}
|
||||
if (playerIds.isEmpty) Future.successful(Seq.empty)
|
||||
else
|
||||
withConnection { implicit conn =>
|
||||
SQL("""select * from loot where player_id in ({player_ids})""")
|
||||
.on("player_ids" -> playerIds)
|
||||
.executeQuery()
|
||||
.as(loot.*)
|
||||
}
|
||||
|
||||
def insertPieceById(loot: Loot)(playerId: Long): Future[Int] =
|
||||
withConnection { implicit conn =>
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2019-2022 Evgeniy Alekseev.
|
||||
* Copyright (c) 2021-2026 Evgeniy Alekseev.
|
||||
*
|
||||
* This file is part of ffxivbis
|
||||
* (see https://github.com/arcan1s/ffxivbis).
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2019-2022 Evgeniy Alekseev.
|
||||
* Copyright (c) 2021-2026 Evgeniy Alekseev.
|
||||
*
|
||||
* This file is part of ffxivbis
|
||||
* (see https://github.com/arcan1s/ffxivbis).
|
||||
@ -101,4 +101,18 @@ trait PlayersProfile extends DatabaseConnection {
|
||||
.executeUpdate()
|
||||
}
|
||||
|
||||
def updateBiSLink(playerId: PlayerId, link: String): Future[Int] =
|
||||
withConnection { implicit conn =>
|
||||
SQL("""update players
|
||||
| set bis_link = {link}
|
||||
| where party_id = {party_id} and nick = {nick} and job = {job}""".stripMargin)
|
||||
.on(
|
||||
"link" -> link,
|
||||
"party_id" -> playerId.partyId,
|
||||
"nick" -> playerId.nick,
|
||||
"job" -> playerId.job.toString
|
||||
)
|
||||
.executeUpdate()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2019-2022 Evgeniy Alekseev.
|
||||
* Copyright (c) 2021-2026 Evgeniy Alekseev.
|
||||
*
|
||||
* This file is part of ffxivbis
|
||||
* (see https://github.com/arcan1s/ffxivbis).
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2019-2022 Evgeniy Alekseev.
|
||||
* Copyright (c) 2021-2026 Evgeniy Alekseev.
|
||||
*
|
||||
* This file is part of ffxivbis
|
||||
* (see https://github.com/arcan1s/ffxivbis).
|
||||
|
||||
@ -52,12 +52,44 @@ object Fixtures {
|
||||
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 link2: String = "https://ffxiv.ariyala.com/1A0WM"
|
||||
lazy val link3: String = "https://etro.gg/gearset/26a67536-b4ce-4adc-a46a-f70e348bb138"
|
||||
lazy val link4: String = "https://etro.gg/gearset/865fc886-994f-4c28-8fc1-4379f160a916"
|
||||
lazy val 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 lootBody: Piece = Piece.Body(pieceType = PieceType.Savage, Job.AnyJob)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user