Compare commits

..

No commits in common. "master" and "0.12.0" have entirely different histories.

109 changed files with 801 additions and 1574 deletions

View File

@ -25,7 +25,7 @@ jobs:
uses: actions/setup-java@v2
with:
distribution: temurin
java-version: 18
java-version: 17
- name: create dist
run: make dist
- name: release

View File

@ -17,6 +17,6 @@ jobs:
uses: actions/setup-java@v2
with:
distribution: temurin
java-version: 18
java-version: 17
- name: run tests
run: make tests

3
TODO.md Normal file
View File

@ -0,0 +1,3 @@
* [x] items improvements
* [x] multiple parties support
* [ ] pretty UI

View File

@ -2,8 +2,9 @@ organization := "me.arcanis"
name := "ffxivbis"
scalaVersion := "2.13.12"
scalaVersion := "2.13.6"
scalacOptions ++= Seq("-deprecation", "-feature")
enablePlugins(JavaAppPackaging)
coverageEnabled := true

View File

@ -1,25 +1,25 @@
val AkkaVersion = "2.8.6"
val AkkaHttpVersion = "10.5.3"
val ScalaTestVersion = "3.2.19"
val AkkaVersion = "2.6.18"
val AkkaHttpVersion = "10.2.7"
val ScalaTestVersion = "3.2.10"
val SlickVersion = "3.3.3"
libraryDependencies += "ch.qos.logback" % "logback-classic" % "1.5.6"
libraryDependencies += "com.typesafe.scala-logging" %% "scala-logging" % "3.9.5"
libraryDependencies += "ch.qos.logback" % "logback-classic" % "1.2.10"
libraryDependencies += "com.typesafe.scala-logging" %% "scala-logging" % "3.9.4"
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.11.0"
libraryDependencies += "jakarta.platform" % "jakarta.jakartaee-web-api" % "10.0.0"
libraryDependencies += "ch.megard" %% "akka-http-cors" % "1.2.0"
libraryDependencies += "com.github.swagger-akka-http" %% "swagger-akka-http" % "2.6.0"
libraryDependencies += "jakarta.platform" % "jakarta.jakartaee-web-api" % "9.1.0"
libraryDependencies += "io.spray" %% "spray-json" % "1.3.6"
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.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.mindrot" % "jbcrypt" % "0.4"
libraryDependencies += "com.google.guava" % "guava" % "31.0.1-jre"

View File

@ -1 +1 @@
sbt.version = 1.7.1
sbt.version = 1.5.8

View File

@ -1,3 +0,0 @@
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');

View File

@ -1 +0,0 @@
update players set bis_link = null where bis_link = '';

View File

@ -1,23 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>FFXIV loot helper API</title>
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<!-- Embed elements Elements via Web Component -->
<script src="https://unpkg.com/@stoplight/elements/web-components.min.js"></script>
<link rel="stylesheet" href="https://unpkg.com/@stoplight/elements/styles.min.css" type="text/css">
<link rel="shortcut icon" href="/static/favicon.ico">
</head>
<body>
<elements-api
apiDescriptionUrl="/api-docs/swagger.json"
router="hash"
layout="sidebar"
/>
</body>
</html>

View File

@ -6,17 +6,17 @@
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="shortcut icon" href="/static/favicon.ico">
<link href="/static/favicon.ico" rel="shortcut icon">
<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.8.3/font/bootstrap-icons.css" type="text/css">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous" type="text/css">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.7.2/font/bootstrap-icons.css" rel="stylesheet" type="text/css">
<link rel="stylesheet" href="https://unpkg.com/bootstrap-table@1.20.2/dist/bootstrap-table.min.css" type="text/css">
<link href="https://unpkg.com/bootstrap-table@1.19.1/dist/bootstrap-table.min.css" rel="stylesheet" type="text/css">
<link rel="stylesheet" href="https://unpkg.com/jquery-resizable-columns@0.2.3/dist/jquery.resizableColumns.css" type="text/css">
<link href="https://unpkg.com/jquery-resizable-columns@0.2.3/dist/jquery.resizableColumns.css" rel="stylesheet" type="text/css">
<link rel="stylesheet" href="/static/styles.css" type="text/css">
<link href="/static/styles.css" rel="stylesheet" type="text/css">
</head>
<body>
@ -86,24 +86,23 @@
<div id="update-bis-dialog" tabindex="-1" role="dialog" class="modal fade">
<div class="modal-dialog" role="document">
<form class="modal-content" action="javascript:" onsubmit="updateBis()">
<div class="modal-header form-group row">
<form class="modal-content">
<div class="modal-header">
<div class="btn-group" role="group" aria-label="Update bis">
<input id="add-piece-btn" name="update-bis" type="radio" class="btn-check" autocomplete="off" checked>
<label class="btn btn-outline-primary" for="add-piece-btn">add piece</label>
<input id="update-bis-btn" name="update-bis" type="radio" class="btn-check" autocomplete="off">
<label class="btn btn-outline-primary" for="update-bis-btn">update bis</label>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="close"></button>
</div>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="close"></button>
</div>
<div class="modal-body">
<div class="form-group row">
<label class="col-sm-4 col-form-label" for="player">player</label>
<div class="col-sm-8">
<select id="player" name="player" class="form-control" title="player" required></select>
<select id="player" name="player" class="form-control" title="player"></select>
</div>
</div>
<div id="piece-row" class="form-group row">
@ -121,14 +120,14 @@
<div id="bis-link-row" class="form-group row" style="display: none">
<label class="col-sm-4 col-form-label" for="bis-link">link</label>
<div class="col-sm-8">
<input id="bis-link" name="link" class="form-control" placeholder="link to bis">
<input id="bis-link" name="link" class="form-control" placeholder="link to bis" onkeyup="disableSubmitBisButton()">
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-danger" data-bs-dismiss="modal">close</button>
<button id="submit-add-bis-btn" type="submit" class="btn btn-primary">add</button>
<button id="submit-set-bis-btn" type="submit" class="btn btn-primary" style="display: none">set</button>
<button id="submit-add-bis-btn" type="button" class="btn btn-primary" data-bs-dismiss="modal" onclick="addPiece()" disabled>add</button>
<button id="submit-update-bis-btn" type="button" class="btn btn-primary" data-bs-dismiss="modal" onclick="setBis()" style="display: none" disabled>set</button>
</div>
</div>
</form>
@ -139,11 +138,10 @@
<footer class="d-flex flex-wrap justify-content-between align-items-center border-top">
<ul class="nav">
<li><a class="nav-link" href="/" title="home">home</a></li>
<li><a class="nav-link" href="/api-docs" title="api">api</a></li>
</ul>
<ul class="nav">
<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" 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 +155,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.20.2/dist/bootstrap-table.min.js"></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/extensions/export/bootstrap-table-export.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/resizable/bootstrap-table-resizable.js"></script>
<script src="https://unpkg.com/bootstrap-table@1.19.1/dist/extensions/resizable/bootstrap-table-resizable.js"></script>
<script src="/static/utils.js"></script>
<script src="/static/load.js"></script>
@ -173,7 +171,7 @@
const updateButton = $("#update-btn");
const submitAddBisButton = $("#submit-add-bis-btn");
const submitSetBisButton = $("#submit-set-bis-btn");
const submitUpdateBisButton = $("#submit-update-bis-btn");
const updateBisDialog = $("#update-bis-dialog");
const addPieceButton = $("#add-piece-btn");
@ -207,11 +205,15 @@
}),
type: "POST",
contentType: "application/json",
success: _ => { reload(); },
error: (jqXHR, _, errorThrown) => { requestAlert(jqXHR, errorThrown); },
success: function (_) { reload(); },
error: function (jqXHR, _, errorThrown) { requestAlert(jqXHR, errorThrown); },
});
updateBisDialog.modal("hide");
return true; // action expects boolean result
}
function disableSubmitBisButton() {
const nonEmpty = (playerInput.val() !== null); // well lol
submitUpdateBisButton.attr("disabled", !(nonEmpty && linkInput.val()));
submitAddBisButton.attr("disabled", !(nonEmpty));
}
function hideControls() {
@ -220,24 +222,20 @@
}
function hideLinkPart() {
disableSubmitBisButton();
bisLinkRow.hide();
linkInput.prop("required", false);
submitSetBisButton.hide();
submitUpdateBisButton.hide();
pieceRow.show();
pieceTypeRow.show();
pieceInput.prop("required", true);
pieceTypeInput.prop("required", true);
submitAddBisButton.show();
}
function hidePiecePart() {
disableSubmitBisButton();
bisLinkRow.show();
linkInput.prop("required", true);
submitSetBisButton.show();
submitUpdateBisButton.show();
pieceRow.hide();
pieceTypeRow.hide();
pieceInput.prop("required", false);
pieceTypeInput.prop("required", false);
submitAddBisButton.hide();
}
@ -247,9 +245,9 @@
url: `/api/v1/party/${partyId}`,
type: "GET",
dataType: "json",
success: response => {
const items = response.map(player => {
return player.bis.map(loot => {
success: function (data) {
const items = data.map(function (player) {
return player.bis.map(function (loot) {
return {
nick: player.nick,
job: player.job,
@ -258,12 +256,12 @@
};
});
});
const payload = items.reduce((left, right) => { return left.concat(right); }, []);
const payload = items.reduce(function (left, right) { return left.concat(right); }, []);
table.bootstrapTable("load", payload);
table.bootstrapTable("uncheckAll");
table.bootstrapTable("hideLoading");
const options = response.map(player => {
const options = data.map(function (player) {
const option = document.createElement("option");
option.innerText = formatPlayerId(player);
option.dataset.nick = player.nick;
@ -271,14 +269,15 @@
return option;
});
playerInput.empty().append(options);
disableSubmitBisButton();
},
error: (jqXHR, _, errorThrown) => { requestAlert(jqXHR, errorThrown); },
error: function (jqXHR, _, errorThrown) { requestAlert(jqXHR, errorThrown); },
});
}
function removePiece() {
const pieces = table.bootstrapTable("getSelections");
pieces.map(loot => {
pieces.map(function (loot) {
$.ajax({
url: `/api/v1/party/${partyId}/bis`,
data: JSON.stringify({
@ -296,8 +295,8 @@
}),
type: "POST",
contentType: "application/json",
success: _ => { reload(); },
error: (jqXHR, _, errorThrown) => { requestAlert(jqXHR, errorThrown); },
success: function (_) { reload(); },
error: function (jqXHR, _, errorThrown) { requestAlert(jqXHR, errorThrown); },
});
});
}
@ -325,40 +324,26 @@
}),
type: "PUT",
contentType: "application/json",
success: _ => { reload(); },
error: (jqXHR, _, errorThrown) => { requestAlert(jqXHR, errorThrown); },
success: function (_) { reload(); },
error: function (jqXHR, _, errorThrown) { requestAlert(jqXHR, errorThrown); },
});
updateBisDialog.modal("hide");
return true; // action expects boolean result
}
function updateBis() {
if (updateBisButton.is(":checked")) {
return setBis();
}
if (addPieceButton.is(":checked")) {
return addPiece();
}
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(() => { reset(); });
addPieceButton.click(() => { reset(); });
updateBisButton.click(function () { reset(); });
addPieceButton.click(function () { reset(); });
table.bootstrapTable({});
reload();
reset();
});
</script>

View File

@ -6,11 +6,11 @@
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="shortcut icon" href="/static/favicon.ico">
<link href="/static/favicon.ico" rel="shortcut icon">
<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 href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous" type="text/css">
<link rel="stylesheet" href="/static/styles.css" type="text/css">
<link href="/static/styles.css" rel="stylesheet" type="text/css">
</head>
<body>
@ -71,12 +71,10 @@
<div class="container">
<footer class="d-flex flex-wrap justify-content-between align-items-center border-top">
<ul class="nav">
<li><a class="nav-link" href="/api-docs" title="api">api</a></li>
</ul>
<ul class="nav"></ul>
<ul class="nav">
<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" 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,9 +85,6 @@
<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");
@ -127,7 +122,7 @@
password: passwordInput.val(),
permission: "admin",
}),
type: "POST",
type: "PUT",
contentType: "application/json",
dataType: "json",
success: function (data) {
@ -179,8 +174,6 @@
}
$(function () {
loadVersion();
signinButton.click(function () { reset(); });
signupButton.click(function () { reset(); });
});

View File

