migrate to bootstrap (#14)

This commit is contained in:
Evgenii Alekseev 2022-01-15 23:15:24 +03:00 committed by GitHub
parent a6991a0a91
commit eeb5178efc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
113 changed files with 2561 additions and 1993 deletions

View File

@ -25,9 +25,9 @@ jobs:
uses: actions/setup-java@v2 uses: actions/setup-java@v2
with: with:
distribution: temurin distribution: temurin
java-version: 8 java-version: 17
- name: create dist - name: create dist
run: sbt -v dist run: make dist
- name: release - name: release
uses: softprops/action-gh-release@v1 uses: softprops/action-gh-release@v1
with: with:

View File

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

35
Makefile Normal file
View File

@ -0,0 +1,35 @@
.PHONY: check clean compile dist push tests version
.DEFAULT_GOAL := compile
PROJECT := ffxivbis
check:
sbt scalafmtCheck
clean:
sbt clean
compile: clean
sbt compile
format:
sbt scalafmt
dist: tests version
sbt dist
push: dist
git add version.sbt
git commit -m "Release $(VERSION)"
git tag "$(VERSION)"
git push
git push --tags
tests: compile check
sbt test
version:
ifndef VERSION
$(error VERSION is required, but not set)
endif
sed -i '/version := "[0-9.]*/s/[^"][^)]*/version := "$(VERSION)"/' version.sbt

View File

@ -14,7 +14,6 @@ libraryDependencies += "com.github.swagger-akka-http" %% "swagger-akka-http" % "
libraryDependencies += "jakarta.platform" % "jakarta.jakartaee-web-api" % "9.1.0" libraryDependencies += "jakarta.platform" % "jakarta.jakartaee-web-api" % "9.1.0"
libraryDependencies += "io.spray" %% "spray-json" % "1.3.6" libraryDependencies += "io.spray" %% "spray-json" % "1.3.6"
libraryDependencies += "com.lihaoyi" %% "scalatags" % "0.9.2"
libraryDependencies += "com.typesafe.slick" %% "slick" % SlickVersion libraryDependencies += "com.typesafe.slick" %% "slick" % SlickVersion
libraryDependencies += "com.typesafe.slick" %% "slick-hikaricp" % SlickVersion libraryDependencies += "com.typesafe.slick" %% "slick-hikaricp" % SlickVersion
@ -23,6 +22,8 @@ libraryDependencies += "org.xerial" % "sqlite-jdbc" % "3.36.0.3"
libraryDependencies += "org.postgresql" % "postgresql" % "42.3.1" libraryDependencies += "org.postgresql" % "postgresql" % "42.3.1"
libraryDependencies += "org.mindrot" % "jbcrypt" % "0.4" libraryDependencies += "org.mindrot" % "jbcrypt" % "0.4"
libraryDependencies += "com.google.guava" % "guava" % "31.0.1-jre"
// testing // testing
libraryDependencies += "org.scalactic" %% "scalactic" % ScalaTestVersion % "test" libraryDependencies += "org.scalactic" %% "scalactic" % ScalaTestVersion % "test"

View File

@ -0,0 +1,349 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Best in slot</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="/static/favicon.ico" rel="shortcut icon">
<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 href="https://unpkg.com/bootstrap-table@1.19.1/dist/bootstrap-table.min.css" rel="stylesheet" type="text/css">
<link href="https://unpkg.com/jquery-resizable-columns@0.2.3/dist/jquery.resizableColumns.css" rel="stylesheet" type="text/css">
<link href="/static/styles.css" rel="stylesheet" type="text/css">
</head>
<body>
<div class="container">
<nav class="navbar navbar-expand-lg justify-content-between align-items-center border-bottom">
<a class="navbar-brand" id="navbar-title">Party</a>
<ul class="navbar-nav">
<a class="nav-item nav-link" id="navbar-bis">best in slot</a>
<a class="nav-item nav-link" id="navbar-loot">looted items</a>
</ul>
<ul class="navbar-nav">
<a class="nav-item nav-link" id="navbar-users">users</a>
</ul>
</nav>
</div>
<div id="alert-placeholder" class="container"></div>
<div class="container">
<h2>Best in slot</h2>
</div>
<div class="container">
<div id="toolbar">
<button id="update-btn" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#update-bis-dialog" hidden>
<i class="bi bi-plus"></i> update
</button>
<button class="btn btn-secondary" onclick="reload()">
<i class="bi bi-arrow-clockwise"></i> reload
</button>
<button id="remove-btn" class="btn btn-danger" onclick="removePiece()" disabled hidden>
<i class="bi bi-trash"></i> remove
</button>
</div>
<table id="bis" class="table table-striped table-hover"
data-click-to-select="true"
data-export-options='{"fileName": "bis"}'
data-page-list="[25, 50, 100, all]"
data-page-size="25"
data-pagination="true"
data-resizable="true"
data-search="true"
data-show-columns="true"
data-show-columns-search="true"
data-show-columns-toggle-all="true"
data-show-export="true"
data-show-fullscreen="true"
data-show-search-clear-button="true"
data-single-select="true"
data-sortable="true"
data-sort-reset="true"
data-toolbar="#toolbar">
<thead class="table-primary">
<tr>
<th data-checkbox="true"></th>
<th data-sortable="true" data-switchable="false" data-field="nick">nick</th>
<th data-sortable="true" data-switchable="false" data-field="job">job</th>
<th data-sortable="true" data-field="piece">piece</th>
<th data-sortable="true" data-field="pieceType">piece type</th>
</tr>
</thead>
</table>
</div>
<div id="update-bis-dialog" tabindex="-1" role="dialog" class="modal fade">
<div class="modal-dialog" role="document">
<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>
</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"></select>
</div>
</div>
<div id="piece-row" 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"></select>
</div>
</div>
<div id="piece-type-row" 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="piece-type"></select>
</div>
</div>
<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" 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="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>
</div>
</div>
<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="/" title="home">home</a></li>
</ul>
<ul class="nav">
<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>
</footer>
</div>
<script src="https://cdn.jsdelivr.net/npm/jquery/dist/jquery.min.js"></script>
<script src="https://unpkg.com/tableexport.jquery.plugin/tableExport.min.js"></script>
<script src="https://unpkg.com/jquery-resizable-columns@0.2.3/dist/jquery.resizableColumns.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p" crossorigin="anonymous"></script>
<script src="https://unpkg.com/bootstrap-table@1.19.1/dist/bootstrap-table.min.js"></script>
<script src="https://unpkg.com/bootstrap-table@1.19.1/dist/extensions/export/bootstrap-table-export.min.js"></script>
<script src="https://unpkg.com/bootstrap-table@1.19.1/dist/extensions/resizable/bootstrap-table-resizable.js"></script>
<script src="/static/utils.js"></script>
<script src="/static/load.js"></script>
<script>
const [partyId, isReadOnly] = getPartyId();
const table = $("#bis");
const removeButton = $("#remove-btn");
const updateButton = $("#update-btn");
const submitAddBisButton = $("#submit-add-bis-btn");
const submitUpdateBisButton = $("#submit-update-bis-btn");
const updateBisDialog = $("#update-bis-dialog");
const addPieceButton = $("#add-piece-btn");
const updateBisButton = $("#update-bis-btn");
const bisLinkRow = $("#bis-link-row");
const pieceRow = $("#piece-row");
const pieceTypeRow = $("#piece-type-row");
const linkInput = $("#bis-link");
const pieceInput = $("#piece");
const pieceTypeInput = $("#piece-type");
const playerInput = $("#player");
function addPiece() {
const player = getCurrentOption(playerInput);
$.ajax({
url: `/api/v1/party/${partyId}/bis`,
data: JSON.stringify({
action: "add",
piece: {
pieceType: pieceTypeInput.val(),
job: player.dataset.job,
piece: pieceInput.val(),
},
playerId: {
partyId: partyId,
nick: player.dataset.nick,
job: player.dataset.job,
},
}),
type: "POST",
contentType: "application/json",
success: function (_) { reload(); },
error: function (jqXHR, _, errorThrown) { requestAlert(jqXHR, errorThrown); },
});
}
function disableSubmitBisButton() {
const nonEmpty = (playerInput.val() !== null); // well lol
submitUpdateBisButton.attr("disabled", !(nonEmpty && linkInput.val()));
submitAddBisButton.attr("disabled", !(nonEmpty));
}
function hideControls() {
removeButton.attr("hidden", isReadOnly);
updateButton.attr("hidden", isReadOnly);
}
function hideLinkPart() {
disableSubmitBisButton();
bisLinkRow.hide();
submitUpdateBisButton.hide();
pieceRow.show();
pieceTypeRow.show();
submitAddBisButton.show();
}
function hidePiecePart() {
disableSubmitBisButton();
bisLinkRow.show();
submitUpdateBisButton.show();
pieceRow.hide();
pieceTypeRow.hide();
submitAddBisButton.hide();
}
function reload() {
table.bootstrapTable("showLoading");
$.ajax({
url: `/api/v1/party/${partyId}`,
type: "GET",
dataType: "json",
success: function (data) {
const items = data.map(function (player) {
return player.bis.map(function (loot) {
return {
nick: player.nick,
job: player.job,
piece: loot.piece,
pieceType: loot.pieceType,
};
});
});
const payload = items.reduce(function (left, right) { return left.concat(right); }, []);
table.bootstrapTable("load", payload);
table.bootstrapTable("uncheckAll");
table.bootstrapTable("hideLoading");
const options = data.map(function (player) {
const option = document.createElement("option");
option.innerText = formatPlayerId(player);
option.dataset.nick = player.nick;
option.dataset.job = player.job;
return option;
});
playerInput.empty().append(options);
disableSubmitBisButton();
},
error: function (jqXHR, _, errorThrown) { requestAlert(jqXHR, errorThrown); },
});
}
function removePiece() {
const pieces = table.bootstrapTable("getSelections");
pieces.map(function (loot) {
$.ajax({
url: `/api/v1/party/${partyId}/bis`,
data: JSON.stringify({
action: "remove",
piece: {
pieceType: loot.pieceType,
job: loot.job,
piece: loot.piece,
},
playerId: {
partyId: partyId,
job: loot.job,
nick: loot.nick,
},
}),
type: "POST",
contentType: "application/json",
success: function (_) { reload(); },
error: function (jqXHR, _, errorThrown) { requestAlert(jqXHR, errorThrown); },
});
});
}
function reset() {
if (updateBisButton.is(":checked")) {
hidePiecePart();
}
if (addPieceButton.is(":checked")) {
hideLinkPart();
}
}
function setBis() {
const player = getCurrentOption(playerInput);
$.ajax({
url: `/api/v1/party/${partyId}/bis`,
data: JSON.stringify({
link: linkInput.val(),
playerId: {
partyId: partyId,
nick: player.dataset.nick,
job: player.dataset.job,
},
}),
type: "PUT",
contentType: "application/json",
success: function (_) { reload(); },
error: function (jqXHR, _, errorThrown) { requestAlert(jqXHR, errorThrown); },
});
}
$(function () {
setupFormClear(updateBisDialog, reset);
setupRemoveButton(table, removeButton);
loadHeader(partyId);
loadTypes("/api/v1/types/pieces", pieceInput);
loadTypes("/api/v1/types/pieces/types", pieceTypeInput);
hideControls();
updateBisButton.click(function () { reset(); });
addPieceButton.click(function () { reset(); });
table.bootstrapTable({});
reload();
});
</script>
</body>
</html>

View File

@ -0,0 +1,183 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>FFXIV loot helper</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="/static/favicon.ico" rel="shortcut icon">
<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="/static/styles.css" rel="stylesheet" type="text/css">
</head>
<body>
<div id="alert-placeholder" class="container"></div>
<div class="container mb-5">
<div class="form-group row">
<div class="btn-group" role="group" aria-label="Sign in">
<input id="signin-btn" name="signin" type="radio" class="btn-check" autocomplete="off" checked>
<label class="btn btn-outline-primary" for="signin-btn">login to existing party</label>
<input id="signup-btn" name="signin" type="radio" class="btn-check" autocomplete="off">
<label class="btn btn-outline-primary" for="signup-btn">create a new party</label>
</div>
</div>
</div>
<form id="signup-form" class="container mb-5" style="display: none">
<div class="form-group row">
<label class="col-sm-2 col-form-label" for="alias">party alias</label>
<div class="col-sm-10">
<input id="alias" name="alias" class="form-control" placeholder="alias">
</div>
</div>
<div class="form-group row">
<label class="col-sm-2 col-form-label" for="username">username</label>
<div class="col-sm-10">
<input id="username" name="username" class="form-control" placeholder="admin user name" onkeyup="disableAddButton()">
</div>
</div>
<div class="form-group row">
<label for="password" class="col-sm-2 col-form-label">password</label>
<div class="col-sm-10">
<input id="password" name="password" type="password" class="form-control" placeholder="admin password" onkeyup="disableAddButton()">
</div>
</div>
<div class="form-group row">
<div class="col-sm-10">
<button id="add-btn" type="button" class="btn btn-primary" onclick="createParty()" disabled>add</button>
</div>
</div>
</form>
<form id="signin-form" class="container mb-5">
<div class="form-group row">
<label class="col-sm-2 col-form-label" for="party-id">party id</label>
<div class="col-sm-10">
<input id="party-id" name="partyId" class="form-control" placeholder="id" onkeyup="disableRedirectButton()">
</div>
</div>
<div class="form-group row">
<div class="col-sm-10">
<button id="redirect-btn" type="button" class="btn btn-primary" onclick="redirectToParty()" disabled>go</button>
</div>
</div>
</form>
<div class="container">
<footer class="d-flex flex-wrap justify-content-between align-items-center border-top">
<ul class="nav"></ul>
<ul class="nav">
<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>
</footer>
</div>
<script src="https://cdn.jsdelivr.net/npm/jquery/dist/jquery.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>
const signinButton = $("#signin-btn");
const signupButton = $("#signup-btn");
const addButton = $("#add-btn");
const redirectButton = $("#redirect-btn");
const signinForm = $("#signin-form");
const signupForm = $("#signup-form");
const aliasInput = $("#alias");
const partyIdInput = $("#party-id");
const passwordInput = $("#password");
const usernameInput = $("#username");
function createDescription(partyId) {
$.ajax({
url: `/api/v1/party/${partyId}/description`,
data: JSON.stringify({
partyId: partyId,
partyAlias: aliasInput.val(),
}),
type: "POST",
contentType: "application/json",
success: function (_) { doRedirect(partyId); },
error: function (jqXHR, _, errorThrown) { requestAlert(jqXHR, errorThrown); },
});
}
function createParty() {
$.ajax({
url: `/api/v1/party`,
data: JSON.stringify({
partyId: "",
username: usernameInput.val(),
password: passwordInput.val(),
permission: "admin",
}),
type: "PUT",
contentType: "application/json",
dataType: "json",
success: function (data) {
if (aliasInput.val()) {
createDescription(data.partyId);
} else {
doRedirect(data.partyId);
}
},
error: function (jqXHR, _, errorThrown) { requestAlert(jqXHR, errorThrown); },
});
}
function disableAddButton() {
addButton.attr("disabled", !(passwordInput.val() && usernameInput.val()));
}
function disableRedirectButton() {
redirectButton.attr("disabled", !partyIdInput.val());
}
function doRedirect(partyId) {
location.href = `/party/${partyId}`;
}
function hideSigninPart() {
signinForm.hide();
signupForm.show();
}
function hideSignupPart() {
signinForm.show();
signupForm.hide();
}
function redirectToParty() {
return doRedirect(partyIdInput.val());
}
function reset() {
signinForm.trigger("reset");
signupForm.trigger("reset");
if (signinButton.is(":checked")) {
hideSignupPart();
}
if (signupButton.is(":checked")) {
hideSigninPart();
}
}
$(function () {
signinButton.click(function () { reset(); });
signupButton.click(function () { reset(); });
});
</script>
</body>
</html>

View File

@ -0,0 +1,338 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Loot table</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="/static/favicon.ico" rel="shortcut icon">
<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 href="https://unpkg.com/bootstrap-table@1.19.1/dist/bootstrap-table.min.css" rel="stylesheet" type="text/css">
<link href="https://unpkg.com/jquery-resizable-columns@0.2.3/dist/jquery.resizableColumns.css" rel="stylesheet" type="text/css">
<link href="/static/styles.css" rel="stylesheet" type="text/css">
</head>
<body>
<div class="container">
<nav class="navbar navbar-expand-lg justify-content-between align-items-center border-bottom">
<a class="navbar-brand" id="navbar-title">Party</a>
<ul class="navbar-nav">
<a class="nav-item nav-link" id="navbar-bis">best in slot</a>
<a class="nav-item nav-link" id="navbar-loot">looted items</a>
</ul>
<ul class="navbar-nav">
<a class="nav-item nav-link" id="navbar-users">users</a>
</ul>
</nav>
</div>
<div id="alert-placeholder" class="container"></div>
<div class="container">
<h2>Looted items</h2>
</div>
<div class="container">
<div id="toolbar">
<button id="add-btn" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#add-loot-dialog" hidden>
<i class="bi bi-plus"></i> add
</button>
<button class="btn btn-secondary" onclick="reload()">
<i class="bi bi-arrow-clockwise"></i> reload
</button>
<button id="remove-btn" class="btn btn-danger" onclick="removeLoot()" disabled hidden>
<i class="bi bi-trash"></i> remove
</button>
</div>
<table id="loot" class="table table-striped table-hover"
data-click-to-select="true"
data-export-options='{"fileName": "loot"}'
data-page-list="[25, 50, 100, all]"
data-page-size="25"
data-pagination="true"
data-resizable="true"
data-search="true"
data-show-columns="true"
data-show-columns-search="true"
data-show-columns-toggle-all="true"
data-show-export="true"
data-show-fullscreen="true"
data-show-search-clear-button="true"
data-single-select="true"
data-sortable="true"
data-sort-reset="true"
data-toolbar="#toolbar">
<thead class="table-primary">
<tr>
<th data-checkbox="true"></th>
<th data-sortable="true" data-switchable="false" data-field="nick">nick</th>
<th data-sortable="true" data-switchable="false" data-field="job">job</th>
<th data-sortable="true" data-field="piece">piece</th>
<th data-sortable="true" data-field="pieceType">piece type</th>
<th data-sortable="true" data-field="isFreeLoot">is free loot</th>
<th data-sortable="true" data-field="timestamp">date</th>
</tr>
</thead>
</table>
</div>
<div id="add-loot-dialog" tabindex="-1" role="dialog" class="modal fade">
<div class="modal-dialog modal-lg" role="document">
<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>
<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"></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"></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"></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"></select>
</div>
</div>
<table id="stats" class="table table-striped table-hover">
<thead class="table-primary">
<tr>
<th data-field="nick">nick</th>
<th data-field="job">job</th>
<th data-field="isRequired">required</th>
<th data-field="lootCount">these pieces looted</th>
<th data-field="lootCountBiS">total bis pieces looted</th>
<th data-field="lootCountTotal">total pieces looted</th>
</tr>
</thead>
</table>
</form>
<div class="modal-footer">
<div class="form-check form-switch">
<input id="free-loot" name="freeLoot" type="checkbox" class="form-check-input">
<label class="form-check-label" for="free-loot">as free loot</label>
</div>
<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 id="submit-btn" type="button" class="btn btn-primary" data-bs-dismiss="modal" onclick="addLoot()" disabled>add</button>
</div>
</div>
</div>
</div>
<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="/" title="home">home</a></li>
</ul>
<ul class="nav">
<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>
</footer>
</div>
<script src="https://cdn.jsdelivr.net/npm/jquery/dist/jquery.min.js"></script>
<script src="https://unpkg.com/tableexport.jquery.plugin/tableExport.min.js"></script>
<script src="https://unpkg.com/jquery-resizable-columns@0.2.3/dist/jquery.resizableColumns.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p" crossorigin="anonymous"></script>
<script src="https://unpkg.com/bootstrap-table@1.19.1/dist/bootstrap-table.min.js"></script>
<script src="https://unpkg.com/bootstrap-table@1.19.1/dist/extensions/export/bootstrap-table-export.min.js"></script>
<script src="https://unpkg.com/bootstrap-table@1.19.1/dist/extensions/resizable/bootstrap-table-resizable.js"></script>
<script src="/static/utils.js"></script>
<script src="/static/load.js"></script>
<script>
const [partyId, isReadOnly] = getPartyId();
const table = $("#loot");
const stats = $("#stats");
const addButton = $("#add-btn");
const removeButton = $("#remove-btn");
const submitLootButton = $("#submit-btn");
const addLootDialog = $("#add-loot-dialog");
const freeLootInput = $("#free-loot");
const jobInput = $("#job");
const pieceInput = $("#piece");
const pieceTypeInput = $("#piece-type");
const playerInput = $("#player");
function addLoot() {
const player = getCurrentOption(playerInput);
$.ajax({
url: `/api/v1/party/${partyId}/loot`,
data: JSON.stringify({
action: "add",
piece: {
pieceType: pieceTypeInput.val(),
job: player.dataset.job,
piece: pieceInput.val(),
},
playerId: {
partyId: partyId,
nick: player.dataset.nick,
job: player.dataset.job,
},
isFreeLoot: freeLootInput.is(":checked"),
}),
type: "POST",
contentType: "application/json",
success: function (_) { reload(); },
error: function (jqXHR, _, errorThrown) { requestAlert(jqXHR, errorThrown); },
});
}
function hideControls() {
addButton.attr("hidden", isReadOnly);
removeButton.attr("hidden", isReadOnly);
}
function reload() {
table.bootstrapTable("showLoading");
$.ajax({
url: `/api/v1/party/${partyId}`,
type: "GET",
dataType: "json",
success: function (data) {
const items = data.map(function (player) {
return player.loot.map(function (loot) {
return {
nick: player.nick,
job: player.job,
piece: loot.piece.piece,
pieceType: loot.piece.pieceType,
isFreeLoot: loot.isFreeLoot ? "yes" : "no",
timestamp: loot.timestamp,
};
});
});
const payload = items.reduce(function (left, right) { return left.concat(right); }, []);
table.bootstrapTable("load", payload);
table.bootstrapTable("uncheckAll");
table.bootstrapTable("hideLoading");
const options = data.map(function (player) {
const option = document.createElement("option");
option.innerText = formatPlayerId(player);
option.dataset.nick = player.nick;
option.dataset.job = player.job;
return option;
});
playerInput.empty().append(options);
submitLootButton.attr("disabled", options.length === 0);
},
error: function (jqXHR, _, errorThrown) { requestAlert(jqXHR, errorThrown); },
});
}
function removeLoot() {
const pieces = table.bootstrapTable("getSelections");
pieces.map(function (loot) {
$.ajax({
url: `/api/v1/party/${partyId}/loot`,
data: JSON.stringify({
action: "remove",
piece: {
pieceType: loot.pieceType,
job: loot.job,
piece: loot.piece,
},
playerId: {
partyId: partyId,
nick: loot.nick,
job: loot.job,
},
isFreeLoot: loot.isFreeLoot === "yes",
}),
type: "POST",
contentType: "application/json",
success: function (_) { reload(); },
error: function (jqXHR, _, errorThrown) { requestAlert(jqXHR, errorThrown); },
});
});
}
function suggestLoot() {
stats.bootstrapTable("showLoading");
$.ajax({
url: `/api/v1/party/${partyId}/loot`,
data: JSON.stringify({
pieceType: pieceTypeInput.val(),
job: jobInput.val(),
piece: pieceInput.val(),
}),
type: "PUT",
contentType: "application/json",
dataType: "json",
success: function (data) {
const payload = data.map(function (stat) {
return {
nick: stat.nick,
job: stat.job,
isRequired: stat.isRequired,
lootCount: stat.lootCount,
lootCountBiS: stat.lootCountBiS,
lootCountTotal: stat.lootCountTotal,
};
});
stats.bootstrapTable("load", payload);
stats.bootstrapTable("uncheckAll");
stats.bootstrapTable("hideLoading");
},
error: function (jqXHR, _, errorThrown) { requestAlert(jqXHR, errorThrown); },
});
}
$(function () {
setupFormClear(addLootDialog);
setupRemoveButton(table, removeButton);
loadHeader(partyId);
loadTypes("/api/v1/types/jobs/all", jobInput);
loadTypes("/api/v1/types/pieces", pieceInput);
loadTypes("/api/v1/types/pieces/types", pieceTypeInput);
hideControls();
table.bootstrapTable({});
stats.bootstrapTable({});
reload();
});
</script>
</body>
</html>

View File

@ -0,0 +1,258 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>FFXIV loot helper</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="/static/favicon.ico" rel="shortcut icon">
<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 href="https://unpkg.com/bootstrap-table@1.19.1/dist/bootstrap-table.min.css" rel="stylesheet" type="text/css">
<link href="https://unpkg.com/jquery-resizable-columns@0.2.3/dist/jquery.resizableColumns.css" rel="stylesheet" type="text/css">
<link href="/static/styles.css" rel="stylesheet" type="text/css">
</head>
<body>
<div class="container">
<nav class="navbar navbar-expand-lg justify-content-between align-items-center border-bottom">
<a class="navbar-brand" id="navbar-title">Party</a>
<ul class="navbar-nav">
<a class="nav-item nav-link" id="navbar-bis">best in slot</a>
<a class="nav-item nav-link" id="navbar-loot">looted items</a>
</ul>
<ul class="navbar-nav">
<a class="nav-item nav-link" id="navbar-users">users</a>
</ul>
</nav>
</div>
<div id="alert-placeholder" class="container"></div>
<div class="container">
<div id="toolbar">
<button id="add-btn" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#add-player-dialog" hidden>
<i class="bi bi-plus"></i> add
</button>
<button class="btn btn-secondary" onclick="reload()">
<i class="bi bi-arrow-clockwise"></i> reload
</button>
<button id="remove-btn" class="btn btn-danger" onclick="removePlayers()" disabled hidden>
<i class="bi bi-trash"></i> remove
</button>
</div>
<table id="players" class="table table-striped table-hover"
data-click-to-select="true"
data-export-options='{"fileName": "players"}'
data-page-list="[25, 50, 100, all]"
data-page-size="25"
data-pagination="true"
data-resizable="true"
data-search="true"
data-show-columns="true"
data-show-columns-search="true"
data-show-columns-toggle-all="true"
data-show-export="true"
data-show-fullscreen="true"
data-show-search-clear-button="true"
data-single-select="true"
data-sortable="true"
data-sort-reset="true"
data-toolbar="#toolbar">
<thead class="table-primary">
<tr>
<th data-checkbox="true"></th>
<th data-sortable="true" data-switchable="false" data-field="nick">nick</th>
<th data-sortable="true" data-field="job">job</th>
<th data-sortable="true" data-field="link" data-formatter="bisLinkFormatter">best in slot link</th>
<th data-sortable="true" data-field="lootCountBiS">total bis pieces looted</th>
<th data-sortable="true" data-field="lootCountTotal">total pieces looted</th>
<th data-sortable="true" data-field="priority">priority</th>
</tr>
</thead>
</table>
</div>
<div id="add-player-dialog" tabindex="-1" role="dialog" class="modal fade">
<div class="modal-dialog" role="document">
<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>
<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" 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"></select>
</div>
</div>
<div class="form-group row">
<label class="col-sm-4 col-form-label" for="link">link to best in slot</label>
<div class="col-sm-8">
<input id="link" name="link" class="form-control" placeholder="link to bis">
</div>
</div>
<div class="form-group row">
<label class="col-sm-4 col-form-label" for="priority">priority</label>
<div class="col-sm-8">
<input id="priority" name="priority" type="number" class="form-control" value="0">
</div>
</div>
</form>
<div class="modal-footer">
<button type="button" class="btn btn-danger" data-bs-dismiss="modal">close</button>
<button id="submit-player-btn" type="button" class="btn btn-primary" data-bs-dismiss="modal" onclick="addPlayer()" disabled>add</button>
</div>
</div>
</div>
</div>
<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="/" title="home">home</a></li>
</ul>
<ul class="nav">
<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>
</footer>
</div>
<script src="https://cdn.jsdelivr.net/npm/jquery/dist/jquery.min.js"></script>
<script src="https://unpkg.com/tableexport.jquery.plugin/tableExport.min.js"></script>
<script src="https://unpkg.com/jquery-resizable-columns@0.2.3/dist/jquery.resizableColumns.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p" crossorigin="anonymous"></script>
<script src="https://unpkg.com/bootstrap-table@1.19.1/dist/bootstrap-table.min.js"></script>
<script src="https://unpkg.com/bootstrap-table@1.19.1/dist/extensions/export/bootstrap-table-export.min.js"></script>
<script src="https://unpkg.com/bootstrap-table@1.19.1/dist/extensions/resizable/bootstrap-table-resizable.js"></script>
<script src="/static/utils.js"></script>
<script src="/static/load.js"></script>
<script>
const [partyId, isReadOnly] = getPartyId();
const table = $("#players");
const addButton = $("#add-btn");
const removeButton = $("#remove-btn");
const addPlayerDialog = $("#add-player-dialog");
const submitPlayerButton = $("#submit-player-btn");
const jobInput = $("#job");
const linkInput = $("#link");
const nickInput = $("#nick");
const priorityInput = $("#priority");
function addPlayer() {
$.ajax({
url: `/api/v1/party/${partyId}`,
data: JSON.stringify({
action: "add",
playerId: {
partyId: partyId,
job: jobInput.val(),
nick: nickInput.val(),
link: linkInput.val() || null,
priority: parseInt(priorityInput.val(), 10),
},
}),
type: "POST",
contentType: "application/json",
success: function (_) { reload(); },
error: function (jqXHR, _, errorThrown) { requestAlert(jqXHR, errorThrown); },
});
}
function bisLinkFormatter(link, row) {
if (link) {
return `<a href="${safe(link)}" title="${safe(row.nick)} best in slot for ${safe(row.job)}">${safe(link)}</a>`;
} else {
return "-";
}
}
function disableAddPlayerForm() {
submitPlayerButton.attr("disabled", !nickInput.val());
}
function hideControls() {
addButton.attr("hidden", isReadOnly);
removeButton.attr("hidden", isReadOnly);
}
function reload() {
table.bootstrapTable("showLoading");
$.ajax({
url: `/api/v1/party/${partyId}`,
type: "GET",
dataType: "json",
success: function (data) {
table.bootstrapTable("load", data);
table.bootstrapTable("uncheckAll");
table.bootstrapTable("hideLoading");
},
error: function (jqXHR, _, errorThrown) { requestAlert(jqXHR, errorThrown); },
});
}
function removePlayers() {
const players = table.bootstrapTable("getSelections");
players.map(function (player) {
$.ajax({
url: `/api/v1/party/${partyId}`,
data: JSON.stringify({
action: "remove",
playerId: {
partyId: partyId,
job: player.job,
nick: player.nick,
},
}),
type: "POST",
contentType: "application/json",
success: function (_) { reload(); },
error: function (jqXHR, _, errorThrown) { requestAlert(jqXHR, errorThrown); },
});
});
}
$(function () {
setupFormClear(addPlayerDialog);
setupRemoveButton(table, removeButton);
loadHeader(partyId);
loadTypes("/api/v1/types/jobs", jobInput);
hideControls();
table.bootstrapTable({});
reload();
});
</script>
</body>
</html>

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

@ -1,24 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>FFXIV loot tracker API</title>
<!-- needed for adaptive design -->
<meta charset="utf-8"/>
<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">
<!--
ReDoc doesn't change outer page styles
-->
<style>
body {
margin: 0;
padding: 0;
}
</style>
</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

@ -0,0 +1,230 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>User management</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="/static/favicon.ico" rel="shortcut icon">
<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 href="https://unpkg.com/bootstrap-table@1.19.1/dist/bootstrap-table.min.css" rel="stylesheet" type="text/css">
<link href="https://unpkg.com/jquery-resizable-columns@0.2.3/dist/jquery.resizableColumns.css" rel="stylesheet" type="text/css">
<link href="/static/styles.css" rel="stylesheet" type="text/css">
</head>
<body>
<div class="container">
<nav class="navbar navbar-expand-lg justify-content-between align-items-center border-bottom">
<a class="navbar-brand" id="navbar-title">Party</a>
<ul class="navbar-nav">
<a class="nav-item nav-link" id="navbar-bis">best in slot</a>
<a class="nav-item nav-link" id="navbar-loot">looted items</a>
</ul>
<ul class="navbar-nav">
<a class="nav-item nav-link" id="navbar-users">users</a>
</ul>
</nav>
</div>
<div id="alert-placeholder" class="container"></div>
<div class="container">
<h2>Users</h2>
</div>
<div class="container">
<div id="toolbar">
<button id="add-btn" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#add-user-dialog" hidden>
<i class="bi bi-plus"></i> add
</button>
<button class="btn btn-secondary" onclick="reload()">
<i class="bi bi-arrow-clockwise"></i> reload
</button>
<button id="remove-btn" class="btn btn-danger" onclick="removeUsers()" disabled hidden>
<i class="bi bi-trash"></i> remove
</button>
</div>
<table id="users" class="table table-striped table-hover"
data-click-to-select="true"
data-export-options='{"fileName": "users"}'
data-page-list="[25, 50, 100, all]"
data-page-size="25"
data-pagination="true"
data-resizable="true"
data-search="true"
data-show-columns="true"
data-show-columns-search="true"
data-show-columns-toggle-all="true"
data-show-export="true"
data-show-fullscreen="true"
data-show-search-clear-button="true"
data-single-select="true"
data-sortable="true"
data-sort-reset="true"
data-toolbar="#toolbar">
<thead class="table-primary">
<tr>
<th data-checkbox="true"></th>
<th data-sortable="true" data-switchable="false" data-field="username">username</th>
<th data-sortable="true" data-field="permission">permission</th>
</tr>
</thead>
</table>
</div>
<div id="add-user-dialog" tabindex="-1" role="dialog" class="modal fade">
<div class="modal-dialog" role="document">
<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>
<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" 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" 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"></select>
</div>
</div>
</form>
<div class="modal-footer">
<button type="button" class="btn btn-danger" data-bs-dismiss="modal">close</button>
<button id="submit-btn" type="button" class="btn btn-primary" data-bs-dismiss="modal" onclick="addUser()" disabled>add</button>
</div>
</div>
</div>
</div>
<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="/" title="home">home</a></li>
</ul>
<ul class="nav">
<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>
</footer>
</div>
<script src="https://cdn.jsdelivr.net/npm/jquery/dist/jquery.min.js"></script>
<script src="https://unpkg.com/tableexport.jquery.plugin/tableExport.min.js"></script>
<script src="https://unpkg.com/jquery-resizable-columns@0.2.3/dist/jquery.resizableColumns.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p" crossorigin="anonymous"></script>
<script src="https://unpkg.com/bootstrap-table@1.19.1/dist/bootstrap-table.min.js"></script>
<script src="https://unpkg.com/bootstrap-table@1.19.1/dist/extensions/export/bootstrap-table-export.min.js"></script>
<script src="https://unpkg.com/bootstrap-table@1.19.1/dist/extensions/resizable/bootstrap-table-resizable.js"></script>
<script src="/static/utils.js"></script>
<script src="/static/load.js"></script>
<script>
const [partyId, isReadOnly] = getPartyId();
const table = $("#users");
const addButton = $("#add-btn");
const removeButton = $("#remove-btn");
const addUserDialog = $("#add-user-dialog");
const submitUserButton = $("#submit-btn");
const usernameInput = $("#username");
const passwordInput = $("#password");
const permissionInput = $("#permission");
function addUser() {
$.ajax({
url: `/api/v1/party/${partyId}/users`,
data: JSON.stringify({
partyId: partyId,
username: usernameInput.val(),
password: passwordInput.val(),
permission: permissionInput.val(),
}),
type: "POST",
contentType: "application/json",
success: function (_) { reload(); },
error: function (jqXHR, _, errorThrown) { requestAlert(jqXHR, errorThrown); },
});
}
function disableAddUserForm() {
submitUserButton.attr("disabled", !(usernameInput.val() && passwordInput.val()));
}
function hideControls() {
addButton.attr("hidden", isReadOnly);
removeButton.attr("hidden", isReadOnly);
}
function reload() {
table.bootstrapTable("showLoading");
$.ajax({
url: `/api/v1/party/${partyId}/users`,
type: "GET",
dataType: "json",
success: function (data) {
table.bootstrapTable("load", data);
table.bootstrapTable("uncheckAll");
table.bootstrapTable("hideLoading");
},
error: function (jqXHR, _, errorThrown) { requestAlert(jqXHR, errorThrown); },
});
}
function removeUsers() {
const users = table.bootstrapTable("getSelections");
users.map(function (user) {
$.ajax({
url: `/api/v1/party/${partyId}/users/${user.username}`,
type: "DELETE",
success: function (_) { reload(); },
error: function (jqXHR, _, errorThrown) { requestAlert(jqXHR, errorThrown); },
});
});
}
$(function () {
setupFormClear(addUserDialog);
setupRemoveButton(table, removeButton);
loadHeader(partyId);
loadTypes("/api/v1/types/permissions", permissionInput);
hideControls();
table.bootstrapTable({});
reload();
});
</script>
</body>
</html>

View File

@ -18,8 +18,8 @@ me.arcanis.ffxivbis {
profile = "slick.jdbc.SQLiteProfile$" profile = "slick.jdbc.SQLiteProfile$"
db { db {
url = "jdbc:sqlite:ffxivbis.db" url = "jdbc:sqlite:ffxivbis.db"
user = "user" #user = "user"
password = "password" #password = "password"
} }
numThreads = 10 numThreads = 10
} }
@ -28,8 +28,8 @@ me.arcanis.ffxivbis {
profile = "slick.jdbc.PostgresProfile$" profile = "slick.jdbc.PostgresProfile$"
db { db {
url = "jdbc:postgresql://localhost/ffxivbis" url = "jdbc:postgresql://localhost/ffxivbis"
user = "ffxivbis" #user = "ffxivbis"
password = "ffxivbis" #password = "ffxivbis"
connectionPool = disabled connectionPool = disabled
keepAliveConnection = yes keepAliveConnection = yes
@ -57,6 +57,15 @@ me.arcanis.ffxivbis {
port = 8000 port = 8000
# hostname to use in docs, if not set host:port will be used # hostname to use in docs, if not set host:port will be used
#hostname = "127.0.0.1:8000" #hostname = "127.0.0.1:8000"
# enable head requests for GET requests
enable-head-requests = yes
authorization-cache {
# maximum amount of cached logins
cache-size = 1024
# ttl of cached logins
cache-timeout = 1m
}
} }
default-dispatcher { default-dispatcher {

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

View File

@ -0,0 +1,54 @@
function loadHeader(partyId) {
const title = $("#navbar-title");
// because I don't know how to handle relative url if current does not end with slash
title.attr("href", `/party/${partyId}`);
$("#navbar-bis").attr("href", `/party/${partyId}/bis`);
$("#navbar-loot").attr("href", `/party/${partyId}/loot`);
$("#navbar-users").attr("href", `/party/${partyId}/users`);
$.ajax({
url: `/api/v1/party/${partyId}/description`,
type: "GET",
dataType: "json",
success: function (resp) {
title.text(safe(resp.partyAlias || partyId));
},
error: function (jqXHR, _, errorThrown) { requestAlert(jqXHR, errorThrown); },
});
}
function loadTypes(url, selector) {
$.ajax({
url: url,
type: "GET",
dataType: "json",
success: function (data) {
const options = data.map(function (name) {
const option = document.createElement("option");
option.value = name;
option.innerText = name;
return option;
});
selector.empty().append(options);
},
error: function (jqXHR, _, errorThrown) { requestAlert(jqXHR, errorThrown); },
});
}
function setupFormClear(dialog, reset) {
dialog.on("shown.bs.modal", function () {
$(this).find("form").trigger("reset");
$(this).find("table").bootstrapTable("removeAll");
if (reset) {
reset();
}
});
}
function setupRemoveButton(table, removeButton) {
table.on("check.bs.table uncheck.bs.table check-all.bs.table uncheck-all.bs.table",
function () {
removeButton.prop("disabled", !table.bootstrapTable("getSelections").length);
});
}

View File

@ -1,277 +0,0 @@
/* in-text images */
figure.img {
float: right;
border: 0px solid #333;
padding: 0px;
margin: 5px 0px 5px 10px;
}
figure.img img {
max-width: 100%;
height: auto;
}
figure.img figcaption {
margin: 0px;
font-size: 90%;
font-style: italic;
text-align: center;
}
h1 .octicon-link, h2 .octicon-link, h3 .octicon-link, h4 .octicon-link, h5 .octicon-link, h6 .octicon-link {
display: none;
color: #222222;
vertical-align: middle;
}
h1:hover .anchor, h2:hover .anchor, h3:hover .anchor, h4:hover .anchor, h5:hover .anchor, h6:hover .anchor{
padding-left: 8px;
margin-left: -24px;
text-decoration: none;
}
h1:hover .anchor .octicon-link, h2:hover .anchor .octicon-link, h3:hover .anchor .octicon-link, h4:hover .anchor .octicon-link, h5:hover .anchor .octicon-link, h6:hover .anchor .octicon-link {
display: inline-block;
}
body {
padding: 50px;
font: 14px/1.5 "Liberation Sans", Helvetica, Arial, sans-serif;
color: #555555;
background: #eaeaea
}
h1, h2, h3, h4, h5, h6 {
color: #222222;
margin: 0 0 20px;
}
p, ul, ol, table, pre, dl {
margin: 0 0 20px;
text-align: justify;
}
h1, h2, h3 {
line-height: 1.1;
}
h1 {
font-size: 28px;
}
h2 {
color: #393939;
}
h3, h4, h5, h6 {
color: #494949;
}
a {
color: #3399cc;
font-weight: 350;
text-decoration: none;
}
a small {
font-size: 11px;
color: #777777;
margin-top: -0.6em;
display: block;
}
.wrapper {
width: 80%;
margin: 0 auto;
}
blockquote {
border-left: 1px solid #ffffff;
margin: 0;
padding: 0 0 0 20px;
font-style: italic;
}
code, pre {
font-family: "Liberation Mono", Monaco, Bitstream Vera Sans Mono, Lucida Console, Terminal;
color: #222222;
font-size: 12px;
}
pre {
padding: 8px 15px;
border-radius: 5px;
border: 1px solid #e5e5e5;
overflow-x: auto;
overflow-y: auto;
}
input, select{
box-sizing: border-box;
}
table {
width: 100%;
border-collapse: collapse;
}
th, td {
padding: 5px 10px;
border-bottom: 1px solid #ffffff;
}
td {
text-align: justify;
}
dt {
color: #444444;
font-weight: 700;
}
th {
text-align: left;
color: #444444;
}
img {
max-width: 100%;
}
header {
width: 20%;
float: left;
position: fixed;
}
header ul {
list-style: none;
height: 40px;
padding: 0;
background: #eeeeee;
border-radius: 5px;
border: 1px solid #d2d2d2;
box-shadow: inset #fff 0 1px 0, inset rgba(0,0,0,0.03) 0 -1px 0;
width: 15%;
}
header li {
width: 8%;
float: left;
border-right: 1px solid #d2d2d2;
height: 40px;
}
header ul a {
line-height: 1;
font-size: 11px;
color: #999999;
display: block;
text-align: center;
padding-top: 6px;
height: 40px;
}
strong {
color: #222222;
font-weight: 700;
}
header ul li + li {
width: 8%;
border-left: 1px solid #ffffff;
}
header ul li + li + li {
width: 8%;
border-right: none;
}
header ul a strong {
font-size: 14px;
display: block;
color: #222222;
}
section {
width: 70%;
float: right;
padding-bottom: 50px;
}
small {
font-size: 11px;
}
hr {
border: 0;
background: #ffffff;
height: 1px;
margin: 0 0 20px;
}
footer {
width: 20%;
float: left;
position: fixed;
bottom: 50px;
}
@media print, screen and (max-width: 960px) {
div.wrapper {
width: auto;
margin: 0;
}
header, section, footer {
float: none;
position: static;
width: auto;
}
header {
padding-right: 320px;
}
section {
border: 1px solid #e5e5e5;
border-width: 1px 0;
padding: 20px 0;
margin: 0 0 20px;
}
header a small {
display: inline;
}
header ul {
position: absolute;
right: 50px;
top: 52px;
}
}
@media print, screen and (max-width: 720px) {
body {
word-wrap: break-word;
}
header {
padding: 0;
}
header ul, header p.view {
position: static;
}
pre, code {
word-wrap: normal;
}
}
@media print, screen and (max-width: 480px) {
body {
padding: 15px;
}
header ul {
display: none;
}
}
@media print {
body {
padding: 0.4in;
font-size: 12pt;
color: #444444;
}
}

View File

@ -1,31 +0,0 @@
function downloadCsv(csv, filename) {
var csvFile = new Blob([csv], {"type": "text/csv"});
var downloadLink = document.createElement("a");
downloadLink.download = filename;
downloadLink.href = window.URL.createObjectURL(csvFile);
downloadLink.style.display = "none";
document.body.appendChild(downloadLink);
downloadLink.click();
}
function exportTableToCsv(filename) {
var table = document.getElementById("result");
var rows = table.getElementsByTagName("tr");
var csv = [];
for (var i = 0; i < rows.length; i++) {
if (rows[i].style.display === "none")
continue;
var cols = rows[i].querySelectorAll("td, th");
var row = [];
for (var j = 0; j < cols.length; j++)
row.push(cols[j].innerText);
csv.push(row.join(","));
}
downloadCsv(csv.join("\n"), filename);
}

View File

@ -1,21 +0,0 @@
function searchTable() {
var input = document.getElementById("search");
var filter = input.value.toLowerCase();
var table = document.getElementById("result");
var tr = table.getElementsByTagName("tr");
// from 1 coz of header
for (var i = 1; i < tr.length; i++) {
var td = tr[i].getElementsByClassName("include_search");
var display = "none";
for (var j = 0; j < td.length; j++) {
if (td[j].tagName.toLowerCase() === "td") {
if (td[j].innerHTML.toLowerCase().indexOf(filter) > -1) {
display = "";
break;
}
}
}
tr[i].style.display = display;
}
}

View File

@ -0,0 +1,44 @@
function createAlert(message, placeholder) {
const wrapper = document.createElement('div');
wrapper.innerHTML = `<div class="alert alert-danger alert-dismissible" role="alert">${safe(message)}<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button></div>`;
placeholder.append(wrapper);
}
function formatPlayerId(obj) {
return `${obj.nick} (${obj.job})`;
}
function getCurrentOption(select) {
return select.find(":selected")[0];
}
function getPartyId() {
const request = new XMLHttpRequest();
request.open("HEAD", document.location, false);
request.send(null);
// tuple lol
return [
request.getResponseHeader("X-Party-Id"),
request.getResponseHeader("X-User-Permission") === "get",
]
}
function requestAlert(jqXHR, errorThrown) {
let message;
try {
message = $.parseJSON(jqXHR.responseText).message;
} catch (_) {
message = errorThrown;
}
const alert = $("#alert-placeholder");
createAlert(`Error during request: ${message}`, alert);
}
function safe(string) {
return String(string)
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;");
}

View File

@ -11,6 +11,8 @@ REST json API description to interact with FFXIVBiS service.
# Limitations # Limitations
No limitations for the API so far.
# Authentication # Authentication
For the most party utils service requires user to be authenticated. User permission can be one of `get`, `post` or `admin`. For the most party utils service requires user to be authenticated. User permission can be one of `get`, `post` or `admin`.

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2019 Evgeniy Alekseev. * Copyright (c) 2019-2022 Evgeniy Alekseev.
* *
* This file is part of ffxivbis * This file is part of ffxivbis
* (see https://github.com/arcan1s/ffxivbis). * (see https://github.com/arcan1s/ffxivbis).
@ -8,16 +8,16 @@
*/ */
package me.arcanis.ffxivbis package me.arcanis.ffxivbis
import akka.actor.typed.{Behavior, PostStop, Signal}
import akka.actor.typed.scaladsl.{AbstractBehavior, ActorContext, Behaviors} import akka.actor.typed.scaladsl.{AbstractBehavior, ActorContext, Behaviors}
import akka.actor.typed.{Behavior, PostStop, Signal}
import akka.http.scaladsl.Http import akka.http.scaladsl.Http
import akka.http.scaladsl.server.Route import akka.http.scaladsl.server.Route
import akka.stream.Materializer import akka.stream.Materializer
import com.typesafe.scalalogging.StrictLogging import com.typesafe.scalalogging.StrictLogging
import me.arcanis.ffxivbis.http.RootEndpoint import me.arcanis.ffxivbis.http.RootEndpoint
import me.arcanis.ffxivbis.service.PartyService
import me.arcanis.ffxivbis.service.bis.BisProvider import me.arcanis.ffxivbis.service.bis.BisProvider
import me.arcanis.ffxivbis.service.database.Database import me.arcanis.ffxivbis.service.database.Database
import me.arcanis.ffxivbis.service.PartyService
import me.arcanis.ffxivbis.storage.Migration import me.arcanis.ffxivbis.storage.Migration
import scala.concurrent.ExecutionContext import scala.concurrent.ExecutionContext

View File

@ -0,0 +1,23 @@
/*
* 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
import com.typesafe.config.{Config, ConfigFactory, ConfigValueFactory}
object Configuration {
def load(): Config = {
val root = ConfigFactory.load()
root
.withValue(
"akka.http.server.transparent-head-requests",
ConfigValueFactory.fromAnyRef(root.getBoolean("me.arcanis.ffxivbis.web.enable-head-requests"))
)
}
}

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2019 Evgeniy Alekseev. * Copyright (c) 2019-2022 Evgeniy Alekseev.
* *
* This file is part of ffxivbis * This file is part of ffxivbis
* (see https://github.com/arcan1s/ffxivbis). * (see https://github.com/arcan1s/ffxivbis).
@ -9,12 +9,9 @@
package me.arcanis.ffxivbis package me.arcanis.ffxivbis
import akka.actor.typed.ActorSystem import akka.actor.typed.ActorSystem
import com.typesafe.config.ConfigFactory
object ffxivbis { object ffxivbis {
def main(args: Array[String]): Unit = { def main(args: Array[String]): Unit =
val config = ConfigFactory.load() ActorSystem[Nothing](Application(), "ffxivbis", Configuration.load())
ActorSystem[Nothing](Application(), "ffxivbis", config)
}
} }

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2019 Evgeniy Alekseev. * Copyright (c) 2019-2022 Evgeniy Alekseev.
* *
* This file is part of ffxivbis * This file is part of ffxivbis
* (see https://github.com/arcan1s/ffxivbis). * (see https://github.com/arcan1s/ffxivbis).
@ -8,22 +8,18 @@
*/ */
package me.arcanis.ffxivbis.http package me.arcanis.ffxivbis.http
import akka.actor.typed.scaladsl.AskPattern.Askable
import akka.actor.typed.{ActorRef, Scheduler}
import akka.http.scaladsl.model.headers._ import akka.http.scaladsl.model.headers._
import akka.http.scaladsl.server.AuthenticationFailedRejection._ import akka.http.scaladsl.server.AuthenticationFailedRejection._
import akka.http.scaladsl.server.Directives._ import akka.http.scaladsl.server.Directives._
import akka.http.scaladsl.server._ import akka.http.scaladsl.server._
import akka.util.Timeout import me.arcanis.ffxivbis.models.{Permission, User}
import me.arcanis.ffxivbis.messages.{GetUser, Message}
import me.arcanis.ffxivbis.models.Permission
import scala.concurrent.{ExecutionContext, Future} import scala.concurrent.{ExecutionContext, Future}
// idea comes from https://synkre.com/bcrypt-for-akka-http-password-encryption/ // idea comes from https://synkre.com/bcrypt-for-akka-http-password-encryption/
trait Authorization { trait Authorization {
def storage: ActorRef[Message] def auth: AuthorizationProvider
def authenticateBasicBCrypt[T](realm: String, authenticate: (String, String) => Future[Option[T]]): Directive1[T] = { def authenticateBasicBCrypt[T](realm: String, authenticate: (String, String) => Future[Option[T]]): Directive1[T] = {
def challenge = HttpChallenges.basic(realm) def challenge = HttpChallenges.basic(realm)
@ -38,34 +34,26 @@ trait Authorization {
} }
} }
def authenticator(scope: Permission.Value, partyId: String)(username: String, password: String)(implicit
executionContext: ExecutionContext,
timeout: Timeout,
scheduler: Scheduler
): Future[Option[String]] =
storage.ask(GetUser(partyId, username, _)).map {
case Some(user) if user.verify(password) && user.verityScope(scope) => Some(username)
case _ => None
}
def authAdmin(partyId: String)(username: String, password: String)(implicit def authAdmin(partyId: String)(username: String, password: String)(implicit
executionContext: ExecutionContext, executionContext: ExecutionContext
timeout: Timeout, ): Future[Option[User]] =
scheduler: Scheduler
): Future[Option[String]] =
authenticator(Permission.admin, partyId)(username, password) authenticator(Permission.admin, partyId)(username, password)
def authGet(partyId: String)(username: String, password: String)(implicit def authGet(partyId: String)(username: String, password: String)(implicit
executionContext: ExecutionContext, executionContext: ExecutionContext
timeout: Timeout, ): Future[Option[User]] =
scheduler: Scheduler
): Future[Option[String]] =
authenticator(Permission.get, partyId)(username, password) authenticator(Permission.get, partyId)(username, password)
def authPost(partyId: String)(username: String, password: String)(implicit def authPost(partyId: String)(username: String, password: String)(implicit
executionContext: ExecutionContext, executionContext: ExecutionContext
timeout: Timeout, ): Future[Option[User]] =
scheduler: Scheduler
): Future[Option[String]] =
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

@ -0,0 +1,51 @@
/*
* 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.http
import akka.actor.typed.scaladsl.AskPattern.Askable
import akka.actor.typed.{ActorRef, Scheduler}
import akka.util.Timeout
import com.google.common.cache.{CacheBuilder, CacheLoader, LoadingCache}
import com.typesafe.config.Config
import me.arcanis.ffxivbis.messages.{GetUser, Message}
import me.arcanis.ffxivbis.models.User
import java.util.concurrent.TimeUnit
import scala.concurrent.Future
trait AuthorizationProvider {
def get(partyId: String, username: String): Future[Option[User]]
}
object 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 =
config.getDuration("me.arcanis.ffxivbis.web.authorization-cache.cache-timeout", TimeUnit.MILLISECONDS)
private val cache: LoadingCache[(String, String), Future[Option[User]]] = CacheBuilder
.newBuilder()
.expireAfterWrite(cacheTimeout, TimeUnit.MILLISECONDS)
.maximumSize(cacheSize)
.build(
new CacheLoader[(String, String), Future[Option[User]]] {
override def load(key: (String, String)): Future[Option[User]] = {
val (partyId, username) = key
storage.ask(GetUser(partyId, username, _))(timeout, scheduler)
}
}
)
override def get(partyId: String, username: String): Future[Option[User]] =
cache.get((partyId, username))
}
}

View File

@ -0,0 +1,74 @@
/*
* 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.http
import akka.http.scaladsl.model.headers.{`User-Agent`, Authorization, BasicHttpCredentials, Referer}
import akka.http.scaladsl.server.Directive0
import akka.http.scaladsl.server.Directives.{extractClientIP, extractRequestContext, mapResponse, optionalHeaderValueByType}
import com.typesafe.scalalogging.Logger
import java.time.{Instant, ZoneId}
import java.time.format.DateTimeFormatter
import java.util.Locale
trait HttpLog {
private val httpLogger = Logger("http")
def withHttpLog: Directive0 =
extractRequestContext.flatMap { context =>
val request = s"${context.request.method.name()} ${context.request.uri.path}"
extractClientIP.flatMap { maybeRemoteAddr =>
val remoteAddr = maybeRemoteAddr.toIP.getOrElse("-")
optionalHeaderValueByType(Referer).flatMap { maybeReferer =>
val referer = maybeReferer.map(_.uri).getOrElse("-")
optionalHeaderValueByType(`User-Agent`).flatMap { maybeUserAgent =>
val userAgent = maybeUserAgent.map(_.products.map(_.toString()).mkString(" ")).getOrElse("-")
optionalHeaderValueByType(Authorization).flatMap { maybeAuth =>
val remoteUser = maybeAuth
.map(_.credentials)
.collect { case BasicHttpCredentials(username, _) =>
username
}
.getOrElse("-")
val start = Instant.now.toEpochMilli
val timeLocal = HttpLog.httpLogDatetimeFormatter.format(Instant.now)
mapResponse { response =>
val time = (Instant.now.toEpochMilli - start) / 1000.0
val status = response.status.intValue()
val bytesSent = response.entity.getContentLengthOption.getAsLong
httpLogger.debug(
s"""$remoteAddr - $remoteUser [$timeLocal] "$request" $status $bytesSent "$referer" "$userAgent" $time"""
)
response
}
}
}
}
}
}
}
object HttpLog {
val httpLogDatetimeFormatter: DateTimeFormatter =
DateTimeFormatter
.ofPattern("dd/MMM/uuuu:HH:mm:ss xx ")
.withLocale(Locale.UK)
.withZone(ZoneId.systemDefault())
}

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2019 Evgeniy Alekseev. * Copyright (c) 2019-2022 Evgeniy Alekseev.
* *
* This file is part of ffxivbis * This file is part of ffxivbis
* (see https://github.com/arcan1s/ffxivbis). * (see https://github.com/arcan1s/ffxivbis).
@ -8,44 +8,30 @@
*/ */
package me.arcanis.ffxivbis.http package me.arcanis.ffxivbis.http
import java.time.Instant
import akka.actor.typed.{ActorRef, ActorSystem, Scheduler} import akka.actor.typed.{ActorRef, ActorSystem, Scheduler}
import akka.http.scaladsl.server.Directives._ import akka.http.scaladsl.server.Directives._
import akka.http.scaladsl.server._ import akka.http.scaladsl.server._
import akka.util.Timeout import akka.util.Timeout
import com.typesafe.scalalogging.{Logger, StrictLogging} import com.typesafe.scalalogging.StrictLogging
import me.arcanis.ffxivbis.http.api.v1.RootApiV1Endpoint import me.arcanis.ffxivbis.http.api.v1.RootApiV1Endpoint
import me.arcanis.ffxivbis.http.view.RootView import me.arcanis.ffxivbis.http.view.RootView
import me.arcanis.ffxivbis.messages.{BiSProviderMessage, Message} import me.arcanis.ffxivbis.messages.{BiSProviderMessage, Message}
class RootEndpoint(system: ActorSystem[Nothing], storage: ActorRef[Message], provider: ActorRef[BiSProviderMessage]) class RootEndpoint(system: ActorSystem[Nothing], storage: ActorRef[Message], provider: ActorRef[BiSProviderMessage])
extends StrictLogging { extends StrictLogging
with HttpLog {
import me.arcanis.ffxivbis.utils.Implicits._ import me.arcanis.ffxivbis.utils.Implicits._
private val config = system.settings.config private val config = system.settings.config
implicit val scheduler: Scheduler = system.scheduler implicit val scheduler: Scheduler = system.scheduler
implicit val timeout: Timeout = implicit val timeout: Timeout = config.getDuration("me.arcanis.ffxivbis.settings.request-timeout")
config.getDuration("me.arcanis.ffxivbis.settings.request-timeout")
private val rootApiV1Endpoint: RootApiV1Endpoint = new RootApiV1Endpoint(storage, provider, config) private val auth = AuthorizationProvider(config, storage, timeout, scheduler)
private val rootView: RootView = new RootView(storage, provider)
private val swagger: Swagger = new Swagger(config)
private val httpLogger = Logger("http")
private val withHttpLog: Directive0 = private val rootApiV1Endpoint = new RootApiV1Endpoint(storage, auth, provider, config)
extractRequestContext.flatMap { context => private val rootView = new RootView(auth)
val start = Instant.now.toEpochMilli private val swagger = new Swagger(config)
mapResponse { response =>
val time = (Instant.now.toEpochMilli - start) / 1000.0
httpLogger.debug(
s"""- - [${Instant.now}] "${context.request.method.name()} ${context.request.uri.path}" ${response.status
.intValue()} ${response.entity.getContentLengthOption.getAsLong} $time"""
)
response
}
}
def route: Route = def route: Route =
withHttpLog { withHttpLog {
@ -68,7 +54,7 @@ class RootEndpoint(system: ActorSystem[Nothing], storage: ActorRef[Message], pro
} ~ rootView.route } ~ rootView.route
private def swaggerUIRoute: Route = private def swaggerUIRoute: Route =
path("swagger") { path("api-docs") {
getFromResource("html/swagger.html") getFromResource("html/redoc.html")
} }
} }

View File

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

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2019 Evgeniy Alekseev. * Copyright (c) 2019-2022 Evgeniy Alekseev.
* *
* This file is part of ffxivbis * This file is part of ffxivbis
* (see https://github.com/arcan1s/ffxivbis). * (see https://github.com/arcan1s/ffxivbis).
@ -21,14 +21,19 @@ import io.swagger.v3.oas.annotations.security.SecurityRequirement
import io.swagger.v3.oas.annotations.{Operation, Parameter} import io.swagger.v3.oas.annotations.{Operation, Parameter}
import jakarta.ws.rs._ import jakarta.ws.rs._
import me.arcanis.ffxivbis.http.api.v1.json._ import me.arcanis.ffxivbis.http.api.v1.json._
import me.arcanis.ffxivbis.http.{Authorization, BiSHelper} import me.arcanis.ffxivbis.http.helpers.BiSHelper
import me.arcanis.ffxivbis.http.{Authorization, AuthorizationProvider}
import me.arcanis.ffxivbis.messages.{BiSProviderMessage, Message} import me.arcanis.ffxivbis.messages.{BiSProviderMessage, Message}
import me.arcanis.ffxivbis.models.PlayerId import me.arcanis.ffxivbis.models.PlayerId
import scala.util.{Failure, Success} import scala.util.{Failure, Success}
@Path("/api/v1") @Path("/api/v1")
class BiSEndpoint(override val storage: ActorRef[Message], override val provider: ActorRef[BiSProviderMessage])(implicit class BiSEndpoint(
override val storage: ActorRef[Message],
override val provider: ActorRef[BiSProviderMessage],
override val auth: AuthorizationProvider
)(implicit
timeout: Timeout, timeout: Timeout,
scheduler: Scheduler scheduler: Scheduler
) extends BiSHelper ) extends BiSHelper
@ -49,29 +54,29 @@ class BiSEndpoint(override val storage: ActorRef[Message], override val provider
requestBody = new RequestBody( requestBody = new RequestBody(
description = "player best in slot description", description = "player best in slot description",
required = true, required = true,
content = Array(new Content(schema = new Schema(implementation = classOf[PlayerBiSLinkResponse]))) content = Array(new Content(schema = new Schema(implementation = classOf[PlayerBiSLinkModel])))
), ),
responses = Array( responses = Array(
new ApiResponse(responseCode = "201", description = "Best in slot set has been created"), new ApiResponse(responseCode = "201", description = "Best in slot set has been created"),
new ApiResponse( new ApiResponse(
responseCode = "400", responseCode = "400",
description = "Invalid parameters were supplied", description = "Invalid parameters were supplied",
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse]))) content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel])))
), ),
new ApiResponse( new ApiResponse(
responseCode = "401", responseCode = "401",
description = "Supplied authorization is invalid", description = "Supplied authorization is invalid",
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse]))) content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel])))
), ),
new ApiResponse( new ApiResponse(
responseCode = "403", responseCode = "403",
description = "Access is forbidden", description = "Access is forbidden",
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse]))) content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel])))
), ),
new ApiResponse( new ApiResponse(
responseCode = "500", responseCode = "500",
description = "Internal server error", description = "Internal server error",
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse]))) content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel])))
), ),
), ),
security = Array(new SecurityRequirement(name = "basic auth", scopes = Array("post"))), security = Array(new SecurityRequirement(name = "basic auth", scopes = Array("post"))),
@ -82,11 +87,10 @@ class BiSEndpoint(override val storage: ActorRef[Message], override val provider
extractExecutionContext { implicit executionContext => extractExecutionContext { implicit executionContext =>
authenticateBasicBCrypt(s"party $partyId", authPost(partyId)) { _ => authenticateBasicBCrypt(s"party $partyId", authPost(partyId)) { _ =>
put { put {
entity(as[PlayerBiSLinkResponse]) { bisLink => entity(as[PlayerBiSLinkModel]) { bisLink =>
val playerId = bisLink.playerId.withPartyId(partyId) val playerId = bisLink.playerId.withPartyId(partyId)
onComplete(putBiS(playerId, bisLink.link)) { onSuccess(putBiS(playerId, bisLink.link)) {
case Success(_) => complete(StatusCodes.Created, HttpEntity.Empty) complete(StatusCodes.Created, HttpEntity.Empty)
case Failure(exception) => throw exception
} }
} }
} }
@ -116,24 +120,24 @@ class BiSEndpoint(override val storage: ActorRef[Message], override val provider
description = "Best in slot", description = "Best in slot",
content = Array( content = Array(
new Content( new Content(
array = new ArraySchema(schema = new Schema(implementation = classOf[PlayerResponse])) array = new ArraySchema(schema = new Schema(implementation = classOf[PlayerModel]))
) )
) )
), ),
new ApiResponse( new ApiResponse(
responseCode = "401", responseCode = "401",
description = "Supplied authorization is invalid", description = "Supplied authorization is invalid",
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse]))) content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel])))
), ),
new ApiResponse( new ApiResponse(
responseCode = "403", responseCode = "403",
description = "Access is forbidden", description = "Access is forbidden",
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse]))) content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel])))
), ),
new ApiResponse( new ApiResponse(
responseCode = "500", responseCode = "500",
description = "Internal server error", description = "Internal server error",
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse]))) content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel])))
), ),
), ),
security = Array(new SecurityRequirement(name = "basic auth", scopes = Array("get"))), security = Array(new SecurityRequirement(name = "basic auth", scopes = Array("get"))),
@ -146,9 +150,8 @@ class BiSEndpoint(override val storage: ActorRef[Message], override val provider
get { get {
parameters("nick".as[String].?, "job".as[String].?) { (maybeNick, maybeJob) => parameters("nick".as[String].?, "job".as[String].?) { (maybeNick, maybeJob) =>
val playerId = PlayerId(partyId, maybeNick, maybeJob) val playerId = PlayerId(partyId, maybeNick, maybeJob)
onComplete(bis(partyId, playerId)) { onSuccess(bis(partyId, playerId)) { response =>
case Success(response) => complete(response.map(PlayerResponse.fromPlayer)) complete(response.map(PlayerModel.fromPlayer))
case Failure(exception) => throw exception
} }
} }
} }
@ -169,29 +172,29 @@ class BiSEndpoint(override val storage: ActorRef[Message], override val provider
requestBody = new RequestBody( requestBody = new RequestBody(
description = "action and piece description", description = "action and piece description",
required = true, required = true,
content = Array(new Content(schema = new Schema(implementation = classOf[PieceActionResponse]))) content = Array(new Content(schema = new Schema(implementation = classOf[PieceActionModel])))
), ),
responses = Array( responses = Array(
new ApiResponse(responseCode = "202", description = "Best in slot set has been modified"), new ApiResponse(responseCode = "202", description = "Best in slot set has been modified"),
new ApiResponse( new ApiResponse(
responseCode = "400", responseCode = "400",
description = "Invalid parameters were supplied", description = "Invalid parameters were supplied",
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse]))) content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel])))
), ),
new ApiResponse( new ApiResponse(
responseCode = "401", responseCode = "401",
description = "Supplied authorization is invalid", description = "Supplied authorization is invalid",
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse]))) content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel])))
), ),
new ApiResponse( new ApiResponse(
responseCode = "403", responseCode = "403",
description = "Access is forbidden", description = "Access is forbidden",
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse]))) content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel])))
), ),
new ApiResponse( new ApiResponse(
responseCode = "500", responseCode = "500",
description = "Internal server error", description = "Internal server error",
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse]))) content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel])))
), ),
), ),
security = Array(new SecurityRequirement(name = "basic auth", scopes = Array("post"))), security = Array(new SecurityRequirement(name = "basic auth", scopes = Array("post"))),
@ -202,11 +205,10 @@ class BiSEndpoint(override val storage: ActorRef[Message], override val provider
extractExecutionContext { implicit executionContext => extractExecutionContext { implicit executionContext =>
authenticateBasicBCrypt(s"party $partyId", authPost(partyId)) { _ => authenticateBasicBCrypt(s"party $partyId", authPost(partyId)) { _ =>
post { post {
entity(as[PieceActionResponse]) { action => entity(as[PieceActionModel]) { action =>
val playerId = action.playerId.withPartyId(partyId) val playerId = action.playerId.withPartyId(partyId)
onComplete(doModifyBiS(action.action, playerId, action.piece.toPiece)) { onSuccess(doModifyBiS(action.action, playerId, action.piece.toPiece)) {
case Success(_) => complete(StatusCodes.Accepted, HttpEntity.Empty) complete(StatusCodes.Accepted, HttpEntity.Empty)
case Failure(exception) => throw exception
} }
} }
} }

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2019 Evgeniy Alekseev. * Copyright (c) 2019-2022 Evgeniy Alekseev.
* *
* This file is part of ffxivbis * This file is part of ffxivbis
* (see https://github.com/arcan1s/ffxivbis). * (see https://github.com/arcan1s/ffxivbis).
@ -19,18 +19,18 @@ trait HttpHandler extends StrictLogging { this: JsonSupport =>
def exceptionHandler: ExceptionHandler = ExceptionHandler { def exceptionHandler: ExceptionHandler = ExceptionHandler {
case ex: IllegalArgumentException => case ex: IllegalArgumentException =>
complete(StatusCodes.BadRequest, ErrorResponse(ex.getMessage)) complete(StatusCodes.BadRequest, ErrorModel(ex.getMessage))
case other: Exception => case other: Exception =>
logger.error("exception during request completion", other) logger.error("exception during request completion", other)
complete(StatusCodes.InternalServerError, ErrorResponse("unknown server error")) complete(StatusCodes.InternalServerError, ErrorModel("unknown server error"))
} }
def rejectionHandler: RejectionHandler = def rejectionHandler: RejectionHandler =
RejectionHandler.default RejectionHandler.default
.mapRejectionResponse { .mapRejectionResponse {
case response @ HttpResponse(_, _, entity: HttpEntity.Strict, _) => case response @ HttpResponse(_, _, entity: HttpEntity.Strict, _) =>
val message = ErrorResponse(entity.data.utf8String).toJson val message = ErrorModel(entity.data.utf8String).toJson
response.withEntity(HttpEntity(ContentTypes.`application/json`, message.compactPrint)) response.withEntity(HttpEntity(ContentTypes.`application/json`, message.compactPrint))
case other => other case other => other
} }

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2019 Evgeniy Alekseev. * Copyright (c) 2019-2022 Evgeniy Alekseev.
* *
* This file is part of ffxivbis * This file is part of ffxivbis
* (see https://github.com/arcan1s/ffxivbis). * (see https://github.com/arcan1s/ffxivbis).
@ -21,20 +21,23 @@ import io.swagger.v3.oas.annotations.security.SecurityRequirement
import io.swagger.v3.oas.annotations.{Operation, Parameter} import io.swagger.v3.oas.annotations.{Operation, Parameter}
import jakarta.ws.rs._ import jakarta.ws.rs._
import me.arcanis.ffxivbis.http.api.v1.json._ import me.arcanis.ffxivbis.http.api.v1.json._
import me.arcanis.ffxivbis.http.{Authorization, LootHelper} import me.arcanis.ffxivbis.http.helpers.LootHelper
import me.arcanis.ffxivbis.http.{Authorization, AuthorizationProvider}
import me.arcanis.ffxivbis.messages.Message import me.arcanis.ffxivbis.messages.Message
import me.arcanis.ffxivbis.models.PlayerId import me.arcanis.ffxivbis.models.PlayerId
import scala.util.{Failure, Success} import scala.util.{Failure, Success}
@Path("/api/v1") @Path("/api/v1")
class LootEndpoint(override val storage: ActorRef[Message])(implicit timeout: Timeout, scheduler: Scheduler) class LootEndpoint(override val storage: ActorRef[Message], override val auth: AuthorizationProvider)(implicit
extends LootHelper timeout: Timeout,
scheduler: Scheduler
) extends LootHelper
with Authorization with Authorization
with JsonSupport with JsonSupport
with HttpHandler { with HttpHandler {
def route: Route = getLoot ~ modifyLoot def route: Route = getLoot ~ modifyLoot ~ suggestLoot
@GET @GET
@Path("party/{partyId}/loot") @Path("party/{partyId}/loot")
@ -58,24 +61,24 @@ class LootEndpoint(override val storage: ActorRef[Message])(implicit timeout: Ti
description = "Loot list", description = "Loot list",
content = Array( content = Array(
new Content( new Content(
array = new ArraySchema(schema = new Schema(implementation = classOf[PlayerResponse])) array = new ArraySchema(schema = new Schema(implementation = classOf[PlayerModel]))
) )
) )
), ),
new ApiResponse( new ApiResponse(
responseCode = "401", responseCode = "401",
description = "Supplied authorization is invalid", description = "Supplied authorization is invalid",
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse]))) content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel])))
), ),
new ApiResponse( new ApiResponse(
responseCode = "403", responseCode = "403",
description = "Access is forbidden", description = "Access is forbidden",
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse]))) content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel])))
), ),
new ApiResponse( new ApiResponse(
responseCode = "500", responseCode = "500",
description = "Internal server error", description = "Internal server error",
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse]))) content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel])))
), ),
), ),
security = Array(new SecurityRequirement(name = "basic auth", scopes = Array("get"))), security = Array(new SecurityRequirement(name = "basic auth", scopes = Array("get"))),
@ -88,9 +91,8 @@ class LootEndpoint(override val storage: ActorRef[Message])(implicit timeout: Ti
get { get {
parameters("nick".as[String].?, "job".as[String].?) { (maybeNick, maybeJob) => parameters("nick".as[String].?, "job".as[String].?) { (maybeNick, maybeJob) =>
val playerId = PlayerId(partyId, maybeNick, maybeJob) val playerId = PlayerId(partyId, maybeNick, maybeJob)
onComplete(loot(partyId, playerId)) { onSuccess(loot(partyId, playerId)) { response =>
case Success(response) => complete(response.map(PlayerResponse.fromPlayer)) complete(response.map(PlayerModel.fromPlayer))
case Failure(exception) => throw exception
} }
} }
} }
@ -110,29 +112,29 @@ class LootEndpoint(override val storage: ActorRef[Message])(implicit timeout: Ti
requestBody = new RequestBody( requestBody = new RequestBody(
description = "action and piece description", description = "action and piece description",
required = true, required = true,
content = Array(new Content(schema = new Schema(implementation = classOf[PieceActionResponse]))) content = Array(new Content(schema = new Schema(implementation = classOf[PieceActionModel])))
), ),
responses = Array( responses = Array(
new ApiResponse(responseCode = "202", description = "Loot list has been modified"), new ApiResponse(responseCode = "202", description = "Loot list has been modified"),
new ApiResponse( new ApiResponse(
responseCode = "400", responseCode = "400",
description = "Invalid parameters were supplied", description = "Invalid parameters were supplied",
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse]))) content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel])))
), ),
new ApiResponse( new ApiResponse(
responseCode = "401", responseCode = "401",
description = "Supplied authorization is invalid", description = "Supplied authorization is invalid",
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse]))) content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel])))
), ),
new ApiResponse( new ApiResponse(
responseCode = "403", responseCode = "403",
description = "Access is forbidden", description = "Access is forbidden",
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse]))) content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel])))
), ),
new ApiResponse( new ApiResponse(
responseCode = "500", responseCode = "500",
description = "Internal server error", description = "Internal server error",
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse]))) content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel])))
), ),
), ),
security = Array(new SecurityRequirement(name = "basic auth", scopes = Array("post"))), security = Array(new SecurityRequirement(name = "basic auth", scopes = Array("post"))),
@ -143,11 +145,10 @@ class LootEndpoint(override val storage: ActorRef[Message])(implicit timeout: Ti
extractExecutionContext { implicit executionContext => extractExecutionContext { implicit executionContext =>
authenticateBasicBCrypt(s"party $partyId", authPost(partyId)) { _ => authenticateBasicBCrypt(s"party $partyId", authPost(partyId)) { _ =>
post { post {
entity(as[PieceActionResponse]) { action => entity(as[PieceActionModel]) { action =>
val playerId = action.playerId.withPartyId(partyId) val playerId = action.playerId.withPartyId(partyId)
onComplete(doModifyLoot(action.action, playerId, action.piece.toPiece, action.isFreeLoot)) { onSuccess(doModifyLoot(action.action, playerId, action.piece.toPiece, action.isFreeLoot)) {
case Success(_) => complete(StatusCodes.Accepted, HttpEntity.Empty) complete(StatusCodes.Accepted, HttpEntity.Empty)
case Failure(exception) => throw exception
} }
} }
} }
@ -168,7 +169,7 @@ class LootEndpoint(override val storage: ActorRef[Message])(implicit timeout: Ti
requestBody = new RequestBody( requestBody = new RequestBody(
description = "piece description", description = "piece description",
required = true, required = true,
content = Array(new Content(schema = new Schema(implementation = classOf[PieceResponse]))) content = Array(new Content(schema = new Schema(implementation = classOf[PieceModel])))
), ),
responses = Array( responses = Array(
new ApiResponse( new ApiResponse(
@ -176,29 +177,29 @@ class LootEndpoint(override val storage: ActorRef[Message])(implicit timeout: Ti
description = "Players with counters ordered by priority to get this item", description = "Players with counters ordered by priority to get this item",
content = Array( content = Array(
new Content( new Content(
array = new ArraySchema(schema = new Schema(implementation = classOf[PlayerIdWithCountersResponse])), array = new ArraySchema(schema = new Schema(implementation = classOf[PlayerIdWithCountersModel])),
) )
) )
), ),
new ApiResponse( new ApiResponse(
responseCode = "400", responseCode = "400",
description = "Invalid parameters were supplied", description = "Invalid parameters were supplied",
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse]))) content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel])))
), ),
new ApiResponse( new ApiResponse(
responseCode = "401", responseCode = "401",
description = "Supplied authorization is invalid", description = "Supplied authorization is invalid",
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse]))) content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel])))
), ),
new ApiResponse( new ApiResponse(
responseCode = "403", responseCode = "403",
description = "Access is forbidden", description = "Access is forbidden",
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse]))) content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel])))
), ),
new ApiResponse( new ApiResponse(
responseCode = "500", responseCode = "500",
description = "Internal server error", description = "Internal server error",
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse]))) content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel])))
), ),
), ),
security = Array(new SecurityRequirement(name = "basic auth", scopes = Array("get"))), security = Array(new SecurityRequirement(name = "basic auth", scopes = Array("get"))),
@ -209,10 +210,9 @@ class LootEndpoint(override val storage: ActorRef[Message])(implicit timeout: Ti
extractExecutionContext { implicit executionContext => extractExecutionContext { implicit executionContext =>
authenticateBasicBCrypt(s"party $partyId", authGet(partyId)) { _ => authenticateBasicBCrypt(s"party $partyId", authGet(partyId)) { _ =>
put { put {
entity(as[PieceResponse]) { piece => entity(as[PieceModel]) { piece =>
onComplete(suggestPiece(partyId, piece.toPiece)) { onSuccess(suggestPiece(partyId, piece.toPiece)) { response =>
case Success(response) => complete(response.map(PlayerIdWithCountersResponse.fromPlayerId)) complete(response.map(PlayerIdWithCountersModel.fromPlayerId))
case Failure(exception) => throw exception
} }
} }
} }

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2020 Evgeniy Alekseev. * Copyright (c) 2019-2022 Evgeniy Alekseev.
* *
* This file is part of ffxivbis * This file is part of ffxivbis
* (see https://github.com/arcan1s/ffxivbis). * (see https://github.com/arcan1s/ffxivbis).
@ -21,14 +21,18 @@ import io.swagger.v3.oas.annotations.security.SecurityRequirement
import io.swagger.v3.oas.annotations.{Operation, Parameter} import io.swagger.v3.oas.annotations.{Operation, Parameter}
import jakarta.ws.rs._ import jakarta.ws.rs._
import me.arcanis.ffxivbis.http.api.v1.json._ import me.arcanis.ffxivbis.http.api.v1.json._
import me.arcanis.ffxivbis.http.{Authorization, PlayerHelper} import me.arcanis.ffxivbis.http.helpers.PlayerHelper
import me.arcanis.ffxivbis.http.{Authorization, AuthorizationProvider}
import me.arcanis.ffxivbis.messages.{BiSProviderMessage, Message} import me.arcanis.ffxivbis.messages.{BiSProviderMessage, Message}
import scala.util.{Failure, Success} import scala.util.{Failure, Success}
@Path("/api/v1") @Path("/api/v1")
class PartyEndpoint(override val storage: ActorRef[Message], override val provider: ActorRef[BiSProviderMessage])( class PartyEndpoint(
implicit override val storage: ActorRef[Message],
override val provider: ActorRef[BiSProviderMessage],
override val auth: AuthorizationProvider
)(implicit
timeout: Timeout, timeout: Timeout,
scheduler: Scheduler scheduler: Scheduler
) extends PlayerHelper ) extends PlayerHelper
@ -51,22 +55,22 @@ class PartyEndpoint(override val storage: ActorRef[Message], override val provid
new ApiResponse( new ApiResponse(
responseCode = "200", responseCode = "200",
description = "Party description", description = "Party description",
content = Array(new Content(schema = new Schema(implementation = classOf[PartyDescriptionResponse]))) content = Array(new Content(schema = new Schema(implementation = classOf[PartyDescriptionModel])))
), ),
new ApiResponse( new ApiResponse(
responseCode = "401", responseCode = "401",
description = "Supplied authorization is invalid", description = "Supplied authorization is invalid",
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse]))) content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel])))
), ),
new ApiResponse( new ApiResponse(
responseCode = "403", responseCode = "403",
description = "Access is forbidden", description = "Access is forbidden",
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse]))) content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel])))
), ),
new ApiResponse( new ApiResponse(
responseCode = "500", responseCode = "500",
description = "Internal server error", description = "Internal server error",
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse]))) content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel])))
), ),
), ),
security = Array(new SecurityRequirement(name = "basic auth", scopes = Array("get"))), security = Array(new SecurityRequirement(name = "basic auth", scopes = Array("get"))),
@ -77,9 +81,8 @@ class PartyEndpoint(override val storage: ActorRef[Message], override val provid
extractExecutionContext { implicit executionContext => extractExecutionContext { implicit executionContext =>
authenticateBasicBCrypt(s"party $partyId", authGet(partyId)) { _ => authenticateBasicBCrypt(s"party $partyId", authGet(partyId)) { _ =>
get { get {
onComplete(getPartyDescription(partyId)) { onSuccess(getPartyDescription(partyId)) { response =>
case Success(response) => complete(PartyDescriptionResponse.fromDescription(response)) complete(PartyDescriptionModel.fromDescription(response))
case Failure(exception) => throw exception
} }
} }
} }
@ -98,29 +101,29 @@ class PartyEndpoint(override val storage: ActorRef[Message], override val provid
requestBody = new RequestBody( requestBody = new RequestBody(
description = "new party description", description = "new party description",
required = true, required = true,
content = Array(new Content(schema = new Schema(implementation = classOf[PartyDescriptionResponse]))) content = Array(new Content(schema = new Schema(implementation = classOf[PartyDescriptionModel])))
), ),
responses = Array( responses = Array(
new ApiResponse(responseCode = "202", description = "Party description has been modified"), new ApiResponse(responseCode = "202", description = "Party description has been modified"),
new ApiResponse( new ApiResponse(
responseCode = "400", responseCode = "400",
description = "Invalid parameters were supplied", description = "Invalid parameters were supplied",
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse]))) content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel])))
), ),
new ApiResponse( new ApiResponse(
responseCode = "401", responseCode = "401",
description = "Supplied authorization is invalid", description = "Supplied authorization is invalid",
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse]))) content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel])))
), ),
new ApiResponse( new ApiResponse(
responseCode = "403", responseCode = "403",
description = "Access is forbidden", description = "Access is forbidden",
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse]))) content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel])))
), ),
new ApiResponse( new ApiResponse(
responseCode = "500", responseCode = "500",
description = "Internal server error", description = "Internal server error",
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse]))) content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel])))
), ),
), ),
security = Array(new SecurityRequirement(name = "basic auth", scopes = Array("post"))), security = Array(new SecurityRequirement(name = "basic auth", scopes = Array("post"))),
@ -131,11 +134,10 @@ class PartyEndpoint(override val storage: ActorRef[Message], override val provid
extractExecutionContext { implicit executionContext => extractExecutionContext { implicit executionContext =>
authenticateBasicBCrypt(s"party $partyId", authPost(partyId)) { _ => authenticateBasicBCrypt(s"party $partyId", authPost(partyId)) { _ =>
post { post {
entity(as[PartyDescriptionResponse]) { partyDescription => entity(as[PartyDescriptionModel]) { partyDescription =>
val description = partyDescription.copy(partyId = partyId) val description = partyDescription.copy(partyId = partyId)
onComplete(updateDescription(description.toDescription)) { onSuccess(updateDescription(description.toDescription)) {
case Success(_) => complete(StatusCodes.Accepted, HttpEntity.Empty) complete(StatusCodes.Accepted, HttpEntity.Empty)
case Failure(exception) => throw exception
} }
} }
} }

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2019 Evgeniy Alekseev. * Copyright (c) 2019-2022 Evgeniy Alekseev.
* *
* This file is part of ffxivbis * This file is part of ffxivbis
* (see https://github.com/arcan1s/ffxivbis). * (see https://github.com/arcan1s/ffxivbis).
@ -21,15 +21,19 @@ import io.swagger.v3.oas.annotations.security.SecurityRequirement
import io.swagger.v3.oas.annotations.{Operation, Parameter} import io.swagger.v3.oas.annotations.{Operation, Parameter}
import jakarta.ws.rs._ import jakarta.ws.rs._
import me.arcanis.ffxivbis.http.api.v1.json._ import me.arcanis.ffxivbis.http.api.v1.json._
import me.arcanis.ffxivbis.http.{Authorization, PlayerHelper} import me.arcanis.ffxivbis.http.helpers.PlayerHelper
import me.arcanis.ffxivbis.http.{Authorization, AuthorizationProvider}
import me.arcanis.ffxivbis.messages.{BiSProviderMessage, Message} import me.arcanis.ffxivbis.messages.{BiSProviderMessage, Message}
import me.arcanis.ffxivbis.models.PlayerId import me.arcanis.ffxivbis.models.PlayerId
import scala.util.{Failure, Success} import scala.util.{Failure, Success}
@Path("/api/v1") @Path("/api/v1")
class PlayerEndpoint(override val storage: ActorRef[Message], override val provider: ActorRef[BiSProviderMessage])( class PlayerEndpoint(
implicit override val storage: ActorRef[Message],
override val provider: ActorRef[BiSProviderMessage],
override val auth: AuthorizationProvider
)(implicit
timeout: Timeout, timeout: Timeout,
scheduler: Scheduler scheduler: Scheduler
) extends PlayerHelper ) extends PlayerHelper
@ -37,7 +41,7 @@ class PlayerEndpoint(override val storage: ActorRef[Message], override val provi
with JsonSupport with JsonSupport
with HttpHandler { with HttpHandler {
def route: Route = getParty ~ modifyParty def route: Route = getParty ~ getPartyStats ~ modifyParty
@GET @GET
@Path("party/{partyId}") @Path("party/{partyId}")
@ -61,24 +65,24 @@ class PlayerEndpoint(override val storage: ActorRef[Message], override val provi
description = "Players list", description = "Players list",
content = Array( content = Array(
new Content( new Content(
array = new ArraySchema(schema = new Schema(implementation = classOf[PlayerResponse])), array = new ArraySchema(schema = new Schema(implementation = classOf[PlayerModel])),
) )
) )
), ),
new ApiResponse( new ApiResponse(
responseCode = "401", responseCode = "401",
description = "Supplied authorization is invalid", description = "Supplied authorization is invalid",
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse]))) content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel])))
), ),
new ApiResponse( new ApiResponse(
responseCode = "403", responseCode = "403",
description = "Access is forbidden", description = "Access is forbidden",
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse]))) content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel])))
), ),
new ApiResponse( new ApiResponse(
responseCode = "500", responseCode = "500",
description = "Internal server error", description = "Internal server error",
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse]))) content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel])))
), ),
), ),
security = Array(new SecurityRequirement(name = "basic auth", scopes = Array("get"))), security = Array(new SecurityRequirement(name = "basic auth", scopes = Array("get"))),
@ -91,9 +95,69 @@ class PlayerEndpoint(override val storage: ActorRef[Message], override val provi
get { get {
parameters("nick".as[String].?, "job".as[String].?) { (maybeNick, maybeJob) => parameters("nick".as[String].?, "job".as[String].?) { (maybeNick, maybeJob) =>
val playerId = PlayerId(partyId, maybeNick, maybeJob) val playerId = PlayerId(partyId, maybeNick, maybeJob)
onComplete(getPlayers(partyId, playerId)) { onSuccess(getPlayers(partyId, playerId)) { response =>
case Success(response) => complete(response.map(PlayerResponse.fromPlayer)) complete(response.map(PlayerModel.fromPlayer))
case Failure(exception) => throw exception }
}
}
}
}
}
@GET
@Path("party/{partyId}/stats")
@Produces(value = Array("application/json"))
@Operation(
summary = "get party statistics",
description = "Return the party statistics",
parameters = Array(
new Parameter(name = "partyId", in = ParameterIn.PATH, description = "unique party ID", example = "abcdefgh"),
new Parameter(
name = "nick",
in = ParameterIn.QUERY,
description = "player nick name to filter",
example = "Siuan Sanche"
),
new Parameter(name = "job", in = ParameterIn.QUERY, description = "player job to filter", example = "DNC"),
),
responses = Array(
new ApiResponse(
responseCode = "200",
description = "Party loot statistics",
content = Array(
new Content(
array = new ArraySchema(schema = new Schema(implementation = classOf[PlayerIdWithCountersModel])),
)
)
),
new ApiResponse(
responseCode = "401",
description = "Supplied authorization is invalid",
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel])))
),
new ApiResponse(
responseCode = "403",
description = "Access is forbidden",
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel])))
),
new ApiResponse(
responseCode = "500",
description = "Internal server error",
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel])))
),
),
security = Array(new SecurityRequirement(name = "basic auth", scopes = Array("get"))),
tags = Array("party"),
)
def getPartyStats: Route =
path("party" / Segment / "stats") { partyId =>
extractExecutionContext { implicit executionContext =>
authenticateBasicBCrypt(s"party $partyId", authGet(partyId)) { _ =>
get {
parameters("nick".as[String].?, "job".as[String].?) { (maybeNick, maybeJob) =>
val playerId = PlayerId(partyId, maybeNick, maybeJob)
onSuccess(getPlayers(partyId, playerId)) { response =>
complete(response.map(player => PlayerIdWithCountersModel.fromPlayerId(player.withCounters(None))))
} }
} }
} }
@ -113,29 +177,29 @@ class PlayerEndpoint(override val storage: ActorRef[Message], override val provi
requestBody = new RequestBody( requestBody = new RequestBody(
description = "player description", description = "player description",
required = true, required = true,
content = Array(new Content(schema = new Schema(implementation = classOf[PlayerActionResponse]))) content = Array(new Content(schema = new Schema(implementation = classOf[PlayerActionModel])))
), ),
responses = Array( responses = Array(
new ApiResponse(responseCode = "202", description = "Party has been modified"), new ApiResponse(responseCode = "202", description = "Party has been modified"),
new ApiResponse( new ApiResponse(
responseCode = "400", responseCode = "400",
description = "Invalid parameters were supplied", description = "Invalid parameters were supplied",
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse]))) content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel])))
), ),
new ApiResponse( new ApiResponse(
responseCode = "401", responseCode = "401",
description = "Supplied authorization is invalid", description = "Supplied authorization is invalid",
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse]))) content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel])))
), ),
new ApiResponse( new ApiResponse(
responseCode = "403", responseCode = "403",
description = "Access is forbidden", description = "Access is forbidden",
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse]))) content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel])))
), ),
new ApiResponse( new ApiResponse(
responseCode = "500", responseCode = "500",
description = "Internal server error", description = "Internal server error",
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse]))) content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel])))
), ),
), ),
security = Array(new SecurityRequirement(name = "basic auth", scopes = Array("post"))), security = Array(new SecurityRequirement(name = "basic auth", scopes = Array("post"))),
@ -145,11 +209,12 @@ class PlayerEndpoint(override val storage: ActorRef[Message], override val provi
path("party" / Segment) { partyId => path("party" / Segment) { partyId =>
extractExecutionContext { implicit executionContext => extractExecutionContext { implicit executionContext =>
authenticateBasicBCrypt(s"party $partyId", authPost(partyId)) { _ => authenticateBasicBCrypt(s"party $partyId", authPost(partyId)) { _ =>
entity(as[PlayerActionResponse]) { action => post {
entity(as[PlayerActionModel]) { action =>
val player = action.playerId.toPlayer.copy(partyId = partyId) val player = action.playerId.toPlayer.copy(partyId = partyId)
onComplete(doModifyPlayer(action.action, player)) { onSuccess(doModifyPlayer(action.action, player)) {
case Success(_) => complete(StatusCodes.Accepted, HttpEntity.Empty) complete(StatusCodes.Accepted, HttpEntity.Empty)
case Failure(exception) => throw exception }
} }
} }
} }

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2019 Evgeniy Alekseev. * Copyright (c) 2019-2022 Evgeniy Alekseev.
* *
* This file is part of ffxivbis * This file is part of ffxivbis
* (see https://github.com/arcan1s/ffxivbis). * (see https://github.com/arcan1s/ffxivbis).
@ -13,21 +13,27 @@ import akka.http.scaladsl.server.Directives._
import akka.http.scaladsl.server.Route import akka.http.scaladsl.server.Route
import akka.util.Timeout import akka.util.Timeout
import com.typesafe.config.Config import com.typesafe.config.Config
import me.arcanis.ffxivbis.http.AuthorizationProvider
import me.arcanis.ffxivbis.http.api.v1.json.JsonSupport import me.arcanis.ffxivbis.http.api.v1.json.JsonSupport
import me.arcanis.ffxivbis.messages.{BiSProviderMessage, Message} import me.arcanis.ffxivbis.messages.{BiSProviderMessage, Message}
class RootApiV1Endpoint(storage: ActorRef[Message], provider: ActorRef[BiSProviderMessage], config: Config)(implicit class RootApiV1Endpoint(
storage: ActorRef[Message],
auth: AuthorizationProvider,
provider: ActorRef[BiSProviderMessage],
config: Config
)(implicit
timeout: Timeout, timeout: Timeout,
scheduler: Scheduler scheduler: Scheduler
) extends JsonSupport ) extends JsonSupport
with HttpHandler { with HttpHandler {
private val biSEndpoint = new BiSEndpoint(storage, provider) private val biSEndpoint = new BiSEndpoint(storage, provider, auth)
private val lootEndpoint = new LootEndpoint(storage) private val lootEndpoint = new LootEndpoint(storage, auth)
private val partyEndpoint = new PartyEndpoint(storage, provider) private val partyEndpoint = new PartyEndpoint(storage, provider, auth)
private val playerEndpoint = new PlayerEndpoint(storage, provider) private val playerEndpoint = new PlayerEndpoint(storage, provider, auth)
private val typesEndpoint = new TypesEndpoint(config) private val typesEndpoint = new TypesEndpoint(config)
private val userEndpoint = new UserEndpoint(storage) private val userEndpoint = new UserEndpoint(storage, auth)
def route: Route = def route: Route =
handleExceptions(exceptionHandler) { handleExceptions(exceptionHandler) {

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2019 Evgeniy Alekseev. * Copyright (c) 2019-2022 Evgeniy Alekseev.
* *
* This file is part of ffxivbis * This file is part of ffxivbis
* (see https://github.com/arcan1s/ffxivbis). * (see https://github.com/arcan1s/ffxivbis).
@ -11,17 +11,48 @@ package me.arcanis.ffxivbis.http.api.v1
import akka.http.scaladsl.server.Directives._ import akka.http.scaladsl.server.Directives._
import akka.http.scaladsl.server._ import akka.http.scaladsl.server._
import com.typesafe.config.Config import com.typesafe.config.Config
import io.swagger.v3.oas.annotations.Operation
import io.swagger.v3.oas.annotations.media.{ArraySchema, Content, Schema} import io.swagger.v3.oas.annotations.media.{ArraySchema, Content, Schema}
import io.swagger.v3.oas.annotations.responses.ApiResponse import io.swagger.v3.oas.annotations.responses.ApiResponse
import io.swagger.v3.oas.annotations.Operation
import jakarta.ws.rs._ import jakarta.ws.rs._
import me.arcanis.ffxivbis.http.api.v1.json._ import me.arcanis.ffxivbis.http.api.v1.json._
import me.arcanis.ffxivbis.models.{Job, Party, Permission, Piece, PieceType} import me.arcanis.ffxivbis.models._
@Path("/api/v1") @Path("/api/v1")
class TypesEndpoint(config: Config) extends JsonSupport { class TypesEndpoint(config: Config) extends JsonSupport {
def route: Route = getJobs ~ getPermissions ~ getPieces ~ getPieceTypes ~ getPriority def route: Route = getAllJobs ~ getJobs ~ getPermissions ~ getPieces ~ getPieceTypes ~ getPriority
@GET
@Path("types/jobs/all")
@Produces(value = Array("application/json"))
@Operation(
summary = "full jobs list",
description = "Returns the available jobs including any job",
responses = Array(
new ApiResponse(
responseCode = "200",
description = "List of available jobs with AnyJob",
content = Array(
new Content(
array = new ArraySchema(schema = new Schema(implementation = classOf[String]))
)
)
),
new ApiResponse(
responseCode = "500",
description = "Internal server error",
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel])))
),
),
tags = Array("types"),
)
def getAllJobs: Route =
path("types" / "jobs" / "all") {
get {
complete(Job.availableWithAnyJob.map(_.toString))
}
}
@GET @GET
@Path("types/jobs") @Path("types/jobs")
@ -42,7 +73,7 @@ class TypesEndpoint(config: Config) extends JsonSupport {
new ApiResponse( new ApiResponse(
responseCode = "500", responseCode = "500",
description = "Internal server error", description = "Internal server error",
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse]))) content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel])))
), ),
), ),
tags = Array("types"), tags = Array("types"),
@ -50,7 +81,7 @@ class TypesEndpoint(config: Config) extends JsonSupport {
def getJobs: Route = def getJobs: Route =
path("types" / "jobs") { path("types" / "jobs") {
get { get {
complete(Job.availableWithAnyJob.map(_.toString)) complete(Job.available.map(_.toString))
} }
} }
@ -73,7 +104,7 @@ class TypesEndpoint(config: Config) extends JsonSupport {
new ApiResponse( new ApiResponse(
responseCode = "500", responseCode = "500",
description = "Internal server error", description = "Internal server error",
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse]))) content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel])))
), ),
), ),
tags = Array("types"), tags = Array("types"),
@ -104,7 +135,7 @@ class TypesEndpoint(config: Config) extends JsonSupport {
new ApiResponse( new ApiResponse(
responseCode = "500", responseCode = "500",
description = "Internal server error", description = "Internal server error",
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse]))) content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel])))
), ),
), ),
tags = Array("types"), tags = Array("types"),
@ -135,7 +166,7 @@ class TypesEndpoint(config: Config) extends JsonSupport {
new ApiResponse( new ApiResponse(
responseCode = "500", responseCode = "500",
description = "Internal server error", description = "Internal server error",
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse]))) content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel])))
), ),
), ),
tags = Array("types"), tags = Array("types"),
@ -166,7 +197,7 @@ class TypesEndpoint(config: Config) extends JsonSupport {
new ApiResponse( new ApiResponse(
responseCode = "500", responseCode = "500",
description = "Internal server error", description = "Internal server error",
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse]))) content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel])))
), ),
), ),
tags = Array("types"), tags = Array("types"),

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2019 Evgeniy Alekseev. * Copyright (c) 2019-2022 Evgeniy Alekseev.
* *
* This file is part of ffxivbis * This file is part of ffxivbis
* (see https://github.com/arcan1s/ffxivbis). * (see https://github.com/arcan1s/ffxivbis).
@ -21,19 +21,22 @@ import io.swagger.v3.oas.annotations.security.SecurityRequirement
import io.swagger.v3.oas.annotations.{Operation, Parameter} import io.swagger.v3.oas.annotations.{Operation, Parameter}
import jakarta.ws.rs._ import jakarta.ws.rs._
import me.arcanis.ffxivbis.http.api.v1.json._ import me.arcanis.ffxivbis.http.api.v1.json._
import me.arcanis.ffxivbis.http.{Authorization, UserHelper} import me.arcanis.ffxivbis.http.helpers.UserHelper
import me.arcanis.ffxivbis.http.{Authorization, AuthorizationProvider}
import me.arcanis.ffxivbis.messages.Message import me.arcanis.ffxivbis.messages.Message
import me.arcanis.ffxivbis.models.Permission import me.arcanis.ffxivbis.models.Permission
import scala.util.{Failure, Success} import scala.util.{Failure, Success}
@Path("/api/v1") @Path("/api/v1")
class UserEndpoint(override val storage: ActorRef[Message])(implicit timeout: Timeout, scheduler: Scheduler) class UserEndpoint(override val storage: ActorRef[Message], override val auth: AuthorizationProvider)(implicit
extends UserHelper timeout: Timeout,
scheduler: Scheduler
) extends UserHelper
with Authorization with Authorization
with JsonSupport { with JsonSupport {
def route: Route = createParty ~ createUser ~ deleteUser ~ getUsers def route: Route = createParty ~ createUser ~ deleteUser ~ getUsers ~ getUsersCurrent
@PUT @PUT
@Path("party") @Path("party")
@ -44,24 +47,28 @@ class UserEndpoint(override val storage: ActorRef[Message])(implicit timeout: Ti
requestBody = new RequestBody( requestBody = new RequestBody(
description = "party administrator description", description = "party administrator description",
required = true, required = true,
content = Array(new Content(schema = new Schema(implementation = classOf[UserResponse]))) content = Array(new Content(schema = new Schema(implementation = classOf[UserModel])))
), ),
responses = Array( responses = Array(
new ApiResponse(responseCode = "200", description = "Party has been created"), new ApiResponse(
responseCode = "200",
description = "Party has been created",
content = Array(new Content(schema = new Schema(implementation = classOf[PartyIdModel])))
),
new ApiResponse( new ApiResponse(
responseCode = "400", responseCode = "400",
description = "Invalid parameters were supplied", description = "Invalid parameters were supplied",
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse]))) content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel])))
), ),
new ApiResponse( new ApiResponse(
responseCode = "406", responseCode = "406",
description = "Party with the specified ID already exists", description = "Party with the specified ID already exists",
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse]))) content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel])))
), ),
new ApiResponse( new ApiResponse(
responseCode = "500", responseCode = "500",
description = "Internal server error", description = "Internal server error",
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse]))) content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel])))
), ),
), ),
tags = Array("party"), tags = Array("party"),
@ -70,15 +77,12 @@ class UserEndpoint(override val storage: ActorRef[Message])(implicit timeout: Ti
path("party") { path("party") {
extractExecutionContext { implicit executionContext => extractExecutionContext { implicit executionContext =>
put { put {
entity(as[UserResponse]) { user => entity(as[UserModel]) { user =>
onComplete(newPartyId) { onSuccess(newPartyId) { partyId =>
case Success(partyId) =>
val admin = user.toUser.copy(partyId = partyId, permission = Permission.admin) val admin = user.toUser.copy(partyId = partyId, permission = Permission.admin)
onComplete(addUser(admin, isHashedPassword = false)) { onSuccess(addUser(admin, isHashedPassword = false)) {
case Success(_) => complete(PartyIdResponse(partyId)) complete(PartyIdModel(partyId))
case Failure(exception) => throw exception
} }
case Failure(exception) => throw exception
} }
} }
} }
@ -97,29 +101,29 @@ class UserEndpoint(override val storage: ActorRef[Message])(implicit timeout: Ti
requestBody = new RequestBody( requestBody = new RequestBody(
description = "user description", description = "user description",
required = true, required = true,
content = Array(new Content(schema = new Schema(implementation = classOf[UserResponse]))) content = Array(new Content(schema = new Schema(implementation = classOf[UserModel])))
), ),
responses = Array( responses = Array(
new ApiResponse(responseCode = "201", description = "User has been created"), new ApiResponse(responseCode = "201", description = "User has been created"),
new ApiResponse( new ApiResponse(
responseCode = "400", responseCode = "400",
description = "Invalid parameters were supplied", description = "Invalid parameters were supplied",
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse]))) content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel])))
), ),
new ApiResponse( new ApiResponse(
responseCode = "401", responseCode = "401",
description = "Supplied authorization is invalid", description = "Supplied authorization is invalid",
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse]))) content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel])))
), ),
new ApiResponse( new ApiResponse(
responseCode = "403", responseCode = "403",
description = "Access is forbidden", description = "Access is forbidden",
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse]))) content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel])))
), ),
new ApiResponse( new ApiResponse(
responseCode = "500", responseCode = "500",
description = "Internal server error", description = "Internal server error",
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse]))) content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel])))
), ),
), ),
security = Array(new SecurityRequirement(name = "basic auth", scopes = Array("admin"))), security = Array(new SecurityRequirement(name = "basic auth", scopes = Array("admin"))),
@ -130,11 +134,10 @@ class UserEndpoint(override val storage: ActorRef[Message])(implicit timeout: Ti
extractExecutionContext { implicit executionContext => extractExecutionContext { implicit executionContext =>
authenticateBasicBCrypt(s"party $partyId", authAdmin(partyId)) { _ => authenticateBasicBCrypt(s"party $partyId", authAdmin(partyId)) { _ =>
post { post {
entity(as[UserResponse]) { user => entity(as[UserModel]) { user =>
val withPartyId = user.toUser.copy(partyId = partyId) val withPartyId = user.toUser.copy(partyId = partyId)
onComplete(addUser(withPartyId, isHashedPassword = false)) { onSuccess(addUser(withPartyId, isHashedPassword = false)) {
case Success(_) => complete(StatusCodes.Accepted, HttpEntity.Empty) complete(StatusCodes.Accepted, HttpEntity.Empty)
case Failure(exception) => throw exception
} }
} }
} }
@ -156,17 +159,17 @@ class UserEndpoint(override val storage: ActorRef[Message])(implicit timeout: Ti
new ApiResponse( new ApiResponse(
responseCode = "401", responseCode = "401",
description = "Supplied authorization is invalid", description = "Supplied authorization is invalid",
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse]))) content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel])))
), ),
new ApiResponse( new ApiResponse(
responseCode = "403", responseCode = "403",
description = "Access is forbidden", description = "Access is forbidden",
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse]))) content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel])))
), ),
new ApiResponse( new ApiResponse(
responseCode = "500", responseCode = "500",
description = "Internal server error", description = "Internal server error",
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse]))) content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel])))
), ),
), ),
security = Array(new SecurityRequirement(name = "basic auth", scopes = Array("admin"))), security = Array(new SecurityRequirement(name = "basic auth", scopes = Array("admin"))),
@ -177,9 +180,8 @@ class UserEndpoint(override val storage: ActorRef[Message])(implicit timeout: Ti
extractExecutionContext { implicit executionContext => extractExecutionContext { implicit executionContext =>
authenticateBasicBCrypt(s"party $partyId", authAdmin(partyId)) { _ => authenticateBasicBCrypt(s"party $partyId", authAdmin(partyId)) { _ =>
delete { delete {
onComplete(removeUser(partyId, username)) { onSuccess(removeUser(partyId, username)) {
case Success(_) => complete(StatusCodes.Accepted, HttpEntity.Empty) complete(StatusCodes.Accepted, HttpEntity.Empty)
case Failure(exception) => throw exception
} }
} }
} }
@ -201,27 +203,27 @@ class UserEndpoint(override val storage: ActorRef[Message])(implicit timeout: Ti
description = "Users list", description = "Users list",
content = Array( content = Array(
new Content( new Content(
array = new ArraySchema(schema = new Schema(implementation = classOf[UserResponse])), array = new ArraySchema(schema = new Schema(implementation = classOf[UserModel])),
) )
) )
), ),
new ApiResponse( new ApiResponse(
responseCode = "401", responseCode = "401",
description = "Supplied authorization is invalid", description = "Supplied authorization is invalid",
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse]))) content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel])))
), ),
new ApiResponse( new ApiResponse(
responseCode = "403", responseCode = "403",
description = "Access is forbidden", description = "Access is forbidden",
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse]))) content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel])))
), ),
new ApiResponse( new ApiResponse(
responseCode = "500", responseCode = "500",
description = "Internal server error", description = "Internal server error",
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse]))) content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel])))
), ),
), ),
security = Array(new SecurityRequirement(name = "basic auth", scopes = Array("admin"))), security = Array(new SecurityRequirement(name = "basic auth", scopes = Array("get"))),
tags = Array("users"), tags = Array("users"),
) )
def getUsers: Route = def getUsers: Route =
@ -229,12 +231,56 @@ class UserEndpoint(override val storage: ActorRef[Message])(implicit timeout: Ti
extractExecutionContext { implicit executionContext => extractExecutionContext { implicit executionContext =>
authenticateBasicBCrypt(s"party $partyId", authAdmin(partyId)) { _ => authenticateBasicBCrypt(s"party $partyId", authAdmin(partyId)) { _ =>
get { get {
onComplete(users(partyId)) { onSuccess(users(partyId)) { response =>
case Success(response) => complete(response.map(UserResponse.fromUser)) complete(response.map(UserModel.fromUser))
case Failure(exception) => throw exception
} }
} }
} }
} }
} }
@GET
@Path("party/{partyId}/users/current")
@Produces(value = Array("application/json"))
@Operation(
summary = "get current user",
description = "Return the current user descriptor",
parameters = Array(
new Parameter(name = "partyId", in = ParameterIn.PATH, description = "unique party ID", example = "abcdefgh"),
),
responses = Array(
new ApiResponse(
responseCode = "200",
description = "User descriptor",
content = Array(new Content(schema = new Schema(implementation = classOf[UserModel])))
),
new ApiResponse(
responseCode = "401",
description = "Supplied authorization is invalid",
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel])))
),
new ApiResponse(
responseCode = "403",
description = "Access is forbidden",
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel])))
),
new ApiResponse(
responseCode = "500",
description = "Internal server error",
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel])))
),
),
security = Array(new SecurityRequirement(name = "basic auth", scopes = Array("admin"))),
tags = Array("users"),
)
def getUsersCurrent: Route =
path("party" / Segment / "users" / "current") { partyId =>
extractExecutionContext { implicit executionContext =>
authenticateBasicBCrypt(s"party $partyId", authGet(partyId)) { user =>
get {
complete(UserModel.fromUser(user))
}
}
}
}
} }

