mirror of
https://github.com/arcan1s/ffxivbis.git
synced 2025-07-06 10:25:53 +00:00
Compare commits
18 Commits
Author | SHA1 | Date | |
---|---|---|---|
feea01a47e | |||
fcacd9f15c | |||
b2256784dd | |||
fee87ddbc8 | |||
dc882b74bf | |||
7a6cd84ce3 | |||
33b750123d | |||
d049238dcf | |||
5d72852420 | |||
78a00e2cab | |||
786c3d7d48 | |||
8a1d99b319 | |||
ac0e0ac899 | |||
e88c9d51b0 | |||
ced781bba2 | |||
012cdd2d8b | |||
c5b0832d29 | |||
b36240765a |
1
.gitignore
vendored
1
.gitignore
vendored
@ -75,6 +75,7 @@ lib_managed/
|
||||
src_managed/
|
||||
project/boot/
|
||||
project/plugins/project/
|
||||
.bsp/
|
||||
|
||||
# Scala-IDE specific
|
||||
.scala_dependencies
|
||||
|
4
Makefile
4
Makefile
@ -15,10 +15,10 @@ compile: clean
|
||||
format:
|
||||
sbt scalafmt
|
||||
|
||||
dist: tests version
|
||||
dist: tests
|
||||
sbt dist
|
||||
|
||||
push: dist
|
||||
push: version dist
|
||||
git add version.sbt
|
||||
git commit -m "Release $(VERSION)"
|
||||
git tag "$(VERSION)"
|
||||
|
@ -22,10 +22,10 @@ from the extracted archive root.
|
||||
|
||||
## Web service
|
||||
|
||||
REST API documentation is available at `http://0.0.0.0:8000/swagger`. HTML representation is available at `http://0.0.0.0:8000`.
|
||||
REST API documentation is available at `http://0.0.0.0:8000/api-docs`. HTML representation is available at `http://0.0.0.0:8000`.
|
||||
|
||||
*Note*: host and port depend on configuration settings.
|
||||
|
||||
## Public service
|
||||
|
||||
There is also public service which is available at http://ffxivbis.arcanis.me.
|
||||
There is also public service which is available at https://ffxivbis.arcanis.me.
|
||||
|
11
build.sbt
11
build.sbt
@ -1,3 +1,5 @@
|
||||
organization := "me.arcanis"
|
||||
|
||||
name := "ffxivbis"
|
||||
|
||||
scalaVersion := "2.13.6"
|
||||
@ -5,12 +7,3 @@ scalaVersion := "2.13.6"
|
||||
scalacOptions ++= Seq("-deprecation", "-feature")
|
||||
|
||||
enablePlugins(JavaAppPackaging)
|
||||
|
||||
assemblyMergeStrategy in assembly := {
|
||||
case PathList("META-INF", xs @ _*) => MergeStrategy.discard
|
||||
case "application.conf" => MergeStrategy.concat
|
||||
case "module-info.class" => MergeStrategy.first
|
||||
case x =>
|
||||
val oldStrategy = (assemblyMergeStrategy in assembly).value
|
||||
oldStrategy(x)
|
||||
}
|
@ -1,9 +1,9 @@
|
||||
val AkkaVersion = "2.6.17"
|
||||
val AkkaVersion = "2.6.18"
|
||||
val AkkaHttpVersion = "10.2.7"
|
||||
val ScalaTestVersion = "3.2.10"
|
||||
val SlickVersion = "3.3.3"
|
||||
|
||||
libraryDependencies += "ch.qos.logback" % "logback-classic" % "1.2.9"
|
||||
libraryDependencies += "ch.qos.logback" % "logback-classic" % "1.2.10"
|
||||
libraryDependencies += "com.typesafe.scala-logging" %% "scala-logging" % "3.9.4"
|
||||
|
||||
libraryDependencies += "com.typesafe.akka" %% "akka-http" % AkkaHttpVersion
|
||||
@ -15,16 +15,15 @@ libraryDependencies += "jakarta.platform" % "jakarta.jakartaee-web-api" % "9.1.0
|
||||
|
||||
libraryDependencies += "io.spray" %% "spray-json" % "1.3.6"
|
||||
|
||||
libraryDependencies += "com.typesafe.slick" %% "slick" % SlickVersion
|
||||
libraryDependencies += "com.typesafe.slick" %% "slick-hikaricp" % SlickVersion
|
||||
libraryDependencies += "org.flywaydb" % "flyway-core" % "8.2.2"
|
||||
libraryDependencies += "org.playframework.anorm" %% "anorm" % "2.6.10"
|
||||
libraryDependencies += "com.zaxxer" % "HikariCP" % "5.0.1" exclude("org.slf4j", "slf4j-api")
|
||||
libraryDependencies += "org.flywaydb" % "flyway-core" % "8.4.1"
|
||||
libraryDependencies += "org.xerial" % "sqlite-jdbc" % "3.36.0.3"
|
||||
libraryDependencies += "org.postgresql" % "postgresql" % "42.3.1"
|
||||
|
||||
libraryDependencies += "org.mindrot" % "jbcrypt" % "0.4"
|
||||
libraryDependencies += "com.google.guava" % "guava" % "31.0.1-jre"
|
||||
|
||||
|
||||
// testing
|
||||
libraryDependencies += "org.scalactic" %% "scalactic" % ScalaTestVersion % "test"
|
||||
libraryDependencies += "org.scalatest" %% "scalatest" % ScalaTestVersion % "test"
|
||||
|
@ -1 +1 @@
|
||||
sbt.version = 1.3.3
|
||||
sbt.version = 1.5.8
|
||||
|
@ -1,4 +1,4 @@
|
||||
addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.14.6")
|
||||
addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.4.3")
|
||||
addSbtPlugin("org.scoverage" % "sbt-scoverage" % "1.9.3")
|
||||
addSbtPlugin("com.typesafe.sbt" % "sbt-native-packager" % "1.3.4")
|
||||
addSbtPlugin("net.virtual-void" % "sbt-dependency-graph" % "0.10.0-RC1")
|
||||
addDependencyTreePlugin
|
||||
|
@ -0,0 +1,2 @@
|
||||
drop index bis_piece_type_player_id_idx;
|
||||
create unique index bis_piece_type_player_id_idx on bis(player_id, piece, piece_type);
|
@ -0,0 +1,2 @@
|
||||
drop index bis_piece_type_player_id_idx;
|
||||
create unique index bis_piece_type_player_id_idx on bis(player_id, piece, piece_type);
|
@ -6,17 +6,17 @@
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
|
||||
<link href="/static/favicon.ico" rel="shortcut icon">
|
||||
<link rel="shortcut icon" href="/static/favicon.ico">
|
||||
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous" type="text/css">
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.7.2/font/bootstrap-icons.css" rel="stylesheet" type="text/css">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous" type="text/css">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.7.2/font/bootstrap-icons.css" type="text/css">
|
||||
|
||||
<link href="https://unpkg.com/bootstrap-table@1.19.1/dist/bootstrap-table.min.css" rel="stylesheet" type="text/css">
|
||||
<link rel="stylesheet" href="https://unpkg.com/bootstrap-table@1.19.1/dist/bootstrap-table.min.css" type="text/css">
|
||||
|
||||
|
||||
<link href="https://unpkg.com/jquery-resizable-columns@0.2.3/dist/jquery.resizableColumns.css" rel="stylesheet" type="text/css">
|
||||
<link rel="stylesheet" href="https://unpkg.com/jquery-resizable-columns@0.2.3/dist/jquery.resizableColumns.css" type="text/css">
|
||||
|
||||
<link href="/static/styles.css" rel="stylesheet" type="text/css">
|
||||
<link rel="stylesheet" href="/static/styles.css" type="text/css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@ -68,6 +68,8 @@
|
||||
data-show-search-clear-button="true"
|
||||
data-single-select="true"
|
||||
data-sortable="true"
|
||||
data-sort-name="nick"
|
||||
data-sort-order="asc"
|
||||
data-sort-reset="true"
|
||||
data-toolbar="#toolbar">
|
||||
<thead class="table-primary">
|
||||
@ -84,23 +86,24 @@
|
||||
|
||||
<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">
|
||||
<form class="modal-content" action="javascript:" onsubmit="updateBis()">
|
||||
<div class="modal-header form-group row">
|
||||
<div class="btn-group" role="group" aria-label="Update bis">
|
||||
<input id="add-piece-btn" name="update-bis" type="radio" class="btn-check" autocomplete="off" checked>
|
||||
<label class="btn btn-outline-primary" for="add-piece-btn">add piece</label>
|
||||
|
||||
<input id="update-bis-btn" name="update-bis" type="radio" class="btn-check" autocomplete="off">
|
||||
<label class="btn btn-outline-primary" for="update-bis-btn">update bis</label>
|
||||
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="close"></button>
|
||||
</div>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="close"></button>
|
||||
</div>
|
||||
|
||||
<div class="modal-body">
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-4 col-form-label" for="player">player</label>
|
||||
<div class="col-sm-8">
|
||||
<select id="player" name="player" class="form-control" title="player"></select>
|
||||
<select id="player" name="player" class="form-control" title="player" required></select>
|
||||
</div>
|
||||
</div>
|
||||
<div id="piece-row" class="form-group row">
|
||||
@ -118,14 +121,14 @@
|
||||
<div id="bis-link-row" class="form-group row" style="display: none">
|
||||
<label class="col-sm-4 col-form-label" for="bis-link">link</label>
|
||||
<div class="col-sm-8">
|
||||
<input id="bis-link" name="link" class="form-control" placeholder="link to bis" onkeyup="disableSubmitBisButton()">
|
||||
<input id="bis-link" name="link" class="form-control" placeholder="link to bis">
|
||||
</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>
|
||||
<button id="submit-add-bis-btn" type="submit" class="btn btn-primary">add</button>
|
||||
<button id="submit-set-bis-btn" type="submit" class="btn btn-primary" style="display: none">set</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
@ -169,7 +172,7 @@
|
||||
const updateButton = $("#update-btn");
|
||||
|
||||
const submitAddBisButton = $("#submit-add-bis-btn");
|
||||
const submitUpdateBisButton = $("#submit-update-bis-btn");
|
||||
const submitSetBisButton = $("#submit-set-bis-btn");
|
||||
const updateBisDialog = $("#update-bis-dialog");
|
||||
|
||||
const addPieceButton = $("#add-piece-btn");
|
||||
@ -206,12 +209,8 @@
|
||||
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));
|
||||
updateBisDialog.modal("hide");
|
||||
return true; // action expects boolean result
|
||||
}
|
||||
|
||||
function hideControls() {
|
||||
@ -220,20 +219,24 @@
|
||||
}
|
||||
|
||||
function hideLinkPart() {
|
||||
disableSubmitBisButton();
|
||||
bisLinkRow.hide();
|
||||
submitUpdateBisButton.hide();
|
||||
linkInput.prop("required", false);
|
||||
submitSetBisButton.hide();
|
||||
pieceRow.show();
|
||||
pieceTypeRow.show();
|
||||
pieceInput.prop("required", true);
|
||||
pieceTypeInput.prop("required", true);
|
||||
submitAddBisButton.show();
|
||||
}
|
||||
|
||||
function hidePiecePart() {
|
||||
disableSubmitBisButton();
|
||||
bisLinkRow.show();
|
||||
submitUpdateBisButton.show();
|
||||
linkInput.prop("required", true);
|
||||
submitSetBisButton.show();
|
||||
pieceRow.hide();
|
||||
pieceTypeRow.hide();
|
||||
pieceInput.prop("required", false);
|
||||
pieceTypeInput.prop("required", false);
|
||||
submitAddBisButton.hide();
|
||||
}
|
||||
|
||||
@ -267,7 +270,6 @@
|
||||
return option;
|
||||
});
|
||||
playerInput.empty().append(options);
|
||||
disableSubmitBisButton();
|
||||
},
|
||||
error: function (jqXHR, _, errorThrown) { requestAlert(jqXHR, errorThrown); },
|
||||
});
|
||||
@ -325,6 +327,18 @@
|
||||
success: function (_) { reload(); },
|
||||
error: function (jqXHR, _, errorThrown) { requestAlert(jqXHR, errorThrown); },
|
||||
});
|
||||
updateBisDialog.modal("hide");
|
||||
return true; // action expects boolean result
|
||||
}
|
||||
|
||||
function updateBis() {
|
||||
if (updateBisButton.is(":checked")) {
|
||||
return setBis();
|
||||
}
|
||||
if (addPieceButton.is(":checked")) {
|
||||
return addPiece();
|
||||
}
|
||||
return false; // should not happen
|
||||
}
|
||||
|
||||
$(function () {
|
||||
@ -342,6 +356,7 @@
|
||||
|
||||
table.bootstrapTable({});
|
||||
reload();
|
||||
reset();
|
||||
});
|
||||
</script>
|
||||
|
||||
|
@ -6,11 +6,11 @@
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
|
||||
<link href="/static/favicon.ico" rel="shortcut icon">
|
||||
<link rel="shortcut icon" href="/static/favicon.ico">
|
||||
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous" type="text/css">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous" type="text/css">
|
||||
|
||||
<link href="/static/styles.css" rel="stylesheet" type="text/css">
|
||||
<link rel="stylesheet" href="/static/styles.css" type="text/css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
@ -6,17 +6,17 @@
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
|
||||
<link href="/static/favicon.ico" rel="shortcut icon">
|
||||
<link rel="shortcut icon" href="/static/favicon.ico">
|
||||
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous" type="text/css">
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.7.2/font/bootstrap-icons.css" rel="stylesheet" type="text/css">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous" type="text/css">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.7.2/font/bootstrap-icons.css" type="text/css">
|
||||
|
||||
<link href="https://unpkg.com/bootstrap-table@1.19.1/dist/bootstrap-table.min.css" rel="stylesheet" type="text/css">
|
||||
<link rel="stylesheet" href="https://unpkg.com/bootstrap-table@1.19.1/dist/bootstrap-table.min.css" type="text/css">
|
||||
|
||||
|
||||
<link href="https://unpkg.com/jquery-resizable-columns@0.2.3/dist/jquery.resizableColumns.css" rel="stylesheet" type="text/css">
|
||||
<link rel="stylesheet" href="https://unpkg.com/jquery-resizable-columns@0.2.3/dist/jquery.resizableColumns.css" type="text/css">
|
||||
|
||||
<link href="/static/styles.css" rel="stylesheet" type="text/css">
|
||||
<link rel="stylesheet" href="/static/styles.css" type="text/css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@ -68,6 +68,8 @@
|
||||
data-show-search-clear-button="true"
|
||||
data-single-select="true"
|
||||
data-sortable="true"
|
||||
data-sort-name="timestamp"
|
||||
data-sort-order="desc"
|
||||
data-sort-reset="true"
|
||||
data-toolbar="#toolbar">
|
||||
<thead class="table-primary">
|
||||
@ -86,35 +88,44 @@
|
||||
|
||||
<div id="add-loot-dialog" tabindex="-1" role="dialog" class="modal fade">
|
||||
<div class="modal-dialog modal-lg" role="document">
|
||||
<div class="modal-content">
|
||||
<form class="modal-content" action="javascript:" onsubmit="addLoot()">
|
||||
<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="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>
|
||||
<select id="player" name="player" class="form-control" title="player" required></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>
|
||||
<select id="piece" name="piece" class="form-control" title="piece" required></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>
|
||||
<select id="piece-type" name="pieceType" class="form-control" title="pieceType" required></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>
|
||||
<select id="job" name="job" class="form-control" title="job" required></select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<div class="col-sm-4"></div>
|
||||
<div class="col-sm-8">
|
||||
<div class="form-check">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -130,18 +141,14 @@
|
||||
</tr>
|
||||
</thead>
|
||||
</table>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
<button type="submit" class="btn btn-primary">add</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -182,7 +189,6 @@
|
||||
const addButton = $("#add-btn");
|
||||
const removeButton = $("#remove-btn");
|
||||
|
||||
const submitLootButton = $("#submit-btn");
|
||||
const addLootDialog = $("#add-loot-dialog");
|
||||
|
||||
const freeLootInput = $("#free-loot");
|
||||
@ -214,6 +220,8 @@
|
||||
success: function (_) { reload(); },
|
||||
error: function (jqXHR, _, errorThrown) { requestAlert(jqXHR, errorThrown); },
|
||||
});
|
||||
addLootDialog.modal("hide");
|
||||
return true; // action expects boolean result
|
||||
}
|
||||
|
||||
function hideControls() {
|
||||
@ -253,7 +261,6 @@
|
||||
return option;
|
||||
});
|
||||
playerInput.empty().append(options);
|
||||
submitLootButton.attr("disabled", options.length === 0);
|
||||
},
|
||||
error: function (jqXHR, _, errorThrown) { requestAlert(jqXHR, errorThrown); },
|
||||
});
|
||||
@ -303,7 +310,7 @@
|
||||
return {
|
||||
nick: stat.nick,
|
||||
job: stat.job,
|
||||
isRequired: stat.isRequired,
|
||||
isRequired: stat.isRequired ? "yes" : "no",
|
||||
lootCount: stat.lootCount,
|
||||
lootCountBiS: stat.lootCountBiS,
|
||||
lootCountTotal: stat.lootCountTotal,
|
||||
|
@ -6,17 +6,17 @@
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
|
||||
<link href="/static/favicon.ico" rel="shortcut icon">
|
||||
<link rel="shortcut icon" href="/static/favicon.ico">
|
||||
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous" type="text/css">
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.7.2/font/bootstrap-icons.css" rel="stylesheet" type="text/css">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous" type="text/css">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.7.2/font/bootstrap-icons.css" type="text/css">
|
||||
|
||||
<link href="https://unpkg.com/bootstrap-table@1.19.1/dist/bootstrap-table.min.css" rel="stylesheet" type="text/css">
|
||||
<link rel="stylesheet" href="https://unpkg.com/bootstrap-table@1.19.1/dist/bootstrap-table.min.css" type="text/css">
|
||||
|
||||
|
||||
<link href="https://unpkg.com/jquery-resizable-columns@0.2.3/dist/jquery.resizableColumns.css" rel="stylesheet" type="text/css">
|
||||
<link rel="stylesheet" href="https://unpkg.com/jquery-resizable-columns@0.2.3/dist/jquery.resizableColumns.css" type="text/css">
|
||||
|
||||
<link href="/static/styles.css" rel="stylesheet" type="text/css">
|
||||
<link rel="stylesheet" href="/static/styles.css" type="text/css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@ -64,6 +64,8 @@
|
||||
data-show-search-clear-button="true"
|
||||
data-single-select="true"
|
||||
data-sortable="true"
|
||||
data-sort-name="nick"
|
||||
data-sort-order="asc"
|
||||
data-sort-reset="true"
|
||||
data-toolbar="#toolbar">
|
||||
<thead class="table-primary">
|
||||
@ -82,23 +84,23 @@
|
||||
|
||||
<div id="add-player-dialog" tabindex="-1" role="dialog" class="modal fade">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<form class="modal-content" action="javascript:" onsubmit="addPlayer()">
|
||||
<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="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()">
|
||||
<input id="nick" name="nick" class="form-control" placeholder="nick" required>
|
||||
</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>
|
||||
<select id="job" name="job" class="form-control" title="job" required></select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
@ -113,13 +115,13 @@
|
||||
<input id="priority" name="priority" type="number" class="form-control" value="0">
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
<button type="submit" class="btn btn-primary">add</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -160,7 +162,6 @@
|
||||
const removeButton = $("#remove-btn");
|
||||
|
||||
const addPlayerDialog = $("#add-player-dialog");
|
||||
const submitPlayerButton = $("#submit-player-btn");
|
||||
|
||||
const jobInput = $("#job");
|
||||
const linkInput = $("#link");
|
||||
@ -185,6 +186,8 @@
|
||||
success: function (_) { reload(); },
|
||||
error: function (jqXHR, _, errorThrown) { requestAlert(jqXHR, errorThrown); },
|
||||
});
|
||||
addPlayerDialog.modal("hide");
|
||||
return true; // action expects boolean result
|
||||
}
|
||||
|
||||
function bisLinkFormatter(link, row) {
|
||||
@ -195,10 +198,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
function disableAddPlayerForm() {
|
||||
submitPlayerButton.attr("disabled", !nickInput.val());
|
||||
}
|
||||
|
||||
function hideControls() {
|
||||
addButton.attr("hidden", isReadOnly);
|
||||
removeButton.attr("hidden", isReadOnly);
|
||||
|
@ -6,9 +6,9 @@
|
||||
|
||||
<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 rel="stylesheet" href="https://fonts.googleapis.com/css?family=Montserrat:300,400,700|Roboto:300,400,700" type="text/css">
|
||||
|
||||
<link href="/static/favicon.ico" rel="shortcut icon">
|
||||
<link rel="shortcut icon" href="/static/favicon.ico">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
|
@ -6,17 +6,17 @@
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
|
||||
<link href="/static/favicon.ico" rel="shortcut icon">
|
||||
<link rel="shortcut icon" href="/static/favicon.ico">
|
||||
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous" type="text/css">
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.7.2/font/bootstrap-icons.css" rel="stylesheet" type="text/css">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous" type="text/css">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.7.2/font/bootstrap-icons.css" type="text/css">
|
||||
|
||||
<link href="https://unpkg.com/bootstrap-table@1.19.1/dist/bootstrap-table.min.css" rel="stylesheet" type="text/css">
|
||||
<link rel="stylesheet" href="https://unpkg.com/bootstrap-table@1.19.1/dist/bootstrap-table.min.css" type="text/css">
|
||||
|
||||
|
||||
<link href="https://unpkg.com/jquery-resizable-columns@0.2.3/dist/jquery.resizableColumns.css" rel="stylesheet" type="text/css">
|
||||
<link rel="stylesheet" href="https://unpkg.com/jquery-resizable-columns@0.2.3/dist/jquery.resizableColumns.css" type="text/css">
|
||||
|
||||
<link href="/static/styles.css" rel="stylesheet" type="text/css">
|
||||
<link rel="stylesheet" href="/static/styles.css" type="text/css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@ -68,6 +68,8 @@
|
||||
data-show-search-clear-button="true"
|
||||
data-single-select="true"
|
||||
data-sortable="true"
|
||||
data-sort-name="username"
|
||||
data-sort-order="asc"
|
||||
data-sort-reset="true"
|
||||
data-toolbar="#toolbar">
|
||||
<thead class="table-primary">
|
||||
@ -82,38 +84,38 @@
|
||||
|
||||
<div id="add-user-dialog" tabindex="-1" role="dialog" class="modal fade">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<form class="modal-content" action="javascript:" onsubmit="addUser()">
|
||||
<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="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()">
|
||||
<input id="username" name="username" class="form-control" placeholder="username" required>
|
||||
</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()">
|
||||
<input id="password" name="password" type="password" class="form-control" placeholder="password" required>
|
||||
</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>
|
||||
<select id="permission" name="permission" class="form-control" title="permission" required></select>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
<button type="submit" class="btn btn-primary">add</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -154,7 +156,6 @@
|
||||
const removeButton = $("#remove-btn");
|
||||
|
||||
const addUserDialog = $("#add-user-dialog");
|
||||
const submitUserButton = $("#submit-btn");
|
||||
|
||||
const usernameInput = $("#username");
|
||||
const passwordInput = $("#password");
|
||||
@ -174,10 +175,8 @@
|
||||
success: function (_) { reload(); },
|
||||
error: function (jqXHR, _, errorThrown) { requestAlert(jqXHR, errorThrown); },
|
||||
});
|
||||
}
|
||||
|
||||
function disableAddUserForm() {
|
||||
submitUserButton.attr("disabled", !(usernameInput.val() && passwordInput.val()));
|
||||
addUserDialog.modal("hide");
|
||||
return true; // action expects boolean result
|
||||
}
|
||||
|
||||
function hideControls() {
|
||||
|
@ -3,15 +3,14 @@
|
||||
<include resource="logback-application.xml" />
|
||||
<include resource="logback-http.xml" />
|
||||
|
||||
<root level="debug">
|
||||
<root level="DEBUG">
|
||||
<appender-ref ref="application" />
|
||||
</root>
|
||||
|
||||
<logger name="me.arcanis.ffxivbis" level="DEBUG" />
|
||||
<logger name="http" level="DEBUG" additivity="false">
|
||||
<appender-ref ref="http" />
|
||||
</logger>
|
||||
<logger name="slick" level="INFO" />
|
||||
<logger name="org.flywaydb.core.internal" level="INFO" />
|
||||
<logger name="com.zaxxer.hikari.pool" level="INFO" />
|
||||
|
||||
</configuration>
|
||||
|
@ -15,26 +15,17 @@ me.arcanis.ffxivbis {
|
||||
mode = "sqlite"
|
||||
|
||||
sqlite {
|
||||
profile = "slick.jdbc.SQLiteProfile$"
|
||||
db {
|
||||
url = "jdbc:sqlite:ffxivbis.db"
|
||||
#user = "user"
|
||||
#password = "password"
|
||||
}
|
||||
numThreads = 10
|
||||
driverClassName = "org.sqlite.JDBC"
|
||||
jdbcUrl = "jdbc:sqlite:ffxivbis.db"
|
||||
#username = "user"
|
||||
#password = "password"
|
||||
}
|
||||
|
||||
postgresql {
|
||||
profile = "slick.jdbc.PostgresProfile$"
|
||||
db {
|
||||
url = "jdbc:postgresql://localhost/ffxivbis"
|
||||
#user = "ffxivbis"
|
||||
#password = "ffxivbis"
|
||||
|
||||
connectionPool = disabled
|
||||
keepAliveConnection = yes
|
||||
}
|
||||
numThreads = 10
|
||||
driverClassName = "org.postgresql.Driver"
|
||||
jdbcUrl = "jdbc:postgresql://localhost/ffxivbis"
|
||||
#username = "ffxivbis"
|
||||
#password = "ffxivbis"
|
||||
}
|
||||
}
|
||||
|
||||
@ -66,14 +57,14 @@ me.arcanis.ffxivbis {
|
||||
# ttl of cached logins
|
||||
cache-timeout = 1m
|
||||
}
|
||||
}
|
||||
|
||||
default-dispatcher {
|
||||
type = Dispatcher
|
||||
executor = "thread-pool-executor"
|
||||
thread-pool-executor {
|
||||
fixed-pool-size = 16
|
||||
}
|
||||
throughput = 1
|
||||
}
|
||||
|
||||
default-dispatcher {
|
||||
type = Dispatcher
|
||||
executor = "thread-pool-executor"
|
||||
thread-pool-executor {
|
||||
fixed-pool-size = 16
|
||||
}
|
||||
throughput = 1
|
||||
}
|
||||
}
|
||||
|
@ -37,7 +37,7 @@ function loadTypes(url, selector) {
|
||||
}
|
||||
|
||||
function setupFormClear(dialog, reset) {
|
||||
dialog.on("shown.bs.modal", function () {
|
||||
dialog.on("hide.bs.modal", function () {
|
||||
$(this).find("form").trigger("reset");
|
||||
$(this).find("table").bootstrapTable("removeAll");
|
||||
if (reset) {
|
||||
|
@ -1,4 +1,4 @@
|
||||
REST json API description to interact with FFXIVBiS service.
|
||||
REST json API description to interact with FFXIV Best-in-slot service.
|
||||
|
||||
# Basic workflow
|
||||
|
||||
|
@ -17,8 +17,7 @@ import com.typesafe.scalalogging.StrictLogging
|
||||
import me.arcanis.ffxivbis.http.RootEndpoint
|
||||
import me.arcanis.ffxivbis.service.PartyService
|
||||
import me.arcanis.ffxivbis.service.bis.BisProvider
|
||||
import me.arcanis.ffxivbis.service.database.Database
|
||||
import me.arcanis.ffxivbis.storage.Migration
|
||||
import me.arcanis.ffxivbis.service.database.{Database, Migration}
|
||||
|
||||
import scala.concurrent.ExecutionContext
|
||||
import scala.jdk.CollectionConverters._
|
||||
|
@ -13,8 +13,8 @@ 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.time.{Instant, ZoneId}
|
||||
import java.util.Locale
|
||||
|
||||
trait HttpLog {
|
||||
@ -68,7 +68,7 @@ object HttpLog {
|
||||
|
||||
val httpLogDatetimeFormatter: DateTimeFormatter =
|
||||
DateTimeFormatter
|
||||
.ofPattern("dd/MMM/uuuu:HH:mm:ss xx ")
|
||||
.ofPattern("dd/MMM/uuuu:HH:mm:ss xx")
|
||||
.withLocale(Locale.UK)
|
||||
.withZone(ZoneId.systemDefault())
|
||||
}
|
||||
|
@ -25,7 +25,7 @@ class RootEndpoint(system: ActorSystem[Nothing], storage: ActorRef[Message], pro
|
||||
private val config = system.settings.config
|
||||
|
||||
implicit val scheduler: Scheduler = system.scheduler
|
||||
implicit val timeout: Timeout = config.getDuration("me.arcanis.ffxivbis.settings.request-timeout")
|
||||
implicit val timeout: Timeout = config.getTimeout("me.arcanis.ffxivbis.settings.request-timeout")
|
||||
|
||||
private val auth = AuthorizationProvider(config, storage, timeout, scheduler)
|
||||
|
||||
|
@ -22,6 +22,7 @@ class Swagger(config: Config) extends SwaggerHttpService {
|
||||
classOf[api.v1.LootEndpoint],
|
||||
classOf[api.v1.PartyEndpoint],
|
||||
classOf[api.v1.PlayerEndpoint],
|
||||
classOf[api.v1.StatusEndpoint],
|
||||
classOf[api.v1.TypesEndpoint],
|
||||
classOf[api.v1.UserEndpoint]
|
||||
)
|
||||
@ -35,7 +36,7 @@ class Swagger(config: Config) extends SwaggerHttpService {
|
||||
|
||||
override val host: String =
|
||||
if (config.hasPath("me.arcanis.ffxivbis.web.hostname")) config.getString("me.arcanis.ffxivbis.web.hostname")
|
||||
else s"${config.getString("me.arcanis.ffxivbis.web.host")}:${config.getString("me.arcanis.ffxivbis.web.port")}"
|
||||
else s"${config.getString("me.arcanis.ffxivbis.web.host")}:${config.getInt("me.arcanis.ffxivbis.web.port")}"
|
||||
|
||||
private val basicAuth = new SecurityScheme()
|
||||
.description("basic http auth")
|
||||
|
@ -32,14 +32,15 @@ class RootApiV1Endpoint(
|
||||
private val lootEndpoint = new LootEndpoint(storage, auth)
|
||||
private val partyEndpoint = new PartyEndpoint(storage, provider, auth)
|
||||
private val playerEndpoint = new PlayerEndpoint(storage, provider, auth)
|
||||
private val statusEndpoint = new StatusEndpoint
|
||||
private val typesEndpoint = new TypesEndpoint(config)
|
||||
private val userEndpoint = new UserEndpoint(storage, auth)
|
||||
|
||||
def route: Route =
|
||||
handleExceptions(exceptionHandler) {
|
||||
handleRejections(rejectionHandler) {
|
||||
biSEndpoint.route ~ lootEndpoint.route ~ partyEndpoint.route ~
|
||||
playerEndpoint.route ~ typesEndpoint.route ~ userEndpoint.route
|
||||
biSEndpoint.route ~ lootEndpoint.route ~ partyEndpoint.route ~ playerEndpoint.route ~
|
||||
statusEndpoint.route ~ typesEndpoint.route ~ userEndpoint.route
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,54 @@
|
||||
/*
|
||||
* 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
|
||||
|
||||
import akka.http.scaladsl.server.Directives._
|
||||
import akka.http.scaladsl.server._
|
||||
import io.swagger.v3.oas.annotations.Operation
|
||||
import io.swagger.v3.oas.annotations.media.{Content, Schema}
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse
|
||||
import jakarta.ws.rs._
|
||||
import me.arcanis.ffxivbis.http.api.v1.json._
|
||||
|
||||
@Path("/api/v1")
|
||||
class StatusEndpoint extends JsonSupport {
|
||||
|
||||
def route: Route = getServerStatus
|
||||
|
||||
@GET
|
||||
@Path("status")
|
||||
@Produces(value = Array("application/json"))
|
||||
@Operation(
|
||||
summary = "server status",
|
||||
description = "Returns the server status descriptor",
|
||||
responses = Array(
|
||||
new ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "Service status descriptor",
|
||||
content = Array(new Content(schema = new Schema(implementation = classOf[StatusModel])))
|
||||
),
|
||||
new ApiResponse(
|
||||
responseCode = "500",
|
||||
description = "Internal server error",
|
||||
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel])))
|
||||
),
|
||||
),
|
||||
tags = Array("status"),
|
||||
)
|
||||
def getServerStatus: Route =
|
||||
path("status") {
|
||||
get {
|
||||
complete {
|
||||
StatusModel(
|
||||
version = Option(getClass.getPackage.getImplementationVersion),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -52,5 +52,6 @@ trait JsonSupport extends SprayJsonSupport with DefaultJsonProtocol {
|
||||
implicit val playerBiSLinkFormat: RootJsonFormat[PlayerBiSLinkModel] = jsonFormat2(PlayerBiSLinkModel.apply)
|
||||
implicit val playerIdWithCountersFormat: RootJsonFormat[PlayerIdWithCountersModel] =
|
||||
jsonFormat9(PlayerIdWithCountersModel.apply)
|
||||
implicit val statusFormat: RootJsonFormat[StatusModel] = jsonFormat1(StatusModel.apply)
|
||||
implicit val userFormat: RootJsonFormat[UserModel] = jsonFormat4(UserModel.apply)
|
||||
}
|
||||
|
@ -0,0 +1,13 @@
|
||||
/*
|
||||
* 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
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema
|
||||
|
||||
case class StatusModel(@Schema(description = "server version") version: Option[String])
|
@ -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.http.helpers
|
||||
|
||||
import akka.actor.typed.scaladsl.AskPattern.Askable
|
||||
|
@ -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.http.helpers
|
||||
|
||||
import akka.actor.typed.scaladsl.AskPattern.Askable
|
||||
|
@ -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.http.helpers
|
||||
|
||||
import akka.actor.typed.scaladsl.AskPattern.Askable
|
||||
@ -25,8 +33,8 @@ trait LootHelper {
|
||||
): Future[Unit] =
|
||||
(action, maybeFree) match {
|
||||
case (ApiAction.add, Some(isFreeLoot)) => addPieceLoot(playerId, piece, isFreeLoot)
|
||||
case (ApiAction.remove, _) => removePieceLoot(playerId, piece)
|
||||
case _ => throw new IllegalArgumentException(s"Invalid combinantion of action $action and fee loot $maybeFree")
|
||||
case (ApiAction.remove, Some(isFreeLoot)) => removePieceLoot(playerId, piece, isFreeLoot)
|
||||
case _ => throw new IllegalArgumentException("Loot modification must always contain `isFreeLoot` field")
|
||||
}
|
||||
|
||||
def loot(partyId: String, playerId: Option[PlayerId])(implicit
|
||||
@ -35,8 +43,11 @@ trait LootHelper {
|
||||
): Future[Seq[Player]] =
|
||||
storage.ask(GetLoot(partyId, playerId, _))
|
||||
|
||||
def removePieceLoot(playerId: PlayerId, piece: Piece)(implicit timeout: Timeout, scheduler: Scheduler): Future[Unit] =
|
||||
storage.ask(RemovePieceFrom(playerId, piece, _))
|
||||
def removePieceLoot(playerId: PlayerId, piece: Piece, isFreeLoot: Boolean)(implicit
|
||||
timeout: Timeout,
|
||||
scheduler: Scheduler
|
||||
): Future[Unit] =
|
||||
storage.ask(RemovePieceFrom(playerId, piece, isFreeLoot, _))
|
||||
|
||||
def suggestPiece(partyId: String, piece: Piece)(implicit
|
||||
executionContext: ExecutionContext,
|
||||
|
@ -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.http.helpers
|
||||
|
||||
import akka.actor.typed.scaladsl.AskPattern.Askable
|
||||
|
@ -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.http.helpers
|
||||
|
||||
import akka.actor.typed.scaladsl.AskPattern.Askable
|
||||
|
@ -51,7 +51,8 @@ case class AddPieceTo(playerId: PlayerId, piece: Piece, isFreeLoot: Boolean, rep
|
||||
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 LootDatabaseMessage {
|
||||
case class RemovePieceFrom(playerId: PlayerId, piece: Piece, isFreeLoot: Boolean, replyTo: ActorRef[Unit])
|
||||
extends LootDatabaseMessage {
|
||||
override def partyId: String = playerId.partyId
|
||||
}
|
||||
|
||||
|
@ -13,4 +13,6 @@ import java.time.Instant
|
||||
case class Loot(playerId: Long, piece: Piece, timestamp: Instant, isFreeLoot: Boolean) {
|
||||
|
||||
def isFreeLootToString: String = if (isFreeLoot) "yes" else "no"
|
||||
|
||||
def isFreeLootToInt: Int = if (isFreeLoot) 1 else 0
|
||||
}
|
||||
|
@ -24,14 +24,14 @@ class PartyService(context: ActorContext[Message], storage: ActorRef[DatabaseMes
|
||||
with StrictLogging {
|
||||
import me.arcanis.ffxivbis.utils.Implicits._
|
||||
|
||||
private val cacheTimeout: FiniteDuration =
|
||||
context.system.settings.config.getDuration("me.arcanis.ffxivbis.settings.cache-timeout")
|
||||
private val cacheTimeout =
|
||||
context.system.settings.config.getFiniteDuration("me.arcanis.ffxivbis.settings.cache-timeout")
|
||||
implicit private val executionContext: ExecutionContext = {
|
||||
val selector = DispatcherSelector.fromConfig("me.arcanis.ffxivbis.default-dispatcher")
|
||||
context.system.dispatchers.lookup(selector)
|
||||
}
|
||||
implicit private val timeout: Timeout =
|
||||
context.system.settings.config.getDuration("me.arcanis.ffxivbis.settings.request-timeout")
|
||||
context.system.settings.config.getTimeout("me.arcanis.ffxivbis.settings.request-timeout")
|
||||
implicit private val scheduler: Scheduler = context.system.scheduler
|
||||
|
||||
override def onMessage(msg: Message): Behavior[Message] = handle(Map.empty)(msg)
|
||||
|
@ -18,11 +18,14 @@ import me.arcanis.ffxivbis.service.database.impl.DatabaseImpl
|
||||
import me.arcanis.ffxivbis.storage.DatabaseProfile
|
||||
|
||||
import scala.concurrent.{ExecutionContext, Future}
|
||||
import scala.util.{Failure, Success}
|
||||
|
||||
trait Database extends StrictLogging {
|
||||
|
||||
implicit def executionContext: ExecutionContext
|
||||
|
||||
def config: Config
|
||||
|
||||
def profile: DatabaseProfile
|
||||
|
||||
def filterParty(party: Party, maybePlayerId: Option[PlayerId]): Seq[Player] =
|
||||
@ -35,9 +38,15 @@ trait Database extends StrictLogging {
|
||||
for {
|
||||
partyDescription <- profile.getPartyDescription(partyId)
|
||||
players <- profile.getParty(partyId)
|
||||
bis <- if (withBiS) profile.getPiecesBiS(partyId) else Future(Seq.empty)
|
||||
loot <- if (withLoot) profile.getPieces(partyId) else Future(Seq.empty)
|
||||
bis <- if (withBiS) profile.getPiecesBiS(partyId) else Future.successful(Seq.empty)
|
||||
loot <- if (withLoot) profile.getPieces(partyId) else Future.successful(Seq.empty)
|
||||
} yield Party(partyDescription, config, players, bis, loot)
|
||||
|
||||
protected def run[T](fn: => Future[T])(onSuccess: T => Unit): Unit =
|
||||
fn.onComplete {
|
||||
case Success(value) => onSuccess(value)
|
||||
case Failure(exception) => logger.error("exception during performing database request", exception)
|
||||
}
|
||||
}
|
||||
|
||||
object Database {
|
||||
|
@ -6,9 +6,10 @@
|
||||
*
|
||||
* License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause
|
||||
*/
|
||||
package me.arcanis.ffxivbis.storage
|
||||
package me.arcanis.ffxivbis.service.database
|
||||
|
||||
import com.typesafe.config.Config
|
||||
import me.arcanis.ffxivbis.storage.DatabaseProfile
|
||||
import org.flywaydb.core.Flyway
|
||||
import org.flywaydb.core.api.configuration.ClassicConfiguration
|
||||
import org.flywaydb.core.api.output.MigrateResult
|
||||
@ -17,12 +18,14 @@ import scala.util.Try
|
||||
|
||||
class Migration(config: Config) {
|
||||
|
||||
import me.arcanis.ffxivbis.utils.Implicits._
|
||||
|
||||
def performMigration(): Try[MigrateResult] = {
|
||||
val section = DatabaseProfile.getSection(config)
|
||||
|
||||
val url = section.getString("db.url")
|
||||
val username = Try(section.getString("db.user")).toOption.filter(_.nonEmpty).orNull
|
||||
val password = Try(section.getString("db.password")).toOption.filter(_.nonEmpty).orNull
|
||||
val url = section.getString("jdbcUrl")
|
||||
val username = section.getOptString("username").orNull
|
||||
val password = section.getOptString("password").orNull
|
||||
|
||||
val provider = url match {
|
||||
case s"jdbc:$p:$_" => p
|
@ -8,29 +8,32 @@
|
||||
*/
|
||||
package me.arcanis.ffxivbis.service.database.impl
|
||||
|
||||
import akka.actor.typed.Behavior
|
||||
import akka.actor.typed.scaladsl.Behaviors
|
||||
import me.arcanis.ffxivbis.messages._
|
||||
import me.arcanis.ffxivbis.service.database.Database
|
||||
|
||||
trait DatabaseBiSHandler { this: Database =>
|
||||
|
||||
def bisHandler: DatabaseMessage.Handler = {
|
||||
case AddPieceToBis(playerId, piece, client) =>
|
||||
profile.insertPieceBiS(playerId, piece).foreach(_ => client ! ())
|
||||
Behaviors.same
|
||||
def bisHandler(msg: BisDatabaseMessage): Behavior[DatabaseMessage] =
|
||||
msg match {
|
||||
case AddPieceToBis(playerId, piece, client) =>
|
||||
run(profile.insertPieceBiS(playerId, piece))(_ => client ! ())
|
||||
Behaviors.same
|
||||
|
||||
case GetBiS(partyId, maybePlayerId, client) =>
|
||||
getParty(partyId, withBiS = true, withLoot = false)
|
||||
.map(filterParty(_, maybePlayerId))
|
||||
.foreach(client ! _)
|
||||
Behaviors.same
|
||||
case GetBiS(partyId, maybePlayerId, client) =>
|
||||
run {
|
||||
getParty(partyId, withBiS = true, withLoot = false)
|
||||
.map(filterParty(_, maybePlayerId))
|
||||
}(client ! _)
|
||||
Behaviors.same
|
||||
|
||||
case RemovePieceFromBiS(playerId, piece, client) =>
|
||||
profile.deletePieceBiS(playerId, piece).foreach(_ => client ! ())
|
||||
Behaviors.same
|
||||
case RemovePieceFromBiS(playerId, piece, client) =>
|
||||
run(profile.deletePieceBiS(playerId, piece))(_ => client ! ())
|
||||
Behaviors.same
|
||||
|
||||
case RemovePiecesFromBiS(playerId, client) =>
|
||||
profile.deletePiecesBiS(playerId).foreach(_ => client ! ())
|
||||
Behaviors.same
|
||||
}
|
||||
case RemovePiecesFromBiS(playerId, client) =>
|
||||
run(profile.deletePiecesBiS(playerId))(_ => client ! ())
|
||||
Behaviors.same
|
||||
}
|
||||
}
|
||||
|
@ -11,7 +11,7 @@ package me.arcanis.ffxivbis.service.database.impl
|
||||
import akka.actor.typed.scaladsl.{AbstractBehavior, ActorContext}
|
||||
import akka.actor.typed.{Behavior, DispatcherSelector}
|
||||
import com.typesafe.config.Config
|
||||
import me.arcanis.ffxivbis.messages.DatabaseMessage
|
||||
import me.arcanis.ffxivbis.messages.{BisDatabaseMessage, DatabaseMessage, LootDatabaseMessage, PartyDatabaseMessage, UserDatabaseMessage}
|
||||
import me.arcanis.ffxivbis.service.database.Database
|
||||
import me.arcanis.ffxivbis.storage.DatabaseProfile
|
||||
|
||||
@ -32,8 +32,12 @@ class DatabaseImpl(context: ActorContext[DatabaseMessage])
|
||||
override val config: Config = context.system.settings.config
|
||||
override val profile: DatabaseProfile = new DatabaseProfile(executionContext, config)
|
||||
|
||||
override def onMessage(msg: DatabaseMessage): Behavior[DatabaseMessage] = handle(msg)
|
||||
override def onMessage(msg: DatabaseMessage): Behavior[DatabaseMessage] =
|
||||
msg match {
|
||||
case msg: BisDatabaseMessage => bisHandler(msg)
|
||||
case msg: LootDatabaseMessage => lootHandler(msg)
|
||||
case msg: PartyDatabaseMessage => partyHandler(msg)
|
||||
case msg: UserDatabaseMessage => userHandler(msg)
|
||||
}
|
||||
|
||||
private def handle: DatabaseMessage.Handler =
|
||||
bisHandler.orElse(lootHandler).orElse(partyHandler).orElse(userHandler)
|
||||
}
|
||||
|
@ -8,6 +8,7 @@
|
||||
*/
|
||||
package me.arcanis.ffxivbis.service.database.impl
|
||||
|
||||
import akka.actor.typed.Behavior
|
||||
import akka.actor.typed.scaladsl.Behaviors
|
||||
import me.arcanis.ffxivbis.messages._
|
||||
import me.arcanis.ffxivbis.models.Loot
|
||||
@ -17,26 +18,29 @@ import java.time.Instant
|
||||
|
||||
trait DatabaseLootHandler { this: Database =>
|
||||
|
||||
def lootHandler: DatabaseMessage.Handler = {
|
||||
case AddPieceTo(playerId, piece, isFreeLoot, client) =>
|
||||
val loot = Loot(-1, piece, Instant.now, isFreeLoot)
|
||||
profile.insertPiece(playerId, loot).foreach(_ => client ! ())
|
||||
Behaviors.same
|
||||
def lootHandler(msg: LootDatabaseMessage): Behavior[DatabaseMessage] =
|
||||
msg match {
|
||||
case AddPieceTo(playerId, piece, isFreeLoot, client) =>
|
||||
val loot = Loot(-1, piece, Instant.now, isFreeLoot)
|
||||
run(profile.insertPiece(playerId, loot))(_ => client ! ())
|
||||
Behaviors.same
|
||||
|
||||
case GetLoot(partyId, maybePlayerId, client) =>
|
||||
getParty(partyId, withBiS = false, withLoot = true)
|
||||
.map(filterParty(_, maybePlayerId))
|
||||
.foreach(client ! _)
|
||||
Behaviors.same
|
||||
case GetLoot(partyId, maybePlayerId, client) =>
|
||||
run {
|
||||
getParty(partyId, withBiS = false, withLoot = true)
|
||||
.map(filterParty(_, maybePlayerId))
|
||||
}(client ! _)
|
||||
Behaviors.same
|
||||
|
||||
case RemovePieceFrom(playerId, piece, client) =>
|
||||
profile.deletePiece(playerId, piece).foreach(_ => client ! ())
|
||||
Behaviors.same
|
||||
case RemovePieceFrom(playerId, piece, isFreeLoot, client) =>
|
||||
run(profile.deletePiece(playerId, piece, isFreeLoot))(_ => client ! ())
|
||||
Behaviors.same
|
||||
|
||||
case SuggestLoot(partyId, piece, client) =>
|
||||
getParty(partyId, withBiS = true, withLoot = true)
|
||||
.map(_.suggestLoot(piece))
|
||||
.foreach(client ! _)
|
||||
Behaviors.same
|
||||
}
|
||||
case SuggestLoot(partyId, piece, client) =>
|
||||
run {
|
||||
getParty(partyId, withBiS = true, withLoot = true)
|
||||
.map(_.suggestLoot(piece))
|
||||
}(client ! _)
|
||||
Behaviors.same
|
||||
}
|
||||
}
|
||||
|
@ -8,6 +8,7 @@
|
||||
*/
|
||||
package me.arcanis.ffxivbis.service.database.impl
|
||||
|
||||
import akka.actor.typed.Behavior
|
||||
import akka.actor.typed.scaladsl.Behaviors
|
||||
import me.arcanis.ffxivbis.messages._
|
||||
import me.arcanis.ffxivbis.models.{BiS, Player}
|
||||
@ -17,49 +18,51 @@ import scala.concurrent.Future
|
||||
|
||||
trait DatabasePartyHandler { this: Database =>
|
||||
|
||||
def partyHandler: DatabaseMessage.Handler = {
|
||||
case AddPlayer(player, client) =>
|
||||
profile.insertPlayer(player).foreach(_ => client ! ())
|
||||
Behaviors.same
|
||||
def partyHandler(msg: PartyDatabaseMessage): Behavior[DatabaseMessage] =
|
||||
msg match {
|
||||
case AddPlayer(player, client) =>
|
||||
run(profile.insertPlayer(player))(_ => client ! ())
|
||||
Behaviors.same
|
||||
|
||||
case GetParty(partyId, client) =>
|
||||
getParty(partyId, withBiS = true, withLoot = true).foreach(client ! _)
|
||||
Behaviors.same
|
||||
case GetParty(partyId, client) =>
|
||||
run(getParty(partyId, withBiS = true, withLoot = true))(client ! _)
|
||||
Behaviors.same
|
||||
|
||||
case GetPartyDescription(partyId, client) =>
|
||||
profile.getPartyDescription(partyId).foreach(client ! _)
|
||||
Behaviors.same
|
||||
case GetPartyDescription(partyId, client) =>
|
||||
run(profile.getPartyDescription(partyId))(client ! _)
|
||||
Behaviors.same
|
||||
|
||||
case GetPlayer(playerId, client) =>
|
||||
val player = profile
|
||||
.getPlayerFull(playerId)
|
||||
.flatMap { maybePlayerData =>
|
||||
Future.traverse(maybePlayerData.toSeq) { playerData =>
|
||||
for {
|
||||
bis <- profile.getPiecesBiS(playerId)
|
||||
loot <- profile.getPieces(playerId)
|
||||
} yield Player(
|
||||
playerData.id,
|
||||
playerId.partyId,
|
||||
playerId.job,
|
||||
playerId.nick,
|
||||
BiS(bis.map(_.piece)),
|
||||
loot,
|
||||
playerData.link,
|
||||
playerData.priority
|
||||
)
|
||||
}
|
||||
}
|
||||
.map(_.headOption)
|
||||
player.foreach(client ! _)
|
||||
Behaviors.same
|
||||
case GetPlayer(playerId, client) =>
|
||||
run {
|
||||
profile
|
||||
.getPlayerFull(playerId)
|
||||
.flatMap { maybePlayerData =>
|
||||
Future.traverse(maybePlayerData.toSeq) { playerData =>
|
||||
for {
|
||||
bis <- profile.getPiecesBiS(playerId)
|
||||
loot <- profile.getPieces(playerId)
|
||||
} yield Player(
|
||||
playerData.id,
|
||||
playerId.partyId,
|
||||
playerId.job,
|
||||
playerId.nick,
|
||||
BiS(bis.map(_.piece)),
|
||||
loot,
|
||||
playerData.link,
|
||||
playerData.priority
|
||||
)
|
||||
}
|
||||
}
|
||||
.map(_.headOption)
|
||||
}(client ! _)
|
||||
Behaviors.same
|
||||
|
||||
case RemovePlayer(playerId, client) =>
|
||||
profile.deletePlayer(playerId).foreach(_ => client ! ())
|
||||
Behaviors.same
|
||||
case RemovePlayer(playerId, client) =>
|
||||
run(profile.deletePlayer(playerId))(_ => client ! ())
|
||||
Behaviors.same
|
||||
|
||||
case UpdateParty(description, client) =>
|
||||
profile.insertPartyDescription(description).foreach(_ => client ! ())
|
||||
Behaviors.same
|
||||
}
|
||||
case UpdateParty(description, client) =>
|
||||
run(profile.insertPartyDescription(description))(_ => client ! ())
|
||||
Behaviors.same
|
||||
}
|
||||
}
|
||||
|
@ -8,32 +8,34 @@
|
||||
*/
|
||||
package me.arcanis.ffxivbis.service.database.impl
|
||||
|
||||
import akka.actor.typed.Behavior
|
||||
import akka.actor.typed.scaladsl.Behaviors
|
||||
import me.arcanis.ffxivbis.messages._
|
||||
import me.arcanis.ffxivbis.service.database.Database
|
||||
|
||||
trait DatabaseUserHandler { this: Database =>
|
||||
|
||||
def userHandler: DatabaseMessage.Handler = {
|
||||
case AddUser(user, isHashedPassword, client) =>
|
||||
val toInsert = if (isHashedPassword) user else user.withHashedPassword
|
||||
profile.insertUser(toInsert).foreach(_ => client ! ())
|
||||
Behaviors.same
|
||||
def userHandler(msg: UserDatabaseMessage): Behavior[DatabaseMessage] =
|
||||
msg match {
|
||||
case AddUser(user, isHashedPassword, client) =>
|
||||
val toInsert = if (isHashedPassword) user else user.withHashedPassword
|
||||
run(profile.insertUser(toInsert))(_ => client ! ())
|
||||
Behaviors.same
|
||||
|
||||
case DeleteUser(partyId, username, client) =>
|
||||
profile.deleteUser(partyId, username).foreach(_ => client ! ())
|
||||
Behaviors.same
|
||||
case DeleteUser(partyId, username, client) =>
|
||||
run(profile.deleteUser(partyId, username))(_ => client ! ())
|
||||
Behaviors.same
|
||||
|
||||
case Exists(partyId, client) =>
|
||||
profile.exists(partyId).foreach(client ! _)
|
||||
Behaviors.same
|
||||
case Exists(partyId, client) =>
|
||||
run(profile.exists(partyId))(client ! _)
|
||||
Behaviors.same
|
||||
|
||||
case GetUser(partyId, username, client) =>
|
||||
profile.getUser(partyId, username).foreach(client ! _)
|
||||
Behaviors.same
|
||||
case GetUser(partyId, username, client) =>
|
||||
run(profile.getUser(partyId, username))(client ! _)
|
||||
Behaviors.same
|
||||
|
||||
case GetUsers(partyId, client) =>
|
||||
profile.getUsers(partyId).foreach(client ! _)
|
||||
Behaviors.same
|
||||
}
|
||||
case GetUsers(partyId, client) =>
|
||||
run(profile.getUsers(partyId))(client ! _)
|
||||
Behaviors.same
|
||||
}
|
||||
}
|
||||
|
@ -8,66 +8,72 @@
|
||||
*/
|
||||
package me.arcanis.ffxivbis.storage
|
||||
|
||||
import anorm.SqlParser._
|
||||
import anorm._
|
||||
import me.arcanis.ffxivbis.models.{Job, Loot, Piece, PieceType}
|
||||
import slick.lifted.ForeignKeyQuery
|
||||
|
||||
import java.time.Instant
|
||||
import scala.concurrent.Future
|
||||
|
||||
trait BiSProfile { this: DatabaseProfile =>
|
||||
import dbConfig.profile.api._
|
||||
trait BiSProfile extends DatabaseConnection {
|
||||
|
||||
case class BiSRep(playerId: Long, created: Long, piece: String, pieceType: String, job: String) {
|
||||
|
||||
def toLoot: Loot = Loot(
|
||||
playerId,
|
||||
Piece(piece, PieceType.withName(pieceType), Job.withName(job)),
|
||||
Instant.ofEpochMilli(created),
|
||||
isFreeLoot = false
|
||||
)
|
||||
}
|
||||
|
||||
object BiSRep {
|
||||
|
||||
def fromPiece(playerId: Long, piece: Piece): BiSRep =
|
||||
BiSRep(playerId, DatabaseProfile.now, piece.piece, piece.pieceType.toString, piece.job.toString)
|
||||
}
|
||||
|
||||
class BiSPieces(tag: Tag) extends Table[BiSRep](tag, "bis") {
|
||||
def playerId: Rep[Long] = column[Long]("player_id", O.PrimaryKey)
|
||||
def created: Rep[Long] = column[Long]("created")
|
||||
def piece: Rep[String] = column[String]("piece", O.PrimaryKey)
|
||||
def pieceType: Rep[String] = column[String]("piece_type")
|
||||
def job: Rep[String] = column[String]("job")
|
||||
|
||||
def * =
|
||||
(playerId, created, piece, pieceType, job) <> ((BiSRep.apply _).tupled, BiSRep.unapply)
|
||||
|
||||
def fkPlayerId: ForeignKeyQuery[Players, PlayerRep] =
|
||||
foreignKey("player_id", playerId, playersTable)(_.playerId, onDelete = ForeignKeyAction.Cascade)
|
||||
}
|
||||
private val loot: RowParser[Loot] =
|
||||
(long("player_id") ~ str("piece") ~ str("piece_type")
|
||||
~ str("job") ~ long("created"))
|
||||
.map { case playerId ~ piece ~ pieceType ~ job ~ created =>
|
||||
Loot(
|
||||
playerId = playerId,
|
||||
piece = Piece(
|
||||
piece = piece,
|
||||
pieceType = PieceType.withName(pieceType),
|
||||
job = Job.withName(job)
|
||||
),
|
||||
timestamp = Instant.ofEpochMilli(created),
|
||||
isFreeLoot = false,
|
||||
)
|
||||
}
|
||||
|
||||
def deletePieceBiSById(piece: Piece)(playerId: Long): Future[Int] =
|
||||
db.run(pieceBiS(BiSRep.fromPiece(playerId, piece)).delete)
|
||||
withConnection { implicit conn =>
|
||||
SQL("""delete from bis
|
||||
| where player_id = {player_id}
|
||||
| and piece = {piece}
|
||||
| and piece_type = {piece_type}""".stripMargin)
|
||||
.on("player_id" -> playerId, "piece" -> piece.piece, "piece_type" -> piece.pieceType.toString)
|
||||
.executeUpdate()
|
||||
}
|
||||
|
||||
def deletePiecesBiSById(playerId: Long): Future[Int] =
|
||||
db.run(piecesBiS(Seq(playerId)).delete)
|
||||
withConnection { implicit conn =>
|
||||
SQL("""delete from bis where player_id = {player_id}""")
|
||||
.on("player_id" -> playerId)
|
||||
.executeUpdate()
|
||||
}
|
||||
|
||||
def getPiecesBiSById(playerId: Long): Future[Seq[Loot]] = getPiecesBiSById(Seq(playerId))
|
||||
|
||||
def getPiecesBiSById(playerIds: Seq[Long]): Future[Seq[Loot]] =
|
||||
db.run(piecesBiS(playerIds).result).map(_.map(_.toLoot))
|
||||
withConnection { implicit conn =>
|
||||
SQL("""select * from bis where player_id in ({player_ids})""")
|
||||
.on("player_ids" -> playerIds)
|
||||
.executeQuery()
|
||||
.as(loot.*)
|
||||
}
|
||||
|
||||
def insertPieceBiSById(piece: Piece)(playerId: Long): Future[Int] =
|
||||
getPiecesBiSById(playerId).flatMap {
|
||||
case pieces if pieces.exists(loot => loot.piece.strictEqual(piece)) => Future.successful(0)
|
||||
case _ => db.run(bisTable.insertOrUpdate(BiSRep.fromPiece(playerId, piece)))
|
||||
withConnection { implicit conn =>
|
||||
SQL("""insert into bis
|
||||
| (player_id, piece, piece_type, job, created)
|
||||
| values
|
||||
| ({player_id}, {piece}, {piece_type}, {job}, {created})
|
||||
| on conflict (player_id, piece, piece_type) do nothing""".stripMargin)
|
||||
.on(
|
||||
"player_id" -> playerId,
|
||||
"piece" -> piece.piece,
|
||||
"piece_type" -> piece.pieceType.toString,
|
||||
"job" -> piece.job.toString,
|
||||
"created" -> DatabaseProfile.now
|
||||
)
|
||||
.executeUpdate()
|
||||
}
|
||||
|
||||
private def pieceBiS(piece: BiSRep) =
|
||||
piecesBiS(Seq(piece.playerId)).filter { stored =>
|
||||
(stored.piece === piece.piece) && (stored.pieceType === piece.pieceType)
|
||||
}
|
||||
private def piecesBiS(playerIds: Seq[Long]) =
|
||||
bisTable.filter(_.playerId.inSet(playerIds.toSet))
|
||||
}
|
||||
|
@ -0,0 +1,58 @@
|
||||
/*
|
||||
* 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.storage
|
||||
|
||||
import com.typesafe.config.Config
|
||||
import com.zaxxer.hikari.HikariConfig
|
||||
|
||||
import java.sql.Connection
|
||||
import java.util.Properties
|
||||
import javax.sql.DataSource
|
||||
import scala.concurrent.{ExecutionContext, Future, Promise}
|
||||
import scala.jdk.CollectionConverters._
|
||||
import scala.util.control.NonFatal
|
||||
import scala.util.{Failure, Success, Try}
|
||||
|
||||
trait DatabaseConnection {
|
||||
|
||||
def datasource: DataSource
|
||||
|
||||
def executionContext: ExecutionContext
|
||||
|
||||
def withConnection[T](fn: Connection => T): Future[T] = {
|
||||
val promise = Promise[T]()
|
||||
|
||||
executionContext.execute { () =>
|
||||
Try(datasource.getConnection) match {
|
||||
case Success(conn) =>
|
||||
try {
|
||||
val result = fn(conn)
|
||||
promise.trySuccess(result)
|
||||
} catch {
|
||||
case NonFatal(exception) => promise.tryFailure(exception)
|
||||
} finally
|
||||
conn.close()
|
||||
case Failure(exception) => promise.tryFailure(exception)
|
||||
}
|
||||
}
|
||||
|
||||
promise.future
|
||||
}
|
||||
}
|
||||
|
||||
object DatabaseConnection {
|
||||
|
||||
def getDataSourceConfig(config: Config): HikariConfig = {
|
||||
val properties = new Properties()
|
||||
config.entrySet().asScala.map(_.getKey).foreach { key =>
|
||||
properties.setProperty(key, config.getString(key))
|
||||
}
|
||||
new HikariConfig(properties)
|
||||
}
|
||||
}
|
@ -9,32 +9,32 @@
|
||||
package me.arcanis.ffxivbis.storage
|
||||
|
||||
import com.typesafe.config.Config
|
||||
import com.typesafe.scalalogging.StrictLogging
|
||||
import com.zaxxer.hikari.HikariDataSource
|
||||
import me.arcanis.ffxivbis.models.{Loot, Piece, PlayerId}
|
||||
import slick.basic.DatabaseConfig
|
||||
import slick.jdbc.JdbcProfile
|
||||
|
||||
import java.time.Instant
|
||||
import javax.sql.DataSource
|
||||
import scala.concurrent.{ExecutionContext, Future}
|
||||
|
||||
class DatabaseProfile(context: ExecutionContext, config: Config)
|
||||
extends BiSProfile
|
||||
class DatabaseProfile(override val executionContext: ExecutionContext, config: Config)
|
||||
extends StrictLogging
|
||||
with BiSProfile
|
||||
with LootProfile
|
||||
with PartyProfile
|
||||
with PlayersProfile
|
||||
with UsersProfile {
|
||||
|
||||
implicit val executionContext: ExecutionContext = context
|
||||
|
||||
val dbConfig: DatabaseConfig[JdbcProfile] =
|
||||
DatabaseConfig.forConfig[JdbcProfile]("", DatabaseProfile.getSection(config))
|
||||
import dbConfig.profile.api._
|
||||
val db = dbConfig.db
|
||||
|
||||
val bisTable: TableQuery[BiSPieces] = TableQuery[BiSPieces]
|
||||
val lootTable: TableQuery[LootPieces] = TableQuery[LootPieces]
|
||||
val partiesTable: TableQuery[Parties] = TableQuery[Parties]
|
||||
val playersTable: TableQuery[Players] = TableQuery[Players]
|
||||
val usersTable: TableQuery[Users] = TableQuery[Users]
|
||||
override val datasource: DataSource =
|
||||
try {
|
||||
val profile = DatabaseProfile.getSection(config)
|
||||
val dataSourceConfig = DatabaseConnection.getDataSourceConfig(profile)
|
||||
new HikariDataSource(dataSourceConfig)
|
||||
} catch {
|
||||
case exception: Exception =>
|
||||
logger.error("exception during storage initialization", exception)
|
||||
throw exception
|
||||
}
|
||||
|
||||
// generic bis api
|
||||
def deletePieceBiS(playerId: PlayerId, piece: Piece): Future[Int] =
|
||||
@ -53,9 +53,8 @@ class DatabaseProfile(context: ExecutionContext, config: Config)
|
||||
byPlayerId(playerId, insertPieceBiSById(piece))
|
||||
|
||||
// generic loot api
|
||||
def deletePiece(playerId: PlayerId, piece: Piece): Future[Int] = {
|
||||
// we don't really care here about loot
|
||||
val loot = Loot(-1, piece, Instant.now, isFreeLoot = false)
|
||||
def deletePiece(playerId: PlayerId, piece: Piece, isFreeLoot: Boolean): Future[Int] = {
|
||||
val loot = Loot(-1, piece, Instant.now, isFreeLoot)
|
||||
byPlayerId(playerId, deletePieceById(loot))
|
||||
}
|
||||
|
||||
@ -69,21 +68,23 @@ class DatabaseProfile(context: ExecutionContext, config: Config)
|
||||
byPlayerId(playerId, insertPieceById(loot))
|
||||
|
||||
private def byPartyId[T](partyId: String, callback: Seq[Long] => Future[T]): Future[T] =
|
||||
getPlayers(partyId).flatMap(callback)
|
||||
getPlayers(partyId).flatMap(callback)(executionContext)
|
||||
|
||||
private def byPlayerId[T](playerId: PlayerId, callback: Long => Future[T]): Future[T] =
|
||||
getPlayer(playerId).flatMap {
|
||||
case Some(id) => callback(id)
|
||||
case None => Future.failed(new Error(s"Could not find player $playerId"))
|
||||
}
|
||||
case None => Future.failed(DatabaseProfile.PlayerNotFound(playerId))
|
||||
}(executionContext)
|
||||
}
|
||||
|
||||
object DatabaseProfile {
|
||||
|
||||
def now: Long = Instant.now.toEpochMilli
|
||||
case class PlayerNotFound(playerId: PlayerId) extends Exception(s"Could not find player $playerId")
|
||||
|
||||
def getSection(config: Config): Config = {
|
||||
val section = config.getString("me.arcanis.ffxivbis.database.mode")
|
||||
config.getConfig("me.arcanis.ffxivbis.database").getConfig(section)
|
||||
config.getConfig(s"me.arcanis.ffxivbis.database.$section")
|
||||
}
|
||||
|
||||
def now: Long = Instant.now.toEpochMilli
|
||||
}
|
||||
|
@ -8,81 +8,78 @@
|
||||
*/
|
||||
package me.arcanis.ffxivbis.storage
|
||||
|
||||
import anorm.SqlParser._
|
||||
import anorm._
|
||||
import me.arcanis.ffxivbis.models.{Job, Loot, Piece, PieceType}
|
||||
import slick.lifted.{ForeignKeyQuery, Index}
|
||||
|
||||
import java.time.Instant
|
||||
import scala.concurrent.Future
|
||||
|
||||
trait LootProfile { this: DatabaseProfile =>
|
||||
import dbConfig.profile.api._
|
||||
trait LootProfile extends DatabaseConnection {
|
||||
|
||||
case class LootRep(
|
||||
lootId: Option[Long],
|
||||
playerId: Long,
|
||||
created: Long,
|
||||
piece: String,
|
||||
pieceType: String,
|
||||
job: String,
|
||||
isFreeLoot: Int
|
||||
) {
|
||||
|
||||
def toLoot: Loot = Loot(
|
||||
playerId,
|
||||
Piece(piece, PieceType.withName(pieceType), Job.withName(job)),
|
||||
Instant.ofEpochMilli(created),
|
||||
isFreeLoot == 1
|
||||
)
|
||||
}
|
||||
|
||||
object LootRep {
|
||||
def fromLoot(playerId: Long, loot: Loot): LootRep =
|
||||
LootRep(
|
||||
None,
|
||||
playerId,
|
||||
loot.timestamp.toEpochMilli,
|
||||
loot.piece.piece,
|
||||
loot.piece.pieceType.toString,
|
||||
loot.piece.job.toString,
|
||||
if (loot.isFreeLoot) 1 else 0
|
||||
)
|
||||
}
|
||||
|
||||
class LootPieces(tag: Tag) extends Table[LootRep](tag, "loot") {
|
||||
def lootId: Rep[Long] = column[Long]("loot_id", O.AutoInc, O.PrimaryKey)
|
||||
def playerId: Rep[Long] = column[Long]("player_id")
|
||||
def created: Rep[Long] = column[Long]("created")
|
||||
def piece: Rep[String] = column[String]("piece")
|
||||
def pieceType: Rep[String] = column[String]("piece_type")
|
||||
def job: Rep[String] = column[String]("job")
|
||||
def isFreeLoot: Rep[Int] = column[Int]("is_free_loot")
|
||||
|
||||
def * =
|
||||
(lootId.?, playerId, created, piece, pieceType, job, isFreeLoot) <> ((LootRep.apply _).tupled, LootRep.unapply)
|
||||
|
||||
def fkPlayerId: ForeignKeyQuery[Players, PlayerRep] =
|
||||
foreignKey("player_id", playerId, playersTable)(_.playerId, onDelete = ForeignKeyAction.Cascade)
|
||||
def lootOwnerIdx: Index =
|
||||
index("loot_owner_idx", playerId, unique = false)
|
||||
}
|
||||
private val loot: RowParser[Loot] =
|
||||
(long("player_id") ~ str("piece") ~ str("piece_type")
|
||||
~ str("job") ~ long("created") ~ int("is_free_loot"))
|
||||
.map { case playerId ~ piece ~ pieceType ~ job ~ created ~ isFreeLoot =>
|
||||
Loot(
|
||||
playerId = playerId,
|
||||
piece = Piece(
|
||||
piece = piece,
|
||||
pieceType = PieceType.withName(pieceType),
|
||||
job = Job.withName(job)
|
||||
),
|
||||
timestamp = Instant.ofEpochMilli(created),
|
||||
isFreeLoot = isFreeLoot == 1,
|
||||
)
|
||||
}
|
||||
|
||||
def deletePieceById(loot: Loot)(playerId: Long): Future[Int] =
|
||||
db.run(pieceLoot(LootRep.fromLoot(playerId, loot)).map(_.lootId).max.result).flatMap {
|
||||
case Some(id) => db.run(lootTable.filter(_.lootId === id).delete)
|
||||
case _ => throw new IllegalArgumentException(s"Could not find piece $loot belong to $playerId")
|
||||
withConnection { implicit conn =>
|
||||
SQL("""delete from loot
|
||||
| where loot_id in
|
||||
| (
|
||||
| select loot_id from loot
|
||||
| where player_id = {player_id}
|
||||
| and piece = {piece}
|
||||
| and piece_type = {piece_type}
|
||||
| and job = {job}
|
||||
| and is_free_loot = {is_free_loot}
|
||||
| limit 1
|
||||
| )""".stripMargin)
|
||||
.on(
|
||||
"player_id" -> playerId,
|
||||
"piece" -> loot.piece.piece,
|
||||
"piece_type" -> loot.piece.pieceType.toString,
|
||||
"job" -> loot.piece.job.toString,
|
||||
"is_free_loot" -> loot.isFreeLootToInt
|
||||
)
|
||||
.executeUpdate()
|
||||
}
|
||||
|
||||
def getPiecesById(playerId: Long): Future[Seq[Loot]] = getPiecesById(Seq(playerId))
|
||||
|
||||
def getPiecesById(playerIds: Seq[Long]): Future[Seq[Loot]] =
|
||||
db.run(piecesLoot(playerIds).result).map(_.map(_.toLoot))
|
||||
withConnection { implicit conn =>
|
||||
SQL("""select * from loot where player_id in ({player_ids})""")
|
||||
.on("player_ids" -> playerIds)
|
||||
.executeQuery()
|
||||
.as(loot.*)
|
||||
}
|
||||
|
||||
def insertPieceById(loot: Loot)(playerId: Long): Future[Int] =
|
||||
db.run(lootTable.insertOrUpdate(LootRep.fromLoot(playerId, loot)))
|
||||
|
||||
private def pieceLoot(piece: LootRep) =
|
||||
piecesLoot(Seq(piece.playerId)).filter(_.piece === piece.piece)
|
||||
|
||||
private def piecesLoot(playerIds: Seq[Long]) =
|
||||
lootTable.filter(_.playerId.inSet(playerIds.toSet))
|
||||
withConnection { implicit conn =>
|
||||
SQL("""insert into loot
|
||||
| (player_id, piece, piece_type, job, created, is_free_loot)
|
||||
| values
|
||||
| ({player_id}, {piece}, {piece_type}, {job}, {created}, {is_free_loot})""".stripMargin)
|
||||
.on(
|
||||
"player_id" -> playerId,
|
||||
"piece" -> loot.piece.piece,
|
||||
"piece_type" -> loot.piece.pieceType.toString,
|
||||
"job" -> loot.piece.job.toString,
|
||||
"created" -> DatabaseProfile.now,
|
||||
"is_free_loot" -> loot.isFreeLootToInt
|
||||
)
|
||||
.executeUpdate()
|
||||
}
|
||||
}
|
||||
|
@ -8,47 +8,41 @@
|
||||
*/
|
||||
package me.arcanis.ffxivbis.storage
|
||||
|
||||
import anorm.SqlParser._
|
||||
import anorm._
|
||||
import me.arcanis.ffxivbis.models.PartyDescription
|
||||
|
||||
import scala.concurrent.Future
|
||||
|
||||
trait PartyProfile { this: DatabaseProfile =>
|
||||
import dbConfig.profile.api._
|
||||
trait PartyProfile extends DatabaseConnection {
|
||||
|
||||
case class PartyRep(partyId: Option[Long], partyName: String, partyAlias: Option[String]) {
|
||||
|
||||
def toDescription: PartyDescription = PartyDescription(partyName, partyAlias)
|
||||
}
|
||||
|
||||
object PartyRep {
|
||||
|
||||
def fromDescription(party: PartyDescription, id: Option[Long]): PartyRep =
|
||||
PartyRep(id, party.partyId, party.partyAlias)
|
||||
}
|
||||
|
||||
class Parties(tag: Tag) extends Table[PartyRep](tag, "parties") {
|
||||
def partyId: Rep[Long] = column[Long]("party_id", O.AutoInc, O.PrimaryKey)
|
||||
def partyName: Rep[String] = column[String]("party_name")
|
||||
def partyAlias: Rep[Option[String]] = column[Option[String]]("party_alias")
|
||||
|
||||
def * =
|
||||
(partyId.?, partyName, partyAlias) <> ((PartyRep.apply _).tupled, PartyRep.unapply)
|
||||
}
|
||||
private val description: RowParser[PartyDescription] =
|
||||
(str("party_name") ~ str("party_alias").?)
|
||||
.map { case partyName ~ partyAlias =>
|
||||
PartyDescription(
|
||||
partyId = partyName,
|
||||
partyAlias = partyAlias,
|
||||
)
|
||||
}
|
||||
|
||||
def getPartyDescription(partyId: String): Future[PartyDescription] =
|
||||
db.run(
|
||||
partyDescription(partyId).result.headOption.map(_.map(_.toDescription).getOrElse(PartyDescription.empty(partyId)))
|
||||
)
|
||||
|
||||
def getUniquePartyId(partyId: String): Future[Option[Long]] =
|
||||
db.run(partyDescription(partyId).map(_.partyId).result.headOption)
|
||||
|
||||
def insertPartyDescription(partyDescription: PartyDescription): Future[Int] =
|
||||
getUniquePartyId(partyDescription.partyId).flatMap {
|
||||
case Some(id) => db.run(partiesTable.update(PartyRep.fromDescription(partyDescription, Some(id))))
|
||||
case _ => db.run(partiesTable.insertOrUpdate(PartyRep.fromDescription(partyDescription, None)))
|
||||
withConnection { implicit conn =>
|
||||
SQL("""select * from parties where party_name = {party_name}""")
|
||||
.on("party_name" -> partyId)
|
||||
.executeQuery()
|
||||
.as(description.singleOpt)
|
||||
.getOrElse(PartyDescription.empty(partyId))
|
||||
}
|
||||
|
||||
private def partyDescription(partyId: String) =
|
||||
partiesTable.filter(_.partyName === partyId)
|
||||
def insertPartyDescription(partyDescription: PartyDescription): Future[Int] =
|
||||
withConnection { implicit conn =>
|
||||
SQL("""insert into parties
|
||||
| (party_name, party_alias)
|
||||
| values
|
||||
| ({party_name}, {party_alias})
|
||||
| on conflict (party_name) do update set
|
||||
| party_alias = {party_alias}""".stripMargin)
|
||||
.on("party_name" -> partyDescription.partyId, "party_alias" -> partyDescription.partyAlias)
|
||||
.executeUpdate()
|
||||
}
|
||||
}
|
||||
|
@ -8,76 +8,97 @@
|
||||
*/
|
||||
package me.arcanis.ffxivbis.storage
|
||||
|
||||
import anorm.SqlParser._
|
||||
import anorm._
|
||||
import me.arcanis.ffxivbis.models.{BiS, Job, Player, PlayerId}
|
||||
|
||||
import scala.concurrent.Future
|
||||
|
||||
trait PlayersProfile { this: DatabaseProfile =>
|
||||
import dbConfig.profile.api._
|
||||
trait PlayersProfile extends DatabaseConnection {
|
||||
|
||||
case class PlayerRep(
|
||||
partyId: String,
|
||||
playerId: Option[Long],
|
||||
created: Long,
|
||||
nick: String,
|
||||
job: String,
|
||||
link: Option[String],
|
||||
priority: Int
|
||||
) {
|
||||
private val player: RowParser[Player] =
|
||||
(long("player_id") ~ str("party_id") ~ str("job")
|
||||
~ str("nick") ~ str("bis_link").? ~ int("priority").?)
|
||||
.map { case playerId ~ partyId ~ job ~ nick ~ link ~ priority =>
|
||||
Player(
|
||||
id = playerId,
|
||||
partyId = partyId,
|
||||
job = Job.withName(job),
|
||||
nick = nick,
|
||||
bis = BiS.empty,
|
||||
loot = Seq.empty,
|
||||
link = link,
|
||||
priority = priority.getOrElse(0),
|
||||
)
|
||||
}
|
||||
|
||||
def toPlayer: Player =
|
||||
Player(playerId.getOrElse(-1), partyId, Job.withName(job), nick, BiS.empty, Seq.empty, link, priority)
|
||||
}
|
||||
|
||||
object PlayerRep {
|
||||
|
||||
def fromPlayer(player: Player, id: Option[Long]): PlayerRep =
|
||||
PlayerRep(player.partyId, id, DatabaseProfile.now, player.nick, player.job.toString, player.link, player.priority)
|
||||
}
|
||||
|
||||
class Players(tag: Tag) extends Table[PlayerRep](tag, "players") {
|
||||
def partyId: Rep[String] = column[String]("party_id")
|
||||
def playerId: Rep[Long] = column[Long]("player_id", O.AutoInc, O.PrimaryKey)
|
||||
def created: Rep[Long] = column[Long]("created")
|
||||
def nick: Rep[String] = column[String]("nick")
|
||||
def job: Rep[String] = column[String]("job")
|
||||
def bisLink: Rep[Option[String]] = column[Option[String]]("bis_link")
|
||||
def priority: Rep[Int] = column[Int]("priority", O.Default(1))
|
||||
|
||||
def * =
|
||||
(partyId, playerId.?, created, nick, job, bisLink, priority) <> ((PlayerRep.apply _).tupled, PlayerRep.unapply)
|
||||
}
|
||||
|
||||
def deletePlayer(playerId: PlayerId): Future[Int] = db.run(player(playerId).delete)
|
||||
|
||||
def getParty(partyId: String): Future[Map[Long, Player]] =
|
||||
db.run(players(partyId).result)
|
||||
.map(_.foldLeft(Map.empty[Long, Player]) {
|
||||
case (acc, p @ PlayerRep(_, Some(id), _, _, _, _, _)) => acc + (id -> p.toPlayer)
|
||||
case (acc, _) => acc
|
||||
})
|
||||
|
||||
def getPlayer(playerId: PlayerId): Future[Option[Long]] =
|
||||
db.run(player(playerId).map(_.playerId).result.headOption)
|
||||
|
||||
def getPlayerFull(playerId: PlayerId): Future[Option[Player]] =
|
||||
db.run(player(playerId).result.headOption.map(_.map(_.toPlayer)))
|
||||
|
||||
def getPlayers(partyId: String): Future[Seq[Long]] =
|
||||
db.run(players(partyId).map(_.playerId).result)
|
||||
|
||||
def insertPlayer(playerObj: Player): Future[Int] =
|
||||
getPlayer(playerObj.playerId).flatMap {
|
||||
case Some(id) => db.run(playersTable.update(PlayerRep.fromPlayer(playerObj, Some(id))))
|
||||
case _ => db.run(playersTable.insertOrUpdate(PlayerRep.fromPlayer(playerObj, None)))
|
||||
def deletePlayer(playerId: PlayerId): Future[Int] =
|
||||
withConnection { implicit conn =>
|
||||
SQL("""delete from players
|
||||
| where party_id = {party_id}
|
||||
| and nick = {nick}
|
||||
| and job = {job}""".stripMargin)
|
||||
.on("party_id" -> playerId.partyId, "nick" -> playerId.nick, "job" -> playerId.job.toString)
|
||||
.executeUpdate()
|
||||
}
|
||||
|
||||
private def player(playerId: PlayerId) =
|
||||
playersTable
|
||||
.filter(_.partyId === playerId.partyId)
|
||||
.filter(_.job === playerId.job.toString)
|
||||
.filter(_.nick === playerId.nick)
|
||||
def getParty(partyId: String): Future[Map[Long, Player]] =
|
||||
withConnection { implicit conn =>
|
||||
SQL("""select * from players where party_id = {party_id}""")
|
||||
.on("party_id" -> partyId)
|
||||
.executeQuery()
|
||||
.as(player.*)
|
||||
.map(p => p.id -> p)
|
||||
.toMap
|
||||
}
|
||||
|
||||
def getPlayer(playerId: PlayerId): Future[Option[Long]] =
|
||||
withConnection { implicit conn =>
|
||||
SQL("""select player_id from players
|
||||
| where party_id = {party_id}
|
||||
| and nick = {nick}
|
||||
| and job = {job}""".stripMargin)
|
||||
.on("party_id" -> playerId.partyId, "nick" -> playerId.nick, "job" -> playerId.job.toString)
|
||||
.executeQuery()
|
||||
.as(scalar[Long].singleOpt)
|
||||
}
|
||||
|
||||
def getPlayerFull(playerId: PlayerId): Future[Option[Player]] =
|
||||
withConnection { implicit conn =>
|
||||
SQL("""select * from players
|
||||
| where party_id = {party_id}
|
||||
| and nick = {nick}
|
||||
| and job = {job}""".stripMargin)
|
||||
.on("party_id" -> playerId.partyId, "nick" -> playerId.nick, "job" -> playerId.job.toString)
|
||||
.executeQuery()
|
||||
.as(player.singleOpt)
|
||||
}
|
||||
|
||||
def getPlayers(partyId: String): Future[Seq[Long]] =
|
||||
withConnection { implicit conn =>
|
||||
SQL("""select player_id from players where party_id = {party_id}""")
|
||||
.on("party_id" -> partyId)
|
||||
.executeQuery()
|
||||
.as(scalar[Long].*)
|
||||
}
|
||||
|
||||
def insertPlayer(player: Player): Future[Int] =
|
||||
withConnection { implicit conn =>
|
||||
SQL("""insert into players
|
||||
| (party_id, created, job, nick, bis_link, priority)
|
||||
| values
|
||||
| ({party_id}, {created}, {job}, {nick}, {link}, {priority})
|
||||
| on conflict (party_id, nick, job) do update set
|
||||
| bis_link = {link}, priority = {priority}""".stripMargin)
|
||||
.on(
|
||||
"party_id" -> player.partyId,
|
||||
"created" -> DatabaseProfile.now,
|
||||
"job" -> player.job.toString,
|
||||
"nick" -> player.nick,
|
||||
"link" -> player.link,
|
||||
"priority" -> player.priority
|
||||
)
|
||||
.executeUpdate()
|
||||
}
|
||||
|
||||
private def players(partyId: String) =
|
||||
playersTable.filter(_.partyId === partyId)
|
||||
}
|
||||
|
@ -8,64 +8,67 @@
|
||||
*/
|
||||
package me.arcanis.ffxivbis.storage
|
||||
|
||||
import anorm.SqlParser._
|
||||
import anorm._
|
||||
import me.arcanis.ffxivbis.models.{Permission, User}
|
||||
import slick.lifted.{Index, PrimaryKey}
|
||||
|
||||
import scala.concurrent.Future
|
||||
|
||||
trait UsersProfile { this: DatabaseProfile =>
|
||||
import dbConfig.profile.api._
|
||||
trait UsersProfile extends DatabaseConnection {
|
||||
|
||||
case class UserRep(partyId: String, userId: Option[Long], username: String, password: String, permission: String) {
|
||||
|
||||
def toUser: User = User(partyId, username, password, Permission.withName(permission))
|
||||
}
|
||||
|
||||
object UserRep {
|
||||
|
||||
def fromUser(user: User, id: Option[Long]): UserRep =
|
||||
UserRep(user.partyId, id, user.username, user.password, user.permission.toString)
|
||||
}
|
||||
|
||||
class Users(tag: Tag) extends Table[UserRep](tag, "users") {
|
||||
def partyId: Rep[String] = column[String]("party_id")
|
||||
def userId: Rep[Long] = column[Long]("user_id", O.AutoInc, O.PrimaryKey)
|
||||
def username: Rep[String] = column[String]("username")
|
||||
def password: Rep[String] = column[String]("password")
|
||||
def permission: Rep[String] = column[String]("permission")
|
||||
|
||||
def * =
|
||||
(partyId, userId.?, username, password, permission) <> ((UserRep.apply _).tupled, UserRep.unapply)
|
||||
|
||||
def pk: PrimaryKey = primaryKey("users_username_idx", (partyId, username))
|
||||
def usersUsernameIdx: Index =
|
||||
index("users_username_idx", (partyId, username), unique = true)
|
||||
}
|
||||
private val user: RowParser[User] =
|
||||
(str("party_id") ~ str("username") ~ str("password") ~ str("permission"))
|
||||
.map { case partyId ~ username ~ password ~ permission =>
|
||||
User(
|
||||
partyId = partyId,
|
||||
username = username,
|
||||
password = password,
|
||||
permission = Permission.withName(permission),
|
||||
)
|
||||
}
|
||||
|
||||
def deleteUser(partyId: String, username: String): Future[Int] =
|
||||
db.run(
|
||||
user(partyId, Some(username))
|
||||
.filter(_.permission =!= Permission.admin.toString) // we do not allow to remove admins
|
||||
.delete
|
||||
)
|
||||
|
||||
def exists(partyId: String): Future[Boolean] =
|
||||
db.run(user(partyId, None).exists.result)
|
||||
|
||||
def getUser(partyId: String, username: String): Future[Option[User]] =
|
||||
db.run(user(partyId, Some(username)).result.headOption).map(_.map(_.toUser))
|
||||
|
||||
def getUsers(partyId: String): Future[Seq[User]] =
|
||||
db.run(user(partyId, None).result).map(_.map(_.toUser))
|
||||
|
||||
def insertUser(userObj: User): Future[Int] =
|
||||
db.run(user(userObj.partyId, Some(userObj.username)).map(_.userId).result.headOption).flatMap {
|
||||
case Some(id) => db.run(usersTable.insertOrUpdate(UserRep.fromUser(userObj, Some(id))))
|
||||
case _ => db.run(usersTable.insertOrUpdate(UserRep.fromUser(userObj, None)))
|
||||
withConnection { implicit conn =>
|
||||
SQL("""delete from users
|
||||
| where party_id = {party_id}
|
||||
| and username = {username}
|
||||
| and permission <> {admin}""".stripMargin)
|
||||
.on("party_id" -> partyId, "username" -> username, "admin" -> Permission.admin.toString)
|
||||
.executeUpdate()
|
||||
}
|
||||
|
||||
private def user(partyId: String, username: Option[String]) =
|
||||
usersTable
|
||||
.filter(_.partyId === partyId)
|
||||
.filterIf(username.isDefined)(_.username === username.orNull)
|
||||
def exists(partyId: String): Future[Boolean] = getUsers(partyId).map(_.nonEmpty)(executionContext)
|
||||
|
||||
def getUser(partyId: String, username: String): Future[Option[User]] =
|
||||
withConnection { implicit conn =>
|
||||
SQL("""select * from users where party_id = {party_id} and username = {username}""")
|
||||
.on("party_id" -> partyId, "username" -> username)
|
||||
.executeQuery()
|
||||
.as(user.singleOpt)
|
||||
}
|
||||
|
||||
def getUsers(partyId: String): Future[Seq[User]] =
|
||||
withConnection { implicit conn =>
|
||||
SQL("""select * from users where party_id = {party_id}""")
|
||||
.on("party_id" -> partyId)
|
||||
.executeQuery()
|
||||
.as(user.*)
|
||||
}
|
||||
|
||||
def insertUser(user: User): Future[Int] =
|
||||
withConnection { implicit conn =>
|
||||
SQL("""insert into users
|
||||
| (party_id, username, password, permission)
|
||||
| values
|
||||
| ({party_id}, {username}, {password}, {permission})
|
||||
| on conflict (party_id, username) do update set
|
||||
| password = {password}, permission = {permission}""".stripMargin)
|
||||
.on(
|
||||
"party_id" -> user.partyId,
|
||||
"username" -> user.username,
|
||||
"password" -> user.password,
|
||||
"permission" -> user.permission.toString
|
||||
)
|
||||
.executeUpdate()
|
||||
}
|
||||
}
|
||||
|
@ -9,22 +9,23 @@
|
||||
package me.arcanis.ffxivbis.utils
|
||||
|
||||
import akka.util.Timeout
|
||||
import com.typesafe.config.Config
|
||||
|
||||
import java.time.Duration
|
||||
import java.util.concurrent.TimeUnit
|
||||
import scala.concurrent.duration.FiniteDuration
|
||||
import scala.language.implicitConversions
|
||||
import scala.util.Try
|
||||
|
||||
object Implicits {
|
||||
|
||||
implicit def getBooleanFromOptionString(maybeYes: Option[String]): Boolean = maybeYes.map(_.toLowerCase) match {
|
||||
case Some("yes" | "on") => true
|
||||
case _ => false
|
||||
implicit class ConfigExtension(config: Config) {
|
||||
|
||||
def getFiniteDuration(path: String): FiniteDuration =
|
||||
FiniteDuration(config.getDuration(path, TimeUnit.MILLISECONDS), TimeUnit.MILLISECONDS)
|
||||
|
||||
def getOptString(path: String): Option[String] =
|
||||
Try(config.getString(path)).toOption.filter(_.nonEmpty)
|
||||
|
||||
def getTimeout(path: String): Timeout = getFiniteDuration(path)
|
||||
}
|
||||
|
||||
implicit def getFiniteDuration(duration: Duration): FiniteDuration =
|
||||
FiniteDuration(duration.toNanos, TimeUnit.NANOSECONDS)
|
||||
|
||||
implicit def getTimeout(duration: Duration): Timeout =
|
||||
FiniteDuration(duration.toNanos, TimeUnit.NANOSECONDS)
|
||||
}
|
||||
|
@ -17,7 +17,7 @@ object Settings {
|
||||
}
|
||||
|
||||
def clearDatabase(config: Config): Unit =
|
||||
config.getString("me.arcanis.ffxivbis.database.sqlite.db.url").split(":")
|
||||
config.getString("me.arcanis.ffxivbis.database.sqlite.jdbcUrl").split(":")
|
||||
.lastOption.foreach { databasePath =>
|
||||
val databaseFile = new File(databasePath)
|
||||
if (databaseFile.exists)
|
||||
@ -25,5 +25,5 @@ object Settings {
|
||||
}
|
||||
def randomDatabasePath: String = File.createTempFile("ffxivdb-",".db").toPath.toString
|
||||
def withRandomDatabase: Config =
|
||||
config(Map("me.arcanis.ffxivbis.database.sqlite.db.url" -> s"jdbc:sqlite:$randomDatabasePath"))
|
||||
config(Map("me.arcanis.ffxivbis.database.sqlite.jdbcUrl" -> s"jdbc:sqlite:$randomDatabasePath"))
|
||||
}
|
||||
|
@ -12,8 +12,7 @@ import me.arcanis.ffxivbis.messages.{AddPlayer, AddUser}
|
||||
import me.arcanis.ffxivbis.models.{BiS, Job}
|
||||
import me.arcanis.ffxivbis.service.PartyService
|
||||
import me.arcanis.ffxivbis.service.bis.BisProvider
|
||||
import me.arcanis.ffxivbis.service.database.Database
|
||||
import me.arcanis.ffxivbis.storage.Migration
|
||||
import me.arcanis.ffxivbis.service.database.{Database, Migration}
|
||||
import me.arcanis.ffxivbis.utils.Compare
|
||||
import me.arcanis.ffxivbis.{Fixtures, Settings}
|
||||
import org.scalatest.matchers.should.Matchers
|
||||
|
@ -10,8 +10,7 @@ import com.typesafe.config.Config
|
||||
import me.arcanis.ffxivbis.http.api.v1.json._
|
||||
import me.arcanis.ffxivbis.messages.{AddPlayer, AddUser}
|
||||
import me.arcanis.ffxivbis.service.PartyService
|
||||
import me.arcanis.ffxivbis.service.database.Database
|
||||
import me.arcanis.ffxivbis.storage.Migration
|
||||
import me.arcanis.ffxivbis.service.database.{Database, Migration}
|
||||
import me.arcanis.ffxivbis.{Fixtures, Settings}
|
||||
import org.scalatest.matchers.should.Matchers
|
||||
import org.scalatest.wordspec.AnyWordSpecLike
|
||||
|
@ -12,8 +12,7 @@ import me.arcanis.ffxivbis.messages.AddUser
|
||||
import me.arcanis.ffxivbis.models.PartyDescription
|
||||
import me.arcanis.ffxivbis.service.PartyService
|
||||
import me.arcanis.ffxivbis.service.bis.BisProvider
|
||||
import me.arcanis.ffxivbis.service.database.Database
|
||||
import me.arcanis.ffxivbis.storage.Migration
|
||||
import me.arcanis.ffxivbis.service.database.{Database, Migration}
|
||||
import me.arcanis.ffxivbis.{Fixtures, Settings}
|
||||
import org.scalatest.matchers.should.Matchers
|
||||
import org.scalatest.wordspec.AnyWordSpecLike
|
||||
|
@ -11,8 +11,7 @@ import me.arcanis.ffxivbis.http.api.v1.json._
|
||||
import me.arcanis.ffxivbis.messages.{AddPlayer, AddUser}
|
||||
import me.arcanis.ffxivbis.service.PartyService
|
||||
import me.arcanis.ffxivbis.service.bis.BisProvider
|
||||
import me.arcanis.ffxivbis.service.database.Database
|
||||
import me.arcanis.ffxivbis.storage.Migration
|
||||
import me.arcanis.ffxivbis.service.database.{Database, Migration}
|
||||
import me.arcanis.ffxivbis.{Fixtures, Settings}
|
||||
import org.scalatest.matchers.should.Matchers
|
||||
import org.scalatest.wordspec.AnyWordSpecLike
|
||||
|
@ -0,0 +1,29 @@
|
||||
package me.arcanis.ffxivbis.http.api.v1
|
||||
|
||||
import akka.http.scaladsl.model.StatusCodes
|
||||
import akka.http.scaladsl.testkit.ScalatestRouteTest
|
||||
import com.typesafe.config.Config
|
||||
import me.arcanis.ffxivbis.Settings
|
||||
import me.arcanis.ffxivbis.http.api.v1.json._
|
||||
import org.scalatest.matchers.should.Matchers
|
||||
import org.scalatest.wordspec.AnyWordSpecLike
|
||||
|
||||
import scala.language.postfixOps
|
||||
|
||||
class StatusEndpointTest extends AnyWordSpecLike
|
||||
with Matchers with ScalatestRouteTest with JsonSupport {
|
||||
|
||||
override val testConfig: Config = Settings.withRandomDatabase
|
||||
|
||||
private val route = new StatusEndpoint().route
|
||||
|
||||
"api v1 status endpoint" must {
|
||||
|
||||
"return server status" in {
|
||||
Get("/status") ~> route ~> check {
|
||||
status shouldEqual StatusCodes.OK
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@ -8,8 +8,7 @@ import akka.testkit.TestKit
|
||||
import com.typesafe.config.Config
|
||||
import me.arcanis.ffxivbis.http.api.v1.json._
|
||||
import me.arcanis.ffxivbis.service.PartyService
|
||||
import me.arcanis.ffxivbis.service.database.Database
|
||||
import me.arcanis.ffxivbis.storage.Migration
|
||||
import me.arcanis.ffxivbis.service.database.{Database, Migration}
|
||||
import me.arcanis.ffxivbis.{Fixtures, Settings}
|
||||
import org.scalatest.matchers.should.Matchers
|
||||
import org.scalatest.wordspec.AnyWordSpecLike
|
||||
|
@ -4,7 +4,6 @@ import akka.actor.testkit.typed.scaladsl.ScalaTestWithActorTestKit
|
||||
import akka.actor.typed.scaladsl.AskPattern.Askable
|
||||
import me.arcanis.ffxivbis.messages.{AddPieceToBis, AddPlayer, GetBiS, RemovePieceFromBiS}
|
||||
import me.arcanis.ffxivbis.models._
|
||||
import me.arcanis.ffxivbis.storage.Migration
|
||||
import me.arcanis.ffxivbis.utils.Compare
|
||||
import me.arcanis.ffxivbis.{Fixtures, Settings}
|
||||
import org.scalatest.wordspec.AnyWordSpecLike
|
||||
@ -85,6 +84,6 @@ class DatabaseBiSHandlerTest extends ScalaTestWithActorTestKit(Settings.withRand
|
||||
|
||||
}
|
||||
|
||||
private def partyBiSCompare[T](party: Seq[T], bis: Seq[Piece]): Boolean =
|
||||
Compare.seqEquals(party.foldLeft(Seq.empty[Piece]){ case (acc, player) => acc ++ player.asInstanceOf[Player].bis.pieces }, bis)
|
||||
private def partyBiSCompare(party: Seq[Player], bis: Seq[Piece]): Boolean =
|
||||
Compare.seqEquals(party.foldLeft(Seq.empty[Piece]){ case (acc, player) => acc ++ player.bis.pieces }, bis)
|
||||
}
|
||||
|
@ -2,9 +2,9 @@ package me.arcanis.ffxivbis.service.database
|
||||
|
||||
import akka.actor.testkit.typed.scaladsl.ScalaTestWithActorTestKit
|
||||
import akka.actor.typed.scaladsl.AskPattern.Askable
|
||||
import ch.qos.logback.core.util.FixedDelay
|
||||
import me.arcanis.ffxivbis.messages.{AddPieceTo, AddPlayer, GetLoot, RemovePieceFrom}
|
||||
import me.arcanis.ffxivbis.models._
|
||||
import me.arcanis.ffxivbis.storage.Migration
|
||||
import me.arcanis.ffxivbis.utils.Compare
|
||||
import me.arcanis.ffxivbis.{Fixtures, Settings}
|
||||
import org.scalatest.wordspec.AnyWordSpecLike
|
||||
@ -58,7 +58,7 @@ class DatabaseLootHandlerTest extends ScalaTestWithActorTestKit(Settings.withRan
|
||||
|
||||
"remove loot" in {
|
||||
val updateProbe = testKit.createTestProbe[Unit]()
|
||||
database ! RemovePieceFrom(Fixtures.playerEmpty.playerId, Fixtures.lootBody, updateProbe.ref)
|
||||
database ! RemovePieceFrom(Fixtures.playerEmpty.playerId, Fixtures.lootBody, isFreeLoot = false, updateProbe.ref)
|
||||
updateProbe.expectMessage(askTimeout, ())
|
||||
|
||||
val newLoot = Fixtures.loot.filterNot(_ == Fixtures.lootBody)
|
||||
@ -87,10 +87,24 @@ class DatabaseLootHandlerTest extends ScalaTestWithActorTestKit(Settings.withRan
|
||||
partyLootCompare(party, Fixtures.loot ++ Fixtures.loot) shouldEqual true
|
||||
}
|
||||
|
||||
"remove only one piece" in {
|
||||
val updateProbe = testKit.createTestProbe[Unit]()
|
||||
database ! RemovePieceFrom(Fixtures.playerEmpty.playerId, Fixtures.lootBody, isFreeLoot = false, updateProbe.ref)
|
||||
updateProbe.expectMessage(askTimeout, ())
|
||||
|
||||
val probe = testKit.createTestProbe[Seq[Player]]()
|
||||
database ! GetLoot(Fixtures.playerEmpty.partyId, None, probe.ref)
|
||||
|
||||
val party = probe.expectMessageType[Seq[Player]](askTimeout)
|
||||
val player = party.filter(_.playerId == Fixtures.playerEmpty.playerId)
|
||||
player should not be empty
|
||||
player.flatMap(_.loot).map(_.piece) should contain (Fixtures.lootBody)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private def partyLootCompare[T](party: Seq[T], loot: Seq[Piece]): Boolean =
|
||||
private def partyLootCompare(party: Seq[Player], loot: Seq[Piece]): Boolean =
|
||||
Compare.seqEquals(party.foldLeft(Seq.empty[Piece]){ case (acc, player) =>
|
||||
acc ++ player.asInstanceOf[Player].loot.map(_.piece)
|
||||
acc ++ player.loot.map(_.piece)
|
||||
}, loot)
|
||||
}
|
||||
|
@ -3,7 +3,6 @@ package me.arcanis.ffxivbis.service.database
|
||||
import akka.actor.testkit.typed.scaladsl.ScalaTestWithActorTestKit
|
||||
import me.arcanis.ffxivbis.messages.{AddPlayer, GetParty, GetPlayer, RemovePlayer}
|
||||
import me.arcanis.ffxivbis.models._
|
||||
import me.arcanis.ffxivbis.storage.Migration
|
||||
import me.arcanis.ffxivbis.utils.Compare
|
||||
import me.arcanis.ffxivbis.{Fixtures, Settings}
|
||||
import org.scalatest.wordspec.AnyWordSpecLike
|
||||
|
@ -3,7 +3,6 @@ package me.arcanis.ffxivbis.service.database
|
||||
import akka.actor.testkit.typed.scaladsl.ScalaTestWithActorTestKit
|
||||
import me.arcanis.ffxivbis.messages.{AddUser, DeleteUser, GetUser, GetUsers}
|
||||
import me.arcanis.ffxivbis.models.User
|
||||
import me.arcanis.ffxivbis.storage.Migration
|
||||
import me.arcanis.ffxivbis.utils.Compare
|
||||
import me.arcanis.ffxivbis.{Fixtures, Settings}
|
||||
import org.scalatest.wordspec.AnyWordSpecLike
|
||||
|
@ -1 +1 @@
|
||||
version := "0.10.0"
|
||||
version := "0.12.2"
|
||||
|
Reference in New Issue
Block a user