@ -6,17 +6,17 @@
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="shortcut icon" href="/static/favicon.ico">
<link href="/static/favicon.ico" rel="shortcut icon">
<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.8.3/font/bootstrap-icons.css" type="text/css">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous" type="text/css">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.7.2/font/bootstrap-icons.css" rel="stylesheet" type="text/css">
<link rel="stylesheet" href="https://unpkg.com/bootstrap-table@1.20.2/dist/bootstrap-table.min.css" type="text/css">
<link href="https://unpkg.com/bootstrap-table@1.19.1/dist/bootstrap-table.min.css" rel="stylesheet" type="text/css">
<link rel="stylesheet" href="https://unpkg.com/jquery-resizable-columns@0.2.3/dist/jquery.resizableColumns.css" type="text/css">
<link href="https://unpkg.com/jquery-resizable-columns@0.2.3/dist/jquery.resizableColumns.css" rel="stylesheet" type="text/css">
<link rel="stylesheet" href="/static/styles.css" type="text/css">
<link href="/static/styles.css" rel="stylesheet" type="text/css">
</head>
<body>
@ -88,35 +88,35 @@
<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="addLootModal()">
<div class="modal-content">
<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>
</div>
<div class="modal-body">
<form class="modal-body">
<div class="form-group row">
<label class="col-sm-4 col-form-label" for="player">player</label>
<div class="col-sm-8">
<select id="player" name="player" class="form-control" title="player" required></select>
<select id="player" name="player" class="form-control" title="player"></select>
</div>
</div>
<div class="form-group row">
<label class="col-sm-4 col-form-label" for="piece">piece</label>
<div class="col-sm-8">
<select id="piece" name="piece" class="form-control" title="piece" required></select>
<select id="piece" name="piece" class="form-control" title="piece"></select>
</div>
</div>
<div class="form-group row">
<label class="col-sm-4 col-form-label" for="piece-type">piece type</label>
<div class="col-sm-8">
<select id="piece-type" name="pieceType" class="form-control" title="pieceType" required></select>
<select id="piece-type" name="pieceType" class="form-control" title="pieceType"></select>
</div>
</div>
<div class="form-group row">
<label class="col-sm-4 col-form-label" for="job">job</label>
<div class="col-sm-8">
<select id="job" name="job" class="form-control" title="job" required></select>
<select id="job" name="job" class="form-control" title="job"></select>
</div>
</div>
<div class="form-group row">
@ -132,7 +132,6 @@
<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>
@ -142,14 +141,14 @@
</tr>
</thead>
</table>
</div>
</form>
<div class="modal-footer">
<button type="button" class="btn btn-danger" data-bs-dismiss="modal">close</button>
<button type="button" class="btn btn-secondary" onclick="suggestLoot()">suggest</button>
<button type="submit" class="btn btn-primary">add</button>
<button id="submit-btn" type="button" class="btn btn-primary" data-bs-dismiss="modal" onclick="addLoot()" disabled>add</button>
</div>
</form>
</div>
</div>
</div>
@ -157,11 +156,10 @@
<footer class="d-flex flex-wrap justify-content-between align-items-center border-top">
<ul class="nav">
<li><a class="nav-link" href="/" title="home">home</a></li>
<li><a class="nav-link" href="/api-docs" title="api">api</a></li>
</ul>
<ul class="nav">
<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" 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>
@ -175,11 +173,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.20.2/dist/bootstrap-table.min.js"></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/extensions/export/bootstrap-table-export.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/resizable/bootstrap-table-resizable.js"></script>
<script src="https://unpkg.com/bootstrap-table@1.19.1/dist/extensions/resizable/bootstrap-table-resizable.js"></script>
<script src="/static/utils.js"></script>
<script src="/static/load.js"></script>
@ -191,6 +189,7 @@
const addButton = $("#add-btn");
const removeButton = $("#remove-btn");
const submitLootButton = $("#submit-btn");
const addLootDialog = $("#add-loot-dialog");
const freeLootInput = $("#free-loot");
@ -199,43 +198,31 @@
const pieceTypeInput = $("#piece-type");
const playerInput = $("#player");
function addLoot(nick, job) {
function addLoot() {
const player = getCurrentOption(playerInput);
$.ajax({
url: `/api/v1/party/${partyId}/loot`,
data: JSON.stringify({
action: "add",
piece: {
pieceType: pieceTypeInput.val(),
job: job,
job: player.dataset.job,
piece: pieceInput.val(),
},
playerId: {
partyId: partyId,
nick: nick,
job: job,
nick: player.dataset.nick,
job: player.dataset.job,
},
isFreeLoot: freeLootInput.is(":checked"),
}),
type: "POST",
contentType: "application/json",
success: _ => {
addLootDialog.modal("hide");
reload();
},
error: (jqXHR, _, errorThrown) => { requestAlert(jqXHR, errorThrown); },
success: function (_) { reload(); },
error: function (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
}
function hideControls() {
addButton.attr("hidden", isReadOnly);
removeButton.attr("hidden", isReadOnly);
@ -247,9 +234,9 @@
url: `/api/v1/party/${partyId}`,
type: "GET",
dataType: "json",
success: response => {
const items = response.map(player => {
return player.loot.map(loot => {
success: function (data) {
const items = data.map(function (player) {
return player.loot.map(function (loot) {
return {
nick: player.nick,
job: player.job,
@ -260,12 +247,12 @@
};
});
});
const payload = items.reduce((left, right) => { return left.concat(right); }, []);
const payload = items.reduce(function (left, right) { return left.concat(right); }, []);
table.bootstrapTable("load", payload);
table.bootstrapTable("uncheckAll");
table.bootstrapTable("hideLoading");
const options = response.map(player => {
const options = data.map(function (player) {
const option = document.createElement("option");
option.innerText = formatPlayerId(player);
option.dataset.nick = player.nick;
@ -273,14 +260,15 @@
return option;
});
playerInput.empty().append(options);
submitLootButton.attr("disabled", options.length === 0);
},
error: (jqXHR, _, errorThrown) => { requestAlert(jqXHR, errorThrown); },
error: function (jqXHR, _, errorThrown) { requestAlert(jqXHR, errorThrown); },
});
}
function removeLoot() {
const pieces = table.bootstrapTable("getSelections");
pieces.map(loot => {
pieces.map(function (loot) {
$.ajax({
url: `/api/v1/party/${partyId}/loot`,
data: JSON.stringify({
@ -299,8 +287,8 @@
}),
type: "POST",
contentType: "application/json",
success: _ => { reload(); },
error: (jqXHR, _, errorThrown) => { requestAlert(jqXHR, errorThrown); },
success: function (_) { reload(); },
error: function (jqXHR, _, errorThrown) { requestAlert(jqXHR, errorThrown); },
});
});
}
@ -317,12 +305,12 @@
type: "PUT",
contentType: "application/json",
dataType: "json",
success: response => {
const payload = response.map(stat => {
success: function (data) {
const payload = data.map(function (stat) {
return {
nick: stat.nick,
job: stat.job,
isRequired: stat.isRequired ? "yes" : "no",
isRequired: stat.isRequired,
lootCount: stat.lootCount,
lootCountBiS: stat.lootCountBiS,
lootCountTotal: stat.lootCountTotal,
@ -332,15 +320,14 @@
stats.bootstrapTable("uncheckAll");
stats.bootstrapTable("hideLoading");
},
error: (jqXHR, _, errorThrown) => { requestAlert(jqXHR, errorThrown); },
error: function (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);

View File

@ -6,17 +6,17 @@
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="shortcut icon" href="/static/favicon.ico">
<link href="/static/favicon.ico" rel="shortcut icon">
<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.8.3/font/bootstrap-icons.css" type="text/css">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous" type="text/css">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.7.2/font/bootstrap-icons.css" rel="stylesheet" type="text/css">
<link rel="stylesheet" href="https://unpkg.com/bootstrap-table@1.20.2/dist/bootstrap-table.min.css" type="text/css">
<link href="https://unpkg.com/bootstrap-table@1.19.1/dist/bootstrap-table.min.css" rel="stylesheet" type="text/css">
<link rel="stylesheet" href="https://unpkg.com/jquery-resizable-columns@0.2.3/dist/jquery.resizableColumns.css" type="text/css">
<link href="https://unpkg.com/jquery-resizable-columns@0.2.3/dist/jquery.resizableColumns.css" rel="stylesheet" type="text/css">
<link rel="stylesheet" href="/static/styles.css" type="text/css">
<link href="/static/styles.css" rel="stylesheet" type="text/css">
</head>
<body>
@ -84,23 +84,23 @@
<div id="add-player-dialog" tabindex="-1" role="dialog" class="modal fade">
<div class="modal-dialog" role="document">
<form class="modal-content" action="javascript:" onsubmit="addPlayer()">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title">add new player</h4>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="close"></button>
</div>
<div class="modal-body">
<form class="modal-body">
<div class="form-group row">
<label class="col-sm-4 col-form-label" for="nick">player name</label>
<div class="col-sm-8">
<input id="nick" name="nick" class="form-control" placeholder="nick" required>
<input id="nick" name="nick" class="form-control" placeholder="nick" onkeyup="disableAddPlayerForm()">
</div>
</div>
<div class="form-group row">
<label class="col-sm-4 col-form-label" for="job">player job</label>
<div class="col-sm-8">
<select id="job" name="job" class="form-control" title="job" required></select>
<select id="job" name="job" class="form-control" title="job"></select>
</div>
</div>
<div class="form-group row">
@ -115,13 +115,13 @@
<input id="priority" name="priority" type="number" class="form-control" value="0">
</div>
</div>
</div>
</form>
<div class="modal-footer">
<button type="button" class="btn btn-danger" data-bs-dismiss="modal">close</button>
<button type="submit" class="btn btn-primary">add</button>
<button id="submit-player-btn" type="button" class="btn btn-primary" data-bs-dismiss="modal" onclick="addPlayer()" disabled>add</button>
</div>
</form>
</div>
</div>
</div>
@ -129,11 +129,10 @@
<footer class="d-flex flex-wrap justify-content-between align-items-center border-top">
<ul class="nav">
<li><a class="nav-link" href="/" title="home">home</a></li>
<li><a class="nav-link" href="/api-docs" title="api">api</a></li>
</ul>
<ul class="nav">
<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" 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 +146,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.20.2/dist/bootstrap-table.min.js"></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/extensions/export/bootstrap-table-export.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/resizable/bootstrap-table-resizable.js"></script>
<script src="https://unpkg.com/bootstrap-table@1.19.1/dist/extensions/resizable/bootstrap-table-resizable.js"></script>
<script src="/static/utils.js"></script>
<script src="/static/load.js"></script>
@ -163,6 +162,7 @@
const removeButton = $("#remove-btn");
const addPlayerDialog = $("#add-player-dialog");
const submitPlayerButton = $("#submit-player-btn");
const jobInput = $("#job");
const linkInput = $("#link");
@ -184,11 +184,9 @@
}),
type: "POST",
contentType: "application/json",
success: _ => { reload(); },
error: (jqXHR, _, errorThrown) => { requestAlert(jqXHR, errorThrown); },
success: function (_) { reload(); },
error: function (jqXHR, _, errorThrown) { requestAlert(jqXHR, errorThrown); },
});
addPlayerDialog.modal("hide");
return true; // action expects boolean result
}
function bisLinkFormatter(link, row) {
@ -199,6 +197,10 @@
}
}
function disableAddPlayerForm() {
submitPlayerButton.attr("disabled", !nickInput.val());
}
function hideControls() {
addButton.attr("hidden", isReadOnly);
removeButton.attr("hidden", isReadOnly);
@ -210,18 +212,18 @@
url: `/api/v1/party/${partyId}`,
type: "GET",
dataType: "json",
success: response => {
table.bootstrapTable("load", response);
success: function (data) {
table.bootstrapTable("load", data);
table.bootstrapTable("uncheckAll");
table.bootstrapTable("hideLoading");
},
error: (jqXHR, _, errorThrown) => { requestAlert(jqXHR, errorThrown); },
error: function (jqXHR, _, errorThrown) { requestAlert(jqXHR, errorThrown); },
});
}
function removePlayers() {
const players = table.bootstrapTable("getSelections");
players.map(player => {
players.map(function (player) {
$.ajax({
url: `/api/v1/party/${partyId}`,
data: JSON.stringify({
@ -234,17 +236,16 @@
}),
type: "POST",
contentType: "application/json",
success: _ => { reload(); },
error: (jqXHR, _, errorThrown) => { requestAlert(jqXHR, errorThrown); },
success: function (_) { reload(); },
error: function (jqXHR, _, errorThrown) { requestAlert(jqXHR, errorThrown); },
});
});
}
$(() => {
$(function () {
setupFormClear(addPlayerDialog);
setupRemoveButton(table, removeButton);
loadVersion();
loadHeader(partyId);
loadTypes("/api/v1/types/jobs", jobInput);

View File

@ -0,0 +1,19 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>FFXIV loot helper API</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="https://fonts.googleapis.com/css?family=Montserrat:300,400,700|Roboto:300,400,700" rel="stylesheet" type="text/css">
<link href="/static/favicon.ico" rel="shortcut icon">
</head>
<body>
<redoc spec-url="/api-docs/swagger.json"></redoc>
<script src="https://cdn.jsdelivr.net/npm/redoc@next/bundles/redoc.standalone.js"> </script>
</body>
</html>

View File

@ -6,17 +6,17 @@
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="shortcut icon" href="/static/favicon.ico">
<link href="/static/favicon.ico" rel="shortcut icon">
<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.8.3/font/bootstrap-icons.css" type="text/css">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous" type="text/css">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.7.2/font/bootstrap-icons.css" rel="stylesheet" type="text/css">
<link rel="stylesheet" href="https://unpkg.com/bootstrap-table@1.20.2/dist/bootstrap-table.min.css" type="text/css">
<link href="https://unpkg.com/bootstrap-table@1.19.1/dist/bootstrap-table.min.css" rel="stylesheet" type="text/css">
<link rel="stylesheet" href="https://unpkg.com/jquery-resizable-columns@0.2.3/dist/jquery.resizableColumns.css" type="text/css">
<link href="https://unpkg.com/jquery-resizable-columns@0.2.3/dist/jquery.resizableColumns.css" rel="stylesheet" type="text/css">
<link rel="stylesheet" href="/static/styles.css" type="text/css">
<link href="/static/styles.css" rel="stylesheet" type="text/css">
</head>
<body>
@ -84,38 +84,38 @@
<div id="add-user-dialog" tabindex="-1" role="dialog" class="modal fade">
<div class="modal-dialog" role="document">
<form class="modal-content" action="javascript:" onsubmit="addUser()">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title">add new user</h4>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="close"></button>
</div>
<div class="modal-body">
<form class="modal-body">
<div class="form-group row">
<label class="col-sm-4 col-form-label" for="username">login</label>
<div class="col-sm-8">
<input id="username" name="username" class="form-control" placeholder="username" required>
<input id="username" name="username" class="form-control" placeholder="username" onkeyup="disableAddUserForm()">
</div>
</div>
<div class="form-group row">
<label class="col-sm-4 col-form-label" for="password">password</label>
<div class="col-sm-8">
<input id="password" name="password" type="password" class="form-control" placeholder="password" required>
<input id="password" name="password" type="password" class="form-control" placeholder="password" onkeyup="disableAddUserForm()">
</div>
</div>
<div class="form-group row">
<label class="col-sm-4 col-form-label" for="permission">permission</label>
<div class="col-sm-8">
<select id="permission" name="permission" class="form-control" title="permission" required></select>
<select id="permission" name="permission" class="form-control" title="permission"></select>
</div>
</div>
</div>
</form>
<div class="modal-footer">
<button type="button" class="btn btn-danger" data-bs-dismiss="modal">close</button>
<button type="submit" class="btn btn-primary">add</button>
<button id="submit-btn" type="button" class="btn btn-primary" data-bs-dismiss="modal" onclick="addUser()" disabled>add</button>
</div>
</form>
</div>
</div>
</div>
@ -123,11 +123,10 @@
<footer class="d-flex flex-wrap justify-content-between align-items-center border-top">
<ul class="nav">
<li><a class="nav-link" href="/" title="home">home</a></li>
<li><a class="nav-link" href="/api-docs" title="api">api</a></li>
</ul>
<ul class="nav">
<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" 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 +140,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.20.2/dist/bootstrap-table.min.js"></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/extensions/export/bootstrap-table-export.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/resizable/bootstrap-table-resizable.js"></script>
<script src="https://unpkg.com/bootstrap-table@1.19.1/dist/extensions/resizable/bootstrap-table-resizable.js"></script>
<script src="/static/utils.js"></script>
<script src="/static/load.js"></script>
@ -157,6 +156,7 @@
const removeButton = $("#remove-btn");
const addUserDialog = $("#add-user-dialog");
const submitUserButton = $("#submit-btn");
const usernameInput = $("#username");
const passwordInput = $("#password");
@ -173,11 +173,13 @@
}),
type: "POST",
contentType: "application/json",
success: _ => { reload(); },
error: (jqXHR, _, errorThrown) => { requestAlert(jqXHR, errorThrown); },
success: function (_) { reload(); },
error: function (jqXHR, _, errorThrown) { requestAlert(jqXHR, errorThrown); },
});
addUserDialog.modal("hide");
return true; // action expects boolean result
}
function disableAddUserForm() {
submitUserButton.attr("disabled", !(usernameInput.val() && passwordInput.val()));
}
function hideControls() {
@ -191,32 +193,31 @@
url: `/api/v1/party/${partyId}/users`,
type: "GET",
dataType: "json",
success: response => {
table.bootstrapTable("load", response);
success: function (data) {
table.bootstrapTable("load", data);
table.bootstrapTable("uncheckAll");
table.bootstrapTable("hideLoading");
},
error: (jqXHR, _, errorThrown) => { requestAlert(jqXHR, errorThrown); },
error: function (jqXHR, _, errorThrown) { requestAlert(jqXHR, errorThrown); },
});
}
function removeUsers() {
const users = table.bootstrapTable("getSelections");
users.map(user => {
users.map(function (user) {
$.ajax({
url: `/api/v1/party/${partyId}/users/${user.username}`,
type: "DELETE",
success: _ => { reload(); },
error: (jqXHR, _, errorThrown) => { requestAlert(jqXHR, errorThrown); },
success: function (_) { reload(); },
error: function (jqXHR, _, errorThrown) { requestAlert(jqXHR, errorThrown); },
});
});
}
$(() => {
$(function () {
setupFormClear(addUserDialog);
setupRemoveButton(table, removeButton);
loadVersion();
loadHeader(partyId);
loadTypes("/api/v1/types/permissions", permissionInput);

View File

@ -12,6 +12,5 @@
</logger>
<logger name="org.flywaydb.core.internal" level="INFO" />
<logger name="com.zaxxer.hikari.pool" level="INFO" />
<logger name="io.swagger" level="INFO" />
</configuration>

View File

@ -50,7 +50,6 @@ 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

View File

@ -36,18 +36,8 @@ 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 () {
dialog.on("shown.bs.modal", function () {
$(this).find("form").trigger("reset");
$(this).find("table").bootstrapTable("removeAll");
if (reset) {

View File

@ -50,7 +50,7 @@ class Application(context: ActorContext[Nothing]) extends AbstractBehavior[Nothi
val party = context.spawn(PartyService(storage), "party")
val http = new RootEndpoint(context.system, party, bisProvider)
val flow = Route.toFlow(http.routes)(context.system)
val flow = Route.toFlow(http.route)(context.system)
Http(context.system).newServerAt(host, port).bindFlow(flow)
case Success(result) =>

View File

@ -10,7 +10,8 @@ package me.arcanis.ffxivbis
import akka.actor.typed.ActorSystem
object ffxivbis extends App {
object ffxivbis {
ActorSystem[Nothing](Application(), "ffxivbis", Configuration.load())
def main(args: Array[String]): Unit =
ActorSystem[Nothing](Application(), "ffxivbis", Configuration.load())
}

View File

@ -37,16 +37,23 @@ trait Authorization {
def authAdmin(partyId: String)(username: String, password: String)(implicit
executionContext: ExecutionContext
): Future[Option[User]] =
auth.authenticator(Permission.admin, partyId)(username, password)
authenticator(Permission.admin, partyId)(username, password)
def authGet(partyId: String)(username: String, password: String)(implicit
executionContext: ExecutionContext
): Future[Option[User]] =
auth.authenticator(Permission.get, partyId)(username, password)
authenticator(Permission.get, partyId)(username, password)
def authPost(partyId: String)(username: String, password: String)(implicit
executionContext: ExecutionContext
): Future[Option[User]] =
auth.authenticator(Permission.post, partyId)(username, password)
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
}
}

View File

@ -13,33 +13,20 @@ 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.DatabaseMessage.GetUser
import me.arcanis.ffxivbis.messages.Message
import me.arcanis.ffxivbis.models.{Permission, User}
import me.arcanis.ffxivbis.messages.{GetUser, Message}
import me.arcanis.ffxivbis.models.User
import java.util.concurrent.TimeUnit
import scala.concurrent.{ExecutionContext, Future}
import scala.concurrent.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])(implicit
timeout: Timeout,
scheduler: Scheduler
): AuthorizationProvider =
def apply(config: Config, storage: ActorRef[Message], timeout: Timeout, scheduler: Scheduler): AuthorizationProvider =
new AuthorizationProvider {
private val cacheSize = config.getInt("me.arcanis.ffxivbis.web.authorization-cache.cache-size")
private val cacheTimeout =

View File

@ -12,7 +12,6 @@ import akka.actor.typed.{ActorRef, ActorSystem, Scheduler}
import akka.http.scaladsl.server.Directives._
import akka.http.scaladsl.server._
import akka.util.Timeout
import ch.megard.akka.http.cors.scaladsl.CorsDirectives.cors
import com.typesafe.scalalogging.StrictLogging
import me.arcanis.ffxivbis.http.api.v1.RootApiV1Endpoint
import me.arcanis.ffxivbis.http.view.RootView
@ -28,36 +27,34 @@ 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)
private val auth = AuthorizationProvider(config, storage, timeout, scheduler)
private val rootApiV1Endpoint = new RootApiV1Endpoint(storage, auth, provider, config)
private val rootView = new RootView(auth)
private val swagger = new Swagger(config)
def routes: Route =
def route: Route =
withHttpLog {
ignoreTrailingSlash {
cors() {
apiRoutes ~ htmlRoutes ~ swagger.routes ~ swaggerUIRoutes
}
apiRoute ~ htmlRoute ~ swagger.routes ~ swaggerUIRoute
}
}
private def apiRoutes: Route =
private def apiRoute: Route =
pathPrefix("api") {
pathPrefix(Segment) {
case "v1" => rootApiV1Endpoint.routes
case "v1" => rootApiV1Endpoint.route
case _ => reject
}
}
private def htmlRoutes: Route =
private def htmlRoute: Route =
pathPrefix("static") {
getFromResourceDirectory("static")
} ~ rootView.routes
} ~ rootView.route
private def swaggerUIRoutes: Route =
private def swaggerUIRoute: Route =
path("api-docs") {
getFromResource("html/api.html")
getFromResource("html/redoc.html")
}
}

View File

@ -14,7 +14,6 @@ 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 {
@ -39,15 +38,13 @@ 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("basic" -> basicAuth)
.scheme("bearer")
override val securitySchemes: Map[String, SecurityScheme] = Map("basic auth" -> basicAuth)
override val unwantedDefinitions: Seq[String] =
Seq("Function1", "Function1RequestContextFutureRouteResult", "SeqLootModel", "SeqPieceModel")
Seq("Function1", "Function1RequestContextFutureRouteResult")
}

View File

@ -1,16 +0,0 @@
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("!@#$%^&*()-_=+;:',./? ")
}

View File

@ -40,7 +40,7 @@ class BiSEndpoint(
with Authorization
with JsonSupport {
def routes: Route = createBiS ~ getBiS ~ modifyBiS
def route: Route = createBiS ~ getBiS ~ modifyBiS
@PUT
@Path("party/{partyId}/bis")
@ -49,12 +49,7 @@ 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 = "o3KicHQPW5b0JcOm5yI3"
),
new Parameter(name = "partyId", in = ParameterIn.PATH, description = "unique party ID", example = "abcdefgh"),
),
requestBody = new RequestBody(
description = "player best in slot description",
@ -84,7 +79,7 @@ class BiSEndpoint(
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel])))
),
),
security = Array(new SecurityRequirement(name = "basic", scopes = Array("post"))),
security = Array(new SecurityRequirement(name = "basic auth", scopes = Array("post"))),
tags = Array("best in slot"),
)
def createBiS: Route =
@ -110,12 +105,7 @@ 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 = "o3KicHQPW5b0JcOm5yI3"
),
new Parameter(name = "partyId", in = ParameterIn.PATH, description = "unique party ID", example = "abcdefgh"),
new Parameter(
name = "nick",
in = ParameterIn.QUERY,
@ -150,7 +140,7 @@ class BiSEndpoint(
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel])))
),
),
security = Array(new SecurityRequirement(name = "basic", scopes = Array("get"))),
security = Array(new SecurityRequirement(name = "basic auth", scopes = Array("get"))),
tags = Array("best in slot"),
)
def getBiS: Route =
@ -177,12 +167,7 @@ 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 = "o3KicHQPW5b0JcOm5yI3"
),
new Parameter(name = "partyId", in = ParameterIn.PATH, description = "unique party ID", example = "abcdefgh"),
),
requestBody = new RequestBody(
description = "action and piece description",
@ -212,7 +197,7 @@ class BiSEndpoint(
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel])))
),
),
security = Array(new SecurityRequirement(name = "basic", scopes = Array("post"))),
security = Array(new SecurityRequirement(name = "basic auth", scopes = Array("post"))),
tags = Array("best in slot"),
)
def modifyBiS: Route =