View File

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

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2019 Evgeniy Alekseev. * Copyright (c) 2019-2022 Evgeniy Alekseev.
* *
* This file is part of ffxivbis * This file is part of ffxivbis
* (see https://github.com/arcan1s/ffxivbis). * (see https://github.com/arcan1s/ffxivbis).
@ -10,4 +10,4 @@ package me.arcanis.ffxivbis.http.api.v1.json
import io.swagger.v3.oas.annotations.media.Schema import io.swagger.v3.oas.annotations.media.Schema
case class PartyIdResponse(@Schema(description = "party id", required = true) partyId: String) case class ErrorModel(@Schema(description = "error message", required = true) message: String)

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2019 Evgeniy Alekseev. * Copyright (c) 2019-2022 Evgeniy Alekseev.
* *
* This file is part of ffxivbis * This file is part of ffxivbis
* (see https://github.com/arcan1s/ffxivbis). * (see https://github.com/arcan1s/ffxivbis).
@ -8,12 +8,12 @@
*/ */
package me.arcanis.ffxivbis.http.api.v1.json package me.arcanis.ffxivbis.http.api.v1.json
import java.time.Instant
import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport
import me.arcanis.ffxivbis.models.Permission import me.arcanis.ffxivbis.models.Permission
import spray.json._ import spray.json._
import java.time.Instant
trait JsonSupport extends SprayJsonSupport with DefaultJsonProtocol { trait JsonSupport extends SprayJsonSupport with DefaultJsonProtocol {
private def enumFormat[E <: Enumeration](enum: E): RootJsonFormat[E#Value] = private def enumFormat[E <: Enumeration](enum: E): RootJsonFormat[E#Value] =
@ -38,19 +38,19 @@ trait JsonSupport extends SprayJsonSupport with DefaultJsonProtocol {
implicit val actionFormat: RootJsonFormat[ApiAction.Value] = enumFormat(ApiAction) implicit val actionFormat: RootJsonFormat[ApiAction.Value] = enumFormat(ApiAction)
implicit val permissionFormat: RootJsonFormat[Permission.Value] = enumFormat(Permission) implicit val permissionFormat: RootJsonFormat[Permission.Value] = enumFormat(Permission)
implicit val errorFormat: RootJsonFormat[ErrorResponse] = jsonFormat1(ErrorResponse.apply) implicit val errorFormat: RootJsonFormat[ErrorModel] = jsonFormat1(ErrorModel.apply)
implicit val partyIdFormat: RootJsonFormat[PartyIdResponse] = jsonFormat1(PartyIdResponse.apply) implicit val partyIdFormat: RootJsonFormat[PartyIdModel] = jsonFormat1(PartyIdModel.apply)
implicit val pieceFormat: RootJsonFormat[PieceResponse] = jsonFormat3(PieceResponse.apply) implicit val pieceFormat: RootJsonFormat[PieceModel] = jsonFormat3(PieceModel.apply)
implicit val lootFormat: RootJsonFormat[LootResponse] = jsonFormat3(LootResponse.apply) implicit val lootFormat: RootJsonFormat[LootModel] = jsonFormat3(LootModel.apply)
implicit val partyDescriptionFormat: RootJsonFormat[PartyDescriptionResponse] = jsonFormat2( implicit val partyDescriptionFormat: RootJsonFormat[PartyDescriptionModel] = jsonFormat2(
PartyDescriptionResponse.apply PartyDescriptionModel.apply
) )
implicit val playerFormat: RootJsonFormat[PlayerResponse] = jsonFormat7(PlayerResponse.apply) implicit val playerFormat: RootJsonFormat[PlayerModel] = jsonFormat9(PlayerModel.apply)
implicit val playerActionFormat: RootJsonFormat[PlayerActionResponse] = jsonFormat2(PlayerActionResponse.apply) implicit val playerActionFormat: RootJsonFormat[PlayerActionModel] = jsonFormat2(PlayerActionModel.apply)
implicit val playerIdFormat: RootJsonFormat[PlayerIdResponse] = jsonFormat3(PlayerIdResponse.apply) implicit val playerIdFormat: RootJsonFormat[PlayerIdModel] = jsonFormat3(PlayerIdModel.apply)
implicit val pieceActionFormat: RootJsonFormat[PieceActionResponse] = jsonFormat4(PieceActionResponse.apply) implicit val pieceActionFormat: RootJsonFormat[PieceActionModel] = jsonFormat4(PieceActionModel.apply)
implicit val playerBiSLinkFormat: RootJsonFormat[PlayerBiSLinkResponse] = jsonFormat2(PlayerBiSLinkResponse.apply) implicit val playerBiSLinkFormat: RootJsonFormat[PlayerBiSLinkModel] = jsonFormat2(PlayerBiSLinkModel.apply)
implicit val playerIdWithCountersFormat: RootJsonFormat[PlayerIdWithCountersResponse] = implicit val playerIdWithCountersFormat: RootJsonFormat[PlayerIdWithCountersModel] =
jsonFormat9(PlayerIdWithCountersResponse.apply) jsonFormat9(PlayerIdWithCountersModel.apply)
implicit val userFormat: RootJsonFormat[UserResponse] = jsonFormat4(UserResponse.apply) implicit val userFormat: RootJsonFormat[UserModel] = jsonFormat4(UserModel.apply)
} }

View File

@ -1,12 +1,20 @@
/*
* 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.http.api.v1.json package me.arcanis.ffxivbis.http.api.v1.json
import java.time.Instant
import io.swagger.v3.oas.annotations.media.Schema import io.swagger.v3.oas.annotations.media.Schema
import me.arcanis.ffxivbis.models.Loot import me.arcanis.ffxivbis.models.Loot
case class LootResponse( import java.time.Instant
@Schema(description = "looted piece", required = true) piece: PieceResponse,
case class LootModel(
@Schema(description = "looted piece", required = true) piece: PieceModel,
@Schema(description = "loot timestamp", required = true) timestamp: Instant, @Schema(description = "loot timestamp", required = true) timestamp: Instant,
@Schema(description = "is loot free for all", required = true) isFreeLoot: Boolean @Schema(description = "is loot free for all", required = true) isFreeLoot: Boolean
) { ) {
@ -14,8 +22,8 @@ case class LootResponse(
def toLoot: Loot = Loot(-1, piece.toPiece, timestamp, isFreeLoot) def toLoot: Loot = Loot(-1, piece.toPiece, timestamp, isFreeLoot)
} }
object LootResponse { object LootModel {
def fromLoot(loot: Loot): LootResponse = def fromLoot(loot: Loot): LootModel =
LootResponse(PieceResponse.fromPiece(loot.piece), loot.timestamp, loot.isFreeLoot) LootModel(PieceModel.fromPiece(loot.piece), loot.timestamp, loot.isFreeLoot)
} }

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2020 Evgeniy Alekseev. * Copyright (c) 2019-2022 Evgeniy Alekseev.
* *
* This file is part of ffxivbis * This file is part of ffxivbis
* (see https://github.com/arcan1s/ffxivbis). * (see https://github.com/arcan1s/ffxivbis).
@ -11,16 +11,16 @@ package me.arcanis.ffxivbis.http.api.v1.json
import io.swagger.v3.oas.annotations.media.Schema import io.swagger.v3.oas.annotations.media.Schema
import me.arcanis.ffxivbis.models.PartyDescription import me.arcanis.ffxivbis.models.PartyDescription
case class PartyDescriptionResponse( case class PartyDescriptionModel(
@Schema(description = "party id", required = true) partyId: String, @Schema(description = "party id", required = true, example = "abcdefgh") partyId: String,
@Schema(description = "party name") partyAlias: Option[String] @Schema(description = "party name") partyAlias: Option[String]
) { ) {
def toDescription: PartyDescription = PartyDescription(partyId, partyAlias) def toDescription: PartyDescription = PartyDescription(partyId, partyAlias)
} }
object PartyDescriptionResponse { object PartyDescriptionModel {
def fromDescription(description: PartyDescription): PartyDescriptionResponse = def fromDescription(description: PartyDescription): PartyDescriptionModel =
PartyDescriptionResponse(description.partyId, description.partyAlias) PartyDescriptionModel(description.partyId, description.partyAlias)
} }

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2019 Evgeniy Alekseev. * Copyright (c) 2019-2022 Evgeniy Alekseev.
* *
* This file is part of ffxivbis * This file is part of ffxivbis
* (see https://github.com/arcan1s/ffxivbis). * (see https://github.com/arcan1s/ffxivbis).
@ -10,4 +10,4 @@ package me.arcanis.ffxivbis.http.api.v1.json
import io.swagger.v3.oas.annotations.media.Schema import io.swagger.v3.oas.annotations.media.Schema
case class ErrorResponse(@Schema(description = "error message", required = true) message: String) case class PartyIdModel(@Schema(description = "party id", required = true, example = "abcdefgh") partyId: String)

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2019 Evgeniy Alekseev. * Copyright (c) 2019-2022 Evgeniy Alekseev.
* *
* This file is part of ffxivbis * This file is part of ffxivbis
* (see https://github.com/arcan1s/ffxivbis). * (see https://github.com/arcan1s/ffxivbis).
@ -10,14 +10,14 @@ package me.arcanis.ffxivbis.http.api.v1.json
import io.swagger.v3.oas.annotations.media.Schema import io.swagger.v3.oas.annotations.media.Schema
case class PieceActionResponse( case class PieceActionModel(
@Schema( @Schema(
description = "action to perform", description = "action to perform",
required = true, required = true,
`type` = "string", `type` = "string",
allowableValues = Array("add", "remove") allowableValues = Array("add", "remove")
) action: ApiAction.Value, ) action: ApiAction.Value,
@Schema(description = "piece description", required = true) piece: PieceResponse, @Schema(description = "piece description", required = true) piece: PieceModel,
@Schema(description = "player description", required = true) playerId: PlayerIdResponse, @Schema(description = "player description", required = true) playerId: PlayerIdModel,
@Schema(description = "is piece free to roll or not") isFreeLoot: Option[Boolean] @Schema(description = "is piece free to roll or not", `type` = "boolean") isFreeLoot: Option[Boolean]
) )

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2019 Evgeniy Alekseev. * Copyright (c) 2019-2022 Evgeniy Alekseev.
* *
* This file is part of ffxivbis * This file is part of ffxivbis
* (see https://github.com/arcan1s/ffxivbis). * (see https://github.com/arcan1s/ffxivbis).
@ -11,8 +11,8 @@ package me.arcanis.ffxivbis.http.api.v1.json
import io.swagger.v3.oas.annotations.media.Schema import io.swagger.v3.oas.annotations.media.Schema
import me.arcanis.ffxivbis.models.{Job, Piece, PieceType} import me.arcanis.ffxivbis.models.{Job, Piece, PieceType}
case class PieceResponse( case class PieceModel(
@Schema(description = "piece type", required = true) pieceType: String, @Schema(description = "piece type", required = true, example = "Savage") pieceType: String,
@Schema(description = "job name to which piece belong or AnyJob", required = true, example = "DNC") job: String, @Schema(description = "job name to which piece belong or AnyJob", required = true, example = "DNC") job: String,
@Schema(description = "piece name", required = true, example = "body") piece: String @Schema(description = "piece name", required = true, example = "body") piece: String
) { ) {
@ -20,8 +20,8 @@ case class PieceResponse(
def toPiece: Piece = Piece(piece, PieceType.withName(pieceType), Job.withName(job)) def toPiece: Piece = Piece(piece, PieceType.withName(pieceType), Job.withName(job))
} }
object PieceResponse { object PieceModel {
def fromPiece(piece: Piece): PieceResponse = def fromPiece(piece: Piece): PieceModel =
PieceResponse(piece.pieceType.toString, piece.job.toString, piece.piece) PieceModel(piece.pieceType.toString, piece.job.toString, piece.piece)
} }

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2019 Evgeniy Alekseev. * Copyright (c) 2019-2022 Evgeniy Alekseev.
* *
* This file is part of ffxivbis * This file is part of ffxivbis
* (see https://github.com/arcan1s/ffxivbis). * (see https://github.com/arcan1s/ffxivbis).
@ -10,7 +10,7 @@ package me.arcanis.ffxivbis.http.api.v1.json
import io.swagger.v3.oas.annotations.media.Schema import io.swagger.v3.oas.annotations.media.Schema
case class PlayerActionResponse( case class PlayerActionModel(
@Schema( @Schema(
description = "action to perform", description = "action to perform",
required = true, required = true,
@ -18,5 +18,5 @@ case class PlayerActionResponse(
allowableValues = Array("add", "remove"), allowableValues = Array("add", "remove"),
example = "add" example = "add"
) action: ApiAction.Value, ) action: ApiAction.Value,
@Schema(description = "player description", required = true) playerId: PlayerResponse @Schema(description = "player description", required = true) playerId: PlayerModel
) )

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2019 Evgeniy Alekseev. * Copyright (c) 2019-2022 Evgeniy Alekseev.
* *
* This file is part of ffxivbis * This file is part of ffxivbis
* (see https://github.com/arcan1s/ffxivbis). * (see https://github.com/arcan1s/ffxivbis).
@ -10,11 +10,11 @@ package me.arcanis.ffxivbis.http.api.v1.json
import io.swagger.v3.oas.annotations.media.Schema import io.swagger.v3.oas.annotations.media.Schema
case class PlayerBiSLinkResponse( case class PlayerBiSLinkModel(
@Schema( @Schema(
description = "link to player best in slot", description = "link to player best in slot",
required = true, required = true,
example = "https://ffxiv.ariyala.com/19V5R" example = "https://ffxiv.ariyala.com/19V5R"
) link: String, ) link: String,
@Schema(description = "player description", required = true) playerId: PlayerIdResponse @Schema(description = "player description", required = true) playerId: PlayerIdModel
) )

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2019 Evgeniy Alekseev. * Copyright (c) 2019-2022 Evgeniy Alekseev.
* *
* This file is part of ffxivbis * This file is part of ffxivbis
* (see https://github.com/arcan1s/ffxivbis). * (see https://github.com/arcan1s/ffxivbis).
@ -11,7 +11,7 @@ package me.arcanis.ffxivbis.http.api.v1.json
import io.swagger.v3.oas.annotations.media.Schema import io.swagger.v3.oas.annotations.media.Schema
import me.arcanis.ffxivbis.models.{Job, PlayerId} import me.arcanis.ffxivbis.models.{Job, PlayerId}
case class PlayerIdResponse( case class PlayerIdModel(
@Schema(description = "unique party ID. Required in responses", example = "abcdefgh") partyId: Option[String], @Schema(description = "unique party ID. Required in responses", example = "abcdefgh") partyId: Option[String],
@Schema(description = "job name", required = true, example = "DNC") job: 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 = "player nick name", required = true, example = "Siuan Sanche") nick: String
@ -21,8 +21,8 @@ case class PlayerIdResponse(
PlayerId(partyId, Job.withName(job), nick) PlayerId(partyId, Job.withName(job), nick)
} }
object PlayerIdResponse { object PlayerIdModel {
def fromPlayerId(playerId: PlayerId): PlayerIdResponse = def fromPlayerId(playerId: PlayerId): PlayerIdModel =
PlayerIdResponse(Some(playerId.partyId), playerId.job.toString, playerId.nick) PlayerIdModel(Some(playerId.partyId), playerId.job.toString, playerId.nick)
} }

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2019 Evgeniy Alekseev. * Copyright (c) 2019-2022 Evgeniy Alekseev.
* *
* This file is part of ffxivbis * This file is part of ffxivbis
* (see https://github.com/arcan1s/ffxivbis). * (see https://github.com/arcan1s/ffxivbis).
@ -11,22 +11,22 @@ package me.arcanis.ffxivbis.http.api.v1.json
import io.swagger.v3.oas.annotations.media.Schema import io.swagger.v3.oas.annotations.media.Schema
import me.arcanis.ffxivbis.models.PlayerIdWithCounters import me.arcanis.ffxivbis.models.PlayerIdWithCounters
case class PlayerIdWithCountersResponse( case class PlayerIdWithCountersModel(
@Schema(description = "unique party ID", required = true, example = "abcdefgh") 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 = "job name", required = true, example = "DNC") job: String,
@Schema(description = "player nick name", required = true, example = "Siuan Sanche") nick: 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, @Schema(description = "is piece required by player or not", required = true) isRequired: Boolean,
@Schema(description = "player loot priority", required = true) priority: Int, @Schema(description = "player loot priority", required = true) priority: Int,
@Schema(description = "count of savage pieces in best in slot", required = true) bisCountTotal: Int, @Schema(description = "count of savage pieces in best in slot", required = true) bisCountTotal: Int,
@Schema(description = "count of looted pieces", required = true) lootCount: Int, @Schema(description = "count of looted pieces of this type", required = true) lootCount: Int,
@Schema(description = "count of looted pieces which are parts of best in slot", required = true) lootCountBiS: Int, @Schema(description = "count of looted pieces which are parts of best in slot", required = true) lootCountBiS: Int,
@Schema(description = "total count of looted pieces", required = true) lootCountTotal: Int @Schema(description = "total count of looted pieces", required = true) lootCountTotal: Int
) )
object PlayerIdWithCountersResponse { object PlayerIdWithCountersModel {
def fromPlayerId(playerIdWithCounters: PlayerIdWithCounters): PlayerIdWithCountersResponse = def fromPlayerId(playerIdWithCounters: PlayerIdWithCounters): PlayerIdWithCountersModel =
PlayerIdWithCountersResponse( PlayerIdWithCountersModel(
playerIdWithCounters.partyId, playerIdWithCounters.partyId,
playerIdWithCounters.job.toString, playerIdWithCounters.job.toString,
playerIdWithCounters.nick, playerIdWithCounters.nick,

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2019 Evgeniy Alekseev. * Copyright (c) 2019-2022 Evgeniy Alekseev.
* *
* This file is part of ffxivbis * This file is part of ffxivbis
* (see https://github.com/arcan1s/ffxivbis). * (see https://github.com/arcan1s/ffxivbis).
@ -11,14 +11,16 @@ package me.arcanis.ffxivbis.http.api.v1.json
import io.swagger.v3.oas.annotations.media.Schema import io.swagger.v3.oas.annotations.media.Schema
import me.arcanis.ffxivbis.models.{BiS, Job, Player} import me.arcanis.ffxivbis.models.{BiS, Job, Player}
case class PlayerResponse( case class PlayerModel(
@Schema(description = "unique party ID", required = true, example = "abcdefgh") 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 = "job name", required = true, example = "DNC") job: String,
@Schema(description = "player nick name", required = true, example = "Siuan Sanche") nick: String, @Schema(description = "player nick name", required = true, example = "Siuan Sanche") nick: String,
@Schema(description = "pieces in best in slot") bis: Option[Seq[PieceResponse]], @Schema(description = "pieces in best in slot") bis: Option[Seq[PieceModel]],
@Schema(description = "looted pieces") loot: Option[Seq[LootResponse]], @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 = "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 = "player loot priority", `type` = "number") priority: 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],
) { ) {
def toPlayer: Player = def toPlayer: Player =
@ -34,16 +36,18 @@ case class PlayerResponse(
) )
} }
object PlayerResponse { object PlayerModel {
def fromPlayer(player: Player): PlayerResponse = def fromPlayer(player: Player): PlayerModel =
PlayerResponse( PlayerModel(
player.partyId, player.partyId,
player.job.toString, player.job.toString,
player.nick, player.nick,
Some(player.bis.pieces.map(PieceResponse.fromPiece)), Some(player.bis.pieces.map(PieceModel.fromPiece)),
Some(player.loot.map(LootResponse.fromLoot)), Some(player.loot.map(LootModel.fromLoot)),
player.link, player.link,
Some(player.priority) Some(player.priority),
Some(player.lootCountBiS),
Some(player.lootCountTotal),
) )
} }

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2019 Evgeniy Alekseev. * Copyright (c) 2019-2022 Evgeniy Alekseev.
* *
* This file is part of ffxivbis * This file is part of ffxivbis
* (see https://github.com/arcan1s/ffxivbis). * (see https://github.com/arcan1s/ffxivbis).
@ -11,13 +11,14 @@ package me.arcanis.ffxivbis.http.api.v1.json
import io.swagger.v3.oas.annotations.media.Schema import io.swagger.v3.oas.annotations.media.Schema
import me.arcanis.ffxivbis.models.{Permission, User} import me.arcanis.ffxivbis.models.{Permission, User}
case class UserResponse( case class UserModel(
@Schema(description = "unique party ID", required = true, example = "abcdefgh") 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 = "username to login to party", required = true, example = "siuan") username: String,
@Schema(description = "password to login to party", required = true, example = "pa55w0rd") password: String, @Schema(description = "password to login to party", required = true, example = "pa55w0rd") password: String,
@Schema( @Schema(
description = "user permission", description = "user permission",
defaultValue = "get", defaultValue = "get",
`type` = "string",
allowableValues = Array("get", "post", "admin") allowableValues = Array("get", "post", "admin")
) permission: Option[Permission.Value] = None ) permission: Option[Permission.Value] = None
) { ) {
@ -26,8 +27,8 @@ case class UserResponse(
User(partyId, username, password, permission.getOrElse(Permission.get)) User(partyId, username, password, permission.getOrElse(Permission.get))
} }
object UserResponse { object UserModel {
def fromUser(user: User): UserResponse = def fromUser(user: User): UserModel =
UserResponse(user.partyId, user.username, "", Some(user.permission)) UserModel(user.partyId, user.username, "", Some(user.permission))
} }

View File

@ -1,18 +1,10 @@
/* package me.arcanis.ffxivbis.http.helpers
* Copyright (c) 2019 Evgeniy Alekseev.
*
* This file is part of ffxivbis
* (see https://github.com/arcan1s/ffxivbis).
*
* License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause
*/
package me.arcanis.ffxivbis.http
import akka.actor.typed.scaladsl.AskPattern.Askable import akka.actor.typed.scaladsl.AskPattern.Askable
import akka.actor.typed.{ActorRef, Scheduler} import akka.actor.typed.{ActorRef, Scheduler}
import akka.util.Timeout import akka.util.Timeout
import me.arcanis.ffxivbis.http.api.v1.json.ApiAction import me.arcanis.ffxivbis.http.api.v1.json.ApiAction
import me.arcanis.ffxivbis.messages.{AddPieceToBis, GetBiS, Message, RemovePieceFromBiS, RemovePiecesFromBiS} import me.arcanis.ffxivbis.messages._
import me.arcanis.ffxivbis.models.{Piece, Player, PlayerId} import me.arcanis.ffxivbis.models.{Piece, Player, PlayerId}
import scala.concurrent.{ExecutionContext, Future} import scala.concurrent.{ExecutionContext, Future}

View File

@ -1,15 +1,7 @@
/* package me.arcanis.ffxivbis.http.helpers
* Copyright (c) 2019 Evgeniy Alekseev.
*
* This file is part of ffxivbis
* (see https://github.com/arcan1s/ffxivbis).
*
* License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause
*/
package me.arcanis.ffxivbis.http
import akka.actor.typed.{ActorRef, Scheduler}
import akka.actor.typed.scaladsl.AskPattern.Askable import akka.actor.typed.scaladsl.AskPattern.Askable
import akka.actor.typed.{ActorRef, Scheduler}
import akka.util.Timeout import akka.util.Timeout
import me.arcanis.ffxivbis.messages.{BiSProviderMessage, DownloadBiS} import me.arcanis.ffxivbis.messages.{BiSProviderMessage, DownloadBiS}
import me.arcanis.ffxivbis.models.{BiS, Job} import me.arcanis.ffxivbis.models.{BiS, Job}

View File

@ -1,18 +1,10 @@
/* package me.arcanis.ffxivbis.http.helpers
* Copyright (c) 2019 Evgeniy Alekseev.
*
* This file is part of ffxivbis
* (see https://github.com/arcan1s/ffxivbis).
*
* License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause
*/
package me.arcanis.ffxivbis.http
import akka.actor.typed.scaladsl.AskPattern.Askable import akka.actor.typed.scaladsl.AskPattern.Askable
import akka.actor.typed.{ActorRef, Scheduler} import akka.actor.typed.{ActorRef, Scheduler}
import akka.util.Timeout import akka.util.Timeout
import me.arcanis.ffxivbis.http.api.v1.json.ApiAction import me.arcanis.ffxivbis.http.api.v1.json.ApiAction
import me.arcanis.ffxivbis.messages.{AddPieceTo, GetLoot, Message, RemovePieceFrom, SuggestLoot} import me.arcanis.ffxivbis.messages._
import me.arcanis.ffxivbis.models.{Piece, Player, PlayerId, PlayerIdWithCounters} import me.arcanis.ffxivbis.models.{Piece, Player, PlayerId, PlayerIdWithCounters}
import scala.concurrent.{ExecutionContext, Future} import scala.concurrent.{ExecutionContext, Future}

View File

@ -1,18 +1,10 @@
/* package me.arcanis.ffxivbis.http.helpers
* Copyright (c) 2019 Evgeniy Alekseev.
*
* This file is part of ffxivbis
* (see https://github.com/arcan1s/ffxivbis).
*
* License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause
*/
package me.arcanis.ffxivbis.http
import akka.actor.typed.{ActorRef, Scheduler}
import akka.actor.typed.scaladsl.AskPattern.Askable import akka.actor.typed.scaladsl.AskPattern.Askable
import akka.actor.typed.{ActorRef, Scheduler}
import akka.util.Timeout import akka.util.Timeout
import me.arcanis.ffxivbis.http.api.v1.json.ApiAction import me.arcanis.ffxivbis.http.api.v1.json.ApiAction
import me.arcanis.ffxivbis.messages.{AddPieceToBis, AddPlayer, GetParty, GetPartyDescription, GetPlayer, Message, RemovePlayer, UpdateParty} import me.arcanis.ffxivbis.messages._
import me.arcanis.ffxivbis.models.{PartyDescription, Player, PlayerId} import me.arcanis.ffxivbis.models.{PartyDescription, Player, PlayerId}
import scala.concurrent.{ExecutionContext, Future} import scala.concurrent.{ExecutionContext, Future}

View File

@ -1,17 +1,9 @@
/* package me.arcanis.ffxivbis.http.helpers
* Copyright (c) 2019 Evgeniy Alekseev.
*
* This file is part of ffxivbis
* (see https://github.com/arcan1s/ffxivbis).
*
* License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause
*/
package me.arcanis.ffxivbis.http
import akka.actor.typed.scaladsl.AskPattern.Askable import akka.actor.typed.scaladsl.AskPattern.Askable
import akka.actor.typed.{ActorRef, Scheduler} import akka.actor.typed.{ActorRef, Scheduler}
import akka.util.Timeout import akka.util.Timeout
import me.arcanis.ffxivbis.messages.{AddUser, DeleteUser, GetNewPartyId, GetUser, GetUsers, Message} import me.arcanis.ffxivbis.messages._
import me.arcanis.ffxivbis.models.User import me.arcanis.ffxivbis.models.User
import scala.concurrent.Future import scala.concurrent.Future

View File

@ -1,73 +0,0 @@
/*
* Copyright (c) 2019 Evgeniy Alekseev.
*
* This file is part of ffxivbis
* (see https://github.com/arcan1s/ffxivbis).
*
* License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause
*/
package me.arcanis.ffxivbis.http.view
import akka.actor.typed.{ActorRef, Scheduler}
import akka.http.scaladsl.model.StatusCodes
import akka.http.scaladsl.server.Directives._
import akka.http.scaladsl.server._
import akka.util.Timeout
import me.arcanis.ffxivbis.http.{Authorization, PlayerHelper}
import me.arcanis.ffxivbis.messages.{BiSProviderMessage, Message}
import scala.util.{Failure, Success}
class BasePartyView(override val storage: ActorRef[Message], override val provider: ActorRef[BiSProviderMessage])(
implicit
timeout: Timeout,
scheduler: Scheduler
) extends PlayerHelper
with Authorization {
def route: Route = getIndex
def getIndex: Route =
path("party" / Segment) { partyId =>
extractExecutionContext { implicit executionContext =>
authenticateBasicBCrypt(s"party $partyId", authGet(partyId)) { _ =>
get {
onComplete(getPartyDescription(partyId)) {
case Success(description) =>
complete(StatusCodes.OK, RootView.toHtml(BasePartyView.template(partyId, description.alias)))
case Failure(exception) => throw exception
}
}
}
}
}
}
object BasePartyView {
import scalatags.Text
import scalatags.Text.all._
import scalatags.Text.tags2.{title => titleTag}
def root(partyId: String): Text.TypedTag[String] =
a(href := s"/party/$partyId", title := "root")("root")
def template(partyId: String, alias: String): String =
"<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01//EN\" \"http://www.w3.org/TR/html4/strict.dtd\">" +
html(
lang := "en",
head(
titleTag(s"Party $alias"),
link(rel := "stylesheet", `type` := "text/css", href := "/static/styles.css")
),
body(
h2(s"Party $alias"),
br,
h2(a(href := s"/party/$partyId/players", title := "party")("party")),
h2(a(href := s"/party/$partyId/bis", title := "bis management")("best in slot")),
h2(a(href := s"/party/$partyId/loot", title := "loot management")("loot")),
h2(a(href := s"/party/$partyId/suggest", title := "suggest loot")("suggest")),
hr,
h2(a(href := s"/party/$partyId/users", title := "user management")("users"))
)
)
}

View File

@ -1,173 +0,0 @@
/*
* Copyright (c) 2019 Evgeniy Alekseev.
*
* This file is part of ffxivbis
* (see https://github.com/arcan1s/ffxivbis).
*
* License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause
*/
package me.arcanis.ffxivbis.http.view
import akka.actor.typed.{ActorRef, Scheduler}
import akka.http.scaladsl.model.StatusCodes
import akka.http.scaladsl.server.Directives._
import akka.http.scaladsl.server._
import akka.util.Timeout
import me.arcanis.ffxivbis.http.{Authorization, BiSHelper}
import me.arcanis.ffxivbis.messages.{BiSProviderMessage, Message}
import me.arcanis.ffxivbis.models.{Piece, PieceType, Player, PlayerId}
import scala.concurrent.{ExecutionContext, Future}
import scala.util.Try
class BiSView(override val storage: ActorRef[Message], override val provider: ActorRef[BiSProviderMessage])(implicit
timeout: Timeout,
scheduler: Scheduler
) extends BiSHelper
with Authorization {
def route: Route = getBiS ~ modifyBiS
def getBiS: Route =
path("party" / Segment / "bis") { partyId: String =>
extractExecutionContext { implicit executionContext =>
authenticateBasicBCrypt(s"party $partyId", authGet(partyId)) { _ =>
get {
complete {
bis(partyId, None)
.map { players =>
BiSView.template(partyId, players, None)
}
.map { text =>
(StatusCodes.OK, RootView.toHtml(text))
}
}
}
}
}
}
def modifyBiS: Route =
path("party" / Segment / "bis") { partyId: String =>
extractExecutionContext { implicit executionContext =>
authenticateBasicBCrypt(s"party $partyId", authPost(partyId)) { _ =>
post {
formFields(
"player".as[String],
"piece".as[String].?,
"piece_type".as[String].?,
"link".as[String].?,
"action".as[String]
) { (player, maybePiece, maybePieceType, maybeLink, action) =>
onComplete(modifyBiSCall(partyId, player, maybePiece, maybePieceType, maybeLink, action)) { _ =>
redirect(s"/party/$partyId/bis", StatusCodes.Found)
}
}
}
}
}
}
private def modifyBiSCall(
partyId: String,
player: String,
maybePiece: Option[String],
maybePieceType: Option[String],
maybeLink: Option[String],
action: String
)(implicit executionContext: ExecutionContext, timeout: Timeout): Future[Unit] = {
def getPiece(playerId: PlayerId, piece: String, pieceType: String) =
Try(Piece(piece, PieceType.withName(pieceType), playerId.job)).toOption
def bisAction(playerId: PlayerId, piece: String, pieceType: String)(fn: Piece => Future[Unit]) =
getPiece(playerId, piece, pieceType) match {
case Some(item) => fn(item)
case _ => Future.failed(new Error(s"Could not construct piece from `$piece ($pieceType)`"))
}
PlayerId(partyId, player) match {
case Some(playerId) =>
(maybePiece, maybePieceType, action, maybeLink.map(_.trim).filter(_.nonEmpty)) match {
case (Some(piece), Some(pieceType), "add", _) =>
bisAction(playerId, piece, pieceType)(addPieceBiS(playerId, _))
case (Some(piece), Some(pieceType), "remove", _) =>
bisAction(playerId, piece, pieceType)(removePieceBiS(playerId, _))
case (_, _, "create", Some(link)) => putBiS(playerId, link)
case _ => Future.failed(new Error(s"Could not perform $action"))
}
case _ => Future.failed(new Error(s"Could not construct player id from `$player`"))
}
}
}
object BiSView {
import scalatags.Text.all._
import scalatags.Text.tags2.{title => titleTag}
def template(partyId: String, party: Seq[Player], error: Option[String]): String =
"<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01//EN\" \"http://www.w3.org/TR/html4/strict.dtd\">" +
html(
lang := "en",
head(
titleTag("Best in slot"),
link(rel := "stylesheet", `type` := "text/css", href := "/static/styles.css")
),
body(
h2("Best in slot"),
ErrorView.template(error),
SearchLineView.template,
form(action := s"/party/$partyId/bis", method := "post")(
select(name := "player", id := "player", title := "player")(
for (player <- party) yield option(player.playerId.toString)
),
select(name := "piece", id := "piece", title := "piece")(
for (piece <- Piece.available) yield option(piece)
),
select(name := "piece_type", id := "piece_type", title := "piece type")(
for (pieceType <- PieceType.available) yield option(pieceType.toString)
),
input(name := "action", id := "action", `type` := "hidden", value := "add"),
input(name := "add", id := "add", `type` := "submit", value := "add")
),
form(action := s"/party/$partyId/bis", method := "post")(
select(name := "player", id := "player", title := "player")(
for (player <- party) yield option(player.playerId.toString)
),
input(name := "link", id := "link", placeholder := "player bis link", title := "link", `type` := "text"),
input(name := "action", id := "action", `type` := "hidden", value := "create"),
input(name := "add", id := "add", `type` := "submit", value := "add")
),
table(id := "result")(
tr(
th("player"),
th("piece"),
th("piece type"),
th("")
),
for (player <- party; piece <- player.bis.pieces)
yield tr(
td(`class` := "include_search")(player.playerId.toString),
td(`class` := "include_search")(piece.piece),
td(piece.pieceType.toString),
td(
form(action := s"/party/$partyId/bis", method := "post")(
input(name := "player", id := "player", `type` := "hidden", value := player.playerId.toString),
input(name := "piece", id := "piece", `type` := "hidden", value := piece.piece),
input(
name := "piece_type",
id := "piece_type",
`type` := "hidden",
value := piece.pieceType.toString
),
input(name := "action", id := "action", `type` := "hidden", value := "remove"),
input(name := "remove", id := "remove", `type` := "submit", value := "x")
)
)
)
),
ExportToCSVView.template,
BasePartyView.root(partyId),
script(src := "/static/table_search.js", `type` := "text/javascript")
)
)
}

View File

@ -1,20 +0,0 @@
/*
* Copyright (c) 2019 Evgeniy Alekseev.
*
* This file is part of ffxivbis
* (see https://github.com/arcan1s/ffxivbis).
*
* License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause
*/
package me.arcanis.ffxivbis.http.view
import scalatags.Text
import scalatags.Text.all._
object ErrorView {
def template(error: Option[String]): Text.TypedTag[String] = error match {
case Some(text) => p(id := "error", s"Error occurs: $text")
case None => p("")
}
}

View File

@ -1,21 +0,0 @@
/*
* Copyright (c) 2019 Evgeniy Alekseev.
*
* This file is part of ffxivbis
* (see https://github.com/arcan1s/ffxivbis).
*
* License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause
*/
package me.arcanis.ffxivbis.http.view
import scalatags.Text
import scalatags.Text.all._
object ExportToCSVView {
def template: Text.TypedTag[String] =
div(
button(onclick := "exportTableToCsv('result.csv')")("Export to CSV"),
script(src := "/static/table_export.js", `type` := "text/javascript")
)
}

View File

@ -1,104 +0,0 @@
/*
* Copyright (c) 2019 Evgeniy Alekseev.
*
* This file is part of ffxivbis
* (see https://github.com/arcan1s/ffxivbis).
*
* License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause
*/
package me.arcanis.ffxivbis.http.view
import akka.actor.typed.{ActorRef, Scheduler}
import akka.http.scaladsl.model.StatusCodes
import akka.http.scaladsl.server.Directives._
import akka.http.scaladsl.server._
import akka.util.Timeout
import me.arcanis.ffxivbis.http.{PlayerHelper, UserHelper}
import me.arcanis.ffxivbis.messages.{BiSProviderMessage, Message}
import me.arcanis.ffxivbis.models.{PartyDescription, Permission, User}
import scala.concurrent.Future
import scala.util.{Failure, Success}
class IndexView(override val storage: ActorRef[Message], override val provider: ActorRef[BiSProviderMessage])(implicit
timeout: Timeout,
scheduler: Scheduler
) extends PlayerHelper
with UserHelper {
def route: Route = createParty ~ getIndex
def createParty: Route =
path("party") {
extractExecutionContext { implicit executionContext =>
post {
formFields("username".as[String], "password".as[String], "alias".as[String].?) {
(username, password, maybeAlias) =>
onComplete {
newPartyId.flatMap { partyId =>
val user = User(partyId, username, password, Permission.admin)
addUser(user, isHashedPassword = false).flatMap { _ =>
if (maybeAlias.getOrElse("").isEmpty) Future.successful(partyId)
else updateDescription(PartyDescription(partyId, maybeAlias)).map(_ => partyId)
}
}
} {
case Success(partyId) => redirect(s"/party/$partyId", StatusCodes.Found)
case Failure(exception) => throw exception
}
}
}
}
}
def getIndex: Route =
pathEndOrSingleSlash {
get {
parameters("partyId".as[String].?) {
case Some(partyId) => redirect(s"/party/$partyId", StatusCodes.Found)
case _ => complete(StatusCodes.OK, RootView.toHtml(IndexView.template))
}
}
}
}
object IndexView {
import scalatags.Text.all._
import scalatags.Text.tags2.{title => titleTag}
def template: String =
"<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01//EN\" \"http://www.w3.org/TR/html4/strict.dtd\">" +
html(
head(
titleTag("FFXIV loot helper"),
link(rel := "stylesheet", `type` := "text/css", href := "/static/styles.css")
),
body(
form(action := s"party", method := "post")(
label("create a new party"),
input(name := "alias", id := "alias", placeholder := "party alias", title := "alias", `type` := "text"),
input(
name := "username",
id := "username",
placeholder := "username",
title := "username",
`type` := "text"
),
input(
name := "password",
id := "password",
placeholder := "password",
title := "password",
`type` := "password"
),
input(name := "add", id := "add", `type` := "submit", value := "add")
),
br,
form(action := "/", method := "get")(
label("already have party?"),
input(name := "partyId", id := "partyId", placeholder := "party id", title := "party id", `type` := "text"),
input(name := "go", id := "go", `type` := "submit", value := "go")
)
)
)
}

View File

@ -1,163 +0,0 @@
/*
* Copyright (c) 2019 Evgeniy Alekseev.
*
* This file is part of ffxivbis
* (see https://github.com/arcan1s/ffxivbis).
*
* License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause
*/
package me.arcanis.ffxivbis.http.view
import akka.actor.typed.{ActorRef, Scheduler}
import akka.http.scaladsl.model.StatusCodes
import akka.http.scaladsl.server.Directives._
import akka.http.scaladsl.server.Route
import akka.util.Timeout
import me.arcanis.ffxivbis.http.{Authorization, LootHelper}
import me.arcanis.ffxivbis.messages.Message
import me.arcanis.ffxivbis.models.{Job, Piece, PieceType, PlayerIdWithCounters}
import scala.concurrent.{ExecutionContext, Future}
import scala.util.{Failure, Success, Try}
class LootSuggestView(override val storage: ActorRef[Message])(implicit timeout: Timeout, scheduler: Scheduler)
extends LootHelper
with Authorization {
def route: Route = getIndex ~ suggestLoot
def getIndex: Route =
path("party" / Segment / "suggest") { partyId: String =>
extractExecutionContext { implicit executionContext =>
authenticateBasicBCrypt(s"party $partyId", authGet(partyId)) { _ =>
get {
complete {
val text = LootSuggestView.template(partyId, Seq.empty, None, false, None)
(StatusCodes.OK, RootView.toHtml(text))
}
}
}
}
}
def suggestLoot: Route =
path("party" / Segment / "suggest") { partyId: String =>
extractExecutionContext { implicit executionContext =>
authenticateBasicBCrypt(s"party $partyId", authGet(partyId)) { _ =>
post {
formFields("piece".as[String], "job".as[String], "piece_type".as[String], "free_loot".as[String].?) {
(piece, job, pieceType, maybeFreeLoot) =>
import me.arcanis.ffxivbis.utils.Implicits._
val maybePiece = Try(Piece(piece, PieceType.withName(pieceType), Job.withName(job))).toOption
onComplete(suggestLootCall(partyId, maybePiece)) {
case Success(players) =>
val text = LootSuggestView.template(partyId, players, maybePiece, maybeFreeLoot, None)
complete(StatusCodes.OK, RootView.toHtml(text))
case Failure(exception) =>
val text = LootSuggestView.template(partyId, Seq.empty, None, false, Some(exception.getMessage))
complete(StatusCodes.OK, RootView.toHtml(text))
}
}
}
}
}
}
private def suggestLootCall(partyId: String, maybePiece: Option[Piece])(implicit
executionContext: ExecutionContext,
timeout: Timeout
): Future[Seq[PlayerIdWithCounters]] =
maybePiece match {
case Some(piece) => suggestPiece(partyId, piece)
case _ => Future.failed(new Error(s"Could not construct piece from `$maybePiece`"))
}
}
object LootSuggestView {
import scalatags.Text.all._
import scalatags.Text.tags2.{title => titleTag}
def template(
partyId: String,
party: Seq[PlayerIdWithCounters],
piece: Option[Piece],
isFreeLoot: Boolean,
error: Option[String]
): String =
"<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01//EN\" \"http://www.w3.org/TR/html4/strict.dtd\">" +
html(
lang := "en",
head(
titleTag("Suggest loot"),
link(rel := "stylesheet", `type` := "text/css", href := "/static/styles.css")
),
body(
h2("Suggest loot"),
for (part <- piece) yield p(s"Piece ${part.piece} (${part.pieceType})"),
ErrorView.template(error),
SearchLineView.template,
form(action := s"/party/$partyId/suggest", method := "post")(
select(name := "piece", id := "piece", title := "piece")(
for (piece <- Piece.available) yield option(piece)
),
select(name := "job", id := "job", title := "job")(
for (job <- Job.availableWithAnyJob) yield option(job.toString)
),
select(name := "piece_type", id := "piece_type", title := "piece type")(
for (pieceType <- PieceType.available) yield option(pieceType.toString)
),
input(name := "free_loot", id := "free_loot", title := "is free loot", `type` := "checkbox"),
label(`for` := "free_loot")("is free loot"),
input(name := "suggest", id := "suggest", `type` := "submit", value := "suggest")
),
table(id := "result")(
tr(
th("player"),
th("is required"),
th("these pieces looted"),
th("total bis pieces looted"),
th("total pieces looted"),
th("")
),
for (player <- party)
yield tr(
td(`class` := "include_search")(player.playerId.toString),
td(player.isRequiredToString),
td(player.lootCount),
td(player.lootCountBiS),
td(player.lootCountTotal),
td(
form(action := s"/party/$partyId/loot", method := "post")(
input(name := "player", id := "player", `type` := "hidden", value := player.playerId.toString),
input(
name := "piece",
id := "piece",
`type` := "hidden",
value := piece.map(_.piece).getOrElse("")
),
input(
name := "piece_type",
id := "piece_type",
`type` := "hidden",
value := piece.map(_.pieceType.toString).getOrElse("")
),
input(
name := "free_loot",
id := "free_loot",
`type` := "hidden",
value := (if (isFreeLoot) "yes" else "no")
),
input(name := "action", id := "action", `type` := "hidden", value := "add"),
input(name := "add", id := "add", `type` := "submit", value := "add")
)
)
)
),
ExportToCSVView.template,
BasePartyView.root(partyId),
script(src := "/static/table_search.js", `type` := "text/javascript")
)
)
}

View File

@ -1,164 +0,0 @@
/*
* Copyright (c) 2019 Evgeniy Alekseev.
*
* This file is part of ffxivbis
* (see https://github.com/arcan1s/ffxivbis).
*
* License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause
*/
package me.arcanis.ffxivbis.http.view
import akka.actor.typed.{ActorRef, Scheduler}
import akka.http.scaladsl.model.StatusCodes
import akka.http.scaladsl.server.Directives._
import akka.http.scaladsl.server.Route
import akka.util.Timeout
import me.arcanis.ffxivbis.http.{Authorization, LootHelper}
import me.arcanis.ffxivbis.messages.Message
import me.arcanis.ffxivbis.models.{Piece, PieceType, Player, PlayerId}
import scala.concurrent.{ExecutionContext, Future}
import scala.util.Try
class LootView(override val storage: ActorRef[Message])(implicit timeout: Timeout, scheduler: Scheduler)
extends LootHelper
with Authorization {
def route: Route = getLoot ~ modifyLoot
def getLoot: Route =
path("party" / Segment / "loot") { partyId: String =>
extractExecutionContext { implicit executionContext =>
authenticateBasicBCrypt(s"party $partyId", authGet(partyId)) { _ =>
get {
complete {
loot(partyId, None)
.map { players =>
LootView.template(partyId, players, None)
}
.map { text =>
(StatusCodes.OK, RootView.toHtml(text))
}
}
}
}
}
}
def modifyLoot: Route =
path("party" / Segment / "loot") { partyId: String =>
extractExecutionContext { implicit executionContext =>
authenticateBasicBCrypt(s"party $partyId", authPost(partyId)) { _ =>
post {
formFields(
"player".as[String],
"piece".as[String],
"piece_type".as[String],
"action".as[String],
"free_loot".as[String].?
) { (player, piece, pieceType, action, isFreeLoot) =>
onComplete(modifyLootCall(partyId, player, piece, pieceType, isFreeLoot, action)) { _ =>
redirect(s"/party/$partyId/loot", StatusCodes.Found)
}
}
}
}
}
}
private def modifyLootCall(
partyId: String,
player: String,
maybePiece: String,
maybePieceType: String,
maybeFreeLoot: Option[String],
action: String
)(implicit executionContext: ExecutionContext, timeout: Timeout): Future[Unit] = {
import me.arcanis.ffxivbis.utils.Implicits._
def getPiece(playerId: PlayerId) =
Try(Piece(maybePiece, PieceType.withName(maybePieceType), playerId.job)).toOption
PlayerId(partyId, player) match {
case Some(playerId) =>
(getPiece(playerId), action) match {
case (Some(piece), "add") => addPieceLoot(playerId, piece, maybeFreeLoot)
case (Some(piece), "remove") => removePieceLoot(playerId, piece)
case _ => Future.failed(new Error(s"Could not construct piece from `$maybePiece ($maybePieceType)`"))
}
case _ => Future.failed(new Error(s"Could not construct player id from `$player`"))
}
}
}
object LootView {
import scalatags.Text.all._
import scalatags.Text.tags2.{title => titleTag}
def template(partyId: String, party: Seq[Player], error: Option[String]): String =
"<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01//EN\" \"http://www.w3.org/TR/html4/strict.dtd\">" +
html(
lang := "en",
head(
titleTag("Loot"),
link(rel := "stylesheet", `type` := "text/css", href := "/static/styles.css")
),
body(
h2("Loot"),
ErrorView.template(error),
SearchLineView.template,
form(action := s"/party/$partyId/loot", method := "post")(
select(name := "player", id := "player", title := "player")(
for (player <- party) yield option(player.playerId.toString)
),
select(name := "piece", id := "piece", title := "piece")(
for (piece <- Piece.available) yield option(piece)
),
select(name := "piece_type", id := "piece_type", title := "piece type")(
for (pieceType <- PieceType.available) yield option(pieceType.toString)
),
input(name := "free_loot", id := "free_loot", title := "is free loot", `type` := "checkbox"),
label(`for` := "free_loot")("is free loot"),
input(name := "action", id := "action", `type` := "hidden", value := "add"),
input(name := "add", id := "add", `type` := "submit", value := "add")
),
table(id := "result")(
tr(
th("player"),
th("piece"),
th("piece type"),
th("is free loot"),
th("timestamp"),
th("")
),
for (player <- party; loot <- player.loot)
yield tr(
td(`class` := "include_search")(player.playerId.toString),
td(`class` := "include_search")(loot.piece.piece),
td(loot.piece.pieceType.toString),
td(loot.isFreeLootToString),
td(loot.timestamp.toString),
td(
form(action := s"/party/$partyId/loot", method := "post")(
input(name := "player", id := "player", `type` := "hidden", value := player.playerId.toString),
input(name := "piece", id := "piece", `type` := "hidden", value := loot.piece.piece),
input(
name := "piece_type",
id := "piece_type",
`type` := "hidden",
value := loot.piece.pieceType.toString
),
input(name := "free_loot", id := "free_loot", `type` := "hidden", value := loot.isFreeLootToString),
input(name := "action", id := "action", `type` := "hidden", value := "remove"),
input(name := "remove", id := "remove", `type` := "submit", value := "x")
)
)
)
),
ExportToCSVView.template,
BasePartyView.root(partyId),
script(src := "/static/table_search.js", `type` := "text/javascript")
)
)
}

View File

@ -1,152 +0,0 @@
/*
* Copyright (c) 2019 Evgeniy Alekseev.
*
* This file is part of ffxivbis
* (see https://github.com/arcan1s/ffxivbis).
*
* License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause
*/
package me.arcanis.ffxivbis.http.view
import akka.actor.typed.{ActorRef, Scheduler}
import akka.http.scaladsl.model.StatusCodes
import akka.http.scaladsl.server.Directives._
import akka.http.scaladsl.server.Route
import akka.util.Timeout
import me.arcanis.ffxivbis.http.{Authorization, PlayerHelper}
import me.arcanis.ffxivbis.messages.{BiSProviderMessage, Message}
import me.arcanis.ffxivbis.models._
import scala.concurrent.{ExecutionContext, Future}
class PlayerView(override val storage: ActorRef[Message], override val provider: ActorRef[BiSProviderMessage])(implicit
timeout: Timeout,
scheduler: Scheduler
) extends PlayerHelper
with Authorization {
def route: Route = getParty ~ modifyParty
def getParty: Route =
path("party" / Segment / "players") { partyId: String =>
extractExecutionContext { implicit executionContext =>
authenticateBasicBCrypt(s"party $partyId", authGet(partyId)) { _ =>
get {
complete {
getPlayers(partyId, None)
.map { players =>
PlayerView.template(partyId, players.map(_.withCounters(None)), None)
}
.map { text =>
(StatusCodes.OK, RootView.toHtml(text))
}
}
}
}
}
}
def modifyParty: Route =
path("party" / Segment / "players") { partyId: String =>
extractExecutionContext { implicit executionContext =>
authenticateBasicBCrypt(s"party $partyId", authPost(partyId)) { _ =>
post {
formFields(
"nick".as[String],
"job".as[String],
"priority".as[Int].?,
"link".as[String].?,
"action".as[String]
) { (nick, job, maybePriority, maybeLink, action) =>
onComplete(modifyPartyCall(partyId, nick, job, maybePriority, maybeLink, action)) { _ =>
redirect(s"/party/$partyId/players", StatusCodes.Found)
}
}
}
}
}
}
private def modifyPartyCall(
partyId: String,
nick: String,
job: String,
maybePriority: Option[Int],
maybeLink: Option[String],
action: String
)(implicit executionContext: ExecutionContext, timeout: Timeout): Future[Unit] = {
def maybePlayerId = PlayerId(partyId, Some(nick), Some(job))
def player(playerId: PlayerId) =
Player(-1, partyId, playerId.job, playerId.nick, BiS.empty, Seq.empty, maybeLink, maybePriority.getOrElse(0))
(action, maybePlayerId) match {
case ("add", Some(playerId)) => addPlayer(player(playerId))
case ("remove", Some(playerId)) => removePlayer(playerId)
case _ => Future.failed(new Error(s"Could not perform $action with $nick ($job)"))
}
}
}
object PlayerView {
import scalatags.Text.all._
import scalatags.Text.tags2.{title => titleTag}
def template(partyId: String, party: Seq[PlayerIdWithCounters], error: Option[String]): String =
"<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01//EN\" \"http://www.w3.org/TR/html4/strict.dtd\">" +
html(
lang := "en",
head(
titleTag("Party"),
link(rel := "stylesheet", `type` := "text/css", href := "/static/styles.css")
),
body(
h2("Party"),
ErrorView.template(error),
SearchLineView.template,
form(action := s"/party/$partyId/players", method := "post")(
input(name := "nick", id := "nick", placeholder := "nick", title := "nick", `type` := "nick"),
select(name := "job", id := "job", title := "job")(for (job <- Job.available) yield option(job.toString)),
input(name := "link", id := "link", placeholder := "player bis link", title := "link", `type` := "text"),
input(
name := "prioiry",
id := "priority",
placeholder := "priority",
title := "priority",
`type` := "number",
value := "0"
),
input(name := "action", id := "action", `type` := "hidden", value := "add"),
input(name := "add", id := "add", `type` := "submit", value := "add")
),
table(id := "result")(
tr(
th("nick"),
th("job"),
th("total bis pieces looted"),
th("total pieces looted"),
th("priority"),
th("")
),
for (player <- party)
yield tr(
td(`class` := "include_search")(player.nick),
td(`class` := "include_search")(player.job.toString),
td(player.lootCountBiS),
td(player.lootCountTotal),
td(player.priority),
td(
form(action := s"/party/$partyId/players", method := "post")(
input(name := "nick", id := "nick", `type` := "hidden", value := player.nick),
input(name := "job", id := "job", `type` := "hidden", value := player.job.toString),
input(name := "action", id := "action", `type` := "hidden", value := "remove"),
input(name := "remove", id := "remove", `type` := "submit", value := "x")
)
)
)
),
ExportToCSVView.template,
BasePartyView.root(partyId),
script(src := "/static/table_search.js", `type` := "text/javascript")
)
)
}

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2019 Evgeniy Alekseev. * Copyright (c) 2019-2022 Evgeniy Alekseev.
* *
* This file is part of ffxivbis * This file is part of ffxivbis
* (see https://github.com/arcan1s/ffxivbis). * (see https://github.com/arcan1s/ffxivbis).
@ -8,34 +8,73 @@
*/ */
package me.arcanis.ffxivbis.http.view package me.arcanis.ffxivbis.http.view
import akka.actor.typed.{ActorRef, Scheduler} import akka.http.scaladsl.model.headers.RawHeader
import akka.http.scaladsl.model.{ContentTypes, HttpEntity}
import akka.http.scaladsl.server.Directives._ import akka.http.scaladsl.server.Directives._
import akka.http.scaladsl.server.Route import akka.http.scaladsl.server.Route
import akka.util.Timeout import me.arcanis.ffxivbis.http.{Authorization, AuthorizationProvider}
import me.arcanis.ffxivbis.messages.{BiSProviderMessage, Message}
class RootView(storage: ActorRef[Message], provider: ActorRef[BiSProviderMessage])(implicit class RootView(override val auth: AuthorizationProvider) extends Authorization {
timeout: Timeout,
scheduler: Scheduler def route: Route = getBiS ~ getIndex ~ getLoot ~ getParty ~ getUsers
def getBiS: Route =
path("party" / Segment / "bis") { partyId: String =>
extractExecutionContext { implicit executionContext =>
authenticateBasicBCrypt(s"party $partyId", authGet(partyId)) { user =>
respondWithHeaders(
RawHeader("X-Party-Id", partyId),
RawHeader("X-User-Permission", user.permission.toString)
) { ) {
getFromResource("html/bis.html")
private val basePartyView = new BasePartyView(storage, provider) }
private val indexView = new IndexView(storage, provider) }
}
private val biSView = new BiSView(storage, provider)
private val lootView = new LootView(storage)
private val lootSuggestView = new LootSuggestView(storage)
private val playerView = new PlayerView(storage, provider)
private val userView = new UserView(storage)
def route: Route =
basePartyView.route ~ indexView.route ~
biSView.route ~ lootView.route ~ lootSuggestView.route ~ playerView.route ~ userView.route
} }
object RootView { def getIndex: Route =
pathEndOrSingleSlash {
def toHtml(template: String): HttpEntity.Strict = getFromResource("html/index.html")
HttpEntity(ContentTypes.`text/html(UTF-8)`, template) }
def getLoot: Route =
path("party" / Segment / "loot") { partyId: String =>
extractExecutionContext { implicit executionContext =>
authenticateBasicBCrypt(s"party $partyId", authGet(partyId)) { user =>
respondWithHeaders(
RawHeader("X-Party-Id", partyId),
RawHeader("X-User-Permission", user.permission.toString)
) {
getFromResource("html/loot.html")
}
}
}
}
def getParty: Route =
path("party" / Segment) { partyId: String =>
extractExecutionContext { implicit executionContext =>
authenticateBasicBCrypt(s"party $partyId", authGet(partyId)) { user =>
respondWithHeaders(
RawHeader("X-Party-Id", partyId),
RawHeader("X-User-Permission", user.permission.toString)
) {
getFromResource("html/party.html")
}
}
}
}
def getUsers: Route =
path("party" / Segment / "users") { partyId: String =>
extractExecutionContext { implicit executionContext =>
authenticateBasicBCrypt(s"party $partyId", authAdmin(partyId)) { user =>
respondWithHeaders(
RawHeader("X-Party-Id", partyId),
RawHeader("X-User-Permission", user.permission.toString)
) {
getFromResource("html/users.html")
}
}
}
}
} }

View File

@ -1,26 +0,0 @@
/*
* Copyright (c) 2019 Evgeniy Alekseev.
*
* This file is part of ffxivbis
* (see https://github.com/arcan1s/ffxivbis).
*
* License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause
*/
package me.arcanis.ffxivbis.http.view
import scalatags.Text
import scalatags.Text.all._
object SearchLineView {
def template: Text.TypedTag[String] =
div(
input(
`type` := "text",
id := "search",
onkeyup := "searchTable()",
placeholder := "search for data",
title := "search"
)
)
}

View File

@ -1,149 +0,0 @@
/*
* Copyright (c) 2019 Evgeniy Alekseev.
*
* This file is part of ffxivbis
* (see https://github.com/arcan1s/ffxivbis).
*
* License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause
*/
package me.arcanis.ffxivbis.http.view
import akka.actor.typed.{ActorRef, Scheduler}
import akka.http.scaladsl.model.StatusCodes
import akka.http.scaladsl.server.Directives._
import akka.http.scaladsl.server.Route
import akka.util.Timeout
import me.arcanis.ffxivbis.http.{Authorization, UserHelper}
import me.arcanis.ffxivbis.messages.Message
import me.arcanis.ffxivbis.models.{Permission, User}
import scala.concurrent.{ExecutionContext, Future}
import scala.util.Try
class UserView(override val storage: ActorRef[Message])(implicit timeout: Timeout, scheduler: Scheduler)
extends UserHelper
with Authorization {
def route: Route = getUsers ~ modifyUsers
def getUsers: Route =
path("party" / Segment / "users") { partyId: String =>
extractExecutionContext { implicit executionContext =>
authenticateBasicBCrypt(s"party $partyId", authAdmin(partyId)) { _ =>
get {
complete {
users(partyId)
.map { users =>
UserView.template(partyId, users, None)
}
.map { text =>
(StatusCodes.OK, RootView.toHtml(text))
}
}
}
}
}
}
def modifyUsers: Route =
path("party" / Segment / "users") { partyId: String =>
extractExecutionContext { implicit executionContext =>
authenticateBasicBCrypt(s"party $partyId", authAdmin(partyId)) { _ =>
post {
formFields("username".as[String], "password".as[String].?, "permission".as[String].?, "action".as[String]) {
(username, maybePassword, maybePermission, action) =>
onComplete(modifyUsersCall(partyId, username, maybePassword, maybePermission, action)) { case _ =>
redirect(s"/party/$partyId/users", StatusCodes.Found)
}
}
}
}
}
}
private def modifyUsersCall(
partyId: String,
username: String,
maybePassword: Option[String],
maybePermission: Option[String],
action: String
)(implicit executionContext: ExecutionContext, timeout: Timeout): Future[Unit] = {
def permission: Option[Permission.Value] =
maybePermission.flatMap(p => Try(Permission.withName(p)).toOption)
action match {
case "add" =>
(maybePassword, permission) match {
case (Some(password), Some(permission)) =>
addUser(User(partyId, username, password, permission), isHashedPassword = false)
case _ =>
Future.failed(
new Error(s"Could not construct permission/password from `$maybePermission`/`$maybePassword`")
)
}
case "remove" => removeUser(partyId, username)
case _ => Future.failed(new Error(s"Could not perform $action"))
}
}
}
object UserView {
import scalatags.Text.all._
import scalatags.Text.tags2.{title => titleTag}
def template(partyId: String, users: Seq[User], error: Option[String]) =
"<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01//EN\" \"http://www.w3.org/TR/html4/strict.dtd\">" +
html(
lang := "en",
head(
titleTag("Users"),
link(rel := "stylesheet", `type` := "text/css", href := "/static/styles.css")
),
body(
h2("Users"),
ErrorView.template(error),
SearchLineView.template,
form(action := s"/party/$partyId/users", method := "post")(
input(
name := "username",
id := "username",
placeholder := "username",
title := "username",
`type` := "text"
),
input(
name := "password",
id := "password",
placeholder := "password",
title := "password",
`type` := "password"
),
select(name := "permission", id := "permission", title := "permission")(option("get"), option("post")),
input(name := "action", id := "action", `type` := "hidden", value := "add"),
input(name := "add", id := "add", `type` := "submit", value := "add")
),
table(id := "result")(
tr(
th("username"),
th("permission"),
th("")
),
for (user <- users)
yield tr(
td(`class` := "include_search")(user.username),
td(user.permission.toString),
td(
form(action := s"/party/$partyId/users", method := "post")(
input(name := "username", id := "username", `type` := "hidden", value := user.username.toString),
input(name := "action", id := "action", `type` := "hidden", value := "remove"),
input(name := "remove", id := "remove", `type` := "submit", value := "x")
)
)
)
),
ExportToCSVView.template,
BasePartyView.root(partyId),
script(src := "/static/table_search.js", `type` := "text/javascript")
)
)
}

View File

@ -1,3 +1,11 @@
/*
* 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.messages package me.arcanis.ffxivbis.messages
import akka.actor.typed.ActorRef import akka.actor.typed.ActorRef

View File

@ -1,3 +1,11 @@
/*
* 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.messages package me.arcanis.ffxivbis.messages
import akka.actor.typed.ActorRef import akka.actor.typed.ActorRef

View File

@ -1,7 +1,15 @@
/*
* 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.messages package me.arcanis.ffxivbis.messages
import akka.actor.typed.{ActorRef, Behavior} import akka.actor.typed.{ActorRef, Behavior}
import me.arcanis.ffxivbis.models.{Party, PartyDescription, Piece, Player, PlayerId, User} import me.arcanis.ffxivbis.models._
import me.arcanis.ffxivbis.service.LootSelector import me.arcanis.ffxivbis.service.LootSelector
sealed trait DatabaseMessage extends Message { sealed trait DatabaseMessage extends Message {
@ -15,65 +23,75 @@ object DatabaseMessage {
} }
// bis handler // bis handler
case class AddPieceToBis(playerId: PlayerId, piece: Piece, replyTo: ActorRef[Unit]) extends DatabaseMessage { trait BisDatabaseMessage extends DatabaseMessage
case class AddPieceToBis(playerId: PlayerId, piece: Piece, replyTo: ActorRef[Unit]) extends BisDatabaseMessage {
override def partyId: String = playerId.partyId override def partyId: String = playerId.partyId
} }
case class GetBiS(partyId: String, playerId: Option[PlayerId], replyTo: ActorRef[Seq[Player]]) extends DatabaseMessage 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 DatabaseMessage { case class RemovePieceFromBiS(playerId: PlayerId, piece: Piece, replyTo: ActorRef[Unit]) extends BisDatabaseMessage {
override def partyId: String = playerId.partyId override def partyId: String = playerId.partyId
} }
case class RemovePiecesFromBiS(playerId: PlayerId, replyTo: ActorRef[Unit]) extends DatabaseMessage { case class RemovePiecesFromBiS(playerId: PlayerId, replyTo: ActorRef[Unit]) extends BisDatabaseMessage {
override def partyId: String = playerId.partyId override def partyId: String = playerId.partyId
} }
// loot handler // loot handler
trait LootDatabaseMessage extends DatabaseMessage
case class AddPieceTo(playerId: PlayerId, piece: Piece, isFreeLoot: Boolean, replyTo: ActorRef[Unit]) case class AddPieceTo(playerId: PlayerId, piece: Piece, isFreeLoot: Boolean, replyTo: ActorRef[Unit])
extends DatabaseMessage { extends LootDatabaseMessage {
override def partyId: String = playerId.partyId override def partyId: String = playerId.partyId
} }
case class GetLoot(partyId: String, playerId: Option[PlayerId], replyTo: ActorRef[Seq[Player]]) extends DatabaseMessage case class GetLoot(partyId: String, playerId: Option[PlayerId], replyTo: ActorRef[Seq[Player]])
extends LootDatabaseMessage
case class RemovePieceFrom(playerId: PlayerId, piece: Piece, replyTo: ActorRef[Unit]) extends DatabaseMessage { case class RemovePieceFrom(playerId: PlayerId, piece: Piece, replyTo: ActorRef[Unit]) extends LootDatabaseMessage {
override def partyId: String = playerId.partyId override def partyId: String = playerId.partyId
} }
case class SuggestLoot(partyId: String, piece: Piece, replyTo: ActorRef[LootSelector.LootSelectorResult]) case class SuggestLoot(partyId: String, piece: Piece, replyTo: ActorRef[LootSelector.LootSelectorResult])
extends DatabaseMessage extends LootDatabaseMessage
// party handler // party handler
case class AddPlayer(player: Player, replyTo: ActorRef[Unit]) extends DatabaseMessage { trait PartyDatabaseMessage extends DatabaseMessage
case class AddPlayer(player: Player, replyTo: ActorRef[Unit]) extends PartyDatabaseMessage {
override def partyId: String = player.partyId override def partyId: String = player.partyId
} }
case class GetParty(partyId: String, replyTo: ActorRef[Party]) extends DatabaseMessage case class GetParty(partyId: String, replyTo: ActorRef[Party]) extends PartyDatabaseMessage
case class GetPartyDescription(partyId: String, replyTo: ActorRef[PartyDescription]) extends DatabaseMessage case class GetPartyDescription(partyId: String, replyTo: ActorRef[PartyDescription]) extends PartyDatabaseMessage
case class GetPlayer(playerId: PlayerId, replyTo: ActorRef[Option[Player]]) extends DatabaseMessage { case class GetPlayer(playerId: PlayerId, replyTo: ActorRef[Option[Player]]) extends PartyDatabaseMessage {
override def partyId: String = playerId.partyId override def partyId: String = playerId.partyId
} }
case class RemovePlayer(playerId: PlayerId, replyTo: ActorRef[Unit]) extends DatabaseMessage { case class RemovePlayer(playerId: PlayerId, replyTo: ActorRef[Unit]) extends PartyDatabaseMessage {
override def partyId: String = playerId.partyId override def partyId: String = playerId.partyId
} }
case class UpdateParty(partyDescription: PartyDescription, replyTo: ActorRef[Unit]) extends DatabaseMessage { case class UpdateParty(partyDescription: PartyDescription, replyTo: ActorRef[Unit]) extends PartyDatabaseMessage {
override def partyId: String = partyDescription.partyId override def partyId: String = partyDescription.partyId
} }
// user handler // user handler
case class AddUser(user: User, isHashedPassword: Boolean, replyTo: ActorRef[Unit]) extends DatabaseMessage { trait UserDatabaseMessage extends DatabaseMessage
case class AddUser(user: User, isHashedPassword: Boolean, replyTo: ActorRef[Unit]) extends UserDatabaseMessage {
override def partyId: String = user.partyId override def partyId: String = user.partyId
} }
case class DeleteUser(partyId: String, username: String, replyTo: ActorRef[Unit]) extends DatabaseMessage case class DeleteUser(partyId: String, username: String, replyTo: ActorRef[Unit]) extends UserDatabaseMessage
case class Exists(partyId: String, replyTo: ActorRef[Boolean]) extends DatabaseMessage case class Exists(partyId: String, replyTo: ActorRef[Boolean]) extends UserDatabaseMessage
case class GetUser(partyId: String, username: String, replyTo: ActorRef[Option[User]]) extends DatabaseMessage case class GetUser(partyId: String, username: String, replyTo: ActorRef[Option[User]]) extends UserDatabaseMessage
case class GetUsers(partyId: String, replyTo: ActorRef[Seq[User]]) extends DatabaseMessage case class GetUsers(partyId: String, replyTo: ActorRef[Seq[User]]) extends UserDatabaseMessage

View File

@ -1,3 +1,11 @@
/*
* 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.messages package me.arcanis.ffxivbis.messages
import akka.actor.typed.Behavior import akka.actor.typed.Behavior
@ -5,5 +13,6 @@ import akka.actor.typed.Behavior
trait Message trait Message
object Message { object Message {
type Handler = PartialFunction[Message, Behavior[Message]] type Handler = PartialFunction[Message, Behavior[Message]]
} }

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2019 Evgeniy Alekseev. * Copyright (c) 2019-2022 Evgeniy Alekseev.
* *
* This file is part of ffxivbis * This file is part of ffxivbis
* (see https://github.com/arcan1s/ffxivbis). * (see https://github.com/arcan1s/ffxivbis).
@ -25,6 +25,7 @@ case class BiS(pieces: Seq[Piece]) {
.withDefaultValue(0) .withDefaultValue(0)
def withPiece(piece: Piece): BiS = copy(pieces :+ piece) def withPiece(piece: Piece): BiS = copy(pieces :+ piece)
def withoutPiece(piece: Piece): BiS = copy(pieces.filterNot(_.strictEqual(piece))) def withoutPiece(piece: Piece): BiS = copy(pieces.filterNot(_.strictEqual(piece)))
override def equals(obj: Any): Boolean = { override def equals(obj: Any): Boolean = {

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2019 Evgeniy Alekseev. * Copyright (c) 2019-2022 Evgeniy Alekseev.
* *
* This file is part of ffxivbis * This file is part of ffxivbis
* (see https://github.com/arcan1s/ffxivbis). * (see https://github.com/arcan1s/ffxivbis).
@ -29,6 +29,7 @@ object Job {
sealed trait Job extends Equals { sealed trait Job extends Equals {
def leftSide: LeftSide def leftSide: LeftSide
def rightSide: RightSide def rightSide: RightSide
// conversion to string to avoid recursion // conversion to string to avoid recursion

View File

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

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2019 Evgeniy Alekseev. * Copyright (c) 2019-2022 Evgeniy Alekseev.
* *
* This file is part of ffxivbis * This file is part of ffxivbis
* (see https://github.com/arcan1s/ffxivbis). * (see https://github.com/arcan1s/ffxivbis).
@ -17,10 +17,13 @@ import scala.util.Random
case class Party(partyDescription: PartyDescription, rules: Seq[String], players: Map[PlayerId, Player]) case class Party(partyDescription: PartyDescription, rules: Seq[String], players: Map[PlayerId, Player])
extends StrictLogging { extends StrictLogging {
require(players.keys.forall(_.partyId == partyDescription.partyId), "party id must be same") require(players.keys.forall(_.partyId == partyDescription.partyId), "party id must be same")
def getPlayers: Seq[Player] = players.values.toSeq def getPlayers: Seq[Player] = players.values.toSeq
def player(playerId: PlayerId): Option[Player] = players.get(playerId) def player(playerId: PlayerId): Option[Player] = players.get(playerId)
def withPlayer(player: Player): Party = def withPlayer(player: Player): Party =
try { try {
require(player.partyId == partyDescription.partyId, "player must belong to this party") require(player.partyId == partyDescription.partyId, "player must belong to this party")

View File

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

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2019 Evgeniy Alekseev. * Copyright (c) 2019-2022 Evgeniy Alekseev.
* *
* This file is part of ffxivbis * This file is part of ffxivbis
* (see https://github.com/arcan1s/ffxivbis). * (see https://github.com/arcan1s/ffxivbis).
@ -11,7 +11,9 @@ package me.arcanis.ffxivbis.models
sealed trait Piece extends Equals { sealed trait Piece extends Equals {
def pieceType: PieceType.PieceType def pieceType: PieceType.PieceType
def job: Job.Job def job: Job.Job
def piece: String def piece: String
def withJob(other: Job.Job): Piece def withJob(other: Job.Job): Piece

View File

@ -1,3 +1,11 @@
/*
* 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.models package me.arcanis.ffxivbis.models
object PieceType { object PieceType {

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2019 Evgeniy Alekseev. * Copyright (c) 2019-2022 Evgeniy Alekseev.
* *
* This file is part of ffxivbis * This file is part of ffxivbis
* (see https://github.com/arcan1s/ffxivbis). * (see https://github.com/arcan1s/ffxivbis).
@ -21,10 +21,12 @@ case class Player(
require(job ne Job.AnyJob, "AnyJob is not allowed") require(job ne Job.AnyJob, "AnyJob is not allowed")
val playerId: PlayerId = PlayerId(partyId, job, nick) val playerId: PlayerId = PlayerId(partyId, job, nick)
def withBiS(set: Option[BiS]): Player = set match { def withBiS(set: Option[BiS]): Player = set match {
case Some(value) => copy(bis = value) case Some(value) => copy(bis = value)
case None => this case None => this
} }
def withCounters(piece: Option[Piece]): PlayerIdWithCounters = def withCounters(piece: Option[Piece]): PlayerIdWithCounters =
PlayerIdWithCounters( PlayerIdWithCounters(
partyId, partyId,
@ -32,12 +34,14 @@ case class Player(
nick, nick,
isRequired(piece), isRequired(piece),
priority, priority,
bisCountTotal(piece), bisCountTotal,
lootCount(piece), lootCount(piece),
lootCountBiS(piece), lootCountBiS,
lootCountTotal(piece) lootCountTotal
) )
def withLoot(piece: Loot): Player = withLoot(Seq(piece)) def withLoot(piece: Loot): Player = withLoot(Seq(piece))
def withLoot(list: Seq[Loot]): Player = { def withLoot(list: Seq[Loot]): Player = {
require(loot.forall(_.playerId == id), "player id must be same") require(loot.forall(_.playerId == id), "player id must be same")
copy(loot = loot ++ list) copy(loot = loot ++ list)
@ -51,12 +55,16 @@ case class Player(
case Some(_) => lootCount(piece) == 0 case Some(_) => lootCount(piece) == 0
} }
def bisCountTotal(piece: Option[Piece]): Int = bis.pieces.count(_.pieceType == PieceType.Savage) def bisCountTotal: Int = bis.pieces.count(_.pieceType == PieceType.Savage)
def lootCount(piece: Option[Piece]): Int = piece match { def lootCount(piece: Option[Piece]): Int = piece match {
case Some(p) => loot.count(item => !item.isFreeLoot && item.piece == p) case Some(p) => loot.count(item => !item.isFreeLoot && item.piece == p)
case None => lootCountTotal(piece) case None => lootCountTotal
} }
def lootCountBiS(piece: Option[Piece]): Int = loot.map(_.piece).count(bis.hasPiece)
def lootCountTotal(piece: Option[Piece]): Int = loot.count(!_.isFreeLoot) def lootCountBiS: Int = loot.map(_.piece).count(bis.hasPiece)
def lootPriority(piece: Piece): Int = priority
def lootCountTotal: Int = loot.count(!_.isFreeLoot)
def lootPriority: Int = priority
} }

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2019 Evgeniy Alekseev. * Copyright (c) 2019-2022 Evgeniy Alekseev.
* *
* This file is part of ffxivbis * This file is part of ffxivbis
* (see https://github.com/arcan1s/ffxivbis). * (see https://github.com/arcan1s/ffxivbis).
@ -14,6 +14,7 @@ import scala.util.matching.Regex
trait PlayerIdBase { trait PlayerIdBase {
def job: Job.Job def job: Job.Job
def nick: String def nick: String
override def toString: String = s"$nick ($job)" override def toString: String = s"$nick ($job)"

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2019 Evgeniy Alekseev. * Copyright (c) 2019-2022 Evgeniy Alekseev.
* *
* This file is part of ffxivbis * This file is part of ffxivbis
* (see https://github.com/arcan1s/ffxivbis). * (see https://github.com/arcan1s/ffxivbis).
@ -23,7 +23,9 @@ case class PlayerIdWithCounters(
def gt(that: PlayerIdWithCounters, orderBy: Seq[String]): Boolean = def gt(that: PlayerIdWithCounters, orderBy: Seq[String]): Boolean =
withCounters(orderBy) > that.withCounters(orderBy) withCounters(orderBy) > that.withCounters(orderBy)
def isRequiredToString: String = if (isRequired) "yes" else "no" def isRequiredToString: String = if (isRequired) "yes" else "no"
def playerId: PlayerId = PlayerId(partyId, job, nick) def playerId: PlayerId = PlayerId(partyId, job, nick)
private val counters: Map[String, Int] = Map( private val counters: Map[String, Int] = Map(
@ -42,6 +44,7 @@ case class PlayerIdWithCounters(
object PlayerIdWithCounters { object PlayerIdWithCounters {
private case class PlayerCountersComparator(values: Int*) { private case class PlayerCountersComparator(values: Int*) {
def >(that: PlayerCountersComparator): Boolean = { def >(that: PlayerCountersComparator): Boolean = {
@scala.annotation.tailrec @scala.annotation.tailrec
def compareLists(left: List[Int], right: List[Int]): Boolean = def compareLists(left: List[Int], right: List[Int]): Boolean =

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2019 Evgeniy Alekseev. * Copyright (c) 2019-2022 Evgeniy Alekseev.
* *
* This file is part of ffxivbis * This file is part of ffxivbis
* (see https://github.com/arcan1s/ffxivbis). * (see https://github.com/arcan1s/ffxivbis).
@ -17,7 +17,10 @@ object Permission extends Enumeration {
case class User(partyId: String, username: String, password: String, permission: Permission.Value) { case class User(partyId: String, username: String, password: String, permission: Permission.Value) {
def hash: String = BCrypt.hashpw(password, BCrypt.gensalt) def hash: String = BCrypt.hashpw(password, BCrypt.gensalt)
def verify(plain: String): Boolean = BCrypt.checkpw(plain, password) def verify(plain: String): Boolean = BCrypt.checkpw(plain, password)
def verityScope(scope: Permission.Value): Boolean = permission >= scope def verityScope(scope: Permission.Value): Boolean = permission >= scope
def withHashedPassword: User = copy(password = hash) def withHashedPassword: User = copy(password = hash)
} }

View File

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

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2019 Evgeniy Alekseev. * Copyright (c) 2019-2022 Evgeniy Alekseev.
* *
* This file is part of ffxivbis * This file is part of ffxivbis
* (see https://github.com/arcan1s/ffxivbis). * (see https://github.com/arcan1s/ffxivbis).
@ -9,11 +9,11 @@
package me.arcanis.ffxivbis.service package me.arcanis.ffxivbis.service
import akka.actor.typed.scaladsl.AskPattern.Askable import akka.actor.typed.scaladsl.AskPattern.Askable
import akka.actor.typed.{ActorRef, Behavior, DispatcherSelector, Scheduler}
import akka.actor.typed.scaladsl.{AbstractBehavior, ActorContext, Behaviors} import akka.actor.typed.scaladsl.{AbstractBehavior, ActorContext, Behaviors}
import akka.actor.typed.{ActorRef, Behavior, DispatcherSelector, Scheduler}
import akka.util.Timeout import akka.util.Timeout
import com.typesafe.scalalogging.StrictLogging import com.typesafe.scalalogging.StrictLogging
import me.arcanis.ffxivbis.messages.{DatabaseMessage, Exists, ForgetParty, GetNewPartyId, GetParty, Message, StoreParty} import me.arcanis.ffxivbis.messages._
import me.arcanis.ffxivbis.models.Party import me.arcanis.ffxivbis.models.Party
import scala.concurrent.duration.FiniteDuration import scala.concurrent.duration.FiniteDuration

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2019 Evgeniy Alekseev. * Copyright (c) 2019-2022 Evgeniy Alekseev.
* *
* This file is part of ffxivbis * This file is part of ffxivbis
* (see https://github.com/arcan1s/ffxivbis). * (see https://github.com/arcan1s/ffxivbis).
@ -8,10 +8,9 @@
*/ */
package me.arcanis.ffxivbis.service.bis package me.arcanis.ffxivbis.service.bis
import java.nio.file.Paths
import akka.actor.ClassicActorSystemProvider import akka.actor.ClassicActorSystemProvider
import akka.actor.typed.{Behavior, PostStop, Signal}
import akka.actor.typed.scaladsl.{AbstractBehavior, ActorContext, Behaviors} import akka.actor.typed.scaladsl.{AbstractBehavior, ActorContext, Behaviors}
import akka.actor.typed.{Behavior, PostStop, Signal}
import akka.http.scaladsl.model._ import akka.http.scaladsl.model._
import com.typesafe.scalalogging.StrictLogging import com.typesafe.scalalogging.StrictLogging
import me.arcanis.ffxivbis.messages.{BiSProviderMessage, DownloadBiS} import me.arcanis.ffxivbis.messages.{BiSProviderMessage, DownloadBiS}
@ -20,6 +19,7 @@ import me.arcanis.ffxivbis.service.bis.parser.Parser
import me.arcanis.ffxivbis.service.bis.parser.impl.{Ariyala, Etro} import me.arcanis.ffxivbis.service.bis.parser.impl.{Ariyala, Etro}
import spray.json._ import spray.json._
import java.nio.file.Paths
import scala.concurrent.{ExecutionContext, Future} import scala.concurrent.{ExecutionContext, Future}
import scala.util.{Failure, Success} import scala.util.{Failure, Success}

View File

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

View File

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

View File

@ -1,3 +1,11 @@
/*
* Copyright (c) 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 package me.arcanis.ffxivbis.service.bis.parser
import akka.http.scaladsl.model.Uri import akka.http.scaladsl.model.Uri

View File

@ -1,3 +1,11 @@
/*
* 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 package me.arcanis.ffxivbis.service.bis.parser.impl
import akka.http.scaladsl.model.Uri import akka.http.scaladsl.model.Uri

View File

@ -1,3 +1,11 @@
/*
* 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 package me.arcanis.ffxivbis.service.bis.parser.impl
import akka.http.scaladsl.model.Uri import akka.http.scaladsl.model.Uri

View File

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

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2019 Evgeniy Alekseev. * Copyright (c) 2019-2022 Evgeniy Alekseev.
* *
* This file is part of ffxivbis * This file is part of ffxivbis
* (see https://github.com/arcan1s/ffxivbis). * (see https://github.com/arcan1s/ffxivbis).
@ -9,7 +9,7 @@
package me.arcanis.ffxivbis.service.database.impl package me.arcanis.ffxivbis.service.database.impl
import akka.actor.typed.scaladsl.Behaviors import akka.actor.typed.scaladsl.Behaviors
import me.arcanis.ffxivbis.messages.{AddPieceToBis, DatabaseMessage, GetBiS, RemovePieceFromBiS, RemovePiecesFromBiS} import me.arcanis.ffxivbis.messages._
import me.arcanis.ffxivbis.service.database.Database import me.arcanis.ffxivbis.service.database.Database
trait DatabaseBiSHandler { this: Database => trait DatabaseBiSHandler { this: Database =>

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2019 Evgeniy Alekseev. * Copyright (c) 2019-2022 Evgeniy Alekseev.
* *
* This file is part of ffxivbis * This file is part of ffxivbis
* (see https://github.com/arcan1s/ffxivbis). * (see https://github.com/arcan1s/ffxivbis).
@ -8,8 +8,8 @@
*/ */
package me.arcanis.ffxivbis.service.database.impl package me.arcanis.ffxivbis.service.database.impl
import akka.actor.typed.{Behavior, DispatcherSelector}
import akka.actor.typed.scaladsl.{AbstractBehavior, ActorContext} import akka.actor.typed.scaladsl.{AbstractBehavior, ActorContext}
import akka.actor.typed.{Behavior, DispatcherSelector}
import com.typesafe.config.Config import com.typesafe.config.Config
import me.arcanis.ffxivbis.messages.DatabaseMessage import me.arcanis.ffxivbis.messages.DatabaseMessage
import me.arcanis.ffxivbis.service.database.Database import me.arcanis.ffxivbis.service.database.Database

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2019 Evgeniy Alekseev. * Copyright (c) 2019-2022 Evgeniy Alekseev.
* *
* This file is part of ffxivbis * This file is part of ffxivbis
* (see https://github.com/arcan1s/ffxivbis). * (see https://github.com/arcan1s/ffxivbis).
@ -8,12 +8,13 @@
*/ */
package me.arcanis.ffxivbis.service.database.impl package me.arcanis.ffxivbis.service.database.impl
import java.time.Instant
import akka.actor.typed.scaladsl.Behaviors import akka.actor.typed.scaladsl.Behaviors
import me.arcanis.ffxivbis.messages.{AddPieceTo, DatabaseMessage, GetLoot, RemovePieceFrom, SuggestLoot} import me.arcanis.ffxivbis.messages._
import me.arcanis.ffxivbis.models.Loot import me.arcanis.ffxivbis.models.Loot
import me.arcanis.ffxivbis.service.database.Database import me.arcanis.ffxivbis.service.database.Database
import java.time.Instant
trait DatabaseLootHandler { this: Database => trait DatabaseLootHandler { this: Database =>
def lootHandler: DatabaseMessage.Handler = { def lootHandler: DatabaseMessage.Handler = {

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2019 Evgeniy Alekseev. * Copyright (c) 2019-2022 Evgeniy Alekseev.
* *
* This file is part of ffxivbis * This file is part of ffxivbis
* (see https://github.com/arcan1s/ffxivbis). * (see https://github.com/arcan1s/ffxivbis).
@ -9,7 +9,7 @@
package me.arcanis.ffxivbis.service.database.impl package me.arcanis.ffxivbis.service.database.impl
import akka.actor.typed.scaladsl.Behaviors import akka.actor.typed.scaladsl.Behaviors
import me.arcanis.ffxivbis.messages.{AddPlayer, DatabaseMessage, GetParty, GetPartyDescription, GetPlayer, RemovePlayer, UpdateParty} import me.arcanis.ffxivbis.messages._
import me.arcanis.ffxivbis.models.{BiS, Player} import me.arcanis.ffxivbis.models.{BiS, Player}
import me.arcanis.ffxivbis.service.database.Database import me.arcanis.ffxivbis.service.database.Database

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2019 Evgeniy Alekseev. * Copyright (c) 2019-2022 Evgeniy Alekseev.
* *
* This file is part of ffxivbis * This file is part of ffxivbis
* (see https://github.com/arcan1s/ffxivbis). * (see https://github.com/arcan1s/ffxivbis).
@ -9,7 +9,7 @@
package me.arcanis.ffxivbis.service.database.impl package me.arcanis.ffxivbis.service.database.impl
import akka.actor.typed.scaladsl.Behaviors import akka.actor.typed.scaladsl.Behaviors
import me.arcanis.ffxivbis.messages.{AddUser, DatabaseMessage, DeleteUser, Exists, GetUser, GetUsers} import me.arcanis.ffxivbis.messages._
import me.arcanis.ffxivbis.service.database.Database import me.arcanis.ffxivbis.service.database.Database
trait DatabaseUserHandler { this: Database => trait DatabaseUserHandler { this: Database =>

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2019 Evgeniy Alekseev. * Copyright (c) 2019-2022 Evgeniy Alekseev.
* *
* This file is part of ffxivbis * This file is part of ffxivbis
* (see https://github.com/arcan1s/ffxivbis). * (see https://github.com/arcan1s/ffxivbis).
@ -8,17 +8,17 @@
*/ */
package me.arcanis.ffxivbis.storage package me.arcanis.ffxivbis.storage
import java.time.Instant
import me.arcanis.ffxivbis.models.{Job, Loot, Piece, PieceType} import me.arcanis.ffxivbis.models.{Job, Loot, Piece, PieceType}
import slick.lifted.ForeignKeyQuery import slick.lifted.ForeignKeyQuery
import java.time.Instant
import scala.concurrent.Future import scala.concurrent.Future
trait BiSProfile { this: DatabaseProfile => trait BiSProfile { this: DatabaseProfile =>
import dbConfig.profile.api._ import dbConfig.profile.api._
case class BiSRep(playerId: Long, created: Long, piece: String, pieceType: String, job: String) { case class BiSRep(playerId: Long, created: Long, piece: String, pieceType: String, job: String) {
def toLoot: Loot = Loot( def toLoot: Loot = Loot(
playerId, playerId,
Piece(piece, PieceType.withName(pieceType), Job.withName(job)), Piece(piece, PieceType.withName(pieceType), Job.withName(job)),
@ -26,7 +26,9 @@ trait BiSProfile { this: DatabaseProfile =>
isFreeLoot = false isFreeLoot = false
) )
} }
object BiSRep { object BiSRep {
def fromPiece(playerId: Long, piece: Piece): BiSRep = def fromPiece(playerId: Long, piece: Piece): BiSRep =
BiSRep(playerId, DatabaseProfile.now, piece.piece, piece.pieceType.toString, piece.job.toString) BiSRep(playerId, DatabaseProfile.now, piece.piece, piece.pieceType.toString, piece.job.toString)
} }
@ -47,11 +49,15 @@ trait BiSProfile { this: DatabaseProfile =>
def deletePieceBiSById(piece: Piece)(playerId: Long): Future[Int] = def deletePieceBiSById(piece: Piece)(playerId: Long): Future[Int] =
db.run(pieceBiS(BiSRep.fromPiece(playerId, piece)).delete) db.run(pieceBiS(BiSRep.fromPiece(playerId, piece)).delete)
def deletePiecesBiSById(playerId: Long): Future[Int] = def deletePiecesBiSById(playerId: Long): Future[Int] =
db.run(piecesBiS(Seq(playerId)).delete) db.run(piecesBiS(Seq(playerId)).delete)
def getPiecesBiSById(playerId: Long): Future[Seq[Loot]] = getPiecesBiSById(Seq(playerId)) def getPiecesBiSById(playerId: Long): Future[Seq[Loot]] = getPiecesBiSById(Seq(playerId))
def getPiecesBiSById(playerIds: Seq[Long]): Future[Seq[Loot]] = def getPiecesBiSById(playerIds: Seq[Long]): Future[Seq[Loot]] =
db.run(piecesBiS(playerIds).result).map(_.map(_.toLoot)) db.run(piecesBiS(playerIds).result).map(_.map(_.toLoot))
def insertPieceBiSById(piece: Piece)(playerId: Long): Future[Int] = def insertPieceBiSById(piece: Piece)(playerId: Long): Future[Int] =
getPiecesBiSById(playerId).flatMap { getPiecesBiSById(playerId).flatMap {
case pieces if pieces.exists(loot => loot.piece.strictEqual(piece)) => Future.successful(0) case pieces if pieces.exists(loot => loot.piece.strictEqual(piece)) => Future.successful(0)

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2019 Evgeniy Alekseev. * Copyright (c) 2019-2022 Evgeniy Alekseev.
* *
* This file is part of ffxivbis * This file is part of ffxivbis
* (see https://github.com/arcan1s/ffxivbis). * (see https://github.com/arcan1s/ffxivbis).
@ -8,13 +8,12 @@
*/ */
package me.arcanis.ffxivbis.storage package me.arcanis.ffxivbis.storage
import java.time.Instant
import com.typesafe.config.Config import com.typesafe.config.Config
import me.arcanis.ffxivbis.models.{Loot, Piece, PlayerId} import me.arcanis.ffxivbis.models.{Loot, Piece, PlayerId}
import slick.basic.DatabaseConfig import slick.basic.DatabaseConfig
import slick.jdbc.JdbcProfile import slick.jdbc.JdbcProfile
import java.time.Instant
import scala.concurrent.{ExecutionContext, Future} import scala.concurrent.{ExecutionContext, Future}
class DatabaseProfile(context: ExecutionContext, config: Config) class DatabaseProfile(context: ExecutionContext, config: Config)
@ -40,12 +39,16 @@ class DatabaseProfile(context: ExecutionContext, config: Config)
// generic bis api // generic bis api
def deletePieceBiS(playerId: PlayerId, piece: Piece): Future[Int] = def deletePieceBiS(playerId: PlayerId, piece: Piece): Future[Int] =
byPlayerId(playerId, deletePieceBiSById(piece)) byPlayerId(playerId, deletePieceBiSById(piece))
def deletePiecesBiS(playerId: PlayerId): Future[Int] = def deletePiecesBiS(playerId: PlayerId): Future[Int] =
byPlayerId(playerId, deletePiecesBiSById) byPlayerId(playerId, deletePiecesBiSById)
def getPiecesBiS(playerId: PlayerId): Future[Seq[Loot]] = def getPiecesBiS(playerId: PlayerId): Future[Seq[Loot]] =
byPlayerId(playerId, getPiecesBiSById) byPlayerId(playerId, getPiecesBiSById)
def getPiecesBiS(partyId: String): Future[Seq[Loot]] = def getPiecesBiS(partyId: String): Future[Seq[Loot]] =
byPartyId(partyId, getPiecesBiSById) byPartyId(partyId, getPiecesBiSById)
def insertPieceBiS(playerId: PlayerId, piece: Piece): Future[Int] = def insertPieceBiS(playerId: PlayerId, piece: Piece): Future[Int] =
byPlayerId(playerId, insertPieceBiSById(piece)) byPlayerId(playerId, insertPieceBiSById(piece))
@ -55,15 +58,19 @@ class DatabaseProfile(context: ExecutionContext, config: Config)
val loot = Loot(-1, piece, Instant.now, isFreeLoot = false) val loot = Loot(-1, piece, Instant.now, isFreeLoot = false)
byPlayerId(playerId, deletePieceById(loot)) byPlayerId(playerId, deletePieceById(loot))
} }
def getPieces(playerId: PlayerId): Future[Seq[Loot]] = def getPieces(playerId: PlayerId): Future[Seq[Loot]] =
byPlayerId(playerId, getPiecesById) byPlayerId(playerId, getPiecesById)
def getPieces(partyId: String): Future[Seq[Loot]] = def getPieces(partyId: String): Future[Seq[Loot]] =
byPartyId(partyId, getPiecesById) byPartyId(partyId, getPiecesById)
def insertPiece(playerId: PlayerId, loot: Loot): Future[Int] = def insertPiece(playerId: PlayerId, loot: Loot): Future[Int] =
byPlayerId(playerId, insertPieceById(loot)) byPlayerId(playerId, insertPieceById(loot))
private def byPartyId[T](partyId: String, callback: Seq[Long] => Future[T]): Future[T] = private def byPartyId[T](partyId: String, callback: Seq[Long] => Future[T]): Future[T] =
getPlayers(partyId).flatMap(callback) getPlayers(partyId).flatMap(callback)
private def byPlayerId[T](playerId: PlayerId, callback: Long => Future[T]): Future[T] = private def byPlayerId[T](playerId: PlayerId, callback: Long => Future[T]): Future[T] =
getPlayer(playerId).flatMap { getPlayer(playerId).flatMap {
case Some(id) => callback(id) case Some(id) => callback(id)
@ -74,6 +81,7 @@ class DatabaseProfile(context: ExecutionContext, config: Config)
object DatabaseProfile { object DatabaseProfile {
def now: Long = Instant.now.toEpochMilli def now: Long = Instant.now.toEpochMilli
def getSection(config: Config): Config = { def getSection(config: Config): Config = {
val section = config.getString("me.arcanis.ffxivbis.database.mode") val section = config.getString("me.arcanis.ffxivbis.database.mode")
config.getConfig("me.arcanis.ffxivbis.database").getConfig(section) config.getConfig("me.arcanis.ffxivbis.database").getConfig(section)

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2019 Evgeniy Alekseev. * Copyright (c) 2019-2022 Evgeniy Alekseev.
* *
* This file is part of ffxivbis * This file is part of ffxivbis
* (see https://github.com/arcan1s/ffxivbis). * (see https://github.com/arcan1s/ffxivbis).
@ -8,11 +8,10 @@
*/ */
package me.arcanis.ffxivbis.storage package me.arcanis.ffxivbis.storage
import java.time.Instant
import me.arcanis.ffxivbis.models.{Job, Loot, Piece, PieceType} import me.arcanis.ffxivbis.models.{Job, Loot, Piece, PieceType}
import slick.lifted.{ForeignKeyQuery, Index} import slick.lifted.{ForeignKeyQuery, Index}
import java.time.Instant
import scala.concurrent.Future import scala.concurrent.Future
trait LootProfile { this: DatabaseProfile => trait LootProfile { this: DatabaseProfile =>
@ -27,6 +26,7 @@ trait LootProfile { this: DatabaseProfile =>
job: String, job: String,
isFreeLoot: Int isFreeLoot: Int
) { ) {
def toLoot: Loot = Loot( def toLoot: Loot = Loot(
playerId, playerId,
Piece(piece, PieceType.withName(pieceType), Job.withName(job)), Piece(piece, PieceType.withName(pieceType), Job.withName(job)),
@ -34,6 +34,7 @@ trait LootProfile { this: DatabaseProfile =>
isFreeLoot == 1 isFreeLoot == 1
) )
} }
object LootRep { object LootRep {
def fromLoot(playerId: Long, loot: Loot): LootRep = def fromLoot(playerId: Long, loot: Loot): LootRep =
LootRep( LootRep(
@ -70,14 +71,18 @@ trait LootProfile { this: DatabaseProfile =>
case Some(id) => db.run(lootTable.filter(_.lootId === id).delete) case Some(id) => db.run(lootTable.filter(_.lootId === id).delete)
case _ => throw new IllegalArgumentException(s"Could not find piece $loot belong to $playerId") case _ => throw new IllegalArgumentException(s"Could not find piece $loot belong to $playerId")
} }
def getPiecesById(playerId: Long): Future[Seq[Loot]] = getPiecesById(Seq(playerId)) def getPiecesById(playerId: Long): Future[Seq[Loot]] = getPiecesById(Seq(playerId))
def getPiecesById(playerIds: Seq[Long]): Future[Seq[Loot]] = def getPiecesById(playerIds: Seq[Long]): Future[Seq[Loot]] =
db.run(piecesLoot(playerIds).result).map(_.map(_.toLoot)) db.run(piecesLoot(playerIds).result).map(_.map(_.toLoot))
def insertPieceById(loot: Loot)(playerId: Long): Future[Int] = def insertPieceById(loot: Loot)(playerId: Long): Future[Int] =
db.run(lootTable.insertOrUpdate(LootRep.fromLoot(playerId, loot))) db.run(lootTable.insertOrUpdate(LootRep.fromLoot(playerId, loot)))
private def pieceLoot(piece: LootRep) = private def pieceLoot(piece: LootRep) =
piecesLoot(Seq(piece.playerId)).filter(_.piece === piece.piece) piecesLoot(Seq(piece.playerId)).filter(_.piece === piece.piece)
private def piecesLoot(playerIds: Seq[Long]) = private def piecesLoot(playerIds: Seq[Long]) =
lootTable.filter(_.playerId.inSet(playerIds.toSet)) lootTable.filter(_.playerId.inSet(playerIds.toSet))
} }

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2019 Evgeniy Alekseev. * Copyright (c) 2019-2022 Evgeniy Alekseev.
* *
* This file is part of ffxivbis * This file is part of ffxivbis
* (see https://github.com/arcan1s/ffxivbis). * (see https://github.com/arcan1s/ffxivbis).
@ -21,8 +21,8 @@ class Migration(config: Config) {
val section = DatabaseProfile.getSection(config) val section = DatabaseProfile.getSection(config)
val url = section.getString("db.url") val url = section.getString("db.url")
val username = section.getString("db.user") val username = Try(section.getString("db.user")).toOption.filter(_.nonEmpty).orNull
val password = section.getString("db.password") val password = Try(section.getString("db.password")).toOption.filter(_.nonEmpty).orNull
val provider = url match { val provider = url match {
case s"jdbc:$p:$_" => p case s"jdbc:$p:$_" => p

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2020 Evgeniy Alekseev. * Copyright (c) 2019-2022 Evgeniy Alekseev.
* *
* This file is part of ffxivbis * This file is part of ffxivbis
* (see https://github.com/arcan1s/ffxivbis). * (see https://github.com/arcan1s/ffxivbis).
@ -16,9 +16,12 @@ trait PartyProfile { this: DatabaseProfile =>
import dbConfig.profile.api._ import dbConfig.profile.api._
case class PartyRep(partyId: Option[Long], partyName: String, partyAlias: Option[String]) { case class PartyRep(partyId: Option[Long], partyName: String, partyAlias: Option[String]) {
def toDescription: PartyDescription = PartyDescription(partyName, partyAlias) def toDescription: PartyDescription = PartyDescription(partyName, partyAlias)
} }
object PartyRep { object PartyRep {
def fromDescription(party: PartyDescription, id: Option[Long]): PartyRep = def fromDescription(party: PartyDescription, id: Option[Long]): PartyRep =
PartyRep(id, party.partyId, party.partyAlias) PartyRep(id, party.partyId, party.partyAlias)
} }
@ -36,8 +39,10 @@ trait PartyProfile { this: DatabaseProfile =>
db.run( db.run(
partyDescription(partyId).result.headOption.map(_.map(_.toDescription).getOrElse(PartyDescription.empty(partyId))) partyDescription(partyId).result.headOption.map(_.map(_.toDescription).getOrElse(PartyDescription.empty(partyId)))
) )
def getUniquePartyId(partyId: String): Future[Option[Long]] = def getUniquePartyId(partyId: String): Future[Option[Long]] =
db.run(partyDescription(partyId).map(_.partyId).result.headOption) db.run(partyDescription(partyId).map(_.partyId).result.headOption)
def insertPartyDescription(partyDescription: PartyDescription): Future[Int] = def insertPartyDescription(partyDescription: PartyDescription): Future[Int] =
getUniquePartyId(partyDescription.partyId).flatMap { getUniquePartyId(partyDescription.partyId).flatMap {
case Some(id) => db.run(partiesTable.update(PartyRep.fromDescription(partyDescription, Some(id)))) case Some(id) => db.run(partiesTable.update(PartyRep.fromDescription(partyDescription, Some(id))))

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2019 Evgeniy Alekseev. * Copyright (c) 2019-2022 Evgeniy Alekseev.
* *
* This file is part of ffxivbis * This file is part of ffxivbis
* (see https://github.com/arcan1s/ffxivbis). * (see https://github.com/arcan1s/ffxivbis).
@ -24,10 +24,13 @@ trait PlayersProfile { this: DatabaseProfile =>
link: Option[String], link: Option[String],
priority: Int priority: Int
) { ) {
def toPlayer: Player = def toPlayer: Player =
Player(playerId.getOrElse(-1), partyId, Job.withName(job), nick, BiS.empty, Seq.empty, link, priority) Player(playerId.getOrElse(-1), partyId, Job.withName(job), nick, BiS.empty, Seq.empty, link, priority)
} }
object PlayerRep { object PlayerRep {
def fromPlayer(player: Player, id: Option[Long]): PlayerRep = def fromPlayer(player: Player, id: Option[Long]): PlayerRep =
PlayerRep(player.partyId, id, DatabaseProfile.now, player.nick, player.job.toString, player.link, player.priority) PlayerRep(player.partyId, id, DatabaseProfile.now, player.nick, player.job.toString, player.link, player.priority)
} }
@ -46,18 +49,23 @@ trait PlayersProfile { this: DatabaseProfile =>
} }
def deletePlayer(playerId: PlayerId): Future[Int] = db.run(player(playerId).delete) def deletePlayer(playerId: PlayerId): Future[Int] = db.run(player(playerId).delete)
def getParty(partyId: String): Future[Map[Long, Player]] = def getParty(partyId: String): Future[Map[Long, Player]] =
db.run(players(partyId).result) db.run(players(partyId).result)
.map(_.foldLeft(Map.empty[Long, Player]) { .map(_.foldLeft(Map.empty[Long, Player]) {
case (acc, p @ PlayerRep(_, Some(id), _, _, _, _, _)) => acc + (id -> p.toPlayer) case (acc, p @ PlayerRep(_, Some(id), _, _, _, _, _)) => acc + (id -> p.toPlayer)
case (acc, _) => acc case (acc, _) => acc
}) })
def getPlayer(playerId: PlayerId): Future[Option[Long]] = def getPlayer(playerId: PlayerId): Future[Option[Long]] =
db.run(player(playerId).map(_.playerId).result.headOption) db.run(player(playerId).map(_.playerId).result.headOption)
def getPlayerFull(playerId: PlayerId): Future[Option[Player]] = def getPlayerFull(playerId: PlayerId): Future[Option[Player]] =
db.run(player(playerId).result.headOption.map(_.map(_.toPlayer))) db.run(player(playerId).result.headOption.map(_.map(_.toPlayer)))
def getPlayers(partyId: String): Future[Seq[Long]] = def getPlayers(partyId: String): Future[Seq[Long]] =
db.run(players(partyId).map(_.playerId).result) db.run(players(partyId).map(_.playerId).result)
def insertPlayer(playerObj: Player): Future[Int] = def insertPlayer(playerObj: Player): Future[Int] =
getPlayer(playerObj.playerId).flatMap { getPlayer(playerObj.playerId).flatMap {
case Some(id) => db.run(playersTable.update(PlayerRep.fromPlayer(playerObj, Some(id)))) case Some(id) => db.run(playersTable.update(PlayerRep.fromPlayer(playerObj, Some(id))))
@ -69,6 +77,7 @@ trait PlayersProfile { this: DatabaseProfile =>
.filter(_.partyId === playerId.partyId) .filter(_.partyId === playerId.partyId)
.filter(_.job === playerId.job.toString) .filter(_.job === playerId.job.toString)
.filter(_.nick === playerId.nick) .filter(_.nick === playerId.nick)
private def players(partyId: String) = private def players(partyId: String) =
playersTable.filter(_.partyId === partyId) playersTable.filter(_.partyId === partyId)
} }

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