mirror of
https://github.com/arcan1s/ffxivbis.git
synced 2025-04-25 01:37:17 +00:00
Compare commits
25 Commits
Author | SHA1 | Date | |
---|---|---|---|
fa43517b16 | |||
77e99439e7 | |||
c9eb311cfe | |||
d662e303c8 | |||
1c8aaea712 | |||
3c8e5f8da8 | |||
0bcda3233e | |||
c4be6f12f1 | |||
f3535f6e16 | |||
bdf413d494 | |||
7a1a73592e | |||
b1ac894ccf | |||
6023e86570 | |||
a4ab1e49be | |||
cb99486f8a | |||
0e8b95d0dd | |||
118d8faf6b | |||
448880ed91 | |||
ed3cdd62bd | |||
88617eccdf | |||
ccbf581332 | |||
0ab9162cb5 | |||
d3018998cd | |||
d4553b2e50 | |||
8496d105c0 |
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@ -25,7 +25,7 @@ jobs:
|
||||
uses: actions/setup-java@v2
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: 17
|
||||
java-version: 18
|
||||
- name: create dist
|
||||
run: make dist
|
||||
- name: release
|
||||
|
2
.github/workflows/run-tests.yml
vendored
2
.github/workflows/run-tests.yml
vendored
@ -17,6 +17,6 @@ jobs:
|
||||
uses: actions/setup-java@v2
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: 17
|
||||
java-version: 18
|
||||
- name: run 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">
|
||||
@ -143,7 +143,7 @@
|
||||
</ul>
|
||||
|
||||
<ul class="nav">
|
||||
<li><a class="nav-link" href="https://github.com/arcan1s/ffxivbis" title="sources">ffxivbis</a></li>
|
||||
<li><a id="sources-link" class="nav-link" href="https://github.com/arcan1s/ffxivbis" title="sources">ffxivbis</a></li>
|
||||
<li><a class="nav-link" href="https://github.com/arcan1s/ffxivbis/releases" title="releases list">releases</a></li>
|
||||
<li><a class="nav-link" href="https://github.com/arcan1s/ffxivbis/issues" title="issues tracker">report a bug</a></li>
|
||||
</ul>
|
||||
@ -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,18 +342,19 @@
|
||||
return false; // should not happen
|
||||
}
|
||||
|
||||
$(function () {
|
||||
$(() => {
|
||||
setupFormClear(updateBisDialog, reset);
|
||||
setupRemoveButton(table, removeButton);
|
||||
|
||||
loadHeader(partyId);
|
||||
loadVersion();
|
||||
loadTypes("/api/v1/types/pieces", pieceInput);
|
||||
loadTypes("/api/v1/types/pieces/types", pieceTypeInput);
|
||||
|
||||
hideControls();
|
||||
|
||||
updateBisButton.click(function () { reset(); });
|
||||
addPieceButton.click(function () { reset(); });
|
||||
updateBisButton.click(() => { reset(); });
|
||||
addPieceButton.click(() => { reset(); });
|
||||
|
||||
table.bootstrapTable({});
|
||||
reload();
|
||||
|
@ -76,7 +76,7 @@
|
||||
</ul>
|
||||
|
||||
<ul class="nav">
|
||||
<li><a class="nav-link" href="https://github.com/arcan1s/ffxivbis" title="sources">ffxivbis</a></li>
|
||||
<li><a id="sources-link" class="nav-link" href="https://github.com/arcan1s/ffxivbis" title="sources">ffxivbis</a></li>
|
||||
<li><a class="nav-link" href="https://github.com/arcan1s/ffxivbis/releases" title="releases list">releases</a></li>
|
||||
<li><a class="nav-link" href="https://github.com/arcan1s/ffxivbis/issues" title="issues tracker">report a bug</a></li>
|
||||
</ul>
|
||||
@ -87,6 +87,9 @@
|
||||
|
||||
<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>
|
||||
const signinButton = $("#signin-btn");
|
||||
const signupButton = $("#signup-btn");
|
||||
@ -124,7 +127,7 @@
|
||||
password: passwordInput.val(),
|
||||
permission: "admin",
|
||||
}),
|
||||
type: "PUT",
|
||||
type: "POST",
|
||||
contentType: "application/json",
|
||||
dataType: "json",
|
||||
success: function (data) {
|
||||
@ -176,6 +179,8 @@
|
||||
}
|
||||
|
||||
$(function () {
|
||||
loadVersion();
|
||||
|
||||
signinButton.click(function () { reset(); });
|
||||
signupButton.click(function () { reset(); });
|
||||
});
|
||||
|
@ -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">
|
||||
@ -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>
|
||||
@ -160,7 +161,7 @@
|
||||
</ul>
|
||||
|
||||
<ul class="nav">
|
||||
<li><a class="nav-link" href="https://github.com/arcan1s/ffxivbis" title="sources">ffxivbis</a></li>
|
||||
<li><a id="sources-link" class="nav-link" href="https://github.com/arcan1s/ffxivbis" title="sources">ffxivbis</a></li>
|
||||
<li><a class="nav-link" href="https://github.com/arcan1s/ffxivbis/releases" title="releases list">releases</a></li>
|
||||
<li><a class="nav-link" href="https://github.com/arcan1s/ffxivbis/issues" title="issues tracker">report a bug</a></li>
|
||||
</ul>
|
||||
@ -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); },
|
||||
});
|
||||
}
|
||||
|
||||
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,14 +332,15 @@
|
||||
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);
|
||||
|
||||
loadVersion();
|
||||
loadHeader(partyId);
|
||||
loadTypes("/api/v1/types/jobs/all", jobInput);
|
||||
loadTypes("/api/v1/types/pieces", pieceInput);
|
||||
|
@ -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">
|
||||
@ -133,7 +133,7 @@
|
||||
</ul>
|
||||
|
||||
<ul class="nav">
|
||||
<li><a class="nav-link" href="https://github.com/arcan1s/ffxivbis" title="sources">ffxivbis</a></li>
|
||||
<li><a id="sources-link" class="nav-link" href="https://github.com/arcan1s/ffxivbis" title="sources">ffxivbis</a></li>
|
||||
<li><a class="nav-link" href="https://github.com/arcan1s/ffxivbis/releases" title="releases list">releases</a></li>
|
||||
<li><a class="nav-link" href="https://github.com/arcan1s/ffxivbis/issues" title="issues tracker">report a bug</a></li>
|
||||
</ul>
|
||||
@ -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,16 +234,17 @@
|
||||
}),
|
||||
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);
|
||||
|
||||
loadVersion();
|
||||
loadHeader(partyId);
|
||||
loadTypes("/api/v1/types/jobs", jobInput);
|
||||
|
||||
|
@ -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">
|
||||
@ -127,7 +127,7 @@
|
||||
</ul>
|
||||
|
||||
<ul class="nav">
|
||||
<li><a class="nav-link" href="https://github.com/arcan1s/ffxivbis" title="sources">ffxivbis</a></li>
|
||||
<li><a id="sources-link" class="nav-link" href="https://github.com/arcan1s/ffxivbis" title="sources">ffxivbis</a></li>
|
||||
<li><a class="nav-link" href="https://github.com/arcan1s/ffxivbis/releases" title="releases list">releases</a></li>
|
||||
<li><a class="nav-link" href="https://github.com/arcan1s/ffxivbis/issues" title="issues tracker">report a bug</a></li>
|
||||
</ul>
|
||||
@ -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,31 +191,32 @@
|
||||
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);
|
||||
|
||||
loadVersion();
|
||||
loadHeader(partyId);
|
||||
loadTypes("/api/v1/types/permissions", permissionInput);
|
||||
|
||||
|
@ -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>
|
||||
|
@ -50,6 +50,7 @@ me.arcanis.ffxivbis {
|
||||
#hostname = "127.0.0.1:8000"
|
||||
# enable head requests for GET requests
|
||||
enable-head-requests = yes
|
||||
schemes = ["http"]
|
||||
|
||||
authorization-cache {
|
||||
# maximum amount of cached logins
|
||||
|
@ -36,6 +36,16 @@ function loadTypes(url, selector) {
|
||||
});
|
||||
}
|
||||
|
||||
function loadVersion() {
|
||||
$.ajax({
|
||||
url: "/api/v1/status",
|
||||
type: "GET",
|
||||
dataType: "json",
|
||||
success: function (data) { $("#sources-link").text(`ffxivbis ${data.version}`); },
|
||||
error: function (jqXHR, _, errorThrown) { requestAlert(jqXHR, errorThrown); },
|
||||
});
|
||||
}
|
||||
|
||||
function setupFormClear(dialog, reset) {
|
||||
dialog.on("hide.bs.modal", function () {
|
||||
$(this).find("form").trigger("reset");
|
||||
|
@ -10,8 +10,7 @@ package me.arcanis.ffxivbis
|
||||
|
||||
import akka.actor.typed.ActorSystem
|
||||
|
||||
object ffxivbis {
|
||||
object ffxivbis extends App {
|
||||
|
||||
def main(args: Array[String]): Unit =
|
||||
ActorSystem[Nothing](Application(), "ffxivbis", Configuration.load())
|
||||
}
|
||||
|
@ -37,23 +37,16 @@ trait Authorization {
|
||||
def authAdmin(partyId: String)(username: String, password: String)(implicit
|
||||
executionContext: ExecutionContext
|
||||
): Future[Option[User]] =
|
||||
authenticator(Permission.admin, partyId)(username, password)
|
||||
auth.authenticator(Permission.admin, partyId)(username, password)
|
||||
|
||||
def authGet(partyId: String)(username: String, password: String)(implicit
|
||||
executionContext: ExecutionContext
|
||||
): Future[Option[User]] =
|
||||
authenticator(Permission.get, partyId)(username, password)
|
||||
auth.authenticator(Permission.get, partyId)(username, password)
|
||||
|
||||
def authPost(partyId: String)(username: String, password: String)(implicit
|
||||
executionContext: ExecutionContext
|
||||
): Future[Option[User]] =
|
||||
authenticator(Permission.post, partyId)(username, password)
|
||||
auth.authenticator(Permission.post, partyId)(username, password)
|
||||
|
||||
private def authenticator(scope: Permission.Value, partyId: String)(username: String, password: String)(implicit
|
||||
executionContext: ExecutionContext
|
||||
): Future[Option[User]] =
|
||||
auth.get(partyId, username).map {
|
||||
case Some(user) if user.verify(password) && user.verityScope(scope) => Some(user)
|
||||
case _ => None
|
||||
}
|
||||
}
|
||||
|
@ -13,20 +13,33 @@ import akka.actor.typed.{ActorRef, Scheduler}
|
||||
import akka.util.Timeout
|
||||
import com.google.common.cache.{CacheBuilder, CacheLoader, LoadingCache}
|
||||
import com.typesafe.config.Config
|
||||
import me.arcanis.ffxivbis.messages.{GetUser, Message}
|
||||
import me.arcanis.ffxivbis.models.User
|
||||
import me.arcanis.ffxivbis.messages.DatabaseMessage.GetUser
|
||||
import me.arcanis.ffxivbis.messages.Message
|
||||
import me.arcanis.ffxivbis.models.{Permission, User}
|
||||
|
||||
import java.util.concurrent.TimeUnit
|
||||
import scala.concurrent.Future
|
||||
import scala.concurrent.{ExecutionContext, Future}
|
||||
|
||||
trait AuthorizationProvider {
|
||||
|
||||
def get(partyId: String, username: String): Future[Option[User]]
|
||||
|
||||
def authenticator[T](scope: Permission.Value, partyId: String)(username: String, password: String)(implicit
|
||||
executionContext: ExecutionContext,
|
||||
extractor: User => T
|
||||
): Future[Option[T]] =
|
||||
get(partyId, username).map {
|
||||
case Some(user) if user.verify(password) && user.verityScope(scope) => Some(extractor(user))
|
||||
case _ => None
|
||||
}
|
||||
}
|
||||
|
||||
object AuthorizationProvider {
|
||||
|
||||
def apply(config: Config, storage: ActorRef[Message], timeout: Timeout, scheduler: Scheduler): AuthorizationProvider =
|
||||
def apply(config: Config, storage: ActorRef[Message])(implicit
|
||||
timeout: Timeout,
|
||||
scheduler: Scheduler
|
||||
): AuthorizationProvider =
|
||||
new AuthorizationProvider {
|
||||
private val cacheSize = config.getInt("me.arcanis.ffxivbis.web.authorization-cache.cache-size")
|
||||
private val cacheTimeout =
|
||||
|
@ -28,7 +28,7 @@ class RootEndpoint(system: ActorSystem[Nothing], storage: ActorRef[Message], pro
|
||||
implicit val scheduler: Scheduler = system.scheduler
|
||||
implicit val timeout: Timeout = config.getTimeout("me.arcanis.ffxivbis.settings.request-timeout")
|
||||
|
||||
private val auth = AuthorizationProvider(config, storage, timeout, scheduler)
|
||||
private val auth = AuthorizationProvider(config, storage)
|
||||
|
||||
private val rootApiV1Endpoint = new RootApiV1Endpoint(storage, auth, provider, config)
|
||||
private val rootView = new RootView(auth)
|
||||
|
@ -14,6 +14,7 @@ import com.typesafe.config.Config
|
||||
import io.swagger.v3.oas.models.security.SecurityScheme
|
||||
|
||||
import scala.io.Source
|
||||
import scala.jdk.CollectionConverters._
|
||||
|
||||
class Swagger(config: Config) extends SwaggerHttpService {
|
||||
|
||||
@ -38,13 +39,15 @@ class Swagger(config: Config) extends SwaggerHttpService {
|
||||
if (config.hasPath("me.arcanis.ffxivbis.web.hostname")) config.getString("me.arcanis.ffxivbis.web.hostname")
|
||||
else s"${config.getString("me.arcanis.ffxivbis.web.host")}:${config.getInt("me.arcanis.ffxivbis.web.port")}"
|
||||
|
||||
override val schemes: List[String] = config.getStringList("me.arcanis.ffxivbis.web.schemes").asScala.toList
|
||||
|
||||
private val basicAuth = new SecurityScheme()
|
||||
.description("basic http auth")
|
||||
.`type`(SecurityScheme.Type.HTTP)
|
||||
.in(SecurityScheme.In.HEADER)
|
||||
.scheme("basic")
|
||||
override val securitySchemes: Map[String, SecurityScheme] = Map("auth" -> basicAuth)
|
||||
override val securitySchemes: Map[String, SecurityScheme] = Map("basic" -> basicAuth)
|
||||
|
||||
override val unwantedDefinitions: Seq[String] =
|
||||
Seq("Function1", "Function1RequestContextFutureRouteResult")
|
||||
Seq("Function1", "Function1RequestContextFutureRouteResult", "SeqLootModel", "SeqPieceModel")
|
||||
}
|
||||
|
@ -0,0 +1,16 @@
|
||||
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("!@#$%^&*()-_=+;:',./? ")
|
||||
}
|
@ -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",
|
||||
@ -79,7 +84,7 @@ class BiSEndpoint(
|
||||
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel])))
|
||||
),
|
||||
),
|
||||
security = Array(new SecurityRequirement(name = "basic auth", scopes = Array("post"))),
|
||||
security = Array(new SecurityRequirement(name = "basic", scopes = Array("post"))),
|
||||
tags = Array("best in slot"),
|
||||
)
|
||||
def createBiS: Route =
|
||||
@ -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,
|
||||
@ -140,7 +150,7 @@ class BiSEndpoint(
|
||||
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel])))
|
||||
),
|
||||
),
|
||||
security = Array(new SecurityRequirement(name = "basic auth", scopes = Array("get"))),
|
||||
security = Array(new SecurityRequirement(name = "basic", scopes = Array("get"))),
|
||||
tags = Array("best in slot"),
|
||||
)
|
||||
def getBiS: Route =
|
||||
@ -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",
|
||||
@ -197,7 +212,7 @@ class BiSEndpoint(
|
||||
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel])))
|
||||
),
|
||||
),
|
||||
security = Array(new SecurityRequirement(name = "basic auth", scopes = Array("post"))),
|
||||
security = Array(new SecurityRequirement(name = "basic", scopes = Array("post"))),
|
||||
tags = Array("best in slot"),
|
||||
)
|
||||
def modifyBiS: Route =
|
||||
|
@ -16,13 +16,15 @@ import com.typesafe.scalalogging.StrictLogging
|
||||
import me.arcanis.ffxivbis.http.api.v1.json._
|
||||
import spray.json._
|
||||
|
||||
import scala.util.control.NonFatal
|
||||
|
||||
trait HttpHandler extends StrictLogging { this: JsonSupport =>
|
||||
|
||||
def exceptionHandler: ExceptionHandler = ExceptionHandler {
|
||||
case ex: IllegalArgumentException =>
|
||||
complete(StatusCodes.BadRequest, ErrorModel(ex.getMessage))
|
||||
case exception: IllegalArgumentException =>
|
||||
complete(StatusCodes.BadRequest, ErrorModel(exception.getMessage))
|
||||
|
||||
case other: Exception =>
|
||||
case NonFatal(other) =>
|
||||
logger.error("exception during request completion", other)
|
||||
complete(StatusCodes.InternalServerError, ErrorModel("unknown server error"))
|
||||
}
|
||||
|
@ -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,
|
||||
@ -81,7 +86,7 @@ class LootEndpoint(override val storage: ActorRef[Message], override val auth: A
|
||||
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel])))
|
||||
),
|
||||
),
|
||||
security = Array(new SecurityRequirement(name = "basic auth", scopes = Array("get"))),
|
||||
security = Array(new SecurityRequirement(name = "basic", scopes = Array("get"))),
|
||||
tags = Array("loot"),
|
||||
)
|
||||
def getLoot: Route =
|
||||
@ -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",
|
||||
@ -137,7 +147,7 @@ class LootEndpoint(override val storage: ActorRef[Message], override val auth: A
|
||||
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel])))
|
||||
),
|
||||
),
|
||||
security = Array(new SecurityRequirement(name = "basic auth", scopes = Array("post"))),
|
||||
security = Array(new SecurityRequirement(name = "basic", scopes = Array("post"))),
|
||||
tags = Array("loot"),
|
||||
)
|
||||
def modifyLoot: Route =
|
||||
@ -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",
|
||||
@ -202,7 +217,7 @@ class LootEndpoint(override val storage: ActorRef[Message], override val auth: A
|
||||
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel])))
|
||||
),
|
||||
),
|
||||
security = Array(new SecurityRequirement(name = "basic auth", scopes = Array("get"))),
|
||||
security = Array(new SecurityRequirement(name = "basic", scopes = Array("get"))),
|
||||
tags = Array("loot"),
|
||||
)
|
||||
def suggestLoot: Route =
|
||||
|
@ -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(
|
||||
@ -73,7 +78,7 @@ class PartyEndpoint(
|
||||
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel])))
|
||||
),
|
||||
),
|
||||
security = Array(new SecurityRequirement(name = "basic auth", scopes = Array("get"))),
|
||||
security = Array(new SecurityRequirement(name = "basic", scopes = Array("get"))),
|
||||
tags = Array("party"),
|
||||
)
|
||||
def getPartyDescription: Route =
|
||||
@ -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",
|
||||
@ -126,7 +136,7 @@ class PartyEndpoint(
|
||||
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel])))
|
||||
),
|
||||
),
|
||||
security = Array(new SecurityRequirement(name = "basic auth", scopes = Array("post"))),
|
||||
security = Array(new SecurityRequirement(name = "basic", scopes = Array("post"))),
|
||||
tags = Array("party"),
|
||||
)
|
||||
def modifyPartyDescription: Route =
|
||||
|
@ -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,
|
||||
@ -85,7 +90,7 @@ class PlayerEndpoint(
|
||||
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel])))
|
||||
),
|
||||
),
|
||||
security = Array(new SecurityRequirement(name = "basic auth", scopes = Array("get"))),
|
||||
security = Array(new SecurityRequirement(name = "basic", scopes = Array("get"))),
|
||||
tags = Array("party"),
|
||||
)
|
||||
def getParty: Route =
|
||||
@ -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,
|
||||
@ -146,7 +156,7 @@ class PlayerEndpoint(
|
||||
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel])))
|
||||
),
|
||||
),
|
||||
security = Array(new SecurityRequirement(name = "basic auth", scopes = Array("get"))),
|
||||
security = Array(new SecurityRequirement(name = "basic", scopes = Array("get"))),
|
||||
tags = Array("party"),
|
||||
)
|
||||
def getPartyStats: Route =
|
||||
@ -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",
|
||||
@ -202,7 +217,7 @@ class PlayerEndpoint(
|
||||
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel])))
|
||||
),
|
||||
),
|
||||
security = Array(new SecurityRequirement(name = "basic auth", scopes = Array("post"))),
|
||||
security = Array(new SecurityRequirement(name = "basic", scopes = Array("post"))),
|
||||
tags = Array("party"),
|
||||
)
|
||||
def modifyParty: Route =
|
||||
|
@ -38,7 +38,7 @@ class UserEndpoint(override val storage: ActorRef[Message], override val auth: A
|
||||
|
||||
def routes: Route = createParty ~ createUser ~ deleteUser ~ getUsers ~ getUsersCurrent
|
||||
|
||||
@PUT
|
||||
@POST
|
||||
@Path("party")
|
||||
@Consumes(value = Array("application/json"))
|
||||
@Operation(
|
||||
@ -76,7 +76,7 @@ class UserEndpoint(override val storage: ActorRef[Message], override val auth: A
|
||||
def createParty: Route =
|
||||
path("party") {
|
||||
extractExecutionContext { implicit executionContext =>
|
||||
put {
|
||||
post {
|
||||
entity(as[UserModel]) { user =>
|
||||
onSuccess(newPartyId) { partyId =>
|
||||
val admin = user.toUser.copy(partyId = partyId, permission = Permission.admin)
|
||||
@ -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",
|
||||
@ -126,7 +131,7 @@ class UserEndpoint(override val storage: ActorRef[Message], override val auth: A
|
||||
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel])))
|
||||
),
|
||||
),
|
||||
security = Array(new SecurityRequirement(name = "basic auth", scopes = Array("admin"))),
|
||||
security = Array(new SecurityRequirement(name = "basic", scopes = Array("admin"))),
|
||||
tags = Array("users"),
|
||||
)
|
||||
def createUser: Route =
|
||||
@ -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(
|
||||
@ -172,7 +182,7 @@ class UserEndpoint(override val storage: ActorRef[Message], override val auth: A
|
||||
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel])))
|
||||
),
|
||||
),
|
||||
security = Array(new SecurityRequirement(name = "basic auth", scopes = Array("admin"))),
|
||||
security = Array(new SecurityRequirement(name = "basic", scopes = Array("admin"))),
|
||||
tags = Array("users"),
|
||||
)
|
||||
def deleteUser: Route =
|
||||
@ -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(
|
||||
@ -223,7 +238,7 @@ class UserEndpoint(override val storage: ActorRef[Message], override val auth: A
|
||||
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel])))
|
||||
),
|
||||
),
|
||||
security = Array(new SecurityRequirement(name = "basic auth", scopes = Array("get"))),
|
||||
security = Array(new SecurityRequirement(name = "basic", scopes = Array("get"))),
|
||||
tags = Array("users"),
|
||||
)
|
||||
def getUsers: Route =
|
||||
@ -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(
|
||||
@ -270,7 +290,7 @@ class UserEndpoint(override val storage: ActorRef[Message], override val auth: A
|
||||
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel])))
|
||||
),
|
||||
),
|
||||
security = Array(new SecurityRequirement(name = "basic auth", scopes = Array("admin"))),
|
||||
security = Array(new SecurityRequirement(name = "basic", scopes = Array("admin"))),
|
||||
tags = Array("users"),
|
||||
)
|
||||
def getUsersCurrent: Route =
|
||||
|
@ -9,5 +9,6 @@
|
||||
package me.arcanis.ffxivbis.http.api.v1.json
|
||||
|
||||
object ApiAction extends Enumeration {
|
||||
|
||||
val add, remove = Value
|
||||
}
|
||||
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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
|
||||
)
|
||||
|
@ -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"))
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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,
|
||||
|
@ -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(
|
||||
|
@ -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,9 @@
|
||||
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(", ")})"
|
||||
}
|
@ -12,7 +12,8 @@ import akka.actor.typed.scaladsl.AskPattern.Askable
|
||||
import akka.actor.typed.{ActorRef, Scheduler}
|
||||
import akka.util.Timeout
|
||||
import me.arcanis.ffxivbis.http.api.v1.json.ApiAction
|
||||
import me.arcanis.ffxivbis.messages._
|
||||
import me.arcanis.ffxivbis.messages.DatabaseMessage._
|
||||
import me.arcanis.ffxivbis.messages.Message
|
||||
import me.arcanis.ffxivbis.models.{Piece, Player, PlayerId}
|
||||
|
||||
import scala.concurrent.{ExecutionContext, Future}
|
||||
@ -44,13 +45,15 @@ trait BiSHelper extends BisProviderHelper {
|
||||
timeout: Timeout,
|
||||
scheduler: Scheduler
|
||||
): Future[Unit] =
|
||||
storage.ask(RemovePiecesFromBiS(playerId, _)).flatMap { _ =>
|
||||
storage
|
||||
.ask(RemovePiecesFromBiS(playerId, _))
|
||||
.flatMap { _ =>
|
||||
downloadBiS(link, playerId.job)
|
||||
.flatMap { bis =>
|
||||
Future.traverse(bis.pieces)(addPieceBiS(playerId, _))
|
||||
}
|
||||
.map(_ => ())
|
||||
}
|
||||
.flatMap(_ => storage.ask(UpdateBiSLink(playerId, link, _)))
|
||||
|
||||
def removePieceBiS(playerId: PlayerId, piece: Piece)(implicit timeout: Timeout, scheduler: Scheduler): Future[Unit] =
|
||||
storage.ask(RemovePieceFromBiS(playerId, piece, _))
|
||||
|
@ -11,7 +11,8 @@ package me.arcanis.ffxivbis.http.helpers
|
||||
import akka.actor.typed.scaladsl.AskPattern.Askable
|
||||
import akka.actor.typed.{ActorRef, Scheduler}
|
||||
import akka.util.Timeout
|
||||
import me.arcanis.ffxivbis.messages.{BiSProviderMessage, DownloadBiS}
|
||||
import me.arcanis.ffxivbis.messages.BiSProviderMessage
|
||||
import me.arcanis.ffxivbis.messages.BiSProviderMessage._
|
||||
import me.arcanis.ffxivbis.models.{BiS, Job}
|
||||
|
||||
import scala.concurrent.Future
|
||||
@ -20,6 +21,6 @@ trait BisProviderHelper {
|
||||
|
||||
def provider: ActorRef[BiSProviderMessage]
|
||||
|
||||
def downloadBiS(link: String, job: Job.Job)(implicit timeout: Timeout, scheduler: Scheduler): Future[BiS] =
|
||||
def downloadBiS(link: String, job: Job)(implicit timeout: Timeout, scheduler: Scheduler): Future[BiS] =
|
||||
provider.ask(DownloadBiS(link, job, _))
|
||||
}
|
||||
|
@ -12,7 +12,8 @@ import akka.actor.typed.scaladsl.AskPattern.Askable
|
||||
import akka.actor.typed.{ActorRef, Scheduler}
|
||||
import akka.util.Timeout
|
||||
import me.arcanis.ffxivbis.http.api.v1.json.ApiAction
|
||||
import me.arcanis.ffxivbis.messages._
|
||||
import me.arcanis.ffxivbis.messages.DatabaseMessage._
|
||||
import me.arcanis.ffxivbis.messages.Message
|
||||
import me.arcanis.ffxivbis.models.{Piece, Player, PlayerId, PlayerIdWithCounters}
|
||||
|
||||
import scala.concurrent.{ExecutionContext, Future}
|
||||
|
@ -12,7 +12,8 @@ import akka.actor.typed.scaladsl.AskPattern.Askable
|
||||
import akka.actor.typed.{ActorRef, Scheduler}
|
||||
import akka.util.Timeout
|
||||
import me.arcanis.ffxivbis.http.api.v1.json.ApiAction
|
||||
import me.arcanis.ffxivbis.messages._
|
||||
import me.arcanis.ffxivbis.messages.DatabaseMessage._
|
||||
import me.arcanis.ffxivbis.messages.Message
|
||||
import me.arcanis.ffxivbis.models.{PartyDescription, Player, PlayerId}
|
||||
|
||||
import scala.concurrent.{ExecutionContext, Future}
|
||||
@ -26,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
|
||||
|
@ -11,7 +11,9 @@ package me.arcanis.ffxivbis.http.helpers
|
||||
import akka.actor.typed.scaladsl.AskPattern.Askable
|
||||
import akka.actor.typed.{ActorRef, Scheduler}
|
||||
import akka.util.Timeout
|
||||
import me.arcanis.ffxivbis.messages._
|
||||
import me.arcanis.ffxivbis.messages.ControlMessage.GetNewPartyId
|
||||
import me.arcanis.ffxivbis.messages.DatabaseMessage._
|
||||
import me.arcanis.ffxivbis.messages.Message
|
||||
import me.arcanis.ffxivbis.models.User
|
||||
|
||||
import scala.concurrent.Future
|
||||
|
@ -13,7 +13,10 @@ import me.arcanis.ffxivbis.models.{BiS, Job}
|
||||
|
||||
sealed trait BiSProviderMessage
|
||||
|
||||
case class DownloadBiS(link: String, job: Job.Job, replyTo: ActorRef[BiS]) extends BiSProviderMessage {
|
||||
object BiSProviderMessage {
|
||||
|
||||
case class DownloadBiS(link: String, job: Job, replyTo: ActorRef[BiS]) extends BiSProviderMessage {
|
||||
|
||||
require(link.nonEmpty && link.trim == link, "Link must be not empty and contain no spaces")
|
||||
}
|
||||
}
|
||||
|
@ -11,8 +11,13 @@ package me.arcanis.ffxivbis.messages
|
||||
import akka.actor.typed.ActorRef
|
||||
import me.arcanis.ffxivbis.models.Party
|
||||
|
||||
case class ForgetParty(partyId: String) extends Message
|
||||
sealed trait ControlMessage extends Message
|
||||
|
||||
case class GetNewPartyId(replyTo: ActorRef[String]) extends Message
|
||||
object ControlMessage {
|
||||
|
||||
case class StoreParty(partyId: String, party: Party) extends Message
|
||||
case class ForgetParty(partyId: String) extends ControlMessage
|
||||
|
||||
case class GetNewPartyId(replyTo: ActorRef[String]) extends ControlMessage
|
||||
|
||||
case class StoreParty(partyId: String, party: Party) extends ControlMessage
|
||||
}
|
@ -8,91 +8,124 @@
|
||||
*/
|
||||
package me.arcanis.ffxivbis.messages
|
||||
|
||||
import akka.actor.typed.{ActorRef, Behavior}
|
||||
import akka.actor.typed.ActorRef
|
||||
import me.arcanis.ffxivbis.models._
|
||||
import me.arcanis.ffxivbis.service.LootSelector
|
||||
|
||||
sealed trait DatabaseMessage extends Message {
|
||||
|
||||
def partyId: String
|
||||
|
||||
def isReadOnly: Boolean
|
||||
}
|
||||
|
||||
object DatabaseMessage {
|
||||
|
||||
type Handler = PartialFunction[DatabaseMessage, Behavior[DatabaseMessage]]
|
||||
}
|
||||
// bis handler
|
||||
trait BisDatabaseMessage extends DatabaseMessage
|
||||
|
||||
// bis handler
|
||||
trait BisDatabaseMessage extends DatabaseMessage
|
||||
case class AddPieceToBis(playerId: PlayerId, piece: Piece, replyTo: ActorRef[Unit]) extends BisDatabaseMessage {
|
||||
override val partyId: String = playerId.partyId
|
||||
override val isReadOnly: Boolean = false
|
||||
}
|
||||
|
||||
case class AddPieceToBis(playerId: PlayerId, piece: Piece, replyTo: ActorRef[Unit]) extends BisDatabaseMessage {
|
||||
override def partyId: String = playerId.partyId
|
||||
}
|
||||
case class GetBiS(partyId: String, playerId: Option[PlayerId], replyTo: ActorRef[Seq[Player]])
|
||||
extends BisDatabaseMessage {
|
||||
override val isReadOnly: Boolean = true
|
||||
}
|
||||
|
||||
case class GetBiS(partyId: String, playerId: Option[PlayerId], replyTo: ActorRef[Seq[Player]])
|
||||
extends BisDatabaseMessage
|
||||
case class RemovePieceFromBiS(playerId: PlayerId, piece: Piece, replyTo: ActorRef[Unit]) extends BisDatabaseMessage {
|
||||
override val partyId: String = playerId.partyId
|
||||
override val isReadOnly: Boolean = false
|
||||
}
|
||||
|
||||
case class RemovePieceFromBiS(playerId: PlayerId, piece: Piece, replyTo: ActorRef[Unit]) extends BisDatabaseMessage {
|
||||
override def partyId: String = playerId.partyId
|
||||
}
|
||||
case class RemovePiecesFromBiS(playerId: PlayerId, replyTo: ActorRef[Unit]) extends BisDatabaseMessage {
|
||||
override val partyId: String = playerId.partyId
|
||||
override val isReadOnly: Boolean = false
|
||||
}
|
||||
|
||||
case class RemovePiecesFromBiS(playerId: PlayerId, replyTo: ActorRef[Unit]) extends BisDatabaseMessage {
|
||||
override def partyId: String = playerId.partyId
|
||||
}
|
||||
// loot handler
|
||||
trait LootDatabaseMessage extends DatabaseMessage
|
||||
|
||||
// loot handler
|
||||
trait LootDatabaseMessage extends DatabaseMessage
|
||||
|
||||
case class AddPieceTo(playerId: PlayerId, piece: Piece, isFreeLoot: Boolean, replyTo: ActorRef[Unit])
|
||||
case class AddPieceTo(playerId: PlayerId, piece: Piece, isFreeLoot: Boolean, replyTo: ActorRef[Unit])
|
||||
extends LootDatabaseMessage {
|
||||
override def partyId: String = playerId.partyId
|
||||
}
|
||||
override val partyId: String = playerId.partyId
|
||||
override val isReadOnly: Boolean = false
|
||||
}
|
||||
|
||||
case class GetLoot(partyId: String, playerId: Option[PlayerId], replyTo: ActorRef[Seq[Player]])
|
||||
extends LootDatabaseMessage
|
||||
|
||||
case class RemovePieceFrom(playerId: PlayerId, piece: Piece, isFreeLoot: Boolean, replyTo: ActorRef[Unit])
|
||||
case class GetLoot(partyId: String, playerId: Option[PlayerId], replyTo: ActorRef[Seq[Player]])
|
||||
extends LootDatabaseMessage {
|
||||
override def partyId: String = playerId.partyId
|
||||
override val isReadOnly: Boolean = true
|
||||
}
|
||||
|
||||
case class RemovePieceFrom(playerId: PlayerId, piece: Piece, isFreeLoot: Boolean, replyTo: ActorRef[Unit])
|
||||
extends LootDatabaseMessage {
|
||||
override val partyId: String = playerId.partyId
|
||||
override val isReadOnly: Boolean = false
|
||||
}
|
||||
|
||||
case class SuggestLoot(partyId: String, piece: Piece, replyTo: ActorRef[LootSelector.LootSelectorResult])
|
||||
extends LootDatabaseMessage {
|
||||
override val isReadOnly: Boolean = true
|
||||
}
|
||||
|
||||
// party handler
|
||||
trait PartyDatabaseMessage extends DatabaseMessage
|
||||
|
||||
case class AddPlayer(player: Player, replyTo: ActorRef[Unit]) extends PartyDatabaseMessage {
|
||||
override val partyId: String = player.partyId
|
||||
override val isReadOnly: Boolean = false
|
||||
}
|
||||
|
||||
case class GetParty(partyId: String, replyTo: ActorRef[Party]) extends PartyDatabaseMessage {
|
||||
override val isReadOnly: Boolean = true
|
||||
}
|
||||
|
||||
case class GetPartyDescription(partyId: String, replyTo: ActorRef[PartyDescription]) extends PartyDatabaseMessage {
|
||||
override val isReadOnly: Boolean = true
|
||||
}
|
||||
|
||||
case class GetPlayer(playerId: PlayerId, replyTo: ActorRef[Option[Player]]) extends PartyDatabaseMessage {
|
||||
override val partyId: String = playerId.partyId
|
||||
override val isReadOnly: Boolean = true
|
||||
}
|
||||
|
||||
case class RemovePlayer(playerId: PlayerId, replyTo: ActorRef[Unit]) extends PartyDatabaseMessage {
|
||||
override val partyId: String = playerId.partyId
|
||||
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
|
||||
}
|
||||
|
||||
// user handler
|
||||
trait UserDatabaseMessage extends DatabaseMessage
|
||||
|
||||
case class AddUser(user: User, isHashedPassword: Boolean, replyTo: ActorRef[Unit]) extends UserDatabaseMessage {
|
||||
override val partyId: String = user.partyId
|
||||
override val isReadOnly: Boolean = false
|
||||
}
|
||||
|
||||
case class DeleteUser(partyId: String, username: String, replyTo: ActorRef[Unit]) extends UserDatabaseMessage {
|
||||
override val isReadOnly: Boolean = true
|
||||
}
|
||||
|
||||
case class Exists(partyId: String, replyTo: ActorRef[Boolean]) extends UserDatabaseMessage {
|
||||
override val isReadOnly: Boolean = true
|
||||
}
|
||||
|
||||
case class GetUser(partyId: String, username: String, replyTo: ActorRef[Option[User]]) extends UserDatabaseMessage {
|
||||
override val isReadOnly: Boolean = true
|
||||
}
|
||||
|
||||
case class GetUsers(partyId: String, replyTo: ActorRef[Seq[User]]) extends UserDatabaseMessage {
|
||||
override val isReadOnly: Boolean = true
|
||||
}
|
||||
}
|
||||
|
||||
case class SuggestLoot(partyId: String, piece: Piece, replyTo: ActorRef[LootSelector.LootSelectorResult])
|
||||
extends LootDatabaseMessage
|
||||
|
||||
// party handler
|
||||
trait PartyDatabaseMessage extends DatabaseMessage
|
||||
|
||||
case class AddPlayer(player: Player, replyTo: ActorRef[Unit]) extends PartyDatabaseMessage {
|
||||
override def partyId: String = player.partyId
|
||||
}
|
||||
|
||||
case class GetParty(partyId: String, replyTo: ActorRef[Party]) extends PartyDatabaseMessage
|
||||
|
||||
case class GetPartyDescription(partyId: String, replyTo: ActorRef[PartyDescription]) extends PartyDatabaseMessage
|
||||
|
||||
case class GetPlayer(playerId: PlayerId, replyTo: ActorRef[Option[Player]]) extends PartyDatabaseMessage {
|
||||
override def partyId: String = playerId.partyId
|
||||
}
|
||||
|
||||
case class RemovePlayer(playerId: PlayerId, replyTo: ActorRef[Unit]) extends PartyDatabaseMessage {
|
||||
override def partyId: String = playerId.partyId
|
||||
}
|
||||
|
||||
case class UpdateParty(partyDescription: PartyDescription, replyTo: ActorRef[Unit]) extends PartyDatabaseMessage {
|
||||
override def partyId: String = partyDescription.partyId
|
||||
}
|
||||
|
||||
// user handler
|
||||
trait UserDatabaseMessage extends DatabaseMessage
|
||||
|
||||
case class AddUser(user: User, isHashedPassword: Boolean, replyTo: ActorRef[Unit]) extends UserDatabaseMessage {
|
||||
override def partyId: String = user.partyId
|
||||
}
|
||||
|
||||
case class DeleteUser(partyId: String, username: String, replyTo: ActorRef[Unit]) extends UserDatabaseMessage
|
||||
|
||||
case class Exists(partyId: String, replyTo: ActorRef[Boolean]) extends UserDatabaseMessage
|
||||
|
||||
case class GetUser(partyId: String, username: String, replyTo: ActorRef[Option[User]]) extends UserDatabaseMessage
|
||||
|
||||
case class GetUsers(partyId: String, replyTo: ActorRef[Seq[User]]) extends UserDatabaseMessage
|
||||
|
@ -11,14 +11,14 @@ package me.arcanis.ffxivbis.models
|
||||
case class BiS(pieces: Seq[Piece]) {
|
||||
|
||||
def hasPiece(piece: Piece): Boolean = piece match {
|
||||
case upgrade: PieceUpgrade => upgrades.contains(upgrade)
|
||||
case upgrade: Piece.PieceUpgrade => upgrades.contains(upgrade)
|
||||
case _ => pieces.contains(piece)
|
||||
}
|
||||
|
||||
def upgrades: Map[PieceUpgrade, Int] =
|
||||
def upgrades: Map[Piece.PieceUpgrade, Int] =
|
||||
pieces
|
||||
.groupBy(_.upgrade)
|
||||
.foldLeft(Map.empty[PieceUpgrade, Int]) {
|
||||
.foldLeft(Map.empty[Piece.PieceUpgrade, Int]) {
|
||||
case (acc, (Some(k), v)) => acc + (k -> v.size)
|
||||
case (acc, _) => acc
|
||||
}
|
||||
@ -43,5 +43,5 @@ case class BiS(pieces: Seq[Piece]) {
|
||||
|
||||
object BiS {
|
||||
|
||||
def empty: BiS = BiS(Seq.empty)
|
||||
val empty: BiS = BiS(Seq.empty)
|
||||
}
|
||||
|
@ -8,6 +8,26 @@
|
||||
*/
|
||||
package me.arcanis.ffxivbis.models
|
||||
|
||||
sealed trait Job extends Equals {
|
||||
|
||||
def leftSide: Job.LeftSide
|
||||
|
||||
def rightSide: Job.RightSide
|
||||
|
||||
// conversion to string to avoid recursion
|
||||
override def canEqual(that: Any): Boolean = that.isInstanceOf[Job]
|
||||
|
||||
override def equals(obj: Any): Boolean = {
|
||||
def equality(objRepr: String): Boolean = objRepr match {
|
||||
case _ if objRepr == Job.AnyJob.toString => true
|
||||
case _ if this.toString == Job.AnyJob.toString => true
|
||||
case _ => this.toString == objRepr
|
||||
}
|
||||
|
||||
canEqual(obj) && equality(obj.toString)
|
||||
}
|
||||
}
|
||||
|
||||
object Job {
|
||||
|
||||
sealed trait RightSide
|
||||
@ -26,54 +46,38 @@ object Job {
|
||||
object BodyTanks extends LeftSide
|
||||
object BodyRanges extends LeftSide
|
||||
|
||||
sealed trait Job extends Equals {
|
||||
|
||||
def leftSide: LeftSide
|
||||
|
||||
def rightSide: RightSide
|
||||
|
||||
// conversion to string to avoid recursion
|
||||
override def canEqual(that: Any): Boolean = that.isInstanceOf[Job]
|
||||
|
||||
override def equals(obj: Any): Boolean = {
|
||||
def equality(objRepr: String): Boolean = objRepr match {
|
||||
case _ if objRepr == AnyJob.toString => true
|
||||
case _ if this.toString == AnyJob.toString => true
|
||||
case _ => this.toString == objRepr
|
||||
}
|
||||
|
||||
canEqual(obj) && equality(obj.toString)
|
||||
}
|
||||
}
|
||||
|
||||
case object AnyJob extends Job {
|
||||
val leftSide: LeftSide = null
|
||||
val rightSide: RightSide = null
|
||||
override val leftSide: LeftSide = null
|
||||
override val rightSide: RightSide = null
|
||||
}
|
||||
|
||||
trait Casters extends Job {
|
||||
val leftSide: LeftSide = BodyCasters
|
||||
val rightSide: RightSide = AccessoriesInt
|
||||
override val leftSide: LeftSide = BodyCasters
|
||||
override val rightSide: RightSide = AccessoriesInt
|
||||
}
|
||||
trait Healers extends Job {
|
||||
val leftSide: LeftSide = BodyHealers
|
||||
val rightSide: RightSide = AccessoriesMnd
|
||||
override val leftSide: LeftSide = BodyHealers
|
||||
override val rightSide: RightSide = AccessoriesMnd
|
||||
}
|
||||
trait Mnks extends Job {
|
||||
val leftSide: LeftSide = BodyMnks
|
||||
val rightSide: RightSide = AccessoriesStr
|
||||
override val leftSide: LeftSide = BodyMnks
|
||||
override val rightSide: RightSide = AccessoriesStr
|
||||
}
|
||||
trait Drgs extends Job {
|
||||
val leftSide: LeftSide = BodyDrgs
|
||||
val rightSide: RightSide = AccessoriesStr
|
||||
override val leftSide: LeftSide = BodyDrgs
|
||||
override val rightSide: RightSide = AccessoriesStr
|
||||
}
|
||||
trait Nins extends Job {
|
||||
override val leftSide: LeftSide = BodyNins
|
||||
override val rightSide: RightSide = AccessoriesDex
|
||||
}
|
||||
trait Tanks extends Job {
|
||||
val leftSide: LeftSide = BodyTanks
|
||||
val rightSide: RightSide = AccessoriesVit
|
||||
override val leftSide: LeftSide = BodyTanks
|
||||
override val rightSide: RightSide = AccessoriesVit
|
||||
}
|
||||
trait Ranges extends Job {
|
||||
val leftSide: LeftSide = BodyRanges
|
||||
val rightSide: RightSide = AccessoriesDex
|
||||
override val leftSide: LeftSide = BodyRanges
|
||||
override val rightSide: RightSide = AccessoriesDex
|
||||
}
|
||||
|
||||
case object PLD extends Tanks
|
||||
@ -89,11 +93,9 @@ object Job {
|
||||
case object MNK extends Mnks
|
||||
case object DRG extends Drgs
|
||||
case object RPR extends Drgs
|
||||
case object NIN extends Job {
|
||||
val leftSide: LeftSide = BodyNins
|
||||
val rightSide: RightSide = AccessoriesDex
|
||||
}
|
||||
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
|
||||
@ -102,12 +104,13 @@ object Job {
|
||||
case object BLM extends Casters
|
||||
case object SMN extends Casters
|
||||
case object RDM extends Casters
|
||||
case object PCT extends Casters
|
||||
|
||||
lazy val available: Seq[Job] =
|
||||
Seq(PLD, WAR, DRK, GNB, WHM, SCH, AST, SGE, MNK, DRG, RPR, NIN, SAM, BRD, MCH, DNC, BLM, SMN, RDM)
|
||||
lazy val availableWithAnyJob: Seq[Job] = available.prepended(AnyJob)
|
||||
val available: Seq[Job] =
|
||||
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.Job =
|
||||
def withName(job: String): Job =
|
||||
availableWithAnyJob.find(_.toString.equalsIgnoreCase(job)) match {
|
||||
case Some(value) => value
|
||||
case None if job.isEmpty => AnyJob
|
||||
|
@ -12,7 +12,5 @@ import java.time.Instant
|
||||
|
||||
case class Loot(playerId: Long, piece: Piece, timestamp: Instant, isFreeLoot: Boolean) {
|
||||
|
||||
def isFreeLootToString: String = if (isFreeLoot) "yes" else "no"
|
||||
|
||||
def isFreeLootToInt: Int = if (isFreeLoot) 1 else 0
|
||||
lazy val isFreeLootToInt: Int = if (isFreeLoot) 1 else 0
|
||||
}
|
||||
|
@ -14,6 +14,7 @@ import me.arcanis.ffxivbis.service.LootSelector
|
||||
|
||||
import scala.jdk.CollectionConverters._
|
||||
import scala.util.Random
|
||||
import scala.util.control.NonFatal
|
||||
|
||||
case class Party(partyDescription: PartyDescription, rules: Seq[String], players: Map[PlayerId, Player])
|
||||
extends StrictLogging {
|
||||
@ -29,7 +30,7 @@ case class Party(partyDescription: PartyDescription, rules: Seq[String], players
|
||||
require(player.partyId == partyDescription.partyId, "player must belong to this party")
|
||||
copy(players = players + (player.playerId -> player))
|
||||
} catch {
|
||||
case exception: Exception =>
|
||||
case NonFatal(exception) =>
|
||||
logger.error("cannot add player", exception)
|
||||
this
|
||||
}
|
||||
|
@ -8,10 +8,7 @@
|
||||
*/
|
||||
package me.arcanis.ffxivbis.models
|
||||
|
||||
case class PartyDescription(partyId: String, partyAlias: Option[String]) {
|
||||
|
||||
def alias: String = partyAlias.getOrElse(partyId)
|
||||
}
|
||||
case class PartyDescription(partyId: String, partyAlias: Option[String])
|
||||
|
||||
object PartyDescription {
|
||||
|
||||
|
@ -10,20 +10,20 @@ package me.arcanis.ffxivbis.models
|
||||
|
||||
sealed trait Piece extends Equals {
|
||||
|
||||
def pieceType: PieceType.PieceType
|
||||
def pieceType: PieceType
|
||||
|
||||
def job: Job.Job
|
||||
def job: Job
|
||||
|
||||
def piece: String
|
||||
|
||||
def withJob(other: Job.Job): Piece
|
||||
def withJob(other: Job): Piece
|
||||
|
||||
def upgrade: Option[PieceUpgrade] = {
|
||||
def upgrade: Option[Piece.PieceUpgrade] = {
|
||||
val isTome = pieceType == PieceType.Tome
|
||||
Some(this).collect {
|
||||
case _: PieceAccessory if isTome => AccessoryUpgrade
|
||||
case _: PieceBody if isTome => BodyUpgrade
|
||||
case _: PieceWeapon if isTome => WeaponUpgrade
|
||||
case _: Piece.PieceAccessory if isTome => Piece.AccessoryUpgrade
|
||||
case _: Piece.PieceBody if isTome => Piece.BodyUpgrade
|
||||
case _: Piece.PieceWeapon if isTome => Piece.WeaponUpgrade
|
||||
}
|
||||
}
|
||||
|
||||
@ -31,59 +31,61 @@ sealed trait Piece extends Equals {
|
||||
def strictEqual(obj: Any): Boolean = equals(obj)
|
||||
}
|
||||
|
||||
trait PieceAccessory extends Piece
|
||||
trait PieceBody extends Piece
|
||||
trait PieceUpgrade extends Piece {
|
||||
val pieceType: PieceType.PieceType = PieceType.Tome
|
||||
val job: Job.Job = Job.AnyJob
|
||||
def withJob(other: Job.Job): Piece = this
|
||||
}
|
||||
trait PieceWeapon extends Piece
|
||||
object Piece {
|
||||
|
||||
case class Weapon(override val pieceType: PieceType.PieceType, override val job: Job.Job) extends PieceWeapon {
|
||||
val piece: String = "weapon"
|
||||
def withJob(other: Job.Job): Piece = copy(job = other)
|
||||
}
|
||||
trait PieceAccessory extends Piece
|
||||
trait PieceBody extends Piece
|
||||
trait PieceUpgrade extends Piece {
|
||||
override val pieceType: PieceType = PieceType.Tome
|
||||
override val job: Job = Job.AnyJob
|
||||
override def withJob(other: Job): Piece = this
|
||||
}
|
||||
trait PieceWeapon extends Piece
|
||||
|
||||
case class Head(override val pieceType: PieceType.PieceType, override val job: Job.Job) extends PieceBody {
|
||||
val piece: String = "head"
|
||||
def withJob(other: Job.Job): Piece = copy(job = other)
|
||||
}
|
||||
case class Body(override val pieceType: PieceType.PieceType, override val job: Job.Job) extends PieceBody {
|
||||
val piece: String = "body"
|
||||
def withJob(other: Job.Job): Piece = copy(job = other)
|
||||
}
|
||||
case class Hands(override val pieceType: PieceType.PieceType, override val job: Job.Job) extends PieceBody {
|
||||
val piece: String = "hands"
|
||||
def withJob(other: Job.Job): Piece = copy(job = other)
|
||||
}
|
||||
case class Legs(override val pieceType: PieceType.PieceType, override val job: Job.Job) extends PieceBody {
|
||||
val piece: String = "legs"
|
||||
def withJob(other: Job.Job): Piece = copy(job = other)
|
||||
}
|
||||
case class Feet(override val pieceType: PieceType.PieceType, override val job: Job.Job) extends PieceBody {
|
||||
val piece: String = "feet"
|
||||
def withJob(other: Job.Job): Piece = copy(job = other)
|
||||
}
|
||||
case class Weapon(override val pieceType: PieceType, override val job: Job) extends PieceWeapon {
|
||||
override val piece: String = "weapon"
|
||||
override def withJob(other: Job): Piece = copy(job = other)
|
||||
}
|
||||
|
||||
case class Ears(override val pieceType: PieceType.PieceType, override val job: Job.Job) extends PieceAccessory {
|
||||
val piece: String = "ears"
|
||||
def withJob(other: Job.Job): Piece = copy(job = other)
|
||||
}
|
||||
case class Neck(override val pieceType: PieceType.PieceType, override val job: Job.Job) extends PieceAccessory {
|
||||
val piece: String = "neck"
|
||||
def withJob(other: Job.Job): Piece = copy(job = other)
|
||||
}
|
||||
case class Wrist(override val pieceType: PieceType.PieceType, override val job: Job.Job) extends PieceAccessory {
|
||||
val piece: String = "wrist"
|
||||
def withJob(other: Job.Job): Piece = copy(job = other)
|
||||
}
|
||||
case class Ring(
|
||||
override val pieceType: PieceType.PieceType,
|
||||
override val job: Job.Job,
|
||||
case class Head(override val pieceType: PieceType, override val job: Job) extends PieceBody {
|
||||
override val piece: String = "head"
|
||||
override def withJob(other: Job): Piece = copy(job = other)
|
||||
}
|
||||
case class Body(override val pieceType: PieceType, override val job: Job) extends PieceBody {
|
||||
override val piece: String = "body"
|
||||
override def withJob(other: Job): Piece = copy(job = other)
|
||||
}
|
||||
case class Hands(override val pieceType: PieceType, override val job: Job) extends PieceBody {
|
||||
override val piece: String = "hands"
|
||||
override def withJob(other: Job): Piece = copy(job = other)
|
||||
}
|
||||
case class Legs(override val pieceType: PieceType, override val job: Job) extends PieceBody {
|
||||
override val piece: String = "legs"
|
||||
override def withJob(other: Job): Piece = copy(job = other)
|
||||
}
|
||||
case class Feet(override val pieceType: PieceType, override val job: Job) extends PieceBody {
|
||||
override val piece: String = "feet"
|
||||
override def withJob(other: Job): Piece = copy(job = other)
|
||||
}
|
||||
|
||||
case class Ears(override val pieceType: PieceType, override val job: Job) extends PieceAccessory {
|
||||
override val piece: String = "ears"
|
||||
override def withJob(other: Job): Piece = copy(job = other)
|
||||
}
|
||||
case class Neck(override val pieceType: PieceType, override val job: Job) extends PieceAccessory {
|
||||
override val piece: String = "neck"
|
||||
override def withJob(other: Job): Piece = copy(job = other)
|
||||
}
|
||||
case class Wrist(override val pieceType: PieceType, override val job: Job) extends PieceAccessory {
|
||||
override val piece: String = "wrist"
|
||||
override def withJob(other: Job): Piece = copy(job = other)
|
||||
}
|
||||
case class Ring(
|
||||
override val pieceType: PieceType,
|
||||
override val job: Job,
|
||||
override val piece: String = "ring"
|
||||
) extends PieceAccessory {
|
||||
def withJob(other: Job.Job): Piece = copy(job = other)
|
||||
) extends PieceAccessory {
|
||||
override def withJob(other: Job): Piece = copy(job = other)
|
||||
|
||||
override def equals(obj: Any): Boolean = obj match {
|
||||
case Ring(thatPieceType, thatJob, _) => (thatPieceType == pieceType) && (thatJob == job)
|
||||
@ -94,25 +96,24 @@ case class Ring(
|
||||
case ring: Ring => equals(obj) && (ring.piece == this.piece)
|
||||
case _ => false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
case object AccessoryUpgrade extends PieceUpgrade {
|
||||
val piece: String = "accessory upgrade"
|
||||
}
|
||||
case object BodyUpgrade extends PieceUpgrade {
|
||||
val piece: String = "body upgrade"
|
||||
}
|
||||
case object WeaponUpgrade extends PieceUpgrade {
|
||||
val piece: String = "weapon upgrade"
|
||||
}
|
||||
case object AccessoryUpgrade extends PieceUpgrade {
|
||||
override val piece: String = "accessory upgrade"
|
||||
}
|
||||
case object BodyUpgrade extends PieceUpgrade {
|
||||
override val piece: String = "body upgrade"
|
||||
}
|
||||
case object WeaponUpgrade extends PieceUpgrade {
|
||||
override val piece: String = "weapon upgrade"
|
||||
}
|
||||
|
||||
object Piece {
|
||||
def apply(piece: String, pieceType: PieceType.PieceType, job: Job.Job = Job.AnyJob): Piece =
|
||||
def apply(piece: String, pieceType: PieceType, job: Job = Job.AnyJob): Piece =
|
||||
piece.toLowerCase match {
|
||||
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)
|
||||
@ -125,7 +126,7 @@ object Piece {
|
||||
case other => throw new Error(s"Unknown item type $other")
|
||||
}
|
||||
|
||||
lazy val available: Seq[String] = Seq(
|
||||
val available: Seq[String] = Seq(
|
||||
"weapon",
|
||||
"head",
|
||||
"body",
|
||||
|
@ -8,17 +8,16 @@
|
||||
*/
|
||||
package me.arcanis.ffxivbis.models
|
||||
|
||||
sealed trait PieceType
|
||||
|
||||
object PieceType {
|
||||
|
||||
sealed trait PieceType
|
||||
|
||||
case object Crafted extends PieceType
|
||||
case object Tome extends PieceType
|
||||
case object Savage extends PieceType
|
||||
case object Tome extends PieceType
|
||||
case object Crafted extends PieceType
|
||||
case object Artifact extends PieceType
|
||||
|
||||
lazy val available: Seq[PieceType] =
|
||||
Seq(Crafted, Tome, Savage, Artifact)
|
||||
val available: Seq[PieceType] = Seq(Savage, Tome, Crafted, Artifact)
|
||||
|
||||
def withName(pieceType: String): PieceType =
|
||||
available.find(_.toString.equalsIgnoreCase(pieceType)) match {
|
||||
|
@ -11,7 +11,7 @@ package me.arcanis.ffxivbis.models
|
||||
case class Player(
|
||||
id: Long,
|
||||
partyId: String,
|
||||
job: Job.Job,
|
||||
job: Job,
|
||||
nick: String,
|
||||
bis: BiS,
|
||||
loot: Seq[Loot],
|
||||
@ -51,7 +51,7 @@ case class Player(
|
||||
piece match {
|
||||
case None => false
|
||||
case Some(p) if !bis.hasPiece(p) => false
|
||||
case Some(p: PieceUpgrade) => bis.upgrades(p) > lootCount(piece)
|
||||
case Some(p: Piece.PieceUpgrade) => bis.upgrades(p) > lootCount(piece)
|
||||
case Some(_) => lootCount(piece) == 0
|
||||
}
|
||||
|
||||
|
@ -13,14 +13,14 @@ import scala.util.matching.Regex
|
||||
|
||||
trait PlayerIdBase {
|
||||
|
||||
def job: Job.Job
|
||||
def job: Job
|
||||
|
||||
def nick: String
|
||||
|
||||
override def toString: String = s"$nick ($job)"
|
||||
}
|
||||
|
||||
case class PlayerId(partyId: String, job: Job.Job, nick: String) extends PlayerIdBase
|
||||
case class PlayerId(partyId: String, job: Job, nick: String) extends PlayerIdBase
|
||||
|
||||
object PlayerId {
|
||||
|
||||
|
@ -10,7 +10,7 @@ package me.arcanis.ffxivbis.models
|
||||
|
||||
case class PlayerIdWithCounters(
|
||||
partyId: String,
|
||||
job: Job.Job,
|
||||
job: Job,
|
||||
nick: String,
|
||||
isRequired: Boolean,
|
||||
priority: Int,
|
||||
@ -24,8 +24,6 @@ case class PlayerIdWithCounters(
|
||||
def gt(that: PlayerIdWithCounters, orderBy: Seq[String]): Boolean =
|
||||
withCounters(orderBy) > that.withCounters(orderBy)
|
||||
|
||||
def isRequiredToString: String = if (isRequired) "yes" else "no"
|
||||
|
||||
def playerId: PlayerId = PlayerId(partyId, job, nick)
|
||||
|
||||
private val counters: Map[String, Int] = Map(
|
||||
@ -47,13 +45,13 @@ object PlayerIdWithCounters {
|
||||
|
||||
def >(that: PlayerCountersComparator): Boolean = {
|
||||
@scala.annotation.tailrec
|
||||
def compareLists(left: List[Int], right: List[Int]): Boolean =
|
||||
def compare(left: Seq[Int], right: Seq[Int]): Boolean =
|
||||
(left, right) match {
|
||||
case (hl :: tl, hr :: tr) => if (hl == hr) compareLists(tl, tr) else hl > hr
|
||||
case (hl :: tl, hr :: tr) => if (hl == hr) compare(tl, tr) else hl > hr
|
||||
case (_ :: _, Nil) => true
|
||||
case (_, _) => false
|
||||
}
|
||||
compareLists(values.toList, that.values.toList)
|
||||
compare(values, that.values)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -11,6 +11,7 @@ package me.arcanis.ffxivbis.models
|
||||
import org.mindrot.jbcrypt.BCrypt
|
||||
|
||||
object Permission extends Enumeration {
|
||||
|
||||
val get, post, admin = Value
|
||||
}
|
||||
|
||||
|
@ -16,7 +16,6 @@ import com.typesafe.scalalogging.StrictLogging
|
||||
import me.arcanis.ffxivbis.messages._
|
||||
import me.arcanis.ffxivbis.models.Party
|
||||
|
||||
import scala.concurrent.duration.FiniteDuration
|
||||
import scala.concurrent.{ExecutionContext, Future}
|
||||
|
||||
class PartyService(context: ActorContext[Message], storage: ActorRef[DatabaseMessage])
|
||||
@ -37,23 +36,24 @@ class PartyService(context: ActorContext[Message], storage: ActorRef[DatabaseMes
|
||||
override def onMessage(msg: Message): Behavior[Message] = handle(Map.empty)(msg)
|
||||
|
||||
private def handle(cache: Map[String, Party]): Message.Handler = {
|
||||
case ForgetParty(partyId) =>
|
||||
case ControlMessage.ForgetParty(partyId) =>
|
||||
Behaviors.receiveMessage(handle(cache - partyId))
|
||||
|
||||
case GetNewPartyId(client) =>
|
||||
case ControlMessage.GetNewPartyId(client) =>
|
||||
getPartyId.foreach(client ! _)
|
||||
Behaviors.same
|
||||
|
||||
case StoreParty(partyId, party) =>
|
||||
case ControlMessage.StoreParty(partyId, party) =>
|
||||
Behaviors.receiveMessage(handle(cache.updated(partyId, party)))
|
||||
|
||||
case GetParty(partyId, client) =>
|
||||
case DatabaseMessage.GetParty(partyId, client) =>
|
||||
val party = cache.get(partyId) match {
|
||||
case Some(party) => Future.successful(party)
|
||||
case None =>
|
||||
storage.ask(ref => GetParty(partyId, ref)).map { party =>
|
||||
context.self ! StoreParty(partyId, party)
|
||||
context.system.scheduler.scheduleOnce(cacheTimeout, () => context.self ! ForgetParty(partyId))
|
||||
storage.ask(ref => DatabaseMessage.GetParty(partyId, ref)).map { party =>
|
||||
context.self ! ControlMessage.StoreParty(partyId, party)
|
||||
context.system.scheduler
|
||||
.scheduleOnce(cacheTimeout, () => context.self ! ControlMessage.ForgetParty(partyId))
|
||||
party
|
||||
}
|
||||
}
|
||||
@ -62,12 +62,13 @@ class PartyService(context: ActorContext[Message], storage: ActorRef[DatabaseMes
|
||||
|
||||
case req: DatabaseMessage =>
|
||||
storage ! req
|
||||
Behaviors.receiveMessage(handle(cache - req.partyId))
|
||||
val result = if (req.isReadOnly) cache else cache - req.partyId
|
||||
Behaviors.receiveMessage(handle(result))
|
||||
}
|
||||
|
||||
private def getPartyId: Future[String] = {
|
||||
val partyId = Party.randomPartyId
|
||||
storage.ask(ref => Exists(partyId, ref)).flatMap {
|
||||
storage.ask(ref => DatabaseMessage.Exists(partyId, ref)).flatMap {
|
||||
case true => getPartyId
|
||||
case false => Future.successful(partyId)
|
||||
}
|
||||
|
@ -13,14 +13,15 @@ import akka.actor.typed.scaladsl.{AbstractBehavior, ActorContext, Behaviors}
|
||||
import akka.actor.typed.{Behavior, PostStop, Signal}
|
||||
import akka.http.scaladsl.model._
|
||||
import com.typesafe.scalalogging.StrictLogging
|
||||
import me.arcanis.ffxivbis.messages.{BiSProviderMessage, DownloadBiS}
|
||||
import me.arcanis.ffxivbis.messages.BiSProviderMessage
|
||||
import me.arcanis.ffxivbis.models.{BiS, Job, Piece, PieceType}
|
||||
import me.arcanis.ffxivbis.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
|
||||
import scala.concurrent.{ExecutionContext, Future}
|
||||
import scala.util.control.NonFatal
|
||||
import scala.util.{Failure, Success}
|
||||
|
||||
class BisProvider(context: ActorContext[BiSProviderMessage])
|
||||
@ -32,7 +33,7 @@ class BisProvider(context: ActorContext[BiSProviderMessage])
|
||||
|
||||
override def onMessage(msg: BiSProviderMessage): Behavior[BiSProviderMessage] =
|
||||
msg match {
|
||||
case DownloadBiS(link, job, client) =>
|
||||
case BiSProviderMessage.DownloadBiS(link, job, client) =>
|
||||
get(link, job).onComplete {
|
||||
case Success(items) => client ! BiS(items)
|
||||
case Failure(exception) =>
|
||||
@ -46,16 +47,19 @@ class BisProvider(context: ActorContext[BiSProviderMessage])
|
||||
Behaviors.same
|
||||
}
|
||||
|
||||
private def get(link: String, job: Job.Job): Future[Seq[Piece]] =
|
||||
private def get(link: String, job: Job): Future[Seq[Piece]] =
|
||||
try {
|
||||
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 {
|
||||
case exception: Exception => Future.failed(exception)
|
||||
case NonFatal(exception) => Future.failed(exception)
|
||||
}
|
||||
}
|
||||
|
||||
@ -65,9 +69,9 @@ object BisProvider {
|
||||
Behaviors.setup[BiSProviderMessage](context => new BisProvider(context))
|
||||
|
||||
private def parseBisJsonToPieces(
|
||||
job: Job.Job,
|
||||
job: Job,
|
||||
idParser: Parser,
|
||||
pieceTypes: Seq[Long] => Future[Map[Long, PieceType.PieceType]]
|
||||
pieceTypes: Seq[Long] => Future[Map[Long, PieceType]]
|
||||
)(js: JsObject)(implicit executionContext: ExecutionContext): Future[Seq[Piece]] =
|
||||
idParser.parse(job, js).flatMap { pieces =>
|
||||
pieceTypes(pieces.values.toSeq).map { types =>
|
||||
@ -80,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
|
||||
}
|
||||
}
|
||||
|
@ -15,6 +15,7 @@ import spray.json._
|
||||
import scala.concurrent.{ExecutionContext, Future}
|
||||
import scala.jdk.CollectionConverters._
|
||||
import scala.util.Try
|
||||
import scala.util.matching.Regex
|
||||
|
||||
trait XivApi extends RequestExecutor {
|
||||
|
||||
@ -22,7 +23,7 @@ trait XivApi extends RequestExecutor {
|
||||
private val xivapiUrl = config.getString("me.arcanis.ffxivbis.bis-provider.xivapi-url")
|
||||
private val xivapiKey = Try(config.getString("me.arcanis.ffxivbis.bis-provider.xivapi-key")).toOption
|
||||
|
||||
private val preloadedItems: Map[Long, PieceType.PieceType] =
|
||||
private val preloadedItems: Map[Long, PieceType] =
|
||||
config
|
||||
.getConfigList("me.arcanis.ffxivbis.bis-provider.cached-items")
|
||||
.asScala
|
||||
@ -31,9 +32,8 @@ trait XivApi extends RequestExecutor {
|
||||
}
|
||||
.toMap
|
||||
|
||||
def getPieceType(itemIds: Seq[Long]): Future[Map[Long, PieceType.PieceType]] = {
|
||||
val (local, remote) = itemIds.foldLeft((Map.empty[Long, PieceType.PieceType], Seq.empty[Long])) {
|
||||
case ((l, r), id) =>
|
||||
def getPieceType(itemIds: Seq[Long]): Future[Map[Long, PieceType]] = {
|
||||
val (local, remote) = itemIds.foldLeft((Map.empty[Long, PieceType], Seq.empty[Long])) { case ((l, r), id) =>
|
||||
if (preloadedItems.contains(id)) (l.updated(id, preloadedItems(id)), r)
|
||||
else (l, r :+ id)
|
||||
}
|
||||
@ -41,7 +41,7 @@ trait XivApi extends RequestExecutor {
|
||||
else remotePieceType(remote).map(_ ++ local)
|
||||
}
|
||||
|
||||
private def remotePieceType(itemIds: Seq[Long]): Future[Map[Long, PieceType.PieceType]] = {
|
||||
private def remotePieceType(itemIds: Seq[Long]): Future[Map[Long, PieceType]] = {
|
||||
val uriForItems = Uri(xivapiUrl)
|
||||
.withPath(Uri.Path / "item")
|
||||
.withQuery(
|
||||
@ -76,6 +76,10 @@ trait XivApi extends RequestExecutor {
|
||||
|
||||
object XivApi {
|
||||
|
||||
private val defaultShop = JsObject("IsUnique" -> JsNumber(1), "StackSize" -> JsNumber(999))
|
||||
|
||||
private val itemRegexp = new Regex("""Item(Receive|Cost)(\d+)""", "type", "index")
|
||||
|
||||
private def parseXivapiJsonToShop(
|
||||
js: JsObject
|
||||
)(implicit executionContext: ExecutionContext): Future[Map[Long, (String, Long)]] = {
|
||||
@ -85,12 +89,12 @@ object XivApi {
|
||||
.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") =>
|
||||
case (shopName, JsArray(array)) if itemRegexp.matches(shopName) =>
|
||||
val shopId = array.head match {
|
||||
case JsNumber(id) => id.toLong
|
||||
case other => throw deserializationError(s"Could not parse $other")
|
||||
}
|
||||
shopName.replace("ItemReceive", "") -> shopId
|
||||
itemRegexp.findFirstMatchIn(shopName).get.group("index") -> shopId
|
||||
}
|
||||
}
|
||||
.getOrElse(throw deserializationError(s"Could not parse $js"))
|
||||
@ -111,7 +115,7 @@ object XivApi {
|
||||
|
||||
private def parseXivapiJsonToType(
|
||||
shops: Map[Long, (String, Long)]
|
||||
)(js: JsObject)(implicit executionContext: ExecutionContext): Future[Map[Long, PieceType.PieceType]] =
|
||||
)(js: JsObject)(implicit executionContext: ExecutionContext): Future[Map[Long, PieceType]] =
|
||||
Future {
|
||||
val shopMap = js.fields("Results") match {
|
||||
case array: JsArray =>
|
||||
@ -129,7 +133,7 @@ object XivApi {
|
||||
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"))
|
||||
.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
|
||||
|
@ -17,7 +17,7 @@ import scala.concurrent.{ExecutionContext, Future}
|
||||
|
||||
trait Parser extends StrictLogging {
|
||||
|
||||
def parse(job: Job.Job, js: JsObject)(implicit executionContext: ExecutionContext): Future[Map[String, Long]]
|
||||
def parse(job: Job, js: JsObject)(implicit executionContext: ExecutionContext): Future[Map[String, Long]]
|
||||
|
||||
def uri(root: Uri, id: String): Uri
|
||||
}
|
||||
|
@ -18,7 +18,7 @@ import scala.concurrent.{ExecutionContext, Future}
|
||||
|
||||
object Ariyala extends Parser {
|
||||
|
||||
override def parse(job: Job.Job, js: JsObject)(implicit
|
||||
override def parse(job: Job, js: JsObject)(implicit
|
||||
executionContext: ExecutionContext
|
||||
): Future[Map[String, Long]] =
|
||||
Future {
|
||||
|
@ -18,7 +18,7 @@ import scala.concurrent.{ExecutionContext, Future}
|
||||
|
||||
object Etro extends Parser {
|
||||
|
||||
override def parse(job: Job.Job, js: JsObject)(implicit
|
||||
override def parse(job: Job, js: JsObject)(implicit
|
||||
executionContext: ExecutionContext
|
||||
): Future[Map[String, Long]] =
|
||||
Future {
|
||||
|
@ -0,0 +1,44 @@
|
||||
/*
|
||||
* Copyright (c) 2019-2022 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)
|
||||
}
|
||||
}
|
@ -10,7 +10,8 @@ package me.arcanis.ffxivbis.service.database.impl
|
||||
|
||||
import akka.actor.typed.Behavior
|
||||
import akka.actor.typed.scaladsl.Behaviors
|
||||
import me.arcanis.ffxivbis.messages._
|
||||
import me.arcanis.ffxivbis.messages.DatabaseMessage
|
||||
import me.arcanis.ffxivbis.messages.DatabaseMessage._
|
||||
import me.arcanis.ffxivbis.service.database.Database
|
||||
|
||||
trait DatabaseBiSHandler { this: Database =>
|
||||
|
@ -11,7 +11,8 @@ package me.arcanis.ffxivbis.service.database.impl
|
||||
import akka.actor.typed.scaladsl.{AbstractBehavior, ActorContext}
|
||||
import akka.actor.typed.{Behavior, DispatcherSelector}
|
||||
import com.typesafe.config.Config
|
||||
import me.arcanis.ffxivbis.messages.{BisDatabaseMessage, DatabaseMessage, LootDatabaseMessage, PartyDatabaseMessage, UserDatabaseMessage}
|
||||
import me.arcanis.ffxivbis.messages.DatabaseMessage
|
||||
import me.arcanis.ffxivbis.messages.DatabaseMessage.{BisDatabaseMessage, LootDatabaseMessage, PartyDatabaseMessage, UserDatabaseMessage}
|
||||
import me.arcanis.ffxivbis.service.database.Database
|
||||
import me.arcanis.ffxivbis.storage.DatabaseProfile
|
||||
|
||||
|
@ -10,7 +10,8 @@ package me.arcanis.ffxivbis.service.database.impl
|
||||
|
||||
import akka.actor.typed.Behavior
|
||||
import akka.actor.typed.scaladsl.Behaviors
|
||||
import me.arcanis.ffxivbis.messages._
|
||||
import me.arcanis.ffxivbis.messages.DatabaseMessage
|
||||
import me.arcanis.ffxivbis.messages.DatabaseMessage._
|
||||
import me.arcanis.ffxivbis.models.Loot
|
||||
import me.arcanis.ffxivbis.service.database.Database
|
||||
|
||||
|
@ -10,7 +10,8 @@ package me.arcanis.ffxivbis.service.database.impl
|
||||
|
||||
import akka.actor.typed.Behavior
|
||||
import akka.actor.typed.scaladsl.Behaviors
|
||||
import me.arcanis.ffxivbis.messages._
|
||||
import me.arcanis.ffxivbis.messages.DatabaseMessage
|
||||
import me.arcanis.ffxivbis.messages.DatabaseMessage._
|
||||
import me.arcanis.ffxivbis.models.{BiS, Player}
|
||||
import me.arcanis.ffxivbis.service.database.Database
|
||||
|
||||
@ -61,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
|
||||
|
@ -10,7 +10,8 @@ package me.arcanis.ffxivbis.service.database.impl
|
||||
|
||||
import akka.actor.typed.Behavior
|
||||
import akka.actor.typed.scaladsl.Behaviors
|
||||
import me.arcanis.ffxivbis.messages._
|
||||
import me.arcanis.ffxivbis.messages.DatabaseMessage
|
||||
import me.arcanis.ffxivbis.messages.DatabaseMessage._
|
||||
import me.arcanis.ffxivbis.service.database.Database
|
||||
|
||||
trait DatabaseUserHandler { this: Database =>
|
||||
|
@ -53,6 +53,8 @@ trait BiSProfile extends DatabaseConnection {
|
||||
def getPiecesBiSById(playerId: Long): Future[Seq[Loot]] = getPiecesBiSById(Seq(playerId))
|
||||
|
||||
def getPiecesBiSById(playerIds: Seq[Long]): Future[Seq[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)
|
||||
|
@ -16,6 +16,7 @@ import me.arcanis.ffxivbis.models.{Loot, Piece, PlayerId}
|
||||
import java.time.Instant
|
||||
import javax.sql.DataSource
|
||||
import scala.concurrent.{ExecutionContext, Future}
|
||||
import scala.util.control.NonFatal
|
||||
|
||||
class DatabaseProfile(override val executionContext: ExecutionContext, config: Config)
|
||||
extends StrictLogging
|
||||
@ -31,7 +32,7 @@ class DatabaseProfile(override val executionContext: ExecutionContext, config: C
|
||||
val dataSourceConfig = DatabaseConnection.getDataSourceConfig(profile)
|
||||
new HikariDataSource(dataSourceConfig)
|
||||
} catch {
|
||||
case exception: Exception =>
|
||||
case NonFatal(exception) =>
|
||||
logger.error("exception during storage initialization", exception)
|
||||
throw exception
|
||||
}
|
||||
|
@ -59,6 +59,8 @@ trait LootProfile extends DatabaseConnection {
|
||||
def getPiecesById(playerId: Long): Future[Seq[Loot]] = getPiecesById(Seq(playerId))
|
||||
|
||||
def getPiecesById(playerIds: Seq[Long]): Future[Seq[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)
|
||||
|
@ -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()
|
||||
}
|
||||
|
||||
}
|
||||
|
16
src/test/scala/me/arcanis/ffxivbis/ApplicationTest.scala
Normal file
16
src/test/scala/me/arcanis/ffxivbis/ApplicationTest.scala
Normal file
@ -0,0 +1,16 @@
|
||||
package me.arcanis.ffxivbis
|
||||
|
||||
import akka.actor.testkit.typed.scaladsl.ScalaTestWithActorTestKit
|
||||
import org.scalatest.wordspec.AnyWordSpecLike
|
||||
|
||||
class ApplicationTest extends ScalaTestWithActorTestKit(Settings.withRandomDatabase)
|
||||
with AnyWordSpecLike {
|
||||
|
||||
"application" must {
|
||||
|
||||
"load" in {
|
||||
testKit.spawn[Nothing](Application())
|
||||
}
|
||||
|
||||
}
|
||||
}
|
20
src/test/scala/me/arcanis/ffxivbis/ConfigurationTest.scala
Normal file
20
src/test/scala/me/arcanis/ffxivbis/ConfigurationTest.scala
Normal file
@ -0,0 +1,20 @@
|
||||
package me.arcanis.ffxivbis
|
||||
|
||||
import org.scalatest.matchers.should.Matchers
|
||||
import org.scalatest.wordspec.AnyWordSpecLike
|
||||
|
||||
class ConfigurationTest extends AnyWordSpecLike with Matchers {
|
||||
|
||||
private val requiredPaths =
|
||||
Seq("akka.http.server.transparent-head-requests")
|
||||
|
||||
"configuration helper" must {
|
||||
|
||||
requiredPaths.foreach { path =>
|
||||
s"has $path propery" in {
|
||||
Configuration.load().hasPath(path) shouldBe true
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@ -6,49 +6,65 @@ import me.arcanis.ffxivbis.models._
|
||||
import scala.concurrent.Future
|
||||
|
||||
object Fixtures {
|
||||
|
||||
lazy val bis: BiS = BiS(
|
||||
Seq(
|
||||
Weapon(pieceType = PieceType.Savage ,Job.DNC),
|
||||
Head(pieceType = PieceType.Savage, Job.DNC),
|
||||
Body(pieceType = PieceType.Savage, Job.DNC),
|
||||
Hands(pieceType = PieceType.Tome, Job.DNC),
|
||||
Legs(pieceType = PieceType.Tome, Job.DNC),
|
||||
Feet(pieceType = PieceType.Savage, Job.DNC),
|
||||
Ears(pieceType = PieceType.Savage, Job.DNC),
|
||||
Neck(pieceType = PieceType.Tome, Job.DNC),
|
||||
Wrist(pieceType = PieceType.Savage, Job.DNC),
|
||||
Ring(pieceType = PieceType.Tome, Job.DNC, "left ring"),
|
||||
Ring(pieceType = PieceType.Tome, Job.DNC, "right ring")
|
||||
Piece.Weapon(pieceType = PieceType.Savage ,Job.DNC),
|
||||
Piece.Head(pieceType = PieceType.Savage, Job.DNC),
|
||||
Piece.Body(pieceType = PieceType.Savage, Job.DNC),
|
||||
Piece.Hands(pieceType = PieceType.Tome, Job.DNC),
|
||||
Piece.Legs(pieceType = PieceType.Tome, Job.DNC),
|
||||
Piece.Feet(pieceType = PieceType.Savage, Job.DNC),
|
||||
Piece.Ears(pieceType = PieceType.Savage, Job.DNC),
|
||||
Piece.Neck(pieceType = PieceType.Tome, Job.DNC),
|
||||
Piece.Wrist(pieceType = PieceType.Savage, Job.DNC),
|
||||
Piece.Ring(pieceType = PieceType.Tome, Job.DNC, "left ring"),
|
||||
Piece.Ring(pieceType = PieceType.Tome, Job.DNC, "right ring")
|
||||
)
|
||||
)
|
||||
lazy val bis2: BiS = BiS(
|
||||
Seq(
|
||||
Weapon(pieceType = PieceType.Savage ,Job.DNC),
|
||||
Head(pieceType = PieceType.Tome, Job.DNC),
|
||||
Body(pieceType = PieceType.Savage, Job.DNC),
|
||||
Hands(pieceType = PieceType.Tome, Job.DNC),
|
||||
Legs(pieceType = PieceType.Savage, Job.DNC),
|
||||
Feet(pieceType = PieceType.Tome, Job.DNC),
|
||||
Ears(pieceType = PieceType.Savage, Job.DNC),
|
||||
Neck(pieceType = PieceType.Savage, Job.DNC),
|
||||
Wrist(pieceType = PieceType.Savage, Job.DNC),
|
||||
Ring(pieceType = PieceType.Tome, Job.DNC, "left ring"),
|
||||
Ring(pieceType = PieceType.Savage, Job.DNC, "right ring")
|
||||
Piece.Weapon(pieceType = PieceType.Savage ,Job.DNC),
|
||||
Piece.Head(pieceType = PieceType.Tome, Job.DNC),
|
||||
Piece.Body(pieceType = PieceType.Savage, Job.DNC),
|
||||
Piece.Hands(pieceType = PieceType.Tome, Job.DNC),
|
||||
Piece.Legs(pieceType = PieceType.Savage, Job.DNC),
|
||||
Piece.Feet(pieceType = PieceType.Tome, Job.DNC),
|
||||
Piece.Ears(pieceType = PieceType.Savage, Job.DNC),
|
||||
Piece.Neck(pieceType = PieceType.Savage, Job.DNC),
|
||||
Piece.Wrist(pieceType = PieceType.Savage, Job.DNC),
|
||||
Piece.Ring(pieceType = PieceType.Tome, Job.DNC, "left ring"),
|
||||
Piece.Ring(pieceType = PieceType.Savage, Job.DNC, "right ring")
|
||||
)
|
||||
)
|
||||
lazy val bis3: BiS = BiS(
|
||||
Seq(
|
||||
Weapon(pieceType = PieceType.Savage ,Job.SGE),
|
||||
Head(pieceType = PieceType.Tome, Job.SGE),
|
||||
Body(pieceType = PieceType.Savage, Job.SGE),
|
||||
Hands(pieceType = PieceType.Tome, Job.SGE),
|
||||
Legs(pieceType = PieceType.Tome, Job.SGE),
|
||||
Feet(pieceType = PieceType.Savage, Job.SGE),
|
||||
Ears(pieceType = PieceType.Savage, Job.SGE),
|
||||
Neck(pieceType = PieceType.Tome, Job.SGE),
|
||||
Wrist(pieceType = PieceType.Savage, Job.SGE),
|
||||
Ring(pieceType = PieceType.Savage, Job.SGE, "left ring"),
|
||||
Ring(pieceType = PieceType.Tome, Job.SGE, "right ring")
|
||||
Piece.Weapon(pieceType = PieceType.Savage ,Job.SGE),
|
||||
Piece.Head(pieceType = PieceType.Tome, Job.SGE),
|
||||
Piece.Body(pieceType = PieceType.Savage, Job.SGE),
|
||||
Piece.Hands(pieceType = PieceType.Tome, Job.SGE),
|
||||
Piece.Legs(pieceType = PieceType.Tome, Job.SGE),
|
||||
Piece.Feet(pieceType = PieceType.Savage, Job.SGE),
|
||||
Piece.Ears(pieceType = PieceType.Savage, Job.SGE),
|
||||
Piece.Neck(pieceType = PieceType.Tome, Job.SGE),
|
||||
Piece.Wrist(pieceType = PieceType.Savage, Job.SGE),
|
||||
Piece.Ring(pieceType = PieceType.Savage, Job.SGE, "left 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")
|
||||
)
|
||||
)
|
||||
|
||||
@ -57,17 +73,18 @@ object Fixtures {
|
||||
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 lootWeapon: Piece = Weapon(pieceType = PieceType.Tome, Job.AnyJob)
|
||||
lazy val lootBody: Piece = Body(pieceType = PieceType.Savage, Job.AnyJob)
|
||||
lazy val lootBodyCrafted: Piece = Body(pieceType = PieceType.Crafted, Job.AnyJob)
|
||||
lazy val lootHands: Piece = Hands(pieceType = PieceType.Tome, Job.AnyJob)
|
||||
lazy val lootLegs: Piece = Legs(pieceType = PieceType.Savage, Job.AnyJob)
|
||||
lazy val lootEars: Piece = Ears(pieceType = PieceType.Savage, Job.AnyJob)
|
||||
lazy val lootRing: Piece = Ring(pieceType = PieceType.Tome, Job.AnyJob)
|
||||
lazy val lootLeftRing: Piece = Ring(pieceType = PieceType.Tome, Job.AnyJob, "left ring")
|
||||
lazy val lootRightRing: Piece = Ring(pieceType = PieceType.Tome, Job.AnyJob, "right ring")
|
||||
lazy val lootUpgrade: Piece = BodyUpgrade
|
||||
lazy val lootWeapon: Piece = Piece.Weapon(pieceType = PieceType.Tome, Job.AnyJob)
|
||||
lazy val lootBody: Piece = Piece.Body(pieceType = PieceType.Savage, Job.AnyJob)
|
||||
lazy val lootBodyCrafted: Piece = Piece.Body(pieceType = PieceType.Crafted, Job.AnyJob)
|
||||
lazy val lootHands: Piece = Piece.Hands(pieceType = PieceType.Tome, Job.AnyJob)
|
||||
lazy val lootLegs: Piece = Piece.Legs(pieceType = PieceType.Savage, Job.AnyJob)
|
||||
lazy val lootEars: Piece = Piece.Ears(pieceType = PieceType.Savage, Job.AnyJob)
|
||||
lazy val lootRing: Piece = Piece.Ring(pieceType = PieceType.Tome, Job.AnyJob)
|
||||
lazy val lootLeftRing: Piece = Piece.Ring(pieceType = PieceType.Tome, Job.AnyJob, "left ring")
|
||||
lazy val lootRightRing: Piece = Piece.Ring(pieceType = PieceType.Tome, Job.AnyJob, "right ring")
|
||||
lazy val lootUpgrade: Piece = Piece.BodyUpgrade
|
||||
lazy val loot: Seq[Piece] = Seq(lootBody, lootHands, lootLegs, lootUpgrade)
|
||||
|
||||
lazy val partyId: String = Party.randomPartyId
|
||||
@ -84,4 +101,5 @@ object Fixtures {
|
||||
lazy val users: Seq[User] = Seq(userAdmin, userGet)
|
||||
|
||||
lazy val authProvider: AuthorizationProvider = (_: String, _: String) => Future.successful(Some(userAdmin))
|
||||
lazy val rejectingAuthProvider: AuthorizationProvider = (_: String, _: String) => Future.successful(None)
|
||||
}
|
||||
|
@ -5,11 +5,12 @@ import com.typesafe.config.{Config, ConfigFactory, ConfigValueFactory}
|
||||
import java.io.File
|
||||
|
||||
object Settings {
|
||||
|
||||
def config(values: Map[String, AnyRef]): Config = {
|
||||
@scala.annotation.tailrec
|
||||
def replace(acc: Config, iter: List[(String, AnyRef)]): Config = iter match {
|
||||
case Nil => acc
|
||||
case (key -> value) :: tail => replace(acc.withValue(key, ConfigValueFactory.fromAnyRef(value)), tail)
|
||||
def replace(config: Config, iter: List[(String, AnyRef)]): Config = iter match {
|
||||
case Nil => config
|
||||
case (key -> value) :: tail => replace(config.withValue(key, ConfigValueFactory.fromAnyRef(value)), tail)
|
||||
}
|
||||
|
||||
val default = ConfigFactory.load()
|
||||
@ -23,7 +24,9 @@ object Settings {
|
||||
if (databaseFile.exists)
|
||||
databaseFile.delete()
|
||||
}
|
||||
|
||||
def randomDatabasePath: String = File.createTempFile("ffxivdb-",".db").toPath.toString
|
||||
|
||||
def withRandomDatabase: Config =
|
||||
config(Map("me.arcanis.ffxivbis.database.sqlite.jdbcUrl" -> s"jdbc:sqlite:$randomDatabasePath"))
|
||||
}
|
||||
|
@ -0,0 +1,44 @@
|
||||
package me.arcanis.ffxivbis.http
|
||||
|
||||
import akka.http.scaladsl.model.headers.{Authorization, BasicHttpCredentials}
|
||||
import akka.http.scaladsl.model.{StatusCodes, Uri}
|
||||
import akka.http.scaladsl.server.Route
|
||||
import akka.http.scaladsl.testkit.ScalatestRouteTest
|
||||
import me.arcanis.ffxivbis.Fixtures
|
||||
import me.arcanis.ffxivbis.http.view.RootView
|
||||
import org.scalatest.matchers.should.Matchers
|
||||
import org.scalatest.wordspec.AnyWordSpecLike
|
||||
|
||||
class AuthorizationTest extends AnyWordSpecLike with Matchers with ScalatestRouteTest {
|
||||
|
||||
private val auth =
|
||||
Authorization(BasicHttpCredentials(Fixtures.userAdmin.username, Fixtures.userPassword))
|
||||
|
||||
"authorization directive" must {
|
||||
|
||||
"accept credentials" in {
|
||||
val route = new RootView(Fixtures.authProvider).routes
|
||||
|
||||
Get(Uri(s"/party/${Fixtures.partyId}")).withHeaders(auth) ~> route ~> check {
|
||||
status shouldEqual StatusCodes.OK
|
||||
}
|
||||
}
|
||||
|
||||
"reject credentials" in {
|
||||
val route = new RootView(Fixtures.rejectingAuthProvider).routes
|
||||
|
||||
Get(Uri(s"/party/${Fixtures.partyId}")).withHeaders(auth) ~> Route.seal(route) ~> check {
|
||||
status shouldEqual StatusCodes.Unauthorized
|
||||
}
|
||||
}
|
||||
|
||||
"reject with empty credentials" in {
|
||||
val route = new RootView(Fixtures.authProvider).routes
|
||||
|
||||
Get(Uri(s"/party/${Fixtures.partyId}")) ~> Route.seal(route) ~> check {
|
||||
status shouldEqual StatusCodes.Unauthorized
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
22
src/test/scala/me/arcanis/ffxivbis/http/HttpLogTest.scala
Normal file
22
src/test/scala/me/arcanis/ffxivbis/http/HttpLogTest.scala
Normal file
@ -0,0 +1,22 @@
|
||||
package me.arcanis.ffxivbis.http
|
||||
|
||||
import akka.http.scaladsl.model.{StatusCodes, Uri}
|
||||
import akka.http.scaladsl.server.Directives.complete
|
||||
import akka.http.scaladsl.testkit.ScalatestRouteTest
|
||||
import org.scalatest.matchers.should.Matchers
|
||||
import org.scalatest.wordspec.AnyWordSpecLike
|
||||
|
||||
class HttpLogTest extends AnyWordSpecLike with Matchers with ScalatestRouteTest {
|
||||
|
||||
private val log = new HttpLog {}
|
||||
|
||||
"log directive" must {
|
||||
|
||||
"work with empty request" in {
|
||||
Get(Uri("/")) ~> log.withHttpLog(complete(StatusCodes.OK)) ~> check {
|
||||
status shouldEqual StatusCodes.OK
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@ -0,0 +1,39 @@
|
||||
package me.arcanis.ffxivbis.http
|
||||
|
||||
import akka.actor.testkit.typed.scaladsl.ActorTestKit
|
||||
import akka.http.scaladsl.model.{StatusCodes, Uri}
|
||||
import akka.http.scaladsl.testkit.ScalatestRouteTest
|
||||
import com.typesafe.config.Config
|
||||
import me.arcanis.ffxivbis.Settings
|
||||
import me.arcanis.ffxivbis.service.PartyService
|
||||
import me.arcanis.ffxivbis.service.bis.BisProvider
|
||||
import me.arcanis.ffxivbis.service.database.Database
|
||||
import org.scalatest.matchers.should.Matchers
|
||||
import org.scalatest.wordspec.AnyWordSpecLike
|
||||
|
||||
class RootEndpointTest extends AnyWordSpecLike with Matchers with ScalatestRouteTest {
|
||||
|
||||
private val testKit = ActorTestKit(Settings.withRandomDatabase)
|
||||
override val testConfig: Config = testKit.system.settings.config
|
||||
|
||||
private val storage = testKit.spawn(Database())
|
||||
private val provider = testKit.spawn(BisProvider())
|
||||
private val party = testKit.spawn(PartyService(storage))
|
||||
private val route = new RootEndpoint(testKit.system, party, provider).routes
|
||||
|
||||
"root route" must {
|
||||
|
||||
"return swagger ui" in {
|
||||
Get(Uri("/api-docs")) ~> route ~> check {
|
||||
status shouldEqual StatusCodes.OK
|
||||
}
|
||||
}
|
||||
|
||||
"return static routes" in {
|
||||
Get(Uri("/static/favicon.ico")) ~> route ~> check {
|
||||
status shouldEqual StatusCodes.OK
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
28
src/test/scala/me/arcanis/ffxivbis/http/SwaggerTest.scala
Normal file
28
src/test/scala/me/arcanis/ffxivbis/http/SwaggerTest.scala
Normal file
@ -0,0 +1,28 @@
|
||||
package me.arcanis.ffxivbis.http
|
||||
|
||||
import akka.http.scaladsl.model.{StatusCodes, Uri}
|
||||
import akka.http.scaladsl.testkit.ScalatestRouteTest
|
||||
import me.arcanis.ffxivbis.Settings
|
||||
import org.scalatest.matchers.should.Matchers
|
||||
import org.scalatest.wordspec.AnyWordSpecLike
|
||||
|
||||
class SwaggerTest extends AnyWordSpecLike with Matchers with ScalatestRouteTest {
|
||||
|
||||
private val swagger = new Swagger(Settings.withRandomDatabase)
|
||||
|
||||
"swagger guard" must {
|
||||
|
||||
"generate json" in {
|
||||
Get(Uri("/api-docs/swagger.json")) ~> swagger.routes ~> check {
|
||||
status shouldEqual StatusCodes.OK
|
||||
}
|
||||
}
|
||||
|
||||
"generate yml" in {
|
||||
Get(Uri("/api-docs/swagger.yaml")) ~> swagger.routes ~> check {
|
||||
status shouldEqual StatusCodes.OK
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@ -8,7 +8,7 @@ import akka.http.scaladsl.testkit.{RouteTestTimeout, ScalatestRouteTest}
|
||||
import akka.testkit.TestKit
|
||||
import com.typesafe.config.Config
|
||||
import me.arcanis.ffxivbis.http.api.v1.json._
|
||||
import me.arcanis.ffxivbis.messages.{AddPlayer, AddUser}
|
||||
import me.arcanis.ffxivbis.messages.DatabaseMessage.{AddPlayer, AddUser}
|
||||
import me.arcanis.ffxivbis.models.{BiS, Job}
|
||||
import me.arcanis.ffxivbis.service.PartyService
|
||||
import me.arcanis.ffxivbis.service.bis.BisProvider
|
||||
@ -53,15 +53,6 @@ class BiSEndpointTest extends AnyWordSpecLike with Matchers with ScalatestRouteT
|
||||
super.afterAll()
|
||||
}
|
||||
|
||||
private def compareBiSResponse(actual: PlayerModel, expected: PlayerModel): Unit = {
|
||||
actual.partyId shouldEqual expected.partyId
|
||||
actual.nick shouldEqual expected.nick
|
||||
actual.job shouldEqual expected.job
|
||||
Compare.seqEquals(actual.bis.get, expected.bis.get) shouldEqual true
|
||||
actual.link shouldEqual expected.link
|
||||
actual.priority shouldEqual expected.priority
|
||||
}
|
||||
|
||||
"api v1 bis endpoint" must {
|
||||
|
||||
"create best in slot set from ariyala" in {
|
||||
@ -216,4 +207,13 @@ class BiSEndpointTest extends AnyWordSpecLike with Matchers with ScalatestRouteT
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private def compareBiSResponse(actual: PlayerModel, expected: PlayerModel): Unit = {
|
||||
actual.partyId shouldEqual expected.partyId
|
||||
actual.nick shouldEqual expected.nick
|
||||
actual.job shouldEqual expected.job
|
||||
Compare.seqEquals(actual.bis.get, expected.bis.get) shouldEqual true
|
||||
actual.link shouldEqual expected.link
|
||||
actual.priority shouldEqual expected.priority
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,78 @@
|
||||
package me.arcanis.ffxivbis.http.api.v1
|
||||
|
||||
import akka.http.scaladsl.model.StatusCodes
|
||||
import akka.http.scaladsl.model.headers.Allow
|
||||
import akka.http.scaladsl.server.Directives.{path, _}
|
||||
import akka.http.scaladsl.server.Route
|
||||
import akka.http.scaladsl.testkit.ScalatestRouteTest
|
||||
import me.arcanis.ffxivbis.http.api.v1.json._
|
||||
import org.scalatest.matchers.should.Matchers
|
||||
import org.scalatest.wordspec.AnyWordSpecLike
|
||||
|
||||
class HttpHandlerTest extends AnyWordSpecLike with Matchers with ScalatestRouteTest with JsonSupport with HttpHandler {
|
||||
|
||||
"http handler" must {
|
||||
|
||||
"convert IllegalArgumentException into 400 response" in {
|
||||
Get("/400") ~> withExceptionHandler("400", failWith(new IllegalArgumentException(""))) ~> check {
|
||||
status shouldEqual StatusCodes.BadRequest
|
||||
responseAs[ErrorModel] shouldEqual ErrorModel("")
|
||||
}
|
||||
}
|
||||
|
||||
"convert IllegalArgumentException into 400 response with details" in {
|
||||
Get("/400") ~> withExceptionHandler("400", failWith(new IllegalArgumentException("message"))) ~> check {
|
||||
status shouldEqual StatusCodes.BadRequest
|
||||
responseAs[ErrorModel] shouldEqual ErrorModel("message")
|
||||
}
|
||||
}
|
||||
|
||||
"convert exception message to error response" in {
|
||||
Get("/500") ~> withExceptionHandler("500", failWith(new ArithmeticException)) ~> check {
|
||||
status shouldEqual StatusCodes.InternalServerError
|
||||
responseAs[ErrorModel] shouldEqual ErrorModel("unknown server error")
|
||||
}
|
||||
}
|
||||
|
||||
"process OPTIONS request" in {
|
||||
Options("/200") ~> withRejectionHandler() ~> check {
|
||||
status shouldEqual StatusCodes.OK
|
||||
headers.collectFirst { case header: Allow => header } should not be empty
|
||||
responseAs[String] shouldBe empty
|
||||
}
|
||||
}
|
||||
|
||||
"reject unknown request" in {
|
||||
Post("/200") ~> withRejectionHandler() ~> check {
|
||||
status shouldEqual StatusCodes.MethodNotAllowed
|
||||
headers.collectFirst { case header: Allow => header } should not be empty
|
||||
responseAs[ErrorModel].message should not be empty
|
||||
}
|
||||
}
|
||||
|
||||
"handle 404 response" in {
|
||||
Get("/404") ~> withRejectionHandler() ~> check {
|
||||
status shouldEqual StatusCodes.NotFound
|
||||
responseAs[ErrorModel] shouldEqual ErrorModel("The requested resource could not be found.")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private def single(uri: String, completeWith: Route) =
|
||||
path(uri)(get(completeWith))
|
||||
|
||||
private def withExceptionHandler(uri: String = "200", completeWith: Route = complete(StatusCodes.OK)) =
|
||||
Route.seal {
|
||||
handleExceptions(exceptionHandler) {
|
||||
single(uri, completeWith)
|
||||
}
|
||||
}
|
||||
|
||||
private def withRejectionHandler(uri: String = "200", completeWith: Route = complete(StatusCodes.OK)) =
|
||||
Route.seal {
|
||||
handleRejections(rejectionHandler) {
|
||||
single(uri, completeWith)
|
||||
}
|
||||
}
|
||||
}
|
@ -8,7 +8,7 @@ import akka.http.scaladsl.testkit.{RouteTestTimeout, ScalatestRouteTest}
|
||||
import akka.testkit.TestKit
|
||||
import com.typesafe.config.Config
|
||||
import me.arcanis.ffxivbis.http.api.v1.json._
|
||||
import me.arcanis.ffxivbis.messages.{AddPlayer, AddUser}
|
||||
import me.arcanis.ffxivbis.messages.DatabaseMessage.{AddPlayer, AddUser}
|
||||
import me.arcanis.ffxivbis.service.PartyService
|
||||
import me.arcanis.ffxivbis.service.database.{Database, Migration}
|
||||
import me.arcanis.ffxivbis.{Fixtures, Settings}
|
||||
@ -95,5 +95,15 @@ class LootEndpointTest extends AnyWordSpecLike with Matchers with ScalatestRoute
|
||||
}
|
||||
}
|
||||
|
||||
"suggest loot" in {
|
||||
val entity = PieceModel.fromPiece(Fixtures.lootBody)
|
||||
val response = Seq(Fixtures.playerEmpty.withCounters(Some(Fixtures.lootBody))).map(PlayerIdWithCountersModel.fromPlayerId)
|
||||
|
||||
Put(endpoint, entity).withHeaders(auth) ~> route ~> check {
|
||||
status shouldEqual StatusCodes.OK
|
||||
responseAs[Seq[PlayerIdWithCountersModel]] shouldEqual response
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -8,7 +8,7 @@ import akka.http.scaladsl.testkit.{RouteTestTimeout, ScalatestRouteTest}
|
||||
import akka.testkit.TestKit
|
||||
import com.typesafe.config.Config
|
||||
import me.arcanis.ffxivbis.http.api.v1.json._
|
||||
import me.arcanis.ffxivbis.messages.AddUser
|
||||
import me.arcanis.ffxivbis.messages.DatabaseMessage.AddUser
|
||||
import me.arcanis.ffxivbis.models.PartyDescription
|
||||
import me.arcanis.ffxivbis.service.PartyService
|
||||
import me.arcanis.ffxivbis.service.bis.BisProvider
|
||||
|
@ -8,7 +8,7 @@ import akka.http.scaladsl.testkit.{RouteTestTimeout, ScalatestRouteTest}
|
||||
import akka.testkit.TestKit
|
||||
import com.typesafe.config.Config
|
||||
import me.arcanis.ffxivbis.http.api.v1.json._
|
||||
import me.arcanis.ffxivbis.messages.{AddPlayer, AddUser}
|
||||
import me.arcanis.ffxivbis.messages.DatabaseMessage.{AddPlayer, AddUser}
|
||||
import me.arcanis.ffxivbis.service.PartyService
|
||||
import me.arcanis.ffxivbis.service.bis.BisProvider
|
||||
import me.arcanis.ffxivbis.service.database.{Database, Migration}
|
||||
@ -52,7 +52,7 @@ class PlayerEndpointTest extends AnyWordSpecLike with Matchers with ScalatestRou
|
||||
|
||||
"api v1 player endpoint" must {
|
||||
|
||||
"get users" in {
|
||||
"get users belonging to the party" in {
|
||||
val response = Seq(PlayerModel.fromPlayer(Fixtures.playerEmpty))
|
||||
|
||||
Get(endpoint).withHeaders(auth) ~> route ~> check {
|
||||
@ -61,5 +61,42 @@ class PlayerEndpointTest extends AnyWordSpecLike with Matchers with ScalatestRou
|
||||
}
|
||||
}
|
||||
|
||||
"get party stats" in {
|
||||
val response = Seq(PlayerIdWithCountersModel.fromPlayerId(Fixtures.playerEmpty.withCounters(None)))
|
||||
|
||||
Get(endpoint.withPath(endpoint.path / "stats")).withHeaders(auth) ~> route ~> check {
|
||||
status shouldEqual StatusCodes.OK
|
||||
responseAs[Seq[PlayerIdWithCountersModel]] shouldEqual response
|
||||
}
|
||||
}
|
||||
|
||||
"add new player to the party" in {
|
||||
val entity = PlayerActionModel(ApiAction.add, PlayerModel.fromPlayer(Fixtures.playerWithBiS))
|
||||
|
||||
Post(endpoint, entity).withHeaders(auth) ~> route ~> check {
|
||||
status shouldEqual StatusCodes.Accepted
|
||||
responseAs[String] shouldEqual ""
|
||||
}
|
||||
|
||||
Get(endpoint).withHeaders(auth) ~> route ~> check {
|
||||
status shouldEqual StatusCodes.OK
|
||||
responseAs[Seq[PlayerModel]].map(_.toPlayer.playerId) should contain(Fixtures.playerWithBiS.playerId)
|
||||
}
|
||||
}
|
||||
|
||||
"remove player from the party" in {
|
||||
val entity = PlayerActionModel(ApiAction.remove, PlayerModel.fromPlayer(Fixtures.playerEmpty))
|
||||
|
||||
Post(endpoint, entity).withHeaders(auth) ~> route ~> check {
|
||||
status shouldEqual StatusCodes.Accepted
|
||||
responseAs[String] shouldEqual ""
|
||||
}
|
||||
|
||||
Get(endpoint).withHeaders(auth) ~> route ~> check {
|
||||
status shouldEqual StatusCodes.OK
|
||||
responseAs[Seq[PlayerModel]].map(_.toPlayer.playerId) should not contain(Fixtures.playerEmpty.playerId)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -2,8 +2,6 @@ package me.arcanis.ffxivbis.http.api.v1
|
||||
|
||||
import akka.http.scaladsl.model.StatusCodes
|
||||
import akka.http.scaladsl.testkit.ScalatestRouteTest
|
||||
import com.typesafe.config.Config
|
||||
import me.arcanis.ffxivbis.Settings
|
||||
import me.arcanis.ffxivbis.http.api.v1.json._
|
||||
import org.scalatest.matchers.should.Matchers
|
||||
import org.scalatest.wordspec.AnyWordSpecLike
|
||||
@ -13,8 +11,6 @@ import scala.language.postfixOps
|
||||
class StatusEndpointTest extends AnyWordSpecLike
|
||||
with Matchers with ScalatestRouteTest with JsonSupport {
|
||||
|
||||
override val testConfig: Config = Settings.withRandomDatabase
|
||||
|
||||
private val route = new StatusEndpoint().routes
|
||||
|
||||
"api v1 status endpoint" must {
|
||||
|
@ -48,16 +48,16 @@ class UserEndpointTest extends AnyWordSpecLike with Matchers with ScalatestRoute
|
||||
|
||||
"create a party" in {
|
||||
val uri = Uri(s"/party")
|
||||
val entity = UserModel.fromUser(Fixtures.userAdmin).copy(password = Fixtures.userPassword)
|
||||
val entity = UserModel.fromUser(Fixtures.userAdmin).copy(password = Some(Fixtures.userPassword))
|
||||
|
||||
Put(uri, entity) ~> route ~> check {
|
||||
Post(uri, entity) ~> route ~> check {
|
||||
status shouldEqual StatusCodes.OK
|
||||
partyId = responseAs[PartyIdModel].partyId
|
||||
}
|
||||
}
|
||||
|
||||
"add user" in {
|
||||
val entity = UserModel.fromUser(Fixtures.userGet).copy(partyId = partyId, password = Fixtures.userPassword2)
|
||||
val entity = UserModel.fromUser(Fixtures.userGet).copy(partyId = partyId, password = Some(Fixtures.userPassword2))
|
||||
|
||||
Post(endpoint, entity).withHeaders(auth) ~> route ~> check {
|
||||
status shouldEqual StatusCodes.Accepted
|
||||
|
@ -0,0 +1,55 @@
|
||||
package me.arcanis.ffxivbis.http.view
|
||||
|
||||
import akka.http.scaladsl.model.{StatusCodes, Uri}
|
||||
import akka.http.scaladsl.model.headers.{Authorization, BasicHttpCredentials}
|
||||
import akka.http.scaladsl.testkit.ScalatestRouteTest
|
||||
import me.arcanis.ffxivbis.Fixtures
|
||||
import org.scalatest.matchers.should.Matchers
|
||||
import org.scalatest.wordspec.AnyWordSpecLike
|
||||
|
||||
class RootViewTest extends AnyWordSpecLike with Matchers with ScalatestRouteTest {
|
||||
|
||||
private val auth =
|
||||
Authorization(BasicHttpCredentials(Fixtures.userAdmin.username, Fixtures.userPassword))
|
||||
|
||||
private val route = new RootView(Fixtures.authProvider).routes
|
||||
|
||||
"html view endpoint" must {
|
||||
|
||||
"return root view" in {
|
||||
Get(Uri("/")) ~> route ~> check {
|
||||
status shouldEqual StatusCodes.OK
|
||||
responseAs[String] should not be empty
|
||||
}
|
||||
}
|
||||
|
||||
"return root party view" in {
|
||||
Get(Uri(s"/party/${Fixtures.partyId}")).withHeaders(auth) ~> route ~> check {
|
||||
status shouldEqual StatusCodes.OK
|
||||
responseAs[String] should not be empty
|
||||
}
|
||||
}
|
||||
|
||||
"return bis view" in {
|
||||
Get(Uri(s"/party/${Fixtures.partyId}/bis")).withHeaders(auth) ~> route ~> check {
|
||||
status shouldEqual StatusCodes.OK
|
||||
responseAs[String] should not be empty
|
||||
}
|
||||
}
|
||||
|
||||
"return loot view" in {
|
||||
Get(Uri(s"/party/${Fixtures.partyId}/loot")).withHeaders(auth) ~> route ~> check {
|
||||
status shouldEqual StatusCodes.OK
|
||||
responseAs[String] should not be empty
|
||||
}
|
||||
}
|
||||
|
||||
"return users view" in {
|
||||
Get(Uri(s"/party/${Fixtures.partyId}/users")).withHeaders(auth) ~> route ~> check {
|
||||
status shouldEqual StatusCodes.OK
|
||||
responseAs[String] should not be empty
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@ -46,7 +46,7 @@ class BiSTest extends AnyWordSpecLike with Matchers {
|
||||
}
|
||||
|
||||
"return upgrade list" in {
|
||||
Compare.mapEquals(Fixtures.bis.upgrades, Map[PieceUpgrade, Int](BodyUpgrade -> 2, AccessoryUpgrade -> 3)) shouldEqual true
|
||||
Compare.mapEquals(Fixtures.bis.upgrades, Map[Piece.PieceUpgrade, Int](Piece.BodyUpgrade -> 2, Piece.AccessoryUpgrade -> 3)) shouldEqual true
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -9,13 +9,13 @@ class PieceTest extends AnyWordSpecLike with Matchers {
|
||||
"piece model" must {
|
||||
|
||||
"return upgrade" in {
|
||||
Fixtures.lootWeapon.upgrade shouldEqual Some(WeaponUpgrade)
|
||||
Fixtures.lootWeapon.upgrade shouldEqual Some(Piece.WeaponUpgrade)
|
||||
Fixtures.lootBody.upgrade shouldEqual None
|
||||
Fixtures.lootHands.upgrade shouldEqual Some(BodyUpgrade)
|
||||
Fixtures.lootHands.upgrade shouldEqual Some(Piece.BodyUpgrade)
|
||||
Fixtures.lootLegs.upgrade shouldEqual None
|
||||
Fixtures.lootEars.upgrade shouldEqual None
|
||||
Fixtures.lootLeftRing.upgrade shouldEqual Some(AccessoryUpgrade)
|
||||
Fixtures.lootRightRing.upgrade shouldEqual Some(AccessoryUpgrade)
|
||||
Fixtures.lootLeftRing.upgrade shouldEqual Some(Piece.AccessoryUpgrade)
|
||||
Fixtures.lootRightRing.upgrade shouldEqual Some(Piece.AccessoryUpgrade)
|
||||
}
|
||||
|
||||
"build piece from string" in {
|
||||
|
@ -2,7 +2,7 @@ package me.arcanis.ffxivbis.service
|
||||
|
||||
import akka.actor.testkit.typed.scaladsl.ActorTestKit
|
||||
import akka.actor.typed.scaladsl.AskPattern.Askable
|
||||
import me.arcanis.ffxivbis.messages.DownloadBiS
|
||||
import me.arcanis.ffxivbis.messages.BiSProviderMessage.DownloadBiS
|
||||
import me.arcanis.ffxivbis.models._
|
||||
import me.arcanis.ffxivbis.service.bis.BisProvider
|
||||
import me.arcanis.ffxivbis.{Fixtures, Settings}
|
||||
@ -28,10 +28,10 @@ class LootSelectorTest extends AnyWordSpecLike with Matchers with BeforeAndAfter
|
||||
val testKit = ActorTestKit(Settings.withRandomDatabase)
|
||||
val provider = testKit.spawn(BisProvider())
|
||||
|
||||
val dncSet = Await.result(provider.ask(DownloadBiS(Fixtures.link, Job.DNC, _) )(timeout, testKit.scheduler), timeout)
|
||||
val dncSet = Await.result(provider.ask(DownloadBiS(Fixtures.link, Job.DNC, _))(timeout, testKit.scheduler), timeout)
|
||||
dnc = dnc.withBiS(Some(dncSet))
|
||||
|
||||
val drgSet = Await.result(provider.ask(DownloadBiS(Fixtures.link2, Job.DRG, _) )(timeout, testKit.scheduler), timeout)
|
||||
val drgSet = Await.result(provider.ask(DownloadBiS(Fixtures.link2, Job.DRG, _))(timeout, testKit.scheduler), timeout)
|
||||
drg = drg.withBiS(Some(drgSet))
|
||||
|
||||
default = default.withPlayer(dnc).withPlayer(drg)
|
||||
@ -43,11 +43,11 @@ class LootSelectorTest extends AnyWordSpecLike with Matchers with BeforeAndAfter
|
||||
"loot selector" must {
|
||||
|
||||
"suggest loot by isRequired" in {
|
||||
toPlayerId(default.suggestLoot(Head(pieceType = PieceType.Savage, Job.AnyJob))) shouldEqual Seq(dnc.playerId, drg.playerId)
|
||||
toPlayerId(default.suggestLoot(Piece.Head(pieceType = PieceType.Savage, Job.AnyJob))) shouldEqual Seq(dnc.playerId, drg.playerId)
|
||||
}
|
||||
|
||||
"suggest loot if a player already have it" in {
|
||||
val piece = Body(pieceType = PieceType.Savage, Job.AnyJob)
|
||||
val piece = Piece.Body(pieceType = PieceType.Savage, Job.AnyJob)
|
||||
val party = default.withPlayer(dnc.withLoot(piece))
|
||||
|
||||
toPlayerId(party.suggestLoot(piece)) shouldEqual Seq(drg.playerId, dnc.playerId)
|
||||
@ -56,34 +56,34 @@ class LootSelectorTest extends AnyWordSpecLike with Matchers with BeforeAndAfter
|
||||
"suggest upgrade" in {
|
||||
val party = default.withPlayer(
|
||||
dnc.withBiS(
|
||||
Some(dnc.bis.withPiece(Weapon(pieceType = PieceType.Tome, Job.DNC)))
|
||||
Some(dnc.bis.withPiece(Piece.Weapon(pieceType = PieceType.Tome, Job.DNC)))
|
||||
)
|
||||
)
|
||||
|
||||
toPlayerId(party.suggestLoot(WeaponUpgrade)) shouldEqual Seq(dnc.playerId, drg.playerId)
|
||||
toPlayerId(party.suggestLoot(Piece.WeaponUpgrade)) shouldEqual Seq(dnc.playerId, drg.playerId)
|
||||
}
|
||||
|
||||
"suggest loot by priority" in {
|
||||
val party = default.withPlayer(dnc.copy(priority = 2))
|
||||
|
||||
toPlayerId(party.suggestLoot(Body(pieceType = PieceType.Savage, Job.AnyJob))) shouldEqual Seq(drg.playerId, dnc.playerId)
|
||||
toPlayerId(party.suggestLoot(Piece.Body(pieceType = PieceType.Savage, Job.AnyJob))) shouldEqual Seq(drg.playerId, dnc.playerId)
|
||||
}
|
||||
|
||||
"suggest loot by bis pieces got" in {
|
||||
val party = default.withPlayer(dnc.withLoot(Head(pieceType = PieceType.Savage, Job.AnyJob)))
|
||||
val party = default.withPlayer(dnc.withLoot(Piece.Head(pieceType = PieceType.Savage, Job.AnyJob)))
|
||||
|
||||
toPlayerId(party.suggestLoot(Body(pieceType = PieceType.Savage, Job.AnyJob))) shouldEqual Seq(drg.playerId, dnc.playerId)
|
||||
toPlayerId(party.suggestLoot(Piece.Body(pieceType = PieceType.Savage, Job.AnyJob))) shouldEqual Seq(drg.playerId, dnc.playerId)
|
||||
}
|
||||
|
||||
"suggest loot by this piece got" in {
|
||||
val piece = Body(pieceType = PieceType.Tome, Job.AnyJob)
|
||||
val piece = Piece.Body(pieceType = PieceType.Tome, Job.AnyJob)
|
||||
val party = default.withPlayer(dnc.withLoot(piece))
|
||||
|
||||
toPlayerId(party.suggestLoot(piece)) shouldEqual Seq(drg.playerId, dnc.playerId)
|
||||
}
|
||||
|
||||
"suggest loot by total piece got" in {
|
||||
val piece = Body(pieceType = PieceType.Tome, Job.AnyJob)
|
||||
val piece = Piece.Body(pieceType = PieceType.Tome, Job.AnyJob)
|
||||
val party = default
|
||||
.withPlayer(dnc.withLoot(Seq(piece, piece).map(pieceToLoot)))
|
||||
.withPlayer(drg.withLoot(piece))
|
||||
|
@ -1,7 +1,7 @@
|
||||
package me.arcanis.ffxivbis.service.bis
|
||||
|
||||
import akka.actor.testkit.typed.scaladsl.ScalaTestWithActorTestKit
|
||||
import me.arcanis.ffxivbis.messages.DownloadBiS
|
||||
import me.arcanis.ffxivbis.messages.BiSProviderMessage.DownloadBiS
|
||||
import me.arcanis.ffxivbis.models._
|
||||
import me.arcanis.ffxivbis.{Fixtures, Settings}
|
||||
import org.scalatest.wordspec.AnyWordSpecLike
|
||||
@ -41,5 +41,11 @@ class BisProviderTest extends ScalaTestWithActorTestKit(Settings.withRandomDatab
|
||||
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)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -2,7 +2,7 @@ package me.arcanis.ffxivbis.service.database
|
||||
|
||||
import akka.actor.testkit.typed.scaladsl.ScalaTestWithActorTestKit
|
||||
import akka.actor.typed.scaladsl.AskPattern.Askable
|
||||
import me.arcanis.ffxivbis.messages.{AddPieceToBis, AddPlayer, GetBiS, RemovePieceFromBiS}
|
||||
import me.arcanis.ffxivbis.messages.DatabaseMessage._
|
||||
import me.arcanis.ffxivbis.models._
|
||||
import me.arcanis.ffxivbis.utils.Compare
|
||||
import me.arcanis.ffxivbis.{Fixtures, Settings}
|
||||
@ -70,7 +70,7 @@ class DatabaseBiSHandlerTest extends ScalaTestWithActorTestKit(Settings.withRand
|
||||
|
||||
"update piece in bis set" in {
|
||||
val updateProbe = testKit.createTestProbe[Unit]()
|
||||
val newPiece = Hands(pieceType = PieceType.Savage, Job.DNC)
|
||||
val newPiece = Piece.Hands(pieceType = PieceType.Savage, Job.DNC)
|
||||
|
||||
database ! AddPieceToBis(Fixtures.playerEmpty.playerId, newPiece, updateProbe.ref)
|
||||
updateProbe.expectMessage(askTimeout, ())
|
||||
|
@ -2,8 +2,7 @@ package me.arcanis.ffxivbis.service.database
|
||||
|
||||
import akka.actor.testkit.typed.scaladsl.ScalaTestWithActorTestKit
|
||||
import akka.actor.typed.scaladsl.AskPattern.Askable
|
||||
import ch.qos.logback.core.util.FixedDelay
|
||||
import me.arcanis.ffxivbis.messages.{AddPieceTo, AddPlayer, GetLoot, RemovePieceFrom}
|
||||
import me.arcanis.ffxivbis.messages.DatabaseMessage._
|
||||
import me.arcanis.ffxivbis.models._
|
||||
import me.arcanis.ffxivbis.utils.Compare
|
||||
import me.arcanis.ffxivbis.{Fixtures, Settings}
|
||||
|
@ -1,7 +1,7 @@
|
||||
package me.arcanis.ffxivbis.service.database
|
||||
|
||||
import akka.actor.testkit.typed.scaladsl.ScalaTestWithActorTestKit
|
||||
import me.arcanis.ffxivbis.messages.{AddPlayer, GetParty, GetPlayer, RemovePlayer}
|
||||
import me.arcanis.ffxivbis.messages.DatabaseMessage._
|
||||
import me.arcanis.ffxivbis.models._
|
||||
import me.arcanis.ffxivbis.utils.Compare
|
||||
import me.arcanis.ffxivbis.{Fixtures, Settings}
|
||||
@ -66,6 +66,18 @@ class DatabasePartyHandlerTest extends ScalaTestWithActorTestKit(Settings.withRa
|
||||
Compare.seqEquals(party.getPlayers, Seq(newPlayer)) shouldEqual true
|
||||
}
|
||||
|
||||
"update bis link" in {
|
||||
val updateProbe = testKit.createTestProbe[Unit]()
|
||||
val newPlayer = Fixtures.playerEmpty.copy(priority = 2, link = Some("link"))
|
||||
|
||||
database ! UpdateBiSLink(Fixtures.playerEmpty.playerId, "link", updateProbe.ref)
|
||||
updateProbe.expectMessage(askTimeout, ())
|
||||
|
||||
val probe = testKit.createTestProbe[Option[Player]]()
|
||||
database ! GetPlayer(Fixtures.playerEmpty.playerId, probe.ref)
|
||||
probe.expectMessage(askTimeout, Some(newPlayer))
|
||||
}
|
||||
|
||||
"remove player" in {
|
||||
val updateProbe = testKit.createTestProbe[Unit]()
|
||||
database ! RemovePlayer(Fixtures.playerEmpty.playerId, updateProbe.ref)
|
||||
|
@ -1,7 +1,7 @@
|
||||
package me.arcanis.ffxivbis.service.database
|
||||
|
||||
import akka.actor.testkit.typed.scaladsl.ScalaTestWithActorTestKit
|
||||
import me.arcanis.ffxivbis.messages.{AddUser, DeleteUser, GetUser, GetUsers}
|
||||
import me.arcanis.ffxivbis.messages.DatabaseMessage._
|
||||
import me.arcanis.ffxivbis.models.User
|
||||
import me.arcanis.ffxivbis.utils.Compare
|
||||
import me.arcanis.ffxivbis.{Fixtures, Settings}
|
||||
@ -80,5 +80,6 @@ class DatabaseUserHandlerTest extends ScalaTestWithActorTestKit(Settings.withRan
|
||||
database ! GetUser(Fixtures.partyId, Fixtures.userGet.username, probe.ref)
|
||||
probe.expectMessage(askTimeout, None)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
package me.arcanis.ffxivbis.utils
|
||||
|
||||
object Compare {
|
||||
|
||||
def mapEquals[K, T](left: Map[K, T], right: Map[K, T]): Boolean =
|
||||
left.forall {
|
||||
case (key, value) => right.contains(key) && right(key) == value
|
||||
|
@ -6,5 +6,6 @@ import java.time.Instant
|
||||
import scala.language.implicitConversions
|
||||
|
||||
object Converters {
|
||||
|
||||
implicit def pieceToLoot(piece: Piece): Loot = Loot(-1, piece, Instant.ofEpochMilli(0), isFreeLoot = false)
|
||||
}
|
||||
|
43
src/test/scala/me/arcanis/ffxivbis/utils/ImplicitsTest.scala
Normal file
43
src/test/scala/me/arcanis/ffxivbis/utils/ImplicitsTest.scala
Normal file
@ -0,0 +1,43 @@
|
||||
package me.arcanis.ffxivbis.utils
|
||||
|
||||
import akka.util.Timeout
|
||||
import me.arcanis.ffxivbis.Settings
|
||||
import org.scalatest.matchers.should.Matchers
|
||||
import org.scalatest.wordspec.AnyWordSpecLike
|
||||
|
||||
import java.util.concurrent.TimeUnit
|
||||
import scala.concurrent.duration.FiniteDuration
|
||||
|
||||
class ImplicitsTest extends AnyWordSpecLike with Matchers {
|
||||
|
||||
import me.arcanis.ffxivbis.utils.Implicits._
|
||||
|
||||
"configuration extension" must {
|
||||
|
||||
"return finite duration" in {
|
||||
val config = Settings.config(Map("value" -> "1m"))
|
||||
config.getFiniteDuration("value") shouldBe FiniteDuration(1, TimeUnit.MINUTES)
|
||||
}
|
||||
|
||||
"return optional string" in {
|
||||
val config = Settings.config(Map("value" -> "string"))
|
||||
config.getOptString("value") shouldBe Some("string")
|
||||
}
|
||||
|
||||
"return None for missing optional string" in {
|
||||
val config = Settings.config(Map.empty)
|
||||
config.getOptString("value") shouldBe None
|
||||
}
|
||||
|
||||
"return None for empty optional string" in {
|
||||
val config = Settings.config(Map("value" -> ""))
|
||||
config.getOptString("value") shouldBe None
|
||||
}
|
||||
|
||||
"return timeout" in {
|
||||
val config = Settings.config(Map("value" -> "1m"))
|
||||
config.getTimeout("value") shouldBe Timeout(1, TimeUnit.MINUTES)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user