View File

@ -11,45 +11,25 @@ package me.arcanis.ffxivbis.http.api.v1
import akka.http.scaladsl.model._
import akka.http.scaladsl.server.Directives._
import akka.http.scaladsl.server._
import ch.megard.akka.http.cors.scaladsl.CorsDirectives.corsRejectionHandler
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 exception: IllegalArgumentException =>
complete(StatusCodes.BadRequest, ErrorModel(exception.getMessage))
case ex: IllegalArgumentException =>
complete(StatusCodes.BadRequest, ErrorModel(ex.getMessage))
case NonFatal(other) =>
case other: Exception =>
logger.error("exception during request completion", other)
complete(StatusCodes.InternalServerError, ErrorModel("unknown server error"))
}
def rejectionHandler: RejectionHandler =
RejectionHandler
.newBuilder()
.handleAll[MethodRejection] { rejections =>
val (methods, names) = rejections.map(r => r.supported -> r.supported.name).unzip
respondWithHeader(headers.Allow(methods)) {
options {
complete(StatusCodes.OK, HttpEntity.Empty)
} ~
complete(
StatusCodes.MethodNotAllowed,
s"HTTP method not allowed, supported methods: ${names.mkString(", ")}"
)
}
}
.result()
.withFallback(corsRejectionHandler)
.seal
RejectionHandler.default
.mapRejectionResponse {
case response @ HttpResponse(_, _, entity: HttpEntity.Strict, _) if entity.data.nonEmpty =>
case response @ HttpResponse(_, _, entity: HttpEntity.Strict, _) =>
val message = ErrorModel(entity.data.utf8String).toJson
response.withEntity(HttpEntity(ContentTypes.`application/json`, message.compactPrint))
case other => other

View File

@ -37,7 +37,7 @@ class LootEndpoint(override val storage: ActorRef[Message], override val auth: A
with JsonSupport
with HttpHandler {
def routes: Route = getLoot ~ modifyLoot ~ suggestLoot
def route: Route = getLoot ~ modifyLoot ~ suggestLoot
@GET
@Path("party/{partyId}/loot")
@ -46,12 +46,7 @@ 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 = "o3KicHQPW5b0JcOm5yI3"
),
new Parameter(name = "partyId", in = ParameterIn.PATH, description = "unique party ID", example = "abcdefgh"),
new Parameter(
name = "nick",
in = ParameterIn.QUERY,
@ -86,7 +81,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", scopes = Array("get"))),
security = Array(new SecurityRequirement(name = "basic auth", scopes = Array("get"))),
tags = Array("loot"),
)
def getLoot: Route =
@ -112,12 +107,7 @@ 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 = "o3KicHQPW5b0JcOm5yI3"
),
new Parameter(name = "partyId", in = ParameterIn.PATH, description = "unique party ID", example = "abcdefgh"),
),
requestBody = new RequestBody(
description = "action and piece description",
@ -147,7 +137,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", scopes = Array("post"))),
security = Array(new SecurityRequirement(name = "basic auth", scopes = Array("post"))),
tags = Array("loot"),
)
def modifyLoot: Route =
@ -174,12 +164,7 @@ 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 = "o3KicHQPW5b0JcOm5yI3"
),
new Parameter(name = "partyId", in = ParameterIn.PATH, description = "unique party ID", example = "abcdefgh"),
),
requestBody = new RequestBody(
description = "piece description",
@ -217,7 +202,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", scopes = Array("get"))),
security = Array(new SecurityRequirement(name = "basic auth", scopes = Array("get"))),
tags = Array("loot"),
)
def suggestLoot: Route =

View File

@ -40,7 +40,7 @@ class PartyEndpoint(
with JsonSupport
with HttpHandler {
def routes: Route = getPartyDescription ~ modifyPartyDescription
def route: Route = getPartyDescription ~ modifyPartyDescription
@GET
@Path("party/{partyId}/description")
@ -49,12 +49,7 @@ 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 = "o3KicHQPW5b0JcOm5yI3"
),
new Parameter(name = "partyId", in = ParameterIn.PATH, description = "unique party ID", example = "abcdefgh"),
),
responses = Array(
new ApiResponse(
@ -78,7 +73,7 @@ class PartyEndpoint(
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel])))
),
),
security = Array(new SecurityRequirement(name = "basic", scopes = Array("get"))),
security = Array(new SecurityRequirement(name = "basic auth", scopes = Array("get"))),
tags = Array("party"),
)
def getPartyDescription: Route =
@ -101,12 +96,7 @@ class PartyEndpoint(
summary = "modify party description",
description = "Edit party description",
parameters = Array(
new Parameter(
name = "partyId",
in = ParameterIn.PATH,
description = "unique party ID",
example = "o3KicHQPW5b0JcOm5yI3"
),
new Parameter(name = "partyId", in = ParameterIn.PATH, description = "unique party ID", example = "abcdefgh"),
),
requestBody = new RequestBody(
description = "new party description",
@ -136,7 +126,7 @@ class PartyEndpoint(
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel])))
),
),
security = Array(new SecurityRequirement(name = "basic", scopes = Array("post"))),
security = Array(new SecurityRequirement(name = "basic auth", scopes = Array("post"))),
tags = Array("party"),
)
def modifyPartyDescription: Route =

View File

@ -41,7 +41,7 @@ class PlayerEndpoint(
with JsonSupport
with HttpHandler {
def routes: Route = getParty ~ getPartyStats ~ modifyParty
def route: Route = getParty ~ getPartyStats ~ modifyParty
@GET
@Path("party/{partyId}")
@ -50,12 +50,7 @@ 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 = "o3KicHQPW5b0JcOm5yI3"
),
new Parameter(name = "partyId", in = ParameterIn.PATH, description = "unique party ID", example = "abcdefgh"),
new Parameter(
name = "nick",
in = ParameterIn.QUERY,
@ -90,7 +85,7 @@ class PlayerEndpoint(
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel])))
),
),
security = Array(new SecurityRequirement(name = "basic", scopes = Array("get"))),
security = Array(new SecurityRequirement(name = "basic auth", scopes = Array("get"))),
tags = Array("party"),
)
def getParty: Route =
@ -116,12 +111,7 @@ 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 = "o3KicHQPW5b0JcOm5yI3"
),
new Parameter(name = "partyId", in = ParameterIn.PATH, description = "unique party ID", example = "abcdefgh"),
new Parameter(
name = "nick",
in = ParameterIn.QUERY,
@ -156,7 +146,7 @@ class PlayerEndpoint(
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel])))
),
),
security = Array(new SecurityRequirement(name = "basic", scopes = Array("get"))),
security = Array(new SecurityRequirement(name = "basic auth", scopes = Array("get"))),
tags = Array("party"),
)
def getPartyStats: Route =
@ -182,12 +172,7 @@ 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 = "o3KicHQPW5b0JcOm5yI3"
),
new Parameter(name = "partyId", in = ParameterIn.PATH, description = "unique party ID", example = "abcdefgh"),
),
requestBody = new RequestBody(
description = "player description",
@ -217,7 +202,7 @@ class PlayerEndpoint(
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel])))
),
),
security = Array(new SecurityRequirement(name = "basic", scopes = Array("post"))),
security = Array(new SecurityRequirement(name = "basic auth", scopes = Array("post"))),
tags = Array("party"),
)
def modifyParty: Route =

View File

@ -36,11 +36,11 @@ class RootApiV1Endpoint(
private val typesEndpoint = new TypesEndpoint(config)
private val userEndpoint = new UserEndpoint(storage, auth)
def routes: Route =
def route: Route =
handleExceptions(exceptionHandler) {
handleRejections(rejectionHandler) {
biSEndpoint.routes ~ lootEndpoint.routes ~ partyEndpoint.routes ~ playerEndpoint.routes ~
statusEndpoint.routes ~ typesEndpoint.routes ~ userEndpoint.routes
biSEndpoint.route ~ lootEndpoint.route ~ partyEndpoint.route ~ playerEndpoint.route ~
statusEndpoint.route ~ typesEndpoint.route ~ userEndpoint.route
}
}
}

View File

@ -19,7 +19,7 @@ import me.arcanis.ffxivbis.http.api.v1.json._
@Path("/api/v1")
class StatusEndpoint extends JsonSupport {
def routes: Route = getServerStatus
def route: Route = getServerStatus
@GET
@Path("status")

View File

@ -21,7 +21,7 @@ import me.arcanis.ffxivbis.models._
@Path("/api/v1")
class TypesEndpoint(config: Config) extends JsonSupport {
def routes: Route = getAllJobs ~ getJobs ~ getPermissions ~ getPieces ~ getPieceTypes ~ getPriority
def route: Route = getAllJobs ~ getJobs ~ getPermissions ~ getPieces ~ getPieceTypes ~ getPriority
@GET
@Path("types/jobs/all")

View File

@ -36,9 +36,9 @@ class UserEndpoint(override val storage: ActorRef[Message], override val auth: A
with Authorization
with JsonSupport {
def routes: Route = createParty ~ createUser ~ deleteUser ~ getUsers ~ getUsersCurrent
def route: Route = createParty ~ createUser ~ deleteUser ~ getUsers ~ getUsersCurrent
@POST
@PUT
@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 =>
post {
put {
entity(as[UserModel]) { user =>
onSuccess(newPartyId) { partyId =>
val admin = user.toUser.copy(partyId = partyId, permission = Permission.admin)
@ -96,12 +96,7 @@ 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 = "o3KicHQPW5b0JcOm5yI3"
),
new Parameter(name = "partyId", in = ParameterIn.PATH, description = "unique party ID", example = "abcdefgh"),
),
requestBody = new RequestBody(
description = "user description",
@ -131,7 +126,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", scopes = Array("admin"))),
security = Array(new SecurityRequirement(name = "basic auth", scopes = Array("admin"))),
tags = Array("users"),
)
def createUser: Route =
@ -156,12 +151,7 @@ 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 = "o3KicHQPW5b0JcOm5yI3"
),
new Parameter(name = "partyId", in = ParameterIn.PATH, description = "unique party ID", example = "abcdefgh"),
new Parameter(name = "username", in = ParameterIn.PATH, description = "username to remove", example = "siuan"),
),
responses = Array(
@ -182,7 +172,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", scopes = Array("admin"))),
security = Array(new SecurityRequirement(name = "basic auth", scopes = Array("admin"))),
tags = Array("users"),
)
def deleteUser: Route =
@ -205,12 +195,7 @@ 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 = "o3KicHQPW5b0JcOm5yI3"
),
new Parameter(name = "partyId", in = ParameterIn.PATH, description = "unique party ID", example = "abcdefgh"),
),
responses = Array(
new ApiResponse(
@ -238,7 +223,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", scopes = Array("get"))),
security = Array(new SecurityRequirement(name = "basic auth", scopes = Array("get"))),
tags = Array("users"),
)
def getUsers: Route =
@ -261,12 +246,7 @@ 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 = "o3KicHQPW5b0JcOm5yI3"
),
new Parameter(name = "partyId", in = ParameterIn.PATH, description = "unique party ID", example = "abcdefgh"),
),
responses = Array(
new ApiResponse(
@ -290,7 +270,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", scopes = Array("admin"))),
security = Array(new SecurityRequirement(name = "basic auth", scopes = Array("admin"))),
tags = Array("users"),
)
def getUsersCurrent: Route =

View File

@ -9,6 +9,5 @@
package me.arcanis.ffxivbis.http.api.v1.json
object ApiAction extends Enumeration {
val add, remove = Value
}

View File

@ -16,12 +16,12 @@ import java.time.Instant
trait JsonSupport extends SprayJsonSupport with DefaultJsonProtocol {
private def enumFormat[E <: Enumeration](enumeration: E): RootJsonFormat[E#Value] =
private def enumFormat[E <: Enumeration](enum: 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) => enumeration(value.toInt)
case JsString(name) => enumeration.withName(name)
case JsNumber(value) => enum(value.toInt)
case JsString(name) => enum.withName(name)
case other => deserializationError(s"String or number expected, got $other")
}
}

View File

@ -12,11 +12,9 @@ import io.swagger.v3.oas.annotations.media.Schema
import me.arcanis.ffxivbis.models.PartyDescription
case class PartyDescriptionModel(
@Schema(description = "party id", required = true, example = "o3KicHQPW5b0JcOm5yI3") partyId: String,
@Schema(description = "party id", required = true, example = "abcdefgh") partyId: String,
@Schema(description = "party name") partyAlias: Option[String]
) extends Validator {
require(partyAlias.forall(isValidString), stringMatchError("Party alias"))
) {
def toDescription: PartyDescription = PartyDescription(partyId, partyAlias)
}

View File

@ -10,6 +10,4 @@ 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 = "o3KicHQPW5b0JcOm5yI3") partyId: String
)
case class PartyIdModel(@Schema(description = "party id", required = true, example = "abcdefgh") partyId: String)

View File

@ -17,7 +17,4 @@ 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"))
}
)

View File

@ -12,14 +12,10 @@ 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 = "o3KicHQPW5b0JcOm5yI3") partyId: Option[
String
],
@Schema(description = "unique party ID. Required in responses", example = "abcdefgh") 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)

View File

@ -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 = "o3KicHQPW5b0JcOm5yI3") partyId: String,
@Schema(description = "unique party ID", required = true, example = "abcdefgh") 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,

View File

@ -12,22 +12,16 @@ 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 = "o3KicHQPW5b0JcOm5yI3") partyId: String,
@Schema(description = "unique party ID", required = true, example = "abcdefgh") 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]],
@Schema(description = "looted pieces") loot: Option[Seq[LootModel]],
@Schema(description = "link to best in slot", example = "https://ffxiv.ariyala.com/19V5R") link: Option[String],
@Schema(description = "player loot priority", `type` = "number") priority: Option[Int],
@Schema(
description = "count of looted pieces which are parts of best in slot",
`type` = "number"
) lootCountBiS: Option[Int],
@Schema(description = "count of looted pieces which are parts of best in slot") 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(

View File

@ -12,30 +12,23 @@ 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 = "o3KicHQPW5b0JcOm5yI3") partyId: String,
@Schema(description = "unique party ID", required = true, example = "abcdefgh") partyId: String,
@Schema(description = "username to login to party", required = true, example = "siuan") username: String,
@Schema(description = "password to login to party, required for user editing", example = "pa55w0rd") password: Option[
String
],
@Schema(description = "password to login to party", required = true, example = "pa55w0rd") password: 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 =
password.fold(throw new IllegalArgumentException("Password must noot be empty"))(
User(partyId, username, _, permission.getOrElse(Permission.get))
)
User(partyId, username, password, permission.getOrElse(Permission.get))
}
object UserModel {
def fromUser(user: User): UserModel =
UserModel(user.partyId, user.username, None, Some(user.permission))
UserModel(user.partyId, user.username, "", Some(user.permission))
}

View File

@ -1,9 +0,0 @@
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(", ")})"
}

View File

@ -12,8 +12,7 @@ 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.DatabaseMessage._
import me.arcanis.ffxivbis.messages.Message
import me.arcanis.ffxivbis.messages._
import me.arcanis.ffxivbis.models.{Piece, Player, PlayerId}
import scala.concurrent.{ExecutionContext, Future}
@ -45,15 +44,13 @@ trait BiSHelper extends BisProviderHelper {
timeout: Timeout,
scheduler: Scheduler
): Future[Unit] =
storage
.ask(RemovePiecesFromBiS(playerId, _))
.flatMap { _ =>
downloadBiS(link, playerId.job)
.flatMap { bis =>
Future.traverse(bis.pieces)(addPieceBiS(playerId, _))
}
}
.flatMap(_ => storage.ask(UpdateBiSLink(playerId, link, _)))
storage.ask(RemovePiecesFromBiS(playerId, _)).flatMap { _ =>
downloadBiS(link, playerId.job)
.flatMap { bis =>
Future.traverse(bis.pieces)(addPieceBiS(playerId, _))
}
.map(_ => ())
}
def removePieceBiS(playerId: PlayerId, piece: Piece)(implicit timeout: Timeout, scheduler: Scheduler): Future[Unit] =
storage.ask(RemovePieceFromBiS(playerId, piece, _))

View File

@ -11,8 +11,7 @@ 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
import me.arcanis.ffxivbis.messages.BiSProviderMessage._
import me.arcanis.ffxivbis.messages.{BiSProviderMessage, DownloadBiS}
import me.arcanis.ffxivbis.models.{BiS, Job}
import scala.concurrent.Future
@ -21,6 +20,6 @@ trait BisProviderHelper {
def provider: ActorRef[BiSProviderMessage]
def downloadBiS(link: String, job: Job)(implicit timeout: Timeout, scheduler: Scheduler): Future[BiS] =
def downloadBiS(link: String, job: Job.Job)(implicit timeout: Timeout, scheduler: Scheduler): Future[BiS] =
provider.ask(DownloadBiS(link, job, _))
}

View File

@ -12,8 +12,7 @@ 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.DatabaseMessage._
import me.arcanis.ffxivbis.messages.Message
import me.arcanis.ffxivbis.messages._
import me.arcanis.ffxivbis.models.{Piece, Player, PlayerId, PlayerIdWithCounters}
import scala.concurrent.{ExecutionContext, Future}

View File

@ -12,8 +12,7 @@ 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.DatabaseMessage._
import me.arcanis.ffxivbis.messages.Message
import me.arcanis.ffxivbis.messages._
import me.arcanis.ffxivbis.models.{PartyDescription, Player, PlayerId}
import scala.concurrent.{ExecutionContext, Future}
@ -27,15 +26,15 @@ trait PlayerHelper extends BisProviderHelper {
)(implicit executionContext: ExecutionContext, timeout: Timeout, scheduler: Scheduler): Future[Unit] =
storage
.ask(ref => AddPlayer(player, ref))
.map { _ =>
.map { res =>
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, _)))
}
.flatMap(_ => storage.ask(UpdateBiSLink(player.playerId, link, _)))
case None => Future.successful(())
.map(_ => res)
case None => Future.successful(res)
}
}
.flatten

View File

@ -11,9 +11,7 @@ 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.ControlMessage.GetNewPartyId
import me.arcanis.ffxivbis.messages.DatabaseMessage._
import me.arcanis.ffxivbis.messages.Message
import me.arcanis.ffxivbis.messages._
import me.arcanis.ffxivbis.models.User
import scala.concurrent.Future

View File

@ -15,7 +15,7 @@ import me.arcanis.ffxivbis.http.{Authorization, AuthorizationProvider}
class RootView(override val auth: AuthorizationProvider) extends Authorization {
def routes: Route = getBiS ~ getIndex ~ getLoot ~ getParty ~ getUsers
def route: Route = getBiS ~ getIndex ~ getLoot ~ getParty ~ getUsers
def getBiS: Route =
path("party" / Segment / "bis") { partyId: String =>

View File

@ -13,10 +13,7 @@ import me.arcanis.ffxivbis.models.{BiS, Job}
sealed trait BiSProviderMessage
object BiSProviderMessage {
case class DownloadBiS(link: String, job: Job.Job, replyTo: ActorRef[BiS]) extends 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")
}
require(link.nonEmpty && link.trim == link, "Link must be not empty and contain no spaces")
}

View File

@ -11,13 +11,8 @@ package me.arcanis.ffxivbis.messages
import akka.actor.typed.ActorRef
import me.arcanis.ffxivbis.models.Party
sealed trait ControlMessage extends Message
case class ForgetParty(partyId: String) extends Message
object ControlMessage {
case class GetNewPartyId(replyTo: ActorRef[String]) 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
}
case class StoreParty(partyId: String, party: Party) extends Message

View File

@ -8,124 +8,91 @@
*/
package me.arcanis.ffxivbis.messages
import akka.actor.typed.ActorRef
import akka.actor.typed.{ActorRef, Behavior}
import me.arcanis.ffxivbis.models._
import me.arcanis.ffxivbis.service.LootSelector
sealed trait DatabaseMessage extends Message {
def partyId: String
def isReadOnly: Boolean
}
object 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 GetBiS(partyId: String, playerId: Option[PlayerId], replyTo: ActorRef[Seq[Player]])
extends BisDatabaseMessage {
override val isReadOnly: Boolean = true
}
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 RemovePiecesFromBiS(playerId: PlayerId, replyTo: ActorRef[Unit]) extends BisDatabaseMessage {
override val partyId: String = playerId.partyId
override val isReadOnly: Boolean = false
}
// loot handler
trait LootDatabaseMessage extends DatabaseMessage
case class AddPieceTo(playerId: PlayerId, piece: Piece, isFreeLoot: Boolean, replyTo: ActorRef[Unit])
extends LootDatabaseMessage {
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 {
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
}
type Handler = PartialFunction[DatabaseMessage, Behavior[DatabaseMessage]]
}
// bis handler
trait BisDatabaseMessage extends DatabaseMessage
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
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 def partyId: String = playerId.partyId
}
// loot handler
trait LootDatabaseMessage extends DatabaseMessage
case class AddPieceTo(playerId: PlayerId, piece: Piece, isFreeLoot: Boolean, replyTo: ActorRef[Unit])
extends LootDatabaseMessage {
override def partyId: String = playerId.partyId
}
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])
extends LootDatabaseMessage {
override def partyId: String = playerId.partyId
}
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

View File

@ -11,14 +11,14 @@ package me.arcanis.ffxivbis.models
case class BiS(pieces: Seq[Piece]) {
def hasPiece(piece: Piece): Boolean = piece match {
case upgrade: Piece.PieceUpgrade => upgrades.contains(upgrade)
case upgrade: PieceUpgrade => upgrades.contains(upgrade)
case _ => pieces.contains(piece)
}
def upgrades: Map[Piece.PieceUpgrade, Int] =
def upgrades: Map[PieceUpgrade, Int] =
pieces
.groupBy(_.upgrade)
.foldLeft(Map.empty[Piece.PieceUpgrade, Int]) {
.foldLeft(Map.empty[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 {
val empty: BiS = BiS(Seq.empty)
def empty: BiS = BiS(Seq.empty)
}

View File

@ -8,26 +8,6 @@
*/
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
@ -46,38 +26,54 @@ 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 {
override val leftSide: LeftSide = null
override val rightSide: RightSide = null
val leftSide: LeftSide = null
val rightSide: RightSide = null
}
trait Casters extends Job {
override val leftSide: LeftSide = BodyCasters
override val rightSide: RightSide = AccessoriesInt
val leftSide: LeftSide = BodyCasters
val rightSide: RightSide = AccessoriesInt
}
trait Healers extends Job {
override val leftSide: LeftSide = BodyHealers
override val rightSide: RightSide = AccessoriesMnd
val leftSide: LeftSide = BodyHealers
val rightSide: RightSide = AccessoriesMnd
}
trait Mnks extends Job {
override val leftSide: LeftSide = BodyMnks
override val rightSide: RightSide = AccessoriesStr
val leftSide: LeftSide = BodyMnks
val rightSide: RightSide = AccessoriesStr
}
trait Drgs extends Job {
override val leftSide: LeftSide = BodyDrgs
override val rightSide: RightSide = AccessoriesStr
}
trait Nins extends Job {
override val leftSide: LeftSide = BodyNins
override val rightSide: RightSide = AccessoriesDex
val leftSide: LeftSide = BodyDrgs
val rightSide: RightSide = AccessoriesStr
}
trait Tanks extends Job {
override val leftSide: LeftSide = BodyTanks
override val rightSide: RightSide = AccessoriesVit
val leftSide: LeftSide = BodyTanks
val rightSide: RightSide = AccessoriesVit
}
trait Ranges extends Job {
override val leftSide: LeftSide = BodyRanges
override val rightSide: RightSide = AccessoriesDex
val leftSide: LeftSide = BodyRanges
val rightSide: RightSide = AccessoriesDex
}
case object PLD extends Tanks
@ -93,9 +89,11 @@ object Job {
case object MNK extends Mnks
case object DRG extends Drgs
case object RPR extends Drgs
case object NIN extends Nins
case object NIN extends Job {
val leftSide: LeftSide = BodyNins
val rightSide: RightSide = AccessoriesDex
}
case object SAM extends Mnks
case object VPR extends Mnks
case object BRD extends Ranges
case object MCH extends Ranges
@ -104,13 +102,12 @@ object Job {
case object BLM extends Casters
case object SMN extends Casters
case object RDM extends Casters
case object PCT extends Casters
val available: Seq[Job] =
Seq(PLD, WAR, DRK, GNB, WHM, SCH, AST, SGE, MNK, DRG, RPR, NIN, SAM, VPR, BRD, MCH, DNC, BLM, SMN, RDM, PCT)
val availableWithAnyJob: Seq[Job] = available.prepended(AnyJob)
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)
def withName(job: String): Job =
def withName(job: String): Job.Job =
availableWithAnyJob.find(_.toString.equalsIgnoreCase(job)) match {
case Some(value) => value
case None if job.isEmpty => AnyJob

View File

@ -12,5 +12,7 @@ import java.time.Instant
case class Loot(playerId: Long, piece: Piece, timestamp: Instant, isFreeLoot: Boolean) {
lazy val isFreeLootToInt: Int = if (isFreeLoot) 1 else 0
def isFreeLootToString: String = if (isFreeLoot) "yes" else "no"
def isFreeLootToInt: Int = if (isFreeLoot) 1 else 0
}

View File

@ -14,7 +14,6 @@ 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 {
@ -30,7 +29,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 NonFatal(exception) =>
case exception: Exception =>
logger.error("cannot add player", exception)
this
}

View File

@ -8,7 +8,10 @@
*/
package me.arcanis.ffxivbis.models
case class PartyDescription(partyId: String, partyAlias: Option[String])
case class PartyDescription(partyId: String, partyAlias: Option[String]) {
def alias: String = partyAlias.getOrElse(partyId)
}
object PartyDescription {

View File

@ -10,20 +10,20 @@ package me.arcanis.ffxivbis.models
sealed trait Piece extends Equals {
def pieceType: PieceType
def pieceType: PieceType.PieceType
def job: Job
def job: Job.Job
def piece: String
def withJob(other: Job): Piece
def withJob(other: Job.Job): Piece
def upgrade: Option[Piece.PieceUpgrade] = {
def upgrade: Option[PieceUpgrade] = {
val isTome = pieceType == PieceType.Tome
Some(this).collect {
case _: Piece.PieceAccessory if isTome => Piece.AccessoryUpgrade
case _: Piece.PieceBody if isTome => Piece.BodyUpgrade
case _: Piece.PieceWeapon if isTome => Piece.WeaponUpgrade
case _: PieceAccessory if isTome => AccessoryUpgrade
case _: PieceBody if isTome => BodyUpgrade
case _: PieceWeapon if isTome => WeaponUpgrade
}
}
@ -31,89 +31,88 @@ 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
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)
}
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 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,
override val piece: String = "ring"
) extends PieceAccessory {
def withJob(other: Job.Job): Piece = copy(job = other)
override def equals(obj: Any): Boolean = obj match {
case Ring(thatPieceType, thatJob, _) => (thatPieceType == pieceType) && (thatJob == job)
case _ => false
}
override def strictEqual(obj: Any): Boolean = obj match {
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"
}
object Piece {
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 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 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 {
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)
case _ => false
}
override def strictEqual(obj: Any): Boolean = obj match {
case ring: Ring => equals(obj) && (ring.piece == this.piece)
case _ => false
}
}
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"
}
def apply(piece: String, pieceType: PieceType, job: Job = Job.AnyJob): Piece =
def apply(piece: String, pieceType: PieceType.PieceType, job: Job.Job = Job.AnyJob): Piece =
piece.toLowerCase match {
case "weapon" => Weapon(pieceType, job)
case "head" => Head(pieceType, job)
case "body" => Body(pieceType, job)
case "hand" | "hands" => Hands(pieceType, job)
case "hands" => Hands(pieceType, job)
case "legs" => Legs(pieceType, job)
case "feet" => Feet(pieceType, job)
case "ears" => Ears(pieceType, job)
@ -126,7 +125,7 @@ object Piece {
case other => throw new Error(s"Unknown item type $other")
}
val available: Seq[String] = Seq(
lazy val available: Seq[String] = Seq(
"weapon",
"head",
"body",

View File

@ -8,16 +8,17 @@
*/
package me.arcanis.ffxivbis.models
sealed trait PieceType
object PieceType {
case object Savage extends PieceType
case object Tome extends PieceType
sealed trait PieceType
case object Crafted extends PieceType
case object Tome extends PieceType
case object Savage extends PieceType
case object Artifact extends PieceType
val available: Seq[PieceType] = Seq(Savage, Tome, Crafted, Artifact)
lazy val available: Seq[PieceType] =
Seq(Crafted, Tome, Savage, Artifact)
def withName(pieceType: String): PieceType =
available.find(_.toString.equalsIgnoreCase(pieceType)) match {

View File

@ -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: Piece.PieceUpgrade) => bis.upgrades(p) > lootCount(piece)
case Some(p: PieceUpgrade) => bis.upgrades(p) > lootCount(piece)
case Some(_) => lootCount(piece) == 0
}

View File

@ -13,14 +13,14 @@ import scala.util.matching.Regex
trait PlayerIdBase {
def job: Job
def job: Job.Job
def nick: String
override def toString: String = s"$nick ($job)"
}
case class PlayerId(partyId: String, job: Job, nick: String) extends PlayerIdBase
case class PlayerId(partyId: String, job: Job.Job, nick: String) extends PlayerIdBase
object PlayerId {

View File

@ -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,6 +24,8 @@ 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(
@ -45,13 +47,13 @@ object PlayerIdWithCounters {
def >(that: PlayerCountersComparator): Boolean = {
@scala.annotation.tailrec
def compare(left: Seq[Int], right: Seq[Int]): Boolean =
def compareLists(left: List[Int], right: List[Int]): Boolean =
(left, right) match {
case (hl :: tl, hr :: tr) => if (hl == hr) compare(tl, tr) else hl > hr
case (hl :: tl, hr :: tr) => if (hl == hr) compareLists(tl, tr) else hl > hr
case (_ :: _, Nil) => true
case (_, _) => false
}
compare(values, that.values)
compareLists(values.toList, that.values.toList)
}
}
}

View File

@ -11,7 +11,6 @@ package me.arcanis.ffxivbis.models
import org.mindrot.jbcrypt.BCrypt
object Permission extends Enumeration {
val get, post, admin = Value
}

View File

@ -16,6 +16,7 @@ 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])
@ -36,24 +37,23 @@ 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 ControlMessage.ForgetParty(partyId) =>
case ForgetParty(partyId) =>
Behaviors.receiveMessage(handle(cache - partyId))
case ControlMessage.GetNewPartyId(client) =>
case GetNewPartyId(client) =>
getPartyId.foreach(client ! _)
Behaviors.same
case ControlMessage.StoreParty(partyId, party) =>
case StoreParty(partyId, party) =>
Behaviors.receiveMessage(handle(cache.updated(partyId, party)))
case DatabaseMessage.GetParty(partyId, client) =>
case GetParty(partyId, client) =>
val party = cache.get(partyId) match {
case Some(party) => Future.successful(party)
case None =>
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))
storage.ask(ref => GetParty(partyId, ref)).map { party =>
context.self ! StoreParty(partyId, party)
context.system.scheduler.scheduleOnce(cacheTimeout, () => context.self ! ForgetParty(partyId))
party
}
}
@ -62,13 +62,12 @@ class PartyService(context: ActorContext[Message], storage: ActorRef[DatabaseMes
case req: DatabaseMessage =>
storage ! req
val result = if (req.isReadOnly) cache else cache - req.partyId
Behaviors.receiveMessage(handle(result))
Behaviors.receiveMessage(handle(cache - req.partyId))
}
private def getPartyId: Future[String] = {
val partyId = Party.randomPartyId
storage.ask(ref => DatabaseMessage.Exists(partyId, ref)).flatMap {
storage.ask(ref => Exists(partyId, ref)).flatMap {
case true => getPartyId
case false => Future.successful(partyId)
}

View File

@ -13,15 +13,14 @@ 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
import me.arcanis.ffxivbis.messages.{BiSProviderMessage, DownloadBiS}
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, XIVGear}
import me.arcanis.ffxivbis.service.bis.parser.impl.{Ariyala, Etro}
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])
@ -33,7 +32,7 @@ class BisProvider(context: ActorContext[BiSProviderMessage])
override def onMessage(msg: BiSProviderMessage): Behavior[BiSProviderMessage] =
msg match {
case BiSProviderMessage.DownloadBiS(link, job, client) =>
case DownloadBiS(link, job, client) =>
get(link, job).onComplete {
case Success(items) => client ! BiS(items)
case Failure(exception) =>
@ -47,19 +46,16 @@ class BisProvider(context: ActorContext[BiSProviderMessage])
Behaviors.same
}
private def get(link: String, job: Job): Future[Seq[Piece]] =
private def get(link: String, job: 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 if (url.authority.host.address().contains("xivgear.app")) XIVGear
else Ariyala
val parser = if (url.authority.host.address().contains("etro")) Etro else Ariyala
val uri = parser.uri(url, id)
sendRequest(uri, BisProvider.parseBisJsonToPieces(job, parser, getPieceType))
} catch {
case NonFatal(exception) => Future.failed(exception)
case exception: Exception => Future.failed(exception)
}
}
@ -69,9 +65,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]]
pieceTypes: Seq[Long] => Future[Map[Long, PieceType.PieceType]]
)(js: JsObject)(implicit executionContext: ExecutionContext): Future[Seq[Piece]] =
idParser.parse(job, js).flatMap { pieces =>
pieceTypes(pieces.values.toSeq).map { types =>
@ -84,11 +80,12 @@ object BisProvider {
}
}
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
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
}
}

View File

@ -15,7 +15,6 @@ 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 {
@ -23,7 +22,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] =
private val preloadedItems: Map[Long, PieceType.PieceType] =
config
.getConfigList("me.arcanis.ffxivbis.bis-provider.cached-items")
.asScala
@ -32,16 +31,17 @@ trait XivApi extends RequestExecutor {
}
.toMap
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)
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) =>
if (preloadedItems.contains(id)) (l.updated(id, preloadedItems(id)), r)
else (l, r :+ id)
}
if (remote.isEmpty) Future.successful(local)
else remotePieceType(remote).map(_ ++ local)
}
private def remotePieceType(itemIds: Seq[Long]): Future[Map[Long, PieceType]] = {
private def remotePieceType(itemIds: Seq[Long]): Future[Map[Long, PieceType.PieceType]] = {
val uriForItems = Uri(xivapiUrl)
.withPath(Uri.Path / "item")
.withQuery(
@ -76,10 +76,6 @@ 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)]] = {
@ -89,12 +85,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 itemRegexp.matches(shopName) =>
case (shopName, JsArray(array)) if shopName.startsWith("ItemReceive") =>
val shopId = array.head match {
case JsNumber(id) => id.toLong
case other => throw deserializationError(s"Could not parse $other")
}
itemRegexp.findFirstMatchIn(shopName).get.group("index") -> shopId
shopName.replace("ItemReceive", "") -> shopId
}
}
.getOrElse(throw deserializationError(s"Could not parse $js"))
@ -115,7 +111,7 @@ object XivApi {
private def parseXivapiJsonToType(
shops: Map[Long, (String, Long)]
)(js: JsObject)(implicit executionContext: ExecutionContext): Future[Map[Long, PieceType]] =
)(js: JsObject)(implicit executionContext: ExecutionContext): Future[Map[Long, PieceType.PieceType]] =
Future {
val shopMap = js.fields("Results") match {
case array: JsArray =>
@ -133,7 +129,7 @@ object XivApi {
if (index == "crafted" && shopId == -1L) PieceType.Crafted
else
Try(shopMap(shopId).fields(s"ItemCost$index").asJsObject)
.getOrElse(defaultShop)
.getOrElse(throw new Exception(s"${shopMap(shopId).fields(s"ItemCost$index")}, $index"))
.getFields("IsUnique", "StackSize") match {
case Seq(JsNumber(isUnique), JsNumber(stackSize)) =>
if (isUnique == 1 || stackSize.toLong != 999) PieceType.Tome // either upgraded gear or tomes found

View File

@ -17,7 +17,7 @@ import scala.concurrent.{ExecutionContext, Future}
trait Parser extends StrictLogging {
def parse(job: Job, js: JsObject)(implicit executionContext: ExecutionContext): Future[Map[String, Long]]
def parse(job: Job.Job, js: JsObject)(implicit executionContext: ExecutionContext): Future[Map[String, Long]]
def uri(root: Uri, id: String): Uri
}

View File

@ -18,7 +18,7 @@ import scala.concurrent.{ExecutionContext, Future}
object Ariyala extends Parser {
override def parse(job: Job, js: JsObject)(implicit
override def parse(job: Job.Job, js: JsObject)(implicit
executionContext: ExecutionContext
): Future[Map[String, Long]] =
Future {

View File

@ -18,7 +18,7 @@ import scala.concurrent.{ExecutionContext, Future}
object Etro extends Parser {
override def parse(job: Job, js: JsObject)(implicit
override def parse(job: Job.Job, js: JsObject)(implicit
executionContext: ExecutionContext
): Future[Map[String, Long]] =
Future {

View File

@ -1,44 +0,0 @@
/*
* 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)
}
}

View File

@ -8,33 +8,30 @@
*/
package me.arcanis.ffxivbis.service.database.impl
import akka.actor.typed.Behavior
import akka.actor.typed.scaladsl.Behaviors
import me.arcanis.ffxivbis.messages.DatabaseMessage
import me.arcanis.ffxivbis.messages.DatabaseMessage._
import me.arcanis.ffxivbis.messages._
import me.arcanis.ffxivbis.service.database.Database
trait DatabaseBiSHandler { this: Database =>
def bisHandler(msg: BisDatabaseMessage): Behavior[DatabaseMessage] =
msg match {
case AddPieceToBis(playerId, piece, client) =>
run(profile.insertPieceBiS(playerId, piece))(_ => client ! ())
Behaviors.same
def bisHandler: DatabaseMessage.Handler = {
case AddPieceToBis(playerId, piece, client) =>
run(profile.insertPieceBiS(playerId, piece))(_ => client ! ())
Behaviors.same
case GetBiS(partyId, maybePlayerId, client) =>
run {
getParty(partyId, withBiS = true, withLoot = false)
.map(filterParty(_, maybePlayerId))
}(client ! _)
Behaviors.same
case GetBiS(partyId, maybePlayerId, client) =>
run {
getParty(partyId, withBiS = true, withLoot = false)
.map(filterParty(_, maybePlayerId))
}(client ! _)
Behaviors.same
case RemovePieceFromBiS(playerId, piece, client) =>
run(profile.deletePieceBiS(playerId, piece))(_ => client ! ())
Behaviors.same
case RemovePieceFromBiS(playerId, piece, client) =>
run(profile.deletePieceBiS(playerId, piece))(_ => client ! ())
Behaviors.same
case RemovePiecesFromBiS(playerId, client) =>
run(profile.deletePiecesBiS(playerId))(_ => client ! ())
Behaviors.same
}
case RemovePiecesFromBiS(playerId, client) =>
run(profile.deletePiecesBiS(playerId))(_ => client ! ())
Behaviors.same
}
}

View File

@ -12,7 +12,6 @@ import akka.actor.typed.scaladsl.{AbstractBehavior, ActorContext}
import akka.actor.typed.{Behavior, DispatcherSelector}
import com.typesafe.config.Config
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
@ -33,12 +32,8 @@ class DatabaseImpl(context: ActorContext[DatabaseMessage])
override val config: Config = context.system.settings.config
override val profile: DatabaseProfile = new DatabaseProfile(executionContext, config)
override def onMessage(msg: DatabaseMessage): Behavior[DatabaseMessage] =
msg match {
case msg: BisDatabaseMessage => bisHandler(msg)
case msg: LootDatabaseMessage => lootHandler(msg)
case msg: PartyDatabaseMessage => partyHandler(msg)
case msg: UserDatabaseMessage => userHandler(msg)
}
override def onMessage(msg: DatabaseMessage): Behavior[DatabaseMessage] = handle(msg)
private def handle: DatabaseMessage.Handler =
bisHandler.orElse(lootHandler).orElse(partyHandler).orElse(userHandler)
}

View File

@ -8,10 +8,8 @@
*/
package me.arcanis.ffxivbis.service.database.impl
import akka.actor.typed.Behavior
import akka.actor.typed.scaladsl.Behaviors
import me.arcanis.ffxivbis.messages.DatabaseMessage
import me.arcanis.ffxivbis.messages.DatabaseMessage._
import me.arcanis.ffxivbis.messages._
import me.arcanis.ffxivbis.models.Loot
import me.arcanis.ffxivbis.service.database.Database
@ -19,29 +17,28 @@ import java.time.Instant
trait DatabaseLootHandler { this: Database =>
def lootHandler(msg: LootDatabaseMessage): Behavior[DatabaseMessage] =
msg match {
case AddPieceTo(playerId, piece, isFreeLoot, client) =>
val loot = Loot(-1, piece, Instant.now, isFreeLoot)
run(profile.insertPiece(playerId, loot))(_ => client ! ())
Behaviors.same
def lootHandler: DatabaseMessage.Handler = {
case AddPieceTo(playerId, piece, isFreeLoot, client) =>
val loot = Loot(-1, piece, Instant.now, isFreeLoot)
run(profile.insertPiece(playerId, loot))(_ => client ! ())
Behaviors.same
case GetLoot(partyId, maybePlayerId, client) =>
run {
getParty(partyId, withBiS = false, withLoot = true)
.map(filterParty(_, maybePlayerId))
}(client ! _)
Behaviors.same
case GetLoot(partyId, maybePlayerId, client) =>
run {
getParty(partyId, withBiS = false, withLoot = true)
.map(filterParty(_, maybePlayerId))
}(client ! _)
Behaviors.same
case RemovePieceFrom(playerId, piece, isFreeLoot, client) =>
run(profile.deletePiece(playerId, piece, isFreeLoot))(_ => client ! ())
Behaviors.same
case RemovePieceFrom(playerId, piece, isFreeLoot, client) =>
run(profile.deletePiece(playerId, piece, isFreeLoot))(_ => client ! ())
Behaviors.same
case SuggestLoot(partyId, piece, client) =>
run {
getParty(partyId, withBiS = true, withLoot = true)
.map(_.suggestLoot(piece))
}(client ! _)
Behaviors.same
}
case SuggestLoot(partyId, piece, client) =>
run {
getParty(partyId, withBiS = true, withLoot = true)
.map(_.suggestLoot(piece))
}(client ! _)
Behaviors.same
}
}

View File

@ -8,10 +8,8 @@
*/
package me.arcanis.ffxivbis.service.database.impl
import akka.actor.typed.Behavior
import akka.actor.typed.scaladsl.Behaviors
import me.arcanis.ffxivbis.messages.DatabaseMessage
import me.arcanis.ffxivbis.messages.DatabaseMessage._
import me.arcanis.ffxivbis.messages._
import me.arcanis.ffxivbis.models.{BiS, Player}
import me.arcanis.ffxivbis.service.database.Database
@ -19,55 +17,50 @@ import scala.concurrent.Future
trait DatabasePartyHandler { this: Database =>
def partyHandler(msg: PartyDatabaseMessage): Behavior[DatabaseMessage] =
msg match {
case AddPlayer(player, client) =>
run(profile.insertPlayer(player))(_ => client ! ())
Behaviors.same
def partyHandler: DatabaseMessage.Handler = {
case AddPlayer(player, client) =>
run(profile.insertPlayer(player))(_ => client ! ())
Behaviors.same
case GetParty(partyId, client) =>
run(getParty(partyId, withBiS = true, withLoot = true))(client ! _)
Behaviors.same
case GetParty(partyId, client) =>
run(getParty(partyId, withBiS = true, withLoot = true))(client ! _)
Behaviors.same
case GetPartyDescription(partyId, client) =>
run(profile.getPartyDescription(partyId))(client ! _)
Behaviors.same
case GetPartyDescription(partyId, client) =>
run(profile.getPartyDescription(partyId))(client ! _)
Behaviors.same
case GetPlayer(playerId, client) =>
run {
profile
.getPlayerFull(playerId)
.flatMap { maybePlayerData =>
Future.traverse(maybePlayerData.toSeq) { playerData =>
for {
bis <- profile.getPiecesBiS(playerId)
loot <- profile.getPieces(playerId)
} yield Player(
playerData.id,
playerId.partyId,
playerId.job,
playerId.nick,
BiS(bis.map(_.piece)),
loot,
playerData.link,
playerData.priority
)
}
case GetPlayer(playerId, client) =>
run {
profile
.getPlayerFull(playerId)
.flatMap { maybePlayerData =>
Future.traverse(maybePlayerData.toSeq) { playerData =>
for {
bis <- profile.getPiecesBiS(playerId)
loot <- profile.getPieces(playerId)
} yield Player(
playerData.id,
playerId.partyId,
playerId.job,
playerId.nick,
BiS(bis.map(_.piece)),
loot,
playerData.link,
playerData.priority
)
}
.map(_.headOption)
}(client ! _)
Behaviors.same
}
.map(_.headOption)
}(client ! _)
Behaviors.same
case RemovePlayer(playerId, client) =>
run(profile.deletePlayer(playerId))(_ => client ! ())
Behaviors.same
case RemovePlayer(playerId, client) =>
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
}
case UpdateParty(description, client) =>
run(profile.insertPartyDescription(description))(_ => client ! ())
Behaviors.same
}
}

View File

@ -8,35 +8,32 @@
*/
package me.arcanis.ffxivbis.service.database.impl
import akka.actor.typed.Behavior
import akka.actor.typed.scaladsl.Behaviors
import me.arcanis.ffxivbis.messages.DatabaseMessage
import me.arcanis.ffxivbis.messages.DatabaseMessage._
import me.arcanis.ffxivbis.messages._
import me.arcanis.ffxivbis.service.database.Database
trait DatabaseUserHandler { this: Database =>
def userHandler(msg: UserDatabaseMessage): Behavior[DatabaseMessage] =
msg match {
case AddUser(user, isHashedPassword, client) =>
val toInsert = if (isHashedPassword) user else user.withHashedPassword
run(profile.insertUser(toInsert))(_ => client ! ())
Behaviors.same
def userHandler: DatabaseMessage.Handler = {
case AddUser(user, isHashedPassword, client) =>
val toInsert = if (isHashedPassword) user else user.withHashedPassword
run(profile.insertUser(toInsert))(_ => client ! ())
Behaviors.same
case DeleteUser(partyId, username, client) =>
run(profile.deleteUser(partyId, username))(_ => client ! ())
Behaviors.same
case DeleteUser(partyId, username, client) =>
run(profile.deleteUser(partyId, username))(_ => client ! ())
Behaviors.same
case Exists(partyId, client) =>
run(profile.exists(partyId))(client ! _)
Behaviors.same
case Exists(partyId, client) =>
run(profile.exists(partyId))(client ! _)
Behaviors.same
case GetUser(partyId, username, client) =>
run(profile.getUser(partyId, username))(client ! _)
Behaviors.same
case GetUser(partyId, username, client) =>
run(profile.getUser(partyId, username))(client ! _)
Behaviors.same
case GetUsers(partyId, client) =>
run(profile.getUsers(partyId))(client ! _)
Behaviors.same
}
case GetUsers(partyId, client) =>
run(profile.getUsers(partyId))(client ! _)
Behaviors.same
}
}

View File

@ -53,14 +53,12 @@ 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)
.executeQuery()
.as(loot.*)
}
withConnection { implicit conn =>
SQL("""select * from bis where player_id in ({player_ids})""")
.on("player_ids" -> playerIds)
.executeQuery()
.as(loot.*)
}
def insertPieceBiSById(piece: Piece)(playerId: Long): Future[Int] =
withConnection { implicit conn =>

View File

@ -16,7 +16,6 @@ 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
@ -32,7 +31,7 @@ class DatabaseProfile(override val executionContext: ExecutionContext, config: C
val dataSourceConfig = DatabaseConnection.getDataSourceConfig(profile)
new HikariDataSource(dataSourceConfig)
} catch {
case NonFatal(exception) =>
case exception: Exception =>
logger.error("exception during storage initialization", exception)
throw exception
}

View File

@ -59,14 +59,12 @@ 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)
.executeQuery()
.as(loot.*)
}
withConnection { implicit conn =>
SQL("""select * from loot where player_id in ({player_ids})""")
.on("player_ids" -> playerIds)
.executeQuery()
.as(loot.*)
}
def insertPieceById(loot: Loot)(playerId: Long): Future[Int] =
withConnection { implicit conn =>

View File

@ -101,18 +101,4 @@ 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()
}
}

View File

@ -1,16 +0,0 @@
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())
}
}
}

View File

@ -1,20 +0,0 @@
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
}
}
}
}

View File

@ -6,65 +6,49 @@ import me.arcanis.ffxivbis.models._
import scala.concurrent.Future
object Fixtures {
lazy val bis: BiS = BiS(
Seq(
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")
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")
)
)
lazy val bis2: BiS = BiS(
Seq(
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")
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")
)
)
lazy val bis3: BiS = BiS(
Seq(
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")
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")
)
)
@ -73,18 +57,17 @@ 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 = 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 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 loot: Seq[Piece] = Seq(lootBody, lootHands, lootLegs, lootUpgrade)
lazy val partyId: String = Party.randomPartyId
@ -101,5 +84,4 @@ 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)
}

View File

@ -5,12 +5,11 @@ 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(config: Config, iter: List[(String, AnyRef)]): Config = iter match {
case Nil => config
case (key -> value) :: tail => replace(config.withValue(key, ConfigValueFactory.fromAnyRef(value)), tail)
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)
}
val default = ConfigFactory.load()
@ -24,9 +23,7 @@ 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"))
}

View File

@ -1,44 +0,0 @@
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
}
}
}
}

View File

@ -1,22 +0,0 @@
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
}
}
}
}

View File

@ -1,39 +0,0 @@
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
}
}
}
}

View File

@ -1,28 +0,0 @@
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
}
}
}
}

View File

@ -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.DatabaseMessage.{AddPlayer, AddUser}
import me.arcanis.ffxivbis.messages.{AddPlayer, AddUser}
import me.arcanis.ffxivbis.models.{BiS, Job}
import me.arcanis.ffxivbis.service.PartyService
import me.arcanis.ffxivbis.service.bis.BisProvider
@ -37,7 +37,7 @@ class BiSEndpointTest extends AnyWordSpecLike with Matchers with ScalatestRouteT
private val storage = testKit.spawn(Database())
private val provider = testKit.spawn(BisProvider())
private val party = testKit.spawn(PartyService(storage))
private val route = new BiSEndpoint(party, provider, Fixtures.authProvider)(askTimeout, testKit.scheduler).routes
private val route = new BiSEndpoint(party, provider, Fixtures.authProvider)(askTimeout, testKit.scheduler).route
override def beforeAll(): Unit = {
super.beforeAll()
@ -53,6 +53,15 @@ 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 {
@ -207,13 +216,4 @@ 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
}
}

View File

@ -1,78 +0,0 @@
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)
}
}
}

View File

@ -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.DatabaseMessage.{AddPlayer, AddUser}
import me.arcanis.ffxivbis.messages.{AddPlayer, AddUser}
import me.arcanis.ffxivbis.service.PartyService
import me.arcanis.ffxivbis.service.database.{Database, Migration}
import me.arcanis.ffxivbis.{Fixtures, Settings}
@ -34,7 +34,7 @@ class LootEndpointTest extends AnyWordSpecLike with Matchers with ScalatestRoute
private val storage = testKit.spawn(Database())
private val party = testKit.spawn(PartyService(storage))
private val route = new LootEndpoint(party, Fixtures.authProvider)(askTimeout, testKit.scheduler).routes
private val route = new LootEndpoint(party, Fixtures.authProvider)(askTimeout, testKit.scheduler).route
override def beforeAll(): Unit = {
super.beforeAll()
@ -95,15 +95,5 @@ 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
}
}
}
}

View File

@ -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.DatabaseMessage.AddUser
import me.arcanis.ffxivbis.messages.AddUser
import me.arcanis.ffxivbis.models.PartyDescription
import me.arcanis.ffxivbis.service.PartyService
import me.arcanis.ffxivbis.service.bis.BisProvider
@ -35,7 +35,7 @@ class PartyEndpointTest extends AnyWordSpecLike with Matchers with ScalatestRout
private val storage = testKit.spawn(Database())
private val provider = testKit.spawn(BisProvider())
private val party = testKit.spawn(PartyService(storage))
private val route = new PartyEndpoint(party, provider, Fixtures.authProvider)(askTimeout, testKit.scheduler).routes
private val route = new PartyEndpoint(party, provider, Fixtures.authProvider)(askTimeout, testKit.scheduler).route
override def beforeAll(): Unit = {
super.beforeAll()

View File

@ -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.DatabaseMessage.{AddPlayer, AddUser}
import me.arcanis.ffxivbis.messages.{AddPlayer, AddUser}
import me.arcanis.ffxivbis.service.PartyService
import me.arcanis.ffxivbis.service.bis.BisProvider
import me.arcanis.ffxivbis.service.database.{Database, Migration}
@ -34,7 +34,7 @@ class PlayerEndpointTest extends AnyWordSpecLike with Matchers with ScalatestRou
private val storage = testKit.spawn(Database())
private val provider = testKit.spawn(BisProvider())
private val party = testKit.spawn(PartyService(storage))
private val route = new PlayerEndpoint(party, provider, Fixtures.authProvider)(askTimeout, testKit.scheduler).routes
private val route = new PlayerEndpoint(party, provider, Fixtures.authProvider)(askTimeout, testKit.scheduler).route
override def beforeAll(): Unit = {
super.beforeAll()
@ -52,7 +52,7 @@ class PlayerEndpointTest extends AnyWordSpecLike with Matchers with ScalatestRou
"api v1 player endpoint" must {
"get users belonging to the party" in {
"get users" in {
val response = Seq(PlayerModel.fromPlayer(Fixtures.playerEmpty))
Get(endpoint).withHeaders(auth) ~> route ~> check {
@ -61,42 +61,5 @@ 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)
}
}
}
}

View File

@ -2,6 +2,8 @@ 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
@ -11,7 +13,9 @@ import scala.language.postfixOps
class StatusEndpointTest extends AnyWordSpecLike
with Matchers with ScalatestRouteTest with JsonSupport {
private val route = new StatusEndpoint().routes
override val testConfig: Config = Settings.withRandomDatabase
private val route = new StatusEndpoint().route
"api v1 status endpoint" must {

View File

@ -16,7 +16,7 @@ class TypesEndpointTest extends AnyWordSpecLike
override val testConfig: Config = Settings.withRandomDatabase
private val route = new TypesEndpoint(testConfig).routes
private val route = new TypesEndpoint(testConfig).route
"api v1 types endpoint" must {

View File

@ -30,7 +30,7 @@ class UserEndpointTest extends AnyWordSpecLike with Matchers with ScalatestRoute
private var partyId = Fixtures.partyId
private val storage = testKit.spawn(Database())
private val party = testKit.spawn(PartyService(storage))
private val route = new UserEndpoint(party, Fixtures.authProvider)(askTimeout, testKit.scheduler).routes
private val route = new UserEndpoint(party, Fixtures.authProvider)(askTimeout, testKit.scheduler).route
override def beforeAll(): Unit = {
super.beforeAll()
@ -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 = Some(Fixtures.userPassword))
val entity = UserModel.fromUser(Fixtures.userAdmin).copy(password = Fixtures.userPassword)
Post(uri, entity) ~> route ~> check {
Put(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 = Some(Fixtures.userPassword2))
val entity = UserModel.fromUser(Fixtures.userGet).copy(partyId = partyId, password = Fixtures.userPassword2)
Post(endpoint, entity).withHeaders(auth) ~> route ~> check {
status shouldEqual StatusCodes.Accepted

View File

@ -1,55 +0,0 @@
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
}
}
}
}

View File

@ -46,7 +46,7 @@ class BiSTest extends AnyWordSpecLike with Matchers {
}
"return upgrade list" in {
Compare.mapEquals(Fixtures.bis.upgrades, Map[Piece.PieceUpgrade, Int](Piece.BodyUpgrade -> 2, Piece.AccessoryUpgrade -> 3)) shouldEqual true
Compare.mapEquals(Fixtures.bis.upgrades, Map[PieceUpgrade, Int](BodyUpgrade -> 2, AccessoryUpgrade -> 3)) shouldEqual true
}
}

View File

@ -9,13 +9,13 @@ class PieceTest extends AnyWordSpecLike with Matchers {
"piece model" must {
"return upgrade" in {
Fixtures.lootWeapon.upgrade shouldEqual Some(Piece.WeaponUpgrade)
Fixtures.lootWeapon.upgrade shouldEqual Some(WeaponUpgrade)
Fixtures.lootBody.upgrade shouldEqual None
Fixtures.lootHands.upgrade shouldEqual Some(Piece.BodyUpgrade)
Fixtures.lootHands.upgrade shouldEqual Some(BodyUpgrade)
Fixtures.lootLegs.upgrade shouldEqual None
Fixtures.lootEars.upgrade shouldEqual None
Fixtures.lootLeftRing.upgrade shouldEqual Some(Piece.AccessoryUpgrade)
Fixtures.lootRightRing.upgrade shouldEqual Some(Piece.AccessoryUpgrade)
Fixtures.lootLeftRing.upgrade shouldEqual Some(AccessoryUpgrade)
Fixtures.lootRightRing.upgrade shouldEqual Some(AccessoryUpgrade)
}
"build piece from string" in {

View File

@ -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.BiSProviderMessage.DownloadBiS
import me.arcanis.ffxivbis.messages.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(Piece.Head(pieceType = PieceType.Savage, Job.AnyJob))) shouldEqual Seq(dnc.playerId, drg.playerId)
toPlayerId(default.suggestLoot(Head(pieceType = PieceType.Savage, Job.AnyJob))) shouldEqual Seq(dnc.playerId, drg.playerId)
}
"suggest loot if a player already have it" in {
val piece = Piece.Body(pieceType = PieceType.Savage, Job.AnyJob)
val 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(Piece.Weapon(pieceType = PieceType.Tome, Job.DNC)))
Some(dnc.bis.withPiece(Weapon(pieceType = PieceType.Tome, Job.DNC)))
)
)
toPlayerId(party.suggestLoot(Piece.WeaponUpgrade)) shouldEqual Seq(dnc.playerId, drg.playerId)
toPlayerId(party.suggestLoot(WeaponUpgrade)) shouldEqual Seq(dnc.playerId, drg.playerId)
}
"suggest loot by priority" in {
val party = default.withPlayer(dnc.copy(priority = 2))
toPlayerId(party.suggestLoot(Piece.Body(pieceType = PieceType.Savage, Job.AnyJob))) shouldEqual Seq(drg.playerId, dnc.playerId)
toPlayerId(party.suggestLoot(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(Piece.Head(pieceType = PieceType.Savage, Job.AnyJob)))
val party = default.withPlayer(dnc.withLoot(Head(pieceType = PieceType.Savage, Job.AnyJob)))
toPlayerId(party.suggestLoot(Piece.Body(pieceType = PieceType.Savage, Job.AnyJob))) shouldEqual Seq(drg.playerId, dnc.playerId)
toPlayerId(party.suggestLoot(Body(pieceType = PieceType.Savage, Job.AnyJob))) shouldEqual Seq(drg.playerId, dnc.playerId)
}
"suggest loot by this piece got" in {
val piece = Piece.Body(pieceType = PieceType.Tome, Job.AnyJob)
val 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 = Piece.Body(pieceType = PieceType.Tome, Job.AnyJob)
val piece = Body(pieceType = PieceType.Tome, Job.AnyJob)
val party = default
.withPlayer(dnc.withLoot(Seq(piece, piece).map(pieceToLoot)))
.withPlayer(drg.withLoot(piece))

Some files were not shown because too many files have changed in this diff Show More