mirror of
https://github.com/arcan1s/ffxivbis.git
synced 2025-04-27 10:47:16 +00:00
Compare commits
55 Commits
Author | SHA1 | Date | |
---|---|---|---|
fa43517b16 | |||
77e99439e7 | |||
c9eb311cfe | |||
d662e303c8 | |||
1c8aaea712 | |||
3c8e5f8da8 | |||
0bcda3233e | |||
c4be6f12f1 | |||
f3535f6e16 | |||
bdf413d494 | |||
7a1a73592e | |||
b1ac894ccf | |||
6023e86570 | |||
a4ab1e49be | |||
cb99486f8a | |||
0e8b95d0dd | |||
118d8faf6b | |||
448880ed91 | |||
ed3cdd62bd | |||
88617eccdf | |||
ccbf581332 | |||
0ab9162cb5 | |||
d3018998cd | |||
d4553b2e50 | |||
8496d105c0 | |||
ec2cfaea38 | |||
963e84f792 | |||
feea01a47e | |||
fcacd9f15c | |||
b2256784dd | |||
fee87ddbc8 | |||
dc882b74bf | |||
7a6cd84ce3 | |||
33b750123d | |||
d049238dcf | |||
5d72852420 | |||
78a00e2cab | |||
786c3d7d48 | |||
8a1d99b319 | |||
ac0e0ac899 | |||
e88c9d51b0 | |||
ced781bba2 | |||
012cdd2d8b | |||
c5b0832d29 | |||
b36240765a | |||
4e3066e0a3 | |||
eeb5178efc | |||
a6991a0a91 | |||
5ec372be87 | |||
bcdc88fa2c | |||
53b42a6fa8 | |||
99ed2705a2 | |||
0ed9e92441 | |||
1866a1bb12 | |||
08f7f4571e |
40
.github/workflows/release.yml
vendored
Normal file
40
.github/workflows/release.yml
vendored
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
name: release
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- '*.*.*'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
make-release:
|
||||||
|
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- name: extract version
|
||||||
|
id: version
|
||||||
|
run: echo ::set-output name=VERSION::${GITHUB_REF#refs/tags/}
|
||||||
|
- name: create changelog
|
||||||
|
id: changelog
|
||||||
|
uses: jaywcjlove/changelog-generator@main
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
filter: 'Release \d+\.\d+\.\d+'
|
||||||
|
- name: setup JDK
|
||||||
|
uses: actions/setup-java@v2
|
||||||
|
with:
|
||||||
|
distribution: temurin
|
||||||
|
java-version: 18
|
||||||
|
- name: create dist
|
||||||
|
run: make dist
|
||||||
|
- name: release
|
||||||
|
uses: softprops/action-gh-release@v1
|
||||||
|
with:
|
||||||
|
body: |
|
||||||
|
${{ steps.changelog.outputs.compareurl }}
|
||||||
|
${{ steps.changelog.outputs.changelog }}
|
||||||
|
files: target/universal/ffxivbis-*.zip
|
||||||
|
fail_on_unmatched_files: true
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
22
.github/workflows/run-tests.yml
vendored
Normal file
22
.github/workflows/run-tests.yml
vendored
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
name: tests
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ master ]
|
||||||
|
pull_request:
|
||||||
|
branches: [ master ]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
run-tests:
|
||||||
|
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- name: setup JDK
|
||||||
|
uses: actions/setup-java@v2
|
||||||
|
with:
|
||||||
|
distribution: temurin
|
||||||
|
java-version: 18
|
||||||
|
- name: run tests
|
||||||
|
run: make tests
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -75,6 +75,7 @@ lib_managed/
|
|||||||
src_managed/
|
src_managed/
|
||||||
project/boot/
|
project/boot/
|
||||||
project/plugins/project/
|
project/plugins/project/
|
||||||
|
.bsp/
|
||||||
|
|
||||||
# Scala-IDE specific
|
# Scala-IDE specific
|
||||||
.scala_dependencies
|
.scala_dependencies
|
||||||
|
35
.scalafmt.conf
Normal file
35
.scalafmt.conf
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
version = 3.3.1
|
||||||
|
|
||||||
|
runner.dialect = "scala213"
|
||||||
|
|
||||||
|
maxColumn = 120
|
||||||
|
|
||||||
|
align.preset = none
|
||||||
|
|
||||||
|
continuationIndent {
|
||||||
|
defnSite = 2
|
||||||
|
extendSite = 2
|
||||||
|
}
|
||||||
|
|
||||||
|
rewrite {
|
||||||
|
rules = [
|
||||||
|
AvoidInfix,
|
||||||
|
RedundantBraces,
|
||||||
|
RedundantParens,
|
||||||
|
SortImports,
|
||||||
|
SortModifiers
|
||||||
|
]
|
||||||
|
|
||||||
|
redundantBraces {
|
||||||
|
generalExpressions = yes
|
||||||
|
ifElseExpressions = yes
|
||||||
|
includeUnitMethods = yes
|
||||||
|
methodBodies = yes
|
||||||
|
parensForOneLineApply = yes
|
||||||
|
stringInterpolation = yes
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
importSelectors = singleLine
|
||||||
|
trailingCommas = preserve
|
@ -1,9 +0,0 @@
|
|||||||
language: scala
|
|
||||||
scala:
|
|
||||||
- 2.13.1
|
|
||||||
|
|
||||||
sbt_args: -no-colors
|
|
||||||
|
|
||||||
script:
|
|
||||||
- sbt compile
|
|
||||||
- sbt test
|
|
35
Makefile
Normal file
35
Makefile
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
.PHONY: check clean compile dist push tests version
|
||||||
|
.DEFAULT_GOAL := compile
|
||||||
|
|
||||||
|
PROJECT := ffxivbis
|
||||||
|
|
||||||
|
check:
|
||||||
|
sbt scalafmtCheck
|
||||||
|
|
||||||
|
clean:
|
||||||
|
sbt clean
|
||||||
|
|
||||||
|
compile: clean
|
||||||
|
sbt compile
|
||||||
|
|
||||||
|
format:
|
||||||
|
sbt scalafmt
|
||||||
|
|
||||||
|
dist: tests
|
||||||
|
sbt dist
|
||||||
|
|
||||||
|
push: version dist
|
||||||
|
git add version.sbt
|
||||||
|
git commit -m "Release $(VERSION)"
|
||||||
|
git tag "$(VERSION)"
|
||||||
|
git push
|
||||||
|
git push --tags
|
||||||
|
|
||||||
|
tests: compile check
|
||||||
|
sbt test
|
||||||
|
|
||||||
|
version:
|
||||||
|
ifndef VERSION
|
||||||
|
$(error VERSION is required, but not set)
|
||||||
|
endif
|
||||||
|
sed -i '/version := "[0-9.]*/s/[^"][^)]*/version := "$(VERSION)"/' version.sbt
|
10
README.md
10
README.md
@ -1,8 +1,8 @@
|
|||||||
# FFXIV BiS
|
# FFXIV BiS
|
||||||
|
|
||||||
[](https://travis-ci.org/arcan1s/ffxivbis) 
|
[](https://github.com/arcan1s/ffxivbis/actions/workflows/run-tests.yml) 
|
||||||
|
|
||||||
Service which allows to manage savage loot distribution easy.
|
Service which allows managing savage loot distribution easy.
|
||||||
|
|
||||||
## Installation and usage
|
## Installation and usage
|
||||||
|
|
||||||
@ -12,7 +12,7 @@ In general compilation process looks like:
|
|||||||
sbt dist
|
sbt dist
|
||||||
```
|
```
|
||||||
|
|
||||||
Or alternatively you can download latest distribution zip from the releases page. Service can be run by using command:
|
Or alternatively you can download the latest distribution zip from the releases page. Service can be run by using command:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
bin/ffxivbis
|
bin/ffxivbis
|
||||||
@ -22,10 +22,10 @@ from the extracted archive root.
|
|||||||
|
|
||||||
## Web service
|
## 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.
|
*Note*: host and port depend on configuration settings.
|
||||||
|
|
||||||
## Public service
|
## 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.
|
||||||
|
3
TODO.md
3
TODO.md
@ -1,3 +0,0 @@
|
|||||||
* [x] items improvements
|
|
||||||
* [x] multiple parties support
|
|
||||||
* [ ] pretty UI
|
|
13
build.sbt
13
build.sbt
@ -1,16 +1,9 @@
|
|||||||
|
organization := "me.arcanis"
|
||||||
|
|
||||||
name := "ffxivbis"
|
name := "ffxivbis"
|
||||||
|
|
||||||
scalaVersion := "2.13.1"
|
scalaVersion := "2.13.12"
|
||||||
|
|
||||||
scalacOptions ++= Seq("-deprecation", "-feature")
|
scalacOptions ++= Seq("-deprecation", "-feature")
|
||||||
|
|
||||||
enablePlugins(JavaAppPackaging)
|
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)
|
|
||||||
}
|
|
56
extract_items.py
Normal file
56
extract_items.py
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
import json
|
||||||
|
import requests
|
||||||
|
|
||||||
|
# NOTE: it does not cover all items, just workaround to extract most gear pieces from patches
|
||||||
|
MIN_ILVL = 580
|
||||||
|
MAX_ILVL = 605
|
||||||
|
|
||||||
|
TOME = (
|
||||||
|
'radiant',
|
||||||
|
)
|
||||||
|
SAVAGE = (
|
||||||
|
'asphodelos',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
payload = {
|
||||||
|
'queries': [
|
||||||
|
{
|
||||||
|
'slots': []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'jobs': [],
|
||||||
|
'minItemLevel': 580,
|
||||||
|
'maxItemLevel': 605
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'existing': []
|
||||||
|
}
|
||||||
|
# it does not support application/json
|
||||||
|
r = requests.post('https://ffxiv.ariyala.com/items.app', data=json.dumps(payload))
|
||||||
|
r.raise_for_status()
|
||||||
|
|
||||||
|
result = []
|
||||||
|
|
||||||
|
for item in r.json():
|
||||||
|
item_id = item['itemID']
|
||||||
|
source_dict = item['source']
|
||||||
|
name = item['name']['en']
|
||||||
|
if 'crafting' in source_dict:
|
||||||
|
source = 'Crafted'
|
||||||
|
elif 'gathering' in source_dict:
|
||||||
|
continue # some random shit
|
||||||
|
elif 'purchase' in source_dict:
|
||||||
|
if any(tome in name.lower() for tome in TOME):
|
||||||
|
source = 'Tome'
|
||||||
|
elif any(savage in name.lower() for savage in SAVAGE):
|
||||||
|
source = 'Savage'
|
||||||
|
else:
|
||||||
|
source = None
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
raise RuntimeError(f'Unknown source {source_dict}')
|
||||||
|
result.append({'id': item_id, 'source': source, 'name': name})
|
||||||
|
|
||||||
|
output = {'cached-items': result}
|
||||||
|
print(json.dumps(output, indent=4, sort_keys=True))
|
@ -1,32 +1,32 @@
|
|||||||
val AkkaVersion = "2.6.10"
|
val AkkaVersion = "2.8.6"
|
||||||
val AkkaHttpVersion = "10.2.1"
|
val AkkaHttpVersion = "10.5.3"
|
||||||
val SlickVersion = "3.3.3"
|
val ScalaTestVersion = "3.2.19"
|
||||||
|
|
||||||
libraryDependencies += "ch.qos.logback" % "logback-classic" % "1.2.3"
|
libraryDependencies += "ch.qos.logback" % "logback-classic" % "1.5.6"
|
||||||
libraryDependencies += "com.typesafe.scala-logging" %% "scala-logging" % "3.9.2"
|
libraryDependencies += "com.typesafe.scala-logging" %% "scala-logging" % "3.9.5"
|
||||||
|
|
||||||
libraryDependencies += "com.typesafe.akka" %% "akka-http" % AkkaHttpVersion
|
libraryDependencies += "com.typesafe.akka" %% "akka-http" % AkkaHttpVersion
|
||||||
libraryDependencies += "com.typesafe.akka" %% "akka-http-spray-json" % AkkaHttpVersion
|
libraryDependencies += "com.typesafe.akka" %% "akka-http-spray-json" % AkkaHttpVersion
|
||||||
libraryDependencies += "com.typesafe.akka" %% "akka-actor-typed" % AkkaVersion
|
libraryDependencies += "com.typesafe.akka" %% "akka-actor-typed" % AkkaVersion
|
||||||
libraryDependencies += "com.typesafe.akka" %% "akka-stream" % AkkaVersion
|
libraryDependencies += "com.typesafe.akka" %% "akka-stream" % AkkaVersion
|
||||||
libraryDependencies += "com.github.swagger-akka-http" %% "swagger-akka-http" % "2.3.0"
|
libraryDependencies += "com.github.swagger-akka-http" %% "swagger-akka-http" % "2.11.0"
|
||||||
libraryDependencies += "javax.ws.rs" % "javax.ws.rs-api" % "2.1.1"
|
libraryDependencies += "jakarta.platform" % "jakarta.jakartaee-web-api" % "10.0.0"
|
||||||
|
libraryDependencies += "ch.megard" %% "akka-http-cors" % "1.2.0"
|
||||||
|
|
||||||
libraryDependencies += "io.spray" %% "spray-json" % "1.3.6"
|
libraryDependencies += "io.spray" %% "spray-json" % "1.3.6"
|
||||||
libraryDependencies += "com.lihaoyi" %% "scalatags" % "0.9.2"
|
|
||||||
|
|
||||||
libraryDependencies += "com.typesafe.slick" %% "slick" % SlickVersion
|
libraryDependencies += "org.playframework.anorm" %% "anorm" % "2.7.0"
|
||||||
libraryDependencies += "com.typesafe.slick" %% "slick-hikaricp" % SlickVersion
|
libraryDependencies += "com.zaxxer" % "HikariCP" % "5.1.0" exclude("org.slf4j", "slf4j-api")
|
||||||
libraryDependencies += "org.flywaydb" % "flyway-core" % "6.0.6"
|
libraryDependencies += "org.flywaydb" % "flyway-core" % "9.16.0"
|
||||||
libraryDependencies += "org.xerial" % "sqlite-jdbc" % "3.32.3.2"
|
libraryDependencies += "org.xerial" % "sqlite-jdbc" % "3.46.0.0"
|
||||||
libraryDependencies += "org.postgresql" % "postgresql" % "42.2.18"
|
libraryDependencies += "org.postgresql" % "postgresql" % "42.7.3"
|
||||||
|
|
||||||
libraryDependencies += "org.mindrot" % "jbcrypt" % "0.3m"
|
|
||||||
|
|
||||||
|
libraryDependencies += "org.mindrot" % "jbcrypt" % "0.4"
|
||||||
|
libraryDependencies += "com.google.guava" % "guava" % "31.0.1-jre"
|
||||||
|
|
||||||
// testing
|
// testing
|
||||||
libraryDependencies += "org.scalactic" %% "scalactic" % "3.1.4" % "test"
|
libraryDependencies += "org.scalactic" %% "scalactic" % ScalaTestVersion % "test"
|
||||||
libraryDependencies += "org.scalatest" %% "scalatest" % "3.1.4" % "test"
|
libraryDependencies += "org.scalatest" %% "scalatest" % ScalaTestVersion % "test"
|
||||||
|
|
||||||
libraryDependencies += "com.typesafe.akka" %% "akka-actor-testkit-typed" % AkkaVersion % "test"
|
libraryDependencies += "com.typesafe.akka" %% "akka-actor-testkit-typed" % AkkaVersion % "test"
|
||||||
libraryDependencies += "com.typesafe.akka" %% "akka-stream-testkit" % AkkaVersion % "test"
|
libraryDependencies += "com.typesafe.akka" %% "akka-stream-testkit" % AkkaVersion % "test"
|
||||||
|
@ -1 +1 @@
|
|||||||
sbt.version = 1.3.3
|
sbt.version = 1.7.1
|
||||||
|
@ -1,3 +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("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,3 @@
|
|||||||
|
update parties set party_alias = regexp_replace(party_alias, '[^A-Za-z0-9!@#$%^&*()\-_=+;:'',./? ]', '', 'g');
|
||||||
|
update players set nick = regexp_replace(nick, '[^A-Za-z0-9!@#$%^&*()\-_=+;:'',./? ]', '', 'g');
|
||||||
|
update users set username = regexp_replace(username, '[^A-Za-z0-9!@#$%^&*()\-_=+;:'',./? ]', '', 'g');
|
@ -0,0 +1 @@
|
|||||||
|
update players set bis_link = null where bis_link = '';
|
@ -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);
|
23
src/main/resources/html/api.html
Normal file
23
src/main/resources/html/api.html
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>FFXIV loot helper API</title>
|
||||||
|
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||||
|
<!-- Embed elements Elements via Web Component -->
|
||||||
|
<script src="https://unpkg.com/@stoplight/elements/web-components.min.js"></script>
|
||||||
|
<link rel="stylesheet" href="https://unpkg.com/@stoplight/elements/styles.min.css" type="text/css">
|
||||||
|
|
||||||
|
<link rel="shortcut icon" href="/static/favicon.ico">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<elements-api
|
||||||
|
apiDescriptionUrl="/api-docs/swagger.json"
|
||||||
|
router="hash"
|
||||||
|
layout="sidebar"
|
||||||
|
/>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
366
src/main/resources/html/bis.html
Normal file
366
src/main/resources/html/bis.html
Normal file
@ -0,0 +1,366 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Best in slot</title>
|
||||||
|
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
|
||||||
|
<link rel="shortcut icon" href="/static/favicon.ico">
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous" type="text/css">
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.8.3/font/bootstrap-icons.css" type="text/css">
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="https://unpkg.com/bootstrap-table@1.20.2/dist/bootstrap-table.min.css" type="text/css">
|
||||||
|
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="https://unpkg.com/jquery-resizable-columns@0.2.3/dist/jquery.resizableColumns.css" type="text/css">
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="/static/styles.css" type="text/css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<nav class="navbar navbar-expand-lg justify-content-between align-items-center border-bottom">
|
||||||
|
<a class="navbar-brand" id="navbar-title">Party</a>
|
||||||
|
<ul class="navbar-nav">
|
||||||
|
<a class="nav-item nav-link" id="navbar-bis">best in slot</a>
|
||||||
|
<a class="nav-item nav-link" id="navbar-loot">looted items</a>
|
||||||
|
</ul>
|
||||||
|
<ul class="navbar-nav">
|
||||||
|
<a class="nav-item nav-link" id="navbar-users">users</a>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="alert-placeholder" class="container"></div>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<h2>Best in slot</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<div id="toolbar">
|
||||||
|
<button id="update-btn" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#update-bis-dialog" hidden>
|
||||||
|
<i class="bi bi-plus"></i> update
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-secondary" onclick="reload()">
|
||||||
|
<i class="bi bi-arrow-clockwise"></i> reload
|
||||||
|
</button>
|
||||||
|
<button id="remove-btn" class="btn btn-danger" onclick="removePiece()" disabled hidden>
|
||||||
|
<i class="bi bi-trash"></i> remove
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<table id="bis" class="table table-striped table-hover"
|
||||||
|
data-click-to-select="true"
|
||||||
|
data-export-options='{"fileName": "bis"}'
|
||||||
|
data-page-list="[25, 50, 100, all]"
|
||||||
|
data-page-size="25"
|
||||||
|
data-pagination="true"
|
||||||
|
data-resizable="true"
|
||||||
|
data-search="true"
|
||||||
|
data-show-columns="true"
|
||||||
|
data-show-columns-search="true"
|
||||||
|
data-show-columns-toggle-all="true"
|
||||||
|
data-show-export="true"
|
||||||
|
data-show-fullscreen="true"
|
||||||
|
data-show-search-clear-button="true"
|
||||||
|
data-single-select="true"
|
||||||
|
data-sortable="true"
|
||||||
|
data-sort-name="nick"
|
||||||
|
data-sort-order="asc"
|
||||||
|
data-sort-reset="true"
|
||||||
|
data-toolbar="#toolbar">
|
||||||
|
<thead class="table-primary">
|
||||||
|
<tr>
|
||||||
|
<th data-checkbox="true"></th>
|
||||||
|
<th data-sortable="true" data-switchable="false" data-field="nick">nick</th>
|
||||||
|
<th data-sortable="true" data-switchable="false" data-field="job">job</th>
|
||||||
|
<th data-sortable="true" data-field="piece">piece</th>
|
||||||
|
<th data-sortable="true" data-field="pieceType">piece type</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="update-bis-dialog" tabindex="-1" role="dialog" class="modal fade">
|
||||||
|
<div class="modal-dialog" role="document">
|
||||||
|
<form class="modal-content" 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>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="form-group row">
|
||||||
|
<label class="col-sm-4 col-form-label" for="player">player</label>
|
||||||
|
<div class="col-sm-8">
|
||||||
|
<select id="player" name="player" class="form-control" title="player" required></select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="piece-row" class="form-group row">
|
||||||
|
<label class="col-sm-4 col-form-label" for="piece">piece</label>
|
||||||
|
<div class="col-sm-8">
|
||||||
|
<select id="piece" name="piece" class="form-control" title="piece"></select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="piece-type-row" class="form-group row">
|
||||||
|
<label class="col-sm-4 col-form-label" for="piece-type">piece type</label>
|
||||||
|
<div class="col-sm-8">
|
||||||
|
<select id="piece-type" name="pieceType" class="form-control" title="piece-type"></select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="bis-link-row" class="form-group row" style="display: none">
|
||||||
|
<label class="col-sm-4 col-form-label" for="bis-link">link</label>
|
||||||
|
<div class="col-sm-8">
|
||||||
|
<input id="bis-link" name="link" class="form-control" placeholder="link to bis">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-danger" data-bs-dismiss="modal">close</button>
|
||||||
|
<button id="submit-add-bis-btn" type="submit" class="btn btn-primary">add</button>
|
||||||
|
<button id="submit-set-bis-btn" type="submit" class="btn btn-primary" style="display: none">set</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<footer class="d-flex flex-wrap justify-content-between align-items-center border-top">
|
||||||
|
<ul class="nav">
|
||||||
|
<li><a class="nav-link" href="/" title="home">home</a></li>
|
||||||
|
<li><a class="nav-link" href="/api-docs" title="api">api</a></li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<ul class="nav">
|
||||||
|
<li><a id="sources-link" class="nav-link" href="https://github.com/arcan1s/ffxivbis" title="sources">ffxivbis</a></li>
|
||||||
|
<li><a class="nav-link" href="https://github.com/arcan1s/ffxivbis/releases" title="releases list">releases</a></li>
|
||||||
|
<li><a class="nav-link" href="https://github.com/arcan1s/ffxivbis/issues" title="issues tracker">report a bug</a></li>
|
||||||
|
</ul>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/jquery/dist/jquery.min.js"></script>
|
||||||
|
|
||||||
|
<script src="https://unpkg.com/tableexport.jquery.plugin/tableExport.min.js"></script>
|
||||||
|
|
||||||
|
<script src="https://unpkg.com/jquery-resizable-columns@0.2.3/dist/jquery.resizableColumns.min.js"></script>
|
||||||
|
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p" crossorigin="anonymous"></script>
|
||||||
|
<script src="https://unpkg.com/bootstrap-table@1.20.2/dist/bootstrap-table.min.js"></script>
|
||||||
|
|
||||||
|
<script src="https://unpkg.com/bootstrap-table@1.20.2/dist/extensions/export/bootstrap-table-export.min.js"></script>
|
||||||
|
|
||||||
|
<script src="https://unpkg.com/bootstrap-table@1.20.2/dist/extensions/resizable/bootstrap-table-resizable.js"></script>
|
||||||
|
|
||||||
|
<script src="/static/utils.js"></script>
|
||||||
|
<script src="/static/load.js"></script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const [partyId, isReadOnly] = getPartyId();
|
||||||
|
const table = $("#bis");
|
||||||
|
const removeButton = $("#remove-btn");
|
||||||
|
const updateButton = $("#update-btn");
|
||||||
|
|
||||||
|
const submitAddBisButton = $("#submit-add-bis-btn");
|
||||||
|
const submitSetBisButton = $("#submit-set-bis-btn");
|
||||||
|
const updateBisDialog = $("#update-bis-dialog");
|
||||||
|
|
||||||
|
const addPieceButton = $("#add-piece-btn");
|
||||||
|
const updateBisButton = $("#update-bis-btn");
|
||||||
|
|
||||||
|
const bisLinkRow = $("#bis-link-row");
|
||||||
|
const pieceRow = $("#piece-row");
|
||||||
|
const pieceTypeRow = $("#piece-type-row");
|
||||||
|
|
||||||
|
const linkInput = $("#bis-link");
|
||||||
|
const pieceInput = $("#piece");
|
||||||
|
const pieceTypeInput = $("#piece-type");
|
||||||
|
const playerInput = $("#player");
|
||||||
|
|
||||||
|
function addPiece() {
|
||||||
|
const player = getCurrentOption(playerInput);
|
||||||
|
$.ajax({
|
||||||
|
url: `/api/v1/party/${partyId}/bis`,
|
||||||
|
data: JSON.stringify({
|
||||||
|
action: "add",
|
||||||
|
piece: {
|
||||||
|
pieceType: pieceTypeInput.val(),
|
||||||
|
job: player.dataset.job,
|
||||||
|
piece: pieceInput.val(),
|
||||||
|
},
|
||||||
|
playerId: {
|
||||||
|
partyId: partyId,
|
||||||
|
nick: player.dataset.nick,
|
||||||
|
job: player.dataset.job,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
type: "POST",
|
||||||
|
contentType: "application/json",
|
||||||
|
success: _ => { reload(); },
|
||||||
|
error: (jqXHR, _, errorThrown) => { requestAlert(jqXHR, errorThrown); },
|
||||||
|
});
|
||||||
|
updateBisDialog.modal("hide");
|
||||||
|
return true; // action expects boolean result
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideControls() {
|
||||||
|
removeButton.attr("hidden", isReadOnly);
|
||||||
|
updateButton.attr("hidden", isReadOnly);
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideLinkPart() {
|
||||||
|
bisLinkRow.hide();
|
||||||
|
linkInput.prop("required", false);
|
||||||
|
submitSetBisButton.hide();
|
||||||
|
pieceRow.show();
|
||||||
|
pieceTypeRow.show();
|
||||||
|
pieceInput.prop("required", true);
|
||||||
|
pieceTypeInput.prop("required", true);
|
||||||
|
submitAddBisButton.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
function hidePiecePart() {
|
||||||
|
bisLinkRow.show();
|
||||||
|
linkInput.prop("required", true);
|
||||||
|
submitSetBisButton.show();
|
||||||
|
pieceRow.hide();
|
||||||
|
pieceTypeRow.hide();
|
||||||
|
pieceInput.prop("required", false);
|
||||||
|
pieceTypeInput.prop("required", false);
|
||||||
|
submitAddBisButton.hide();
|
||||||
|
}
|
||||||
|
|
||||||
|
function reload() {
|
||||||
|
table.bootstrapTable("showLoading");
|
||||||
|
$.ajax({
|
||||||
|
url: `/api/v1/party/${partyId}`,
|
||||||
|
type: "GET",
|
||||||
|
dataType: "json",
|
||||||
|
success: response => {
|
||||||
|
const items = response.map(player => {
|
||||||
|
return player.bis.map(loot => {
|
||||||
|
return {
|
||||||
|
nick: player.nick,
|
||||||
|
job: player.job,
|
||||||
|
piece: loot.piece,
|
||||||
|
pieceType: loot.pieceType,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
});
|
||||||
|
const payload = items.reduce((left, right) => { return left.concat(right); }, []);
|
||||||
|
table.bootstrapTable("load", payload);
|
||||||
|
table.bootstrapTable("uncheckAll");
|
||||||
|
table.bootstrapTable("hideLoading");
|
||||||
|
|
||||||
|
const options = response.map(player => {
|
||||||
|
const option = document.createElement("option");
|
||||||
|
option.innerText = formatPlayerId(player);
|
||||||
|
option.dataset.nick = player.nick;
|
||||||
|
option.dataset.job = player.job;
|
||||||
|
return option;
|
||||||
|
});
|
||||||
|
playerInput.empty().append(options);
|
||||||
|
},
|
||||||
|
error: (jqXHR, _, errorThrown) => { requestAlert(jqXHR, errorThrown); },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function removePiece() {
|
||||||
|
const pieces = table.bootstrapTable("getSelections");
|
||||||
|
pieces.map(loot => {
|
||||||
|
$.ajax({
|
||||||
|
url: `/api/v1/party/${partyId}/bis`,
|
||||||
|
data: JSON.stringify({
|
||||||
|
action: "remove",
|
||||||
|
piece: {
|
||||||
|
pieceType: loot.pieceType,
|
||||||
|
job: loot.job,
|
||||||
|
piece: loot.piece,
|
||||||
|
},
|
||||||
|
playerId: {
|
||||||
|
partyId: partyId,
|
||||||
|
job: loot.job,
|
||||||
|
nick: loot.nick,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
type: "POST",
|
||||||
|
contentType: "application/json",
|
||||||
|
success: _ => { reload(); },
|
||||||
|
error: (jqXHR, _, errorThrown) => { requestAlert(jqXHR, errorThrown); },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function reset() {
|
||||||
|
if (updateBisButton.is(":checked")) {
|
||||||
|
hidePiecePart();
|
||||||
|
}
|
||||||
|
if (addPieceButton.is(":checked")) {
|
||||||
|
hideLinkPart();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setBis() {
|
||||||
|
const player = getCurrentOption(playerInput);
|
||||||
|
$.ajax({
|
||||||
|
url: `/api/v1/party/${partyId}/bis`,
|
||||||
|
data: JSON.stringify({
|
||||||
|
link: linkInput.val(),
|
||||||
|
playerId: {
|
||||||
|
partyId: partyId,
|
||||||
|
nick: player.dataset.nick,
|
||||||
|
job: player.dataset.job,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
type: "PUT",
|
||||||
|
contentType: "application/json",
|
||||||
|
success: _ => { reload(); },
|
||||||
|
error: (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
|
||||||
|
}
|
||||||
|
|
||||||
|
$(() => {
|
||||||
|
setupFormClear(updateBisDialog, reset);
|
||||||
|
setupRemoveButton(table, removeButton);
|
||||||
|
|
||||||
|
loadHeader(partyId);
|
||||||
|
loadVersion();
|
||||||
|
loadTypes("/api/v1/types/pieces", pieceInput);
|
||||||
|
loadTypes("/api/v1/types/pieces/types", pieceTypeInput);
|
||||||
|
|
||||||
|
hideControls();
|
||||||
|
|
||||||
|
updateBisButton.click(() => { reset(); });
|
||||||
|
addPieceButton.click(() => { reset(); });
|
||||||
|
|
||||||
|
table.bootstrapTable({});
|
||||||
|
reload();
|
||||||
|
reset();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
190
src/main/resources/html/index.html
Normal file
190
src/main/resources/html/index.html
Normal file
@ -0,0 +1,190 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>FFXIV loot helper</title>
|
||||||
|
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
|
||||||
|
<link rel="shortcut icon" href="/static/favicon.ico">
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous" type="text/css">
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="/static/styles.css" type="text/css">
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div id="alert-placeholder" class="container"></div>
|
||||||
|
|
||||||
|
<div class="container mb-5">
|
||||||
|
<div class="form-group row">
|
||||||
|
<div class="btn-group" role="group" aria-label="Sign in">
|
||||||
|
<input id="signin-btn" name="signin" type="radio" class="btn-check" autocomplete="off" checked>
|
||||||
|
<label class="btn btn-outline-primary" for="signin-btn">login to existing party</label>
|
||||||
|
|
||||||
|
<input id="signup-btn" name="signin" type="radio" class="btn-check" autocomplete="off">
|
||||||
|
<label class="btn btn-outline-primary" for="signup-btn">create a new party</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form id="signup-form" class="container mb-5" style="display: none">
|
||||||
|
<div class="form-group row">
|
||||||
|
<label class="col-sm-2 col-form-label" for="alias">party alias</label>
|
||||||
|
<div class="col-sm-10">
|
||||||
|
<input id="alias" name="alias" class="form-control" placeholder="alias">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group row">
|
||||||
|
<label class="col-sm-2 col-form-label" for="username">username</label>
|
||||||
|
<div class="col-sm-10">
|
||||||
|
<input id="username" name="username" class="form-control" placeholder="admin user name" onkeyup="disableAddButton()">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group row">
|
||||||
|
<label for="password" class="col-sm-2 col-form-label">password</label>
|
||||||
|
<div class="col-sm-10">
|
||||||
|
<input id="password" name="password" type="password" class="form-control" placeholder="admin password" onkeyup="disableAddButton()">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group row">
|
||||||
|
<div class="col-sm-10">
|
||||||
|
<button id="add-btn" type="button" class="btn btn-primary" onclick="createParty()" disabled>add</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<form id="signin-form" class="container mb-5">
|
||||||
|
<div class="form-group row">
|
||||||
|
<label class="col-sm-2 col-form-label" for="party-id">party id</label>
|
||||||
|
<div class="col-sm-10">
|
||||||
|
<input id="party-id" name="partyId" class="form-control" placeholder="id" onkeyup="disableRedirectButton()">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group row">
|
||||||
|
<div class="col-sm-10">
|
||||||
|
<button id="redirect-btn" type="button" class="btn btn-primary" onclick="redirectToParty()" disabled>go</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<footer class="d-flex flex-wrap justify-content-between align-items-center border-top">
|
||||||
|
<ul class="nav">
|
||||||
|
<li><a class="nav-link" href="/api-docs" title="api">api</a></li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<ul class="nav">
|
||||||
|
<li><a id="sources-link" class="nav-link" href="https://github.com/arcan1s/ffxivbis" title="sources">ffxivbis</a></li>
|
||||||
|
<li><a class="nav-link" href="https://github.com/arcan1s/ffxivbis/releases" title="releases list">releases</a></li>
|
||||||
|
<li><a class="nav-link" href="https://github.com/arcan1s/ffxivbis/issues" title="issues tracker">report a bug</a></li>
|
||||||
|
</ul>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/jquery/dist/jquery.min.js"></script>
|
||||||
|
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p" crossorigin="anonymous"></script>
|
||||||
|
|
||||||
|
<script src="/static/utils.js"></script>
|
||||||
|
<script src="/static/load.js"></script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const signinButton = $("#signin-btn");
|
||||||
|
const signupButton = $("#signup-btn");
|
||||||
|
|
||||||
|
const addButton = $("#add-btn");
|
||||||
|
const redirectButton = $("#redirect-btn");
|
||||||
|
const signinForm = $("#signin-form");
|
||||||
|
const signupForm = $("#signup-form");
|
||||||
|
|
||||||
|
const aliasInput = $("#alias");
|
||||||
|
const partyIdInput = $("#party-id");
|
||||||
|
const passwordInput = $("#password");
|
||||||
|
const usernameInput = $("#username");
|
||||||
|
|
||||||
|
function createDescription(partyId) {
|
||||||
|
$.ajax({
|
||||||
|
url: `/api/v1/party/${partyId}/description`,
|
||||||
|
data: JSON.stringify({
|
||||||
|
partyId: partyId,
|
||||||
|
partyAlias: aliasInput.val(),
|
||||||
|
}),
|
||||||
|
type: "POST",
|
||||||
|
contentType: "application/json",
|
||||||
|
success: function (_) { doRedirect(partyId); },
|
||||||
|
error: function (jqXHR, _, errorThrown) { requestAlert(jqXHR, errorThrown); },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function createParty() {
|
||||||
|
$.ajax({
|
||||||
|
url: `/api/v1/party`,
|
||||||
|
data: JSON.stringify({
|
||||||
|
partyId: "",
|
||||||
|
username: usernameInput.val(),
|
||||||
|
password: passwordInput.val(),
|
||||||
|
permission: "admin",
|
||||||
|
}),
|
||||||
|
type: "POST",
|
||||||
|
contentType: "application/json",
|
||||||
|
dataType: "json",
|
||||||
|
success: function (data) {
|
||||||
|
if (aliasInput.val()) {
|
||||||
|
createDescription(data.partyId);
|
||||||
|
} else {
|
||||||
|
doRedirect(data.partyId);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: function (jqXHR, _, errorThrown) { requestAlert(jqXHR, errorThrown); },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function disableAddButton() {
|
||||||
|
addButton.attr("disabled", !(passwordInput.val() && usernameInput.val()));
|
||||||
|
}
|
||||||
|
|
||||||
|
function disableRedirectButton() {
|
||||||
|
redirectButton.attr("disabled", !partyIdInput.val());
|
||||||
|
}
|
||||||
|
|
||||||
|
function doRedirect(partyId) {
|
||||||
|
location.href = `/party/${partyId}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideSigninPart() {
|
||||||
|
signinForm.hide();
|
||||||
|
signupForm.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideSignupPart() {
|
||||||
|
signinForm.show();
|
||||||
|
signupForm.hide();
|
||||||
|
}
|
||||||
|
|
||||||
|
function redirectToParty() {
|
||||||
|
return doRedirect(partyIdInput.val());
|
||||||
|
}
|
||||||
|
|
||||||
|
function reset() {
|
||||||
|
signinForm.trigger("reset");
|
||||||
|
signupForm.trigger("reset");
|
||||||
|
if (signinButton.is(":checked")) {
|
||||||
|
hideSignupPart();
|
||||||
|
}
|
||||||
|
if (signupButton.is(":checked")) {
|
||||||
|
hideSigninPart();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$(function () {
|
||||||
|
loadVersion();
|
||||||
|
|
||||||
|
signinButton.click(function () { reset(); });
|
||||||
|
signupButton.click(function () { reset(); });
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
358
src/main/resources/html/loot.html
Normal file
358
src/main/resources/html/loot.html
Normal file
@ -0,0 +1,358 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Loot table</title>
|
||||||
|
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
|
||||||
|
<link rel="shortcut icon" href="/static/favicon.ico">
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous" type="text/css">
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.8.3/font/bootstrap-icons.css" type="text/css">
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="https://unpkg.com/bootstrap-table@1.20.2/dist/bootstrap-table.min.css" type="text/css">
|
||||||
|
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="https://unpkg.com/jquery-resizable-columns@0.2.3/dist/jquery.resizableColumns.css" type="text/css">
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="/static/styles.css" type="text/css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<nav class="navbar navbar-expand-lg justify-content-between align-items-center border-bottom">
|
||||||
|
<a class="navbar-brand" id="navbar-title">Party</a>
|
||||||
|
<ul class="navbar-nav">
|
||||||
|
<a class="nav-item nav-link" id="navbar-bis">best in slot</a>
|
||||||
|
<a class="nav-item nav-link" id="navbar-loot">looted items</a>
|
||||||
|
</ul>
|
||||||
|
<ul class="navbar-nav">
|
||||||
|
<a class="nav-item nav-link" id="navbar-users">users</a>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="alert-placeholder" class="container"></div>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<h2>Looted items</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<div id="toolbar">
|
||||||
|
<button id="add-btn" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#add-loot-dialog" hidden>
|
||||||
|
<i class="bi bi-plus"></i> add
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-secondary" onclick="reload()">
|
||||||
|
<i class="bi bi-arrow-clockwise"></i> reload
|
||||||
|
</button>
|
||||||
|
<button id="remove-btn" class="btn btn-danger" onclick="removeLoot()" disabled hidden>
|
||||||
|
<i class="bi bi-trash"></i> remove
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<table id="loot" class="table table-striped table-hover"
|
||||||
|
data-click-to-select="true"
|
||||||
|
data-export-options='{"fileName": "loot"}'
|
||||||
|
data-page-list="[25, 50, 100, all]"
|
||||||
|
data-page-size="25"
|
||||||
|
data-pagination="true"
|
||||||
|
data-resizable="true"
|
||||||
|
data-search="true"
|
||||||
|
data-show-columns="true"
|
||||||
|
data-show-columns-search="true"
|
||||||
|
data-show-columns-toggle-all="true"
|
||||||
|
data-show-export="true"
|
||||||
|
data-show-fullscreen="true"
|
||||||
|
data-show-search-clear-button="true"
|
||||||
|
data-single-select="true"
|
||||||
|
data-sortable="true"
|
||||||
|
data-sort-name="timestamp"
|
||||||
|
data-sort-order="desc"
|
||||||
|
data-sort-reset="true"
|
||||||
|
data-toolbar="#toolbar">
|
||||||
|
<thead class="table-primary">
|
||||||
|
<tr>
|
||||||
|
<th data-checkbox="true"></th>
|
||||||
|
<th data-sortable="true" data-switchable="false" data-field="nick">nick</th>
|
||||||
|
<th data-sortable="true" data-switchable="false" data-field="job">job</th>
|
||||||
|
<th data-sortable="true" data-field="piece">piece</th>
|
||||||
|
<th data-sortable="true" data-field="pieceType">piece type</th>
|
||||||
|
<th data-sortable="true" data-field="isFreeLoot">is free loot</th>
|
||||||
|
<th data-sortable="true" data-field="timestamp">date</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="add-loot-dialog" tabindex="-1" role="dialog" class="modal fade">
|
||||||
|
<div class="modal-dialog modal-lg" role="document">
|
||||||
|
<form class="modal-content" action="javascript:" onsubmit="addLootModal()">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h4 class="modal-title">add looted piece</h4>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="close"></button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="form-group row">
|
||||||
|
<label class="col-sm-4 col-form-label" for="player">player</label>
|
||||||
|
<div class="col-sm-8">
|
||||||
|
<select id="player" name="player" class="form-control" title="player" required></select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group row">
|
||||||
|
<label class="col-sm-4 col-form-label" for="piece">piece</label>
|
||||||
|
<div class="col-sm-8">
|
||||||
|
<select id="piece" name="piece" class="form-control" title="piece" required></select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group row">
|
||||||
|
<label class="col-sm-4 col-form-label" for="piece-type">piece type</label>
|
||||||
|
<div class="col-sm-8">
|
||||||
|
<select id="piece-type" name="pieceType" class="form-control" title="pieceType" required></select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group row">
|
||||||
|
<label class="col-sm-4 col-form-label" for="job">job</label>
|
||||||
|
<div class="col-sm-8">
|
||||||
|
<select id="job" name="job" class="form-control" title="job" required></select>
|
||||||
|
</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>
|
||||||
|
|
||||||
|
<table id="stats" class="table table-striped table-hover">
|
||||||
|
<thead class="table-primary">
|
||||||
|
<tr>
|
||||||
|
<th data-formatter="addLootFormatter"></th>
|
||||||
|
<th data-field="nick">nick</th>
|
||||||
|
<th data-field="job">job</th>
|
||||||
|
<th data-field="isRequired">required</th>
|
||||||
|
<th data-field="lootCount">these pieces looted</th>
|
||||||
|
<th data-field="lootCountBiS">total bis pieces looted</th>
|
||||||
|
<th data-field="lootCountTotal">total pieces looted</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-danger" data-bs-dismiss="modal">close</button>
|
||||||
|
<button type="button" class="btn btn-secondary" onclick="suggestLoot()">suggest</button>
|
||||||
|
<button type="submit" class="btn btn-primary">add</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<footer class="d-flex flex-wrap justify-content-between align-items-center border-top">
|
||||||
|
<ul class="nav">
|
||||||
|
<li><a class="nav-link" href="/" title="home">home</a></li>
|
||||||
|
<li><a class="nav-link" href="/api-docs" title="api">api</a></li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<ul class="nav">
|
||||||
|
<li><a id="sources-link" class="nav-link" href="https://github.com/arcan1s/ffxivbis" title="sources">ffxivbis</a></li>
|
||||||
|
<li><a class="nav-link" href="https://github.com/arcan1s/ffxivbis/releases" title="releases list">releases</a></li>
|
||||||
|
<li><a class="nav-link" href="https://github.com/arcan1s/ffxivbis/issues" title="issues tracker">report a bug</a></li>
|
||||||
|
</ul>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/jquery/dist/jquery.min.js"></script>
|
||||||
|
|
||||||
|
<script src="https://unpkg.com/tableexport.jquery.plugin/tableExport.min.js"></script>
|
||||||
|
|
||||||
|
<script src="https://unpkg.com/jquery-resizable-columns@0.2.3/dist/jquery.resizableColumns.min.js"></script>
|
||||||
|
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p" crossorigin="anonymous"></script>
|
||||||
|
<script src="https://unpkg.com/bootstrap-table@1.20.2/dist/bootstrap-table.min.js"></script>
|
||||||
|
|
||||||
|
<script src="https://unpkg.com/bootstrap-table@1.20.2/dist/extensions/export/bootstrap-table-export.min.js"></script>
|
||||||
|
|
||||||
|
<script src="https://unpkg.com/bootstrap-table@1.20.2/dist/extensions/resizable/bootstrap-table-resizable.js"></script>
|
||||||
|
|
||||||
|
<script src="/static/utils.js"></script>
|
||||||
|
<script src="/static/load.js"></script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const [partyId, isReadOnly] = getPartyId();
|
||||||
|
const table = $("#loot");
|
||||||
|
const stats = $("#stats");
|
||||||
|
const addButton = $("#add-btn");
|
||||||
|
const removeButton = $("#remove-btn");
|
||||||
|
|
||||||
|
const addLootDialog = $("#add-loot-dialog");
|
||||||
|
|
||||||
|
const freeLootInput = $("#free-loot");
|
||||||
|
const jobInput = $("#job");
|
||||||
|
const pieceInput = $("#piece");
|
||||||
|
const pieceTypeInput = $("#piece-type");
|
||||||
|
const playerInput = $("#player");
|
||||||
|
|
||||||
|
function addLoot(nick, job) {
|
||||||
|
$.ajax({
|
||||||
|
url: `/api/v1/party/${partyId}/loot`,
|
||||||
|
data: JSON.stringify({
|
||||||
|
action: "add",
|
||||||
|
piece: {
|
||||||
|
pieceType: pieceTypeInput.val(),
|
||||||
|
job: job,
|
||||||
|
piece: pieceInput.val(),
|
||||||
|
},
|
||||||
|
playerId: {
|
||||||
|
partyId: partyId,
|
||||||
|
nick: nick,
|
||||||
|
job: job,
|
||||||
|
},
|
||||||
|
isFreeLoot: freeLootInput.is(":checked"),
|
||||||
|
}),
|
||||||
|
type: "POST",
|
||||||
|
contentType: "application/json",
|
||||||
|
success: _ => {
|
||||||
|
addLootDialog.modal("hide");
|
||||||
|
reload();
|
||||||
|
},
|
||||||
|
error: (jqXHR, _, errorThrown) => { requestAlert(jqXHR, errorThrown); },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function addLootFormatter(value, row, index) {
|
||||||
|
return `<button type="button" class="btn btn-primary" onclick="addLoot('${row.nick}', '${row.job}')"><i class="bi bi-plus"></i></button>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function addLootModal() {
|
||||||
|
const player = getCurrentOption(playerInput);
|
||||||
|
addLoot(player.dataset.nick, player.dataset.job);
|
||||||
|
return true; // action expects boolean result
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideControls() {
|
||||||
|
addButton.attr("hidden", isReadOnly);
|
||||||
|
removeButton.attr("hidden", isReadOnly);
|
||||||
|
}
|
||||||
|
|
||||||
|
function reload() {
|
||||||
|
table.bootstrapTable("showLoading");
|
||||||
|
$.ajax({
|
||||||
|
url: `/api/v1/party/${partyId}`,
|
||||||
|
type: "GET",
|
||||||
|
dataType: "json",
|
||||||
|
success: response => {
|
||||||
|
const items = response.map(player => {
|
||||||
|
return player.loot.map(loot => {
|
||||||
|
return {
|
||||||
|
nick: player.nick,
|
||||||
|
job: player.job,
|
||||||
|
piece: loot.piece.piece,
|
||||||
|
pieceType: loot.piece.pieceType,
|
||||||
|
isFreeLoot: loot.isFreeLoot ? "yes" : "no",
|
||||||
|
timestamp: loot.timestamp,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
});
|
||||||
|
const payload = items.reduce((left, right) => { return left.concat(right); }, []);
|
||||||
|
table.bootstrapTable("load", payload);
|
||||||
|
table.bootstrapTable("uncheckAll");
|
||||||
|
table.bootstrapTable("hideLoading");
|
||||||
|
|
||||||
|
const options = response.map(player => {
|
||||||
|
const option = document.createElement("option");
|
||||||
|
option.innerText = formatPlayerId(player);
|
||||||
|
option.dataset.nick = player.nick;
|
||||||
|
option.dataset.job = player.job;
|
||||||
|
return option;
|
||||||
|
});
|
||||||
|
playerInput.empty().append(options);
|
||||||
|
},
|
||||||
|
error: (jqXHR, _, errorThrown) => { requestAlert(jqXHR, errorThrown); },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeLoot() {
|
||||||
|
const pieces = table.bootstrapTable("getSelections");
|
||||||
|
pieces.map(loot => {
|
||||||
|
$.ajax({
|
||||||
|
url: `/api/v1/party/${partyId}/loot`,
|
||||||
|
data: JSON.stringify({
|
||||||
|
action: "remove",
|
||||||
|
piece: {
|
||||||
|
pieceType: loot.pieceType,
|
||||||
|
job: loot.job,
|
||||||
|
piece: loot.piece,
|
||||||
|
},
|
||||||
|
playerId: {
|
||||||
|
partyId: partyId,
|
||||||
|
nick: loot.nick,
|
||||||
|
job: loot.job,
|
||||||
|
},
|
||||||
|
isFreeLoot: loot.isFreeLoot === "yes",
|
||||||
|
}),
|
||||||
|
type: "POST",
|
||||||
|
contentType: "application/json",
|
||||||
|
success: _ => { reload(); },
|
||||||
|
error: (jqXHR, _, errorThrown) => { requestAlert(jqXHR, errorThrown); },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function suggestLoot() {
|
||||||
|
stats.bootstrapTable("showLoading");
|
||||||
|
$.ajax({
|
||||||
|
url: `/api/v1/party/${partyId}/loot`,
|
||||||
|
data: JSON.stringify({
|
||||||
|
pieceType: pieceTypeInput.val(),
|
||||||
|
job: jobInput.val(),
|
||||||
|
piece: pieceInput.val(),
|
||||||
|
}),
|
||||||
|
type: "PUT",
|
||||||
|
contentType: "application/json",
|
||||||
|
dataType: "json",
|
||||||
|
success: response => {
|
||||||
|
const payload = response.map(stat => {
|
||||||
|
return {
|
||||||
|
nick: stat.nick,
|
||||||
|
job: stat.job,
|
||||||
|
isRequired: stat.isRequired ? "yes" : "no",
|
||||||
|
lootCount: stat.lootCount,
|
||||||
|
lootCountBiS: stat.lootCountBiS,
|
||||||
|
lootCountTotal: stat.lootCountTotal,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
stats.bootstrapTable("load", payload);
|
||||||
|
stats.bootstrapTable("uncheckAll");
|
||||||
|
stats.bootstrapTable("hideLoading");
|
||||||
|
},
|
||||||
|
error: (jqXHR, _, errorThrown) => { requestAlert(jqXHR, errorThrown); },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
$(() => {
|
||||||
|
setupFormClear(addLootDialog);
|
||||||
|
setupRemoveButton(table, removeButton);
|
||||||
|
|
||||||
|
loadVersion();
|
||||||
|
loadHeader(partyId);
|
||||||
|
loadTypes("/api/v1/types/jobs/all", jobInput);
|
||||||
|
loadTypes("/api/v1/types/pieces", pieceInput);
|
||||||
|
loadTypes("/api/v1/types/pieces/types", pieceTypeInput);
|
||||||
|
|
||||||
|
hideControls();
|
||||||
|
|
||||||
|
table.bootstrapTable({});
|
||||||
|
stats.bootstrapTable({});
|
||||||
|
reload();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
259
src/main/resources/html/party.html
Normal file
259
src/main/resources/html/party.html
Normal file
@ -0,0 +1,259 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>FFXIV loot helper</title>
|
||||||
|
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
|
||||||
|
<link rel="shortcut icon" href="/static/favicon.ico">
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous" type="text/css">
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.8.3/font/bootstrap-icons.css" type="text/css">
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="https://unpkg.com/bootstrap-table@1.20.2/dist/bootstrap-table.min.css" type="text/css">
|
||||||
|
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="https://unpkg.com/jquery-resizable-columns@0.2.3/dist/jquery.resizableColumns.css" type="text/css">
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="/static/styles.css" type="text/css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<nav class="navbar navbar-expand-lg justify-content-between align-items-center border-bottom">
|
||||||
|
<a class="navbar-brand" id="navbar-title">Party</a>
|
||||||
|
<ul class="navbar-nav">
|
||||||
|
<a class="nav-item nav-link" id="navbar-bis">best in slot</a>
|
||||||
|
<a class="nav-item nav-link" id="navbar-loot">looted items</a>
|
||||||
|
</ul>
|
||||||
|
<ul class="navbar-nav">
|
||||||
|
<a class="nav-item nav-link" id="navbar-users">users</a>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="alert-placeholder" class="container"></div>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<div id="toolbar">
|
||||||
|
<button id="add-btn" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#add-player-dialog" hidden>
|
||||||
|
<i class="bi bi-plus"></i> add
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-secondary" onclick="reload()">
|
||||||
|
<i class="bi bi-arrow-clockwise"></i> reload
|
||||||
|
</button>
|
||||||
|
<button id="remove-btn" class="btn btn-danger" onclick="removePlayers()" disabled hidden>
|
||||||
|
<i class="bi bi-trash"></i> remove
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<table id="players" class="table table-striped table-hover"
|
||||||
|
data-click-to-select="true"
|
||||||
|
data-export-options='{"fileName": "players"}'
|
||||||
|
data-page-list="[25, 50, 100, all]"
|
||||||
|
data-page-size="25"
|
||||||
|
data-pagination="true"
|
||||||
|
data-resizable="true"
|
||||||
|
data-search="true"
|
||||||
|
data-show-columns="true"
|
||||||
|
data-show-columns-search="true"
|
||||||
|
data-show-columns-toggle-all="true"
|
||||||
|
data-show-export="true"
|
||||||
|
data-show-fullscreen="true"
|
||||||
|
data-show-search-clear-button="true"
|
||||||
|
data-single-select="true"
|
||||||
|
data-sortable="true"
|
||||||
|
data-sort-name="nick"
|
||||||
|
data-sort-order="asc"
|
||||||
|
data-sort-reset="true"
|
||||||
|
data-toolbar="#toolbar">
|
||||||
|
<thead class="table-primary">
|
||||||
|
<tr>
|
||||||
|
<th data-checkbox="true"></th>
|
||||||
|
<th data-sortable="true" data-switchable="false" data-field="nick">nick</th>
|
||||||
|
<th data-sortable="true" data-field="job">job</th>
|
||||||
|
<th data-sortable="true" data-field="link" data-formatter="bisLinkFormatter">best in slot link</th>
|
||||||
|
<th data-sortable="true" data-field="lootCountBiS">total bis pieces looted</th>
|
||||||
|
<th data-sortable="true" data-field="lootCountTotal">total pieces looted</th>
|
||||||
|
<th data-sortable="true" data-field="priority">priority</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="add-player-dialog" tabindex="-1" role="dialog" class="modal fade">
|
||||||
|
<div class="modal-dialog" role="document">
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<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" 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" required></select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group row">
|
||||||
|
<label class="col-sm-4 col-form-label" for="link">link to best in slot</label>
|
||||||
|
<div class="col-sm-8">
|
||||||
|
<input id="link" name="link" class="form-control" placeholder="link to bis">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group row">
|
||||||
|
<label class="col-sm-4 col-form-label" for="priority">priority</label>
|
||||||
|
<div class="col-sm-8">
|
||||||
|
<input id="priority" name="priority" type="number" class="form-control" value="0">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-danger" data-bs-dismiss="modal">close</button>
|
||||||
|
<button type="submit" class="btn btn-primary">add</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<footer class="d-flex flex-wrap justify-content-between align-items-center border-top">
|
||||||
|
<ul class="nav">
|
||||||
|
<li><a class="nav-link" href="/" title="home">home</a></li>
|
||||||
|
<li><a class="nav-link" href="/api-docs" title="api">api</a></li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<ul class="nav">
|
||||||
|
<li><a id="sources-link" class="nav-link" href="https://github.com/arcan1s/ffxivbis" title="sources">ffxivbis</a></li>
|
||||||
|
<li><a class="nav-link" href="https://github.com/arcan1s/ffxivbis/releases" title="releases list">releases</a></li>
|
||||||
|
<li><a class="nav-link" href="https://github.com/arcan1s/ffxivbis/issues" title="issues tracker">report a bug</a></li>
|
||||||
|
</ul>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/jquery/dist/jquery.min.js"></script>
|
||||||
|
|
||||||
|
<script src="https://unpkg.com/tableexport.jquery.plugin/tableExport.min.js"></script>
|
||||||
|
|
||||||
|
<script src="https://unpkg.com/jquery-resizable-columns@0.2.3/dist/jquery.resizableColumns.min.js"></script>
|
||||||
|
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p" crossorigin="anonymous"></script>
|
||||||
|
<script src="https://unpkg.com/bootstrap-table@1.20.2/dist/bootstrap-table.min.js"></script>
|
||||||
|
|
||||||
|
<script src="https://unpkg.com/bootstrap-table@1.20.2/dist/extensions/export/bootstrap-table-export.min.js"></script>
|
||||||
|
|
||||||
|
<script src="https://unpkg.com/bootstrap-table@1.20.2/dist/extensions/resizable/bootstrap-table-resizable.js"></script>
|
||||||
|
|
||||||
|
<script src="/static/utils.js"></script>
|
||||||
|
<script src="/static/load.js"></script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const [partyId, isReadOnly] = getPartyId();
|
||||||
|
const table = $("#players");
|
||||||
|
const addButton = $("#add-btn");
|
||||||
|
const removeButton = $("#remove-btn");
|
||||||
|
|
||||||
|
const addPlayerDialog = $("#add-player-dialog");
|
||||||
|
|
||||||
|
const jobInput = $("#job");
|
||||||
|
const linkInput = $("#link");
|
||||||
|
const nickInput = $("#nick");
|
||||||
|
const priorityInput = $("#priority");
|
||||||
|
|
||||||
|
function addPlayer() {
|
||||||
|
$.ajax({
|
||||||
|
url: `/api/v1/party/${partyId}`,
|
||||||
|
data: JSON.stringify({
|
||||||
|
action: "add",
|
||||||
|
playerId: {
|
||||||
|
partyId: partyId,
|
||||||
|
job: jobInput.val(),
|
||||||
|
nick: nickInput.val(),
|
||||||
|
link: linkInput.val() || null,
|
||||||
|
priority: parseInt(priorityInput.val(), 10),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
type: "POST",
|
||||||
|
contentType: "application/json",
|
||||||
|
success: _ => { reload(); },
|
||||||
|
error: (jqXHR, _, errorThrown) => { requestAlert(jqXHR, errorThrown); },
|
||||||
|
});
|
||||||
|
addPlayerDialog.modal("hide");
|
||||||
|
return true; // action expects boolean result
|
||||||
|
}
|
||||||
|
|
||||||
|
function bisLinkFormatter(link, row) {
|
||||||
|
if (link) {
|
||||||
|
return `<a href="${safe(link)}" title="${safe(row.nick)} best in slot for ${safe(row.job)}">${safe(link)}</a>`;
|
||||||
|
} else {
|
||||||
|
return "-";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideControls() {
|
||||||
|
addButton.attr("hidden", isReadOnly);
|
||||||
|
removeButton.attr("hidden", isReadOnly);
|
||||||
|
}
|
||||||
|
|
||||||
|
function reload() {
|
||||||
|
table.bootstrapTable("showLoading");
|
||||||
|
$.ajax({
|
||||||
|
url: `/api/v1/party/${partyId}`,
|
||||||
|
type: "GET",
|
||||||
|
dataType: "json",
|
||||||
|
success: response => {
|
||||||
|
table.bootstrapTable("load", response);
|
||||||
|
table.bootstrapTable("uncheckAll");
|
||||||
|
table.bootstrapTable("hideLoading");
|
||||||
|
},
|
||||||
|
error: (jqXHR, _, errorThrown) => { requestAlert(jqXHR, errorThrown); },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function removePlayers() {
|
||||||
|
const players = table.bootstrapTable("getSelections");
|
||||||
|
players.map(player => {
|
||||||
|
$.ajax({
|
||||||
|
url: `/api/v1/party/${partyId}`,
|
||||||
|
data: JSON.stringify({
|
||||||
|
action: "remove",
|
||||||
|
playerId: {
|
||||||
|
partyId: partyId,
|
||||||
|
job: player.job,
|
||||||
|
nick: player.nick,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
type: "POST",
|
||||||
|
contentType: "application/json",
|
||||||
|
success: _ => { reload(); },
|
||||||
|
error: (jqXHR, _, errorThrown) => { requestAlert(jqXHR, errorThrown); },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
$(() => {
|
||||||
|
setupFormClear(addPlayerDialog);
|
||||||
|
setupRemoveButton(table, removeButton);
|
||||||
|
|
||||||
|
loadVersion();
|
||||||
|
loadHeader(partyId);
|
||||||
|
loadTypes("/api/v1/types/jobs", jobInput);
|
||||||
|
|
||||||
|
hideControls();
|
||||||
|
|
||||||
|
table.bootstrapTable({});
|
||||||
|
reload();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
231
src/main/resources/html/users.html
Normal file
231
src/main/resources/html/users.html
Normal file
@ -0,0 +1,231 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>User management</title>
|
||||||
|
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
|
||||||
|
<link rel="shortcut icon" href="/static/favicon.ico">
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous" type="text/css">
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.8.3/font/bootstrap-icons.css" type="text/css">
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="https://unpkg.com/bootstrap-table@1.20.2/dist/bootstrap-table.min.css" type="text/css">
|
||||||
|
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="https://unpkg.com/jquery-resizable-columns@0.2.3/dist/jquery.resizableColumns.css" type="text/css">
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="/static/styles.css" type="text/css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<nav class="navbar navbar-expand-lg justify-content-between align-items-center border-bottom">
|
||||||
|
<a class="navbar-brand" id="navbar-title">Party</a>
|
||||||
|
<ul class="navbar-nav">
|
||||||
|
<a class="nav-item nav-link" id="navbar-bis">best in slot</a>
|
||||||
|
<a class="nav-item nav-link" id="navbar-loot">looted items</a>
|
||||||
|
</ul>
|
||||||
|
<ul class="navbar-nav">
|
||||||
|
<a class="nav-item nav-link" id="navbar-users">users</a>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="alert-placeholder" class="container"></div>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<h2>Users</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<div id="toolbar">
|
||||||
|
<button id="add-btn" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#add-user-dialog" hidden>
|
||||||
|
<i class="bi bi-plus"></i> add
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-secondary" onclick="reload()">
|
||||||
|
<i class="bi bi-arrow-clockwise"></i> reload
|
||||||
|
</button>
|
||||||
|
<button id="remove-btn" class="btn btn-danger" onclick="removeUsers()" disabled hidden>
|
||||||
|
<i class="bi bi-trash"></i> remove
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<table id="users" class="table table-striped table-hover"
|
||||||
|
data-click-to-select="true"
|
||||||
|
data-export-options='{"fileName": "users"}'
|
||||||
|
data-page-list="[25, 50, 100, all]"
|
||||||
|
data-page-size="25"
|
||||||
|
data-pagination="true"
|
||||||
|
data-resizable="true"
|
||||||
|
data-search="true"
|
||||||
|
data-show-columns="true"
|
||||||
|
data-show-columns-search="true"
|
||||||
|
data-show-columns-toggle-all="true"
|
||||||
|
data-show-export="true"
|
||||||
|
data-show-fullscreen="true"
|
||||||
|
data-show-search-clear-button="true"
|
||||||
|
data-single-select="true"
|
||||||
|
data-sortable="true"
|
||||||
|
data-sort-name="username"
|
||||||
|
data-sort-order="asc"
|
||||||
|
data-sort-reset="true"
|
||||||
|
data-toolbar="#toolbar">
|
||||||
|
<thead class="table-primary">
|
||||||
|
<tr>
|
||||||
|
<th data-checkbox="true"></th>
|
||||||
|
<th data-sortable="true" data-switchable="false" data-field="username">username</th>
|
||||||
|
<th data-sortable="true" data-field="permission">permission</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="add-user-dialog" tabindex="-1" role="dialog" class="modal fade">
|
||||||
|
<div class="modal-dialog" role="document">
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<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" 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" 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" required></select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-danger" data-bs-dismiss="modal">close</button>
|
||||||
|
<button type="submit" class="btn btn-primary">add</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<footer class="d-flex flex-wrap justify-content-between align-items-center border-top">
|
||||||
|
<ul class="nav">
|
||||||
|
<li><a class="nav-link" href="/" title="home">home</a></li>
|
||||||
|
<li><a class="nav-link" href="/api-docs" title="api">api</a></li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<ul class="nav">
|
||||||
|
<li><a id="sources-link" class="nav-link" href="https://github.com/arcan1s/ffxivbis" title="sources">ffxivbis</a></li>
|
||||||
|
<li><a class="nav-link" href="https://github.com/arcan1s/ffxivbis/releases" title="releases list">releases</a></li>
|
||||||
|
<li><a class="nav-link" href="https://github.com/arcan1s/ffxivbis/issues" title="issues tracker">report a bug</a></li>
|
||||||
|
</ul>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/jquery/dist/jquery.min.js"></script>
|
||||||
|
|
||||||
|
<script src="https://unpkg.com/tableexport.jquery.plugin/tableExport.min.js"></script>
|
||||||
|
|
||||||
|
<script src="https://unpkg.com/jquery-resizable-columns@0.2.3/dist/jquery.resizableColumns.min.js"></script>
|
||||||
|
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p" crossorigin="anonymous"></script>
|
||||||
|
<script src="https://unpkg.com/bootstrap-table@1.20.2/dist/bootstrap-table.min.js"></script>
|
||||||
|
|
||||||
|
<script src="https://unpkg.com/bootstrap-table@1.20.2/dist/extensions/export/bootstrap-table-export.min.js"></script>
|
||||||
|
|
||||||
|
<script src="https://unpkg.com/bootstrap-table@1.20.2/dist/extensions/resizable/bootstrap-table-resizable.js"></script>
|
||||||
|
|
||||||
|
<script src="/static/utils.js"></script>
|
||||||
|
<script src="/static/load.js"></script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const [partyId, isReadOnly] = getPartyId();
|
||||||
|
const table = $("#users");
|
||||||
|
const addButton = $("#add-btn");
|
||||||
|
const removeButton = $("#remove-btn");
|
||||||
|
|
||||||
|
const addUserDialog = $("#add-user-dialog");
|
||||||
|
|
||||||
|
const usernameInput = $("#username");
|
||||||
|
const passwordInput = $("#password");
|
||||||
|
const permissionInput = $("#permission");
|
||||||
|
|
||||||
|
function addUser() {
|
||||||
|
$.ajax({
|
||||||
|
url: `/api/v1/party/${partyId}/users`,
|
||||||
|
data: JSON.stringify({
|
||||||
|
partyId: partyId,
|
||||||
|
username: usernameInput.val(),
|
||||||
|
password: passwordInput.val(),
|
||||||
|
permission: permissionInput.val(),
|
||||||
|
}),
|
||||||
|
type: "POST",
|
||||||
|
contentType: "application/json",
|
||||||
|
success: _ => { reload(); },
|
||||||
|
error: (jqXHR, _, errorThrown) => { requestAlert(jqXHR, errorThrown); },
|
||||||
|
});
|
||||||
|
addUserDialog.modal("hide");
|
||||||
|
return true; // action expects boolean result
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideControls() {
|
||||||
|
addButton.attr("hidden", isReadOnly);
|
||||||
|
removeButton.attr("hidden", isReadOnly);
|
||||||
|
}
|
||||||
|
|
||||||
|
function reload() {
|
||||||
|
table.bootstrapTable("showLoading");
|
||||||
|
$.ajax({
|
||||||
|
url: `/api/v1/party/${partyId}/users`,
|
||||||
|
type: "GET",
|
||||||
|
dataType: "json",
|
||||||
|
success: response => {
|
||||||
|
table.bootstrapTable("load", response);
|
||||||
|
table.bootstrapTable("uncheckAll");
|
||||||
|
table.bootstrapTable("hideLoading");
|
||||||
|
},
|
||||||
|
error: (jqXHR, _, errorThrown) => { requestAlert(jqXHR, errorThrown); },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeUsers() {
|
||||||
|
const users = table.bootstrapTable("getSelections");
|
||||||
|
users.map(user => {
|
||||||
|
$.ajax({
|
||||||
|
url: `/api/v1/party/${partyId}/users/${user.username}`,
|
||||||
|
type: "DELETE",
|
||||||
|
success: _ => { reload(); },
|
||||||
|
error: (jqXHR, _, errorThrown) => { requestAlert(jqXHR, errorThrown); },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
$(() => {
|
||||||
|
setupFormClear(addUserDialog);
|
||||||
|
setupRemoveButton(table, removeButton);
|
||||||
|
|
||||||
|
loadVersion();
|
||||||
|
loadHeader(partyId);
|
||||||
|
loadTypes("/api/v1/types/permissions", permissionInput);
|
||||||
|
|
||||||
|
hideControls();
|
||||||
|
|
||||||
|
table.bootstrapTable({});
|
||||||
|
reload();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
1639
src/main/resources/item_data.json
Normal file
1639
src/main/resources/item_data.json
Normal file
File diff suppressed because it is too large
Load Diff
@ -3,14 +3,15 @@
|
|||||||
<include resource="logback-application.xml" />
|
<include resource="logback-application.xml" />
|
||||||
<include resource="logback-http.xml" />
|
<include resource="logback-http.xml" />
|
||||||
|
|
||||||
<root level="debug">
|
<root level="DEBUG">
|
||||||
<appender-ref ref="application" />
|
<appender-ref ref="application" />
|
||||||
</root>
|
</root>
|
||||||
|
|
||||||
<logger name="me.arcanis.ffxivbis" level="DEBUG" />
|
<logger name="http" level="DEBUG" additivity="false">
|
||||||
<logger name="http" level="DEBUG">
|
|
||||||
<appender-ref ref="http" />
|
<appender-ref ref="http" />
|
||||||
</logger>
|
</logger>
|
||||||
<logger name="slick" level="INFO" />
|
<logger name="org.flywaydb.core.internal" level="INFO" />
|
||||||
|
<logger name="com.zaxxer.hikari.pool" level="INFO" />
|
||||||
|
<logger name="io.swagger" level="INFO" />
|
||||||
|
|
||||||
</configuration>
|
</configuration>
|
||||||
|
@ -1,67 +1,71 @@
|
|||||||
me.arcanis.ffxivbis {
|
me.arcanis.ffxivbis {
|
||||||
bis-provider {
|
|
||||||
# xivapi base url, string, required
|
|
||||||
xivapi-url = "https://xivapi.com"
|
|
||||||
# xivapi developer key, string, optional
|
|
||||||
#xivapi-key = "abcdef"
|
|
||||||
}
|
|
||||||
|
|
||||||
database {
|
bis-provider {
|
||||||
# database section. Section must be declared inside
|
include "item_data.json"
|
||||||
# for more detailed section descriptions refer to slick documentation
|
|
||||||
mode = "sqlite"
|
|
||||||
|
|
||||||
sqlite {
|
# xivapi base url, string, required
|
||||||
profile = "slick.jdbc.SQLiteProfile$"
|
xivapi-url = "https://xivapi.com"
|
||||||
db {
|
# xivapi developer key, string, optional
|
||||||
url = "jdbc:sqlite:ffxivbis.db"
|
#xivapi-key = "abcdef"
|
||||||
user = "user"
|
|
||||||
password = "password"
|
|
||||||
}
|
|
||||||
numThreads = 10
|
|
||||||
}
|
}
|
||||||
|
|
||||||
postgresql {
|
database {
|
||||||
profile = "slick.jdbc.PostgresProfile$"
|
# database section. Section must be declared inside
|
||||||
db {
|
# for more detailed section descriptions refer to slick documentation
|
||||||
url = "jdbc:postgresql://localhost/ffxivbis"
|
mode = "sqlite"
|
||||||
user = "ffxivbis"
|
|
||||||
password = "ffxivbis"
|
|
||||||
|
|
||||||
connectionPool = disabled
|
sqlite {
|
||||||
keepAliveConnection = yes
|
driverClassName = "org.sqlite.JDBC"
|
||||||
}
|
jdbcUrl = "jdbc:sqlite:ffxivbis.db"
|
||||||
numThreads = 10
|
#username = "user"
|
||||||
|
#password = "password"
|
||||||
|
}
|
||||||
|
|
||||||
|
postgresql {
|
||||||
|
driverClassName = "org.postgresql.Driver"
|
||||||
|
jdbcUrl = "jdbc:postgresql://localhost/ffxivbis"
|
||||||
|
#username = "ffxivbis"
|
||||||
|
#password = "ffxivbis"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
settings {
|
settings {
|
||||||
# counters of Player class which will be called to sort players for loot priority
|
# counters of Player class which will be called to sort players for loot priority
|
||||||
# list of strings, required
|
# list of strings, required
|
||||||
priority = [
|
priority = [
|
||||||
"isRequired", "lootCountBiS", "priority", "lootCount", "lootCountTotal"
|
"isRequired", "lootCountBiS", "priority", "lootCount", "lootCountTotal"
|
||||||
]
|
]
|
||||||
# general request timeout, duratin, required
|
# general request timeout, duratin, required
|
||||||
request-timeout = 10s
|
request-timeout = 10s
|
||||||
# party in-memory storage lifetime
|
# party in-memory storage lifetime
|
||||||
cache-timeout = 1m
|
cache-timeout = 1m
|
||||||
}
|
}
|
||||||
|
|
||||||
web {
|
web {
|
||||||
# address to bind, string, required
|
# address to bind, string, required
|
||||||
host = "127.0.0.1"
|
host = "127.0.0.1"
|
||||||
# port to bind, int, required
|
# port to bind, int, required
|
||||||
port = 8000
|
port = 8000
|
||||||
# hostname to use in docs, if not set host:port will be used
|
# hostname to use in docs, if not set host:port will be used
|
||||||
#hostname = "127.0.0.1:8000"
|
#hostname = "127.0.0.1:8000"
|
||||||
}
|
# enable head requests for GET requests
|
||||||
|
enable-head-requests = yes
|
||||||
default-dispatcher {
|
schemes = ["http"]
|
||||||
type = Dispatcher
|
|
||||||
executor = "thread-pool-executor"
|
authorization-cache {
|
||||||
thread-pool-executor {
|
# maximum amount of cached logins
|
||||||
fixed-pool-size = 16
|
cache-size = 1024
|
||||||
|
# ttl of cached logins
|
||||||
|
cache-timeout = 1m
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
default-dispatcher {
|
||||||
|
type = Dispatcher
|
||||||
|
executor = "thread-pool-executor"
|
||||||
|
thread-pool-executor {
|
||||||
|
fixed-pool-size = 16
|
||||||
|
}
|
||||||
|
throughput = 1
|
||||||
}
|
}
|
||||||
throughput = 1
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
BIN
src/main/resources/static/favicon.ico
Normal file
BIN
src/main/resources/static/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 30 KiB |
64
src/main/resources/static/load.js
Normal file
64
src/main/resources/static/load.js
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
function loadHeader(partyId) {
|
||||||
|
const title = $("#navbar-title");
|
||||||
|
|
||||||
|
// because I don't know how to handle relative url if current does not end with slash
|
||||||
|
title.attr("href", `/party/${partyId}`);
|
||||||
|
$("#navbar-bis").attr("href", `/party/${partyId}/bis`);
|
||||||
|
$("#navbar-loot").attr("href", `/party/${partyId}/loot`);
|
||||||
|
$("#navbar-users").attr("href", `/party/${partyId}/users`);
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url: `/api/v1/party/${partyId}/description`,
|
||||||
|
type: "GET",
|
||||||
|
dataType: "json",
|
||||||
|
success: function (resp) {
|
||||||
|
title.text(safe(resp.partyAlias || partyId));
|
||||||
|
},
|
||||||
|
error: function (jqXHR, _, errorThrown) { requestAlert(jqXHR, errorThrown); },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadTypes(url, selector) {
|
||||||
|
$.ajax({
|
||||||
|
url: url,
|
||||||
|
type: "GET",
|
||||||
|
dataType: "json",
|
||||||
|
success: function (data) {
|
||||||
|
const options = data.map(function (name) {
|
||||||
|
const option = document.createElement("option");
|
||||||
|
option.value = name;
|
||||||
|
option.innerText = name;
|
||||||
|
return option;
|
||||||
|
});
|
||||||
|
selector.empty().append(options);
|
||||||
|
},
|
||||||
|
error: function (jqXHR, _, errorThrown) { requestAlert(jqXHR, errorThrown); },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadVersion() {
|
||||||
|
$.ajax({
|
||||||
|
url: "/api/v1/status",
|
||||||
|
type: "GET",
|
||||||
|
dataType: "json",
|
||||||
|
success: function (data) { $("#sources-link").text(`ffxivbis ${data.version}`); },
|
||||||
|
error: function (jqXHR, _, errorThrown) { requestAlert(jqXHR, errorThrown); },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function setupFormClear(dialog, reset) {
|
||||||
|
dialog.on("hide.bs.modal", function () {
|
||||||
|
$(this).find("form").trigger("reset");
|
||||||
|
$(this).find("table").bootstrapTable("removeAll");
|
||||||
|
if (reset) {
|
||||||
|
reset();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function setupRemoveButton(table, removeButton) {
|
||||||
|
table.on("check.bs.table uncheck.bs.table check-all.bs.table uncheck-all.bs.table",
|
||||||
|
function () {
|
||||||
|
removeButton.prop("disabled", !table.bootstrapTable("getSelections").length);
|
||||||
|
});
|
||||||
|
}
|
@ -1,277 +0,0 @@
|
|||||||
/* in-text images */
|
|
||||||
figure.img {
|
|
||||||
float: right;
|
|
||||||
border: 0px solid #333;
|
|
||||||
padding: 0px;
|
|
||||||
margin: 5px 0px 5px 10px;
|
|
||||||
}
|
|
||||||
figure.img img {
|
|
||||||
max-width: 100%;
|
|
||||||
height: auto;
|
|
||||||
}
|
|
||||||
figure.img figcaption {
|
|
||||||
margin: 0px;
|
|
||||||
font-size: 90%;
|
|
||||||
font-style: italic;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 .octicon-link, h2 .octicon-link, h3 .octicon-link, h4 .octicon-link, h5 .octicon-link, h6 .octicon-link {
|
|
||||||
display: none;
|
|
||||||
color: #222222;
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1:hover .anchor, h2:hover .anchor, h3:hover .anchor, h4:hover .anchor, h5:hover .anchor, h6:hover .anchor{
|
|
||||||
padding-left: 8px;
|
|
||||||
margin-left: -24px;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1:hover .anchor .octicon-link, h2:hover .anchor .octicon-link, h3:hover .anchor .octicon-link, h4:hover .anchor .octicon-link, h5:hover .anchor .octicon-link, h6:hover .anchor .octicon-link {
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
padding: 50px;
|
|
||||||
font: 14px/1.5 "Liberation Sans", Helvetica, Arial, sans-serif;
|
|
||||||
color: #555555;
|
|
||||||
background: #eaeaea
|
|
||||||
}
|
|
||||||
|
|
||||||
h1, h2, h3, h4, h5, h6 {
|
|
||||||
color: #222222;
|
|
||||||
margin: 0 0 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
p, ul, ol, table, pre, dl {
|
|
||||||
margin: 0 0 20px;
|
|
||||||
text-align: justify;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1, h2, h3 {
|
|
||||||
line-height: 1.1;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
font-size: 28px;
|
|
||||||
}
|
|
||||||
|
|
||||||
h2 {
|
|
||||||
color: #393939;
|
|
||||||
}
|
|
||||||
|
|
||||||
h3, h4, h5, h6 {
|
|
||||||
color: #494949;
|
|
||||||
}
|
|
||||||
|
|
||||||
a {
|
|
||||||
color: #3399cc;
|
|
||||||
font-weight: 350;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
a small {
|
|
||||||
font-size: 11px;
|
|
||||||
color: #777777;
|
|
||||||
margin-top: -0.6em;
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wrapper {
|
|
||||||
width: 80%;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
blockquote {
|
|
||||||
border-left: 1px solid #ffffff;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0 0 0 20px;
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
|
|
||||||
code, pre {
|
|
||||||
font-family: "Liberation Mono", Monaco, Bitstream Vera Sans Mono, Lucida Console, Terminal;
|
|
||||||
color: #222222;
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
pre {
|
|
||||||
padding: 8px 15px;
|
|
||||||
border-radius: 5px;
|
|
||||||
border: 1px solid #e5e5e5;
|
|
||||||
overflow-x: auto;
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
input, select{
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
table {
|
|
||||||
width: 100%;
|
|
||||||
border-collapse: collapse;
|
|
||||||
}
|
|
||||||
|
|
||||||
th, td {
|
|
||||||
padding: 5px 10px;
|
|
||||||
border-bottom: 1px solid #ffffff;
|
|
||||||
}
|
|
||||||
|
|
||||||
td {
|
|
||||||
text-align: justify;
|
|
||||||
}
|
|
||||||
|
|
||||||
dt {
|
|
||||||
color: #444444;
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
|
|
||||||
th {
|
|
||||||
text-align: left;
|
|
||||||
color: #444444;
|
|
||||||
}
|
|
||||||
|
|
||||||
img {
|
|
||||||
max-width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
header {
|
|
||||||
width: 20%;
|
|
||||||
float: left;
|
|
||||||
position: fixed;
|
|
||||||
}
|
|
||||||
|
|
||||||
header ul {
|
|
||||||
list-style: none;
|
|
||||||
height: 40px;
|
|
||||||
padding: 0;
|
|
||||||
background: #eeeeee;
|
|
||||||
border-radius: 5px;
|
|
||||||
border: 1px solid #d2d2d2;
|
|
||||||
box-shadow: inset #fff 0 1px 0, inset rgba(0,0,0,0.03) 0 -1px 0;
|
|
||||||
width: 15%;
|
|
||||||
}
|
|
||||||
|
|
||||||
header li {
|
|
||||||
width: 8%;
|
|
||||||
float: left;
|
|
||||||
border-right: 1px solid #d2d2d2;
|
|
||||||
height: 40px;
|
|
||||||
}
|
|
||||||
|
|
||||||
header ul a {
|
|
||||||
line-height: 1;
|
|
||||||
font-size: 11px;
|
|
||||||
color: #999999;
|
|
||||||
display: block;
|
|
||||||
text-align: center;
|
|
||||||
padding-top: 6px;
|
|
||||||
height: 40px;
|
|
||||||
}
|
|
||||||
|
|
||||||
strong {
|
|
||||||
color: #222222;
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
|
|
||||||
header ul li + li {
|
|
||||||
width: 8%;
|
|
||||||
border-left: 1px solid #ffffff;
|
|
||||||
}
|
|
||||||
|
|
||||||
header ul li + li + li {
|
|
||||||
width: 8%;
|
|
||||||
border-right: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
header ul a strong {
|
|
||||||
font-size: 14px;
|
|
||||||
display: block;
|
|
||||||
color: #222222;
|
|
||||||
}
|
|
||||||
|
|
||||||
section {
|
|
||||||
width: 70%;
|
|
||||||
float: right;
|
|
||||||
padding-bottom: 50px;
|
|
||||||
}
|
|
||||||
|
|
||||||
small {
|
|
||||||
font-size: 11px;
|
|
||||||
}
|
|
||||||
|
|
||||||
hr {
|
|
||||||
border: 0;
|
|
||||||
background: #ffffff;
|
|
||||||
height: 1px;
|
|
||||||
margin: 0 0 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
footer {
|
|
||||||
width: 20%;
|
|
||||||
float: left;
|
|
||||||
position: fixed;
|
|
||||||
bottom: 50px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media print, screen and (max-width: 960px) {
|
|
||||||
div.wrapper {
|
|
||||||
width: auto;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
header, section, footer {
|
|
||||||
float: none;
|
|
||||||
position: static;
|
|
||||||
width: auto;
|
|
||||||
}
|
|
||||||
header {
|
|
||||||
padding-right: 320px;
|
|
||||||
}
|
|
||||||
section {
|
|
||||||
border: 1px solid #e5e5e5;
|
|
||||||
border-width: 1px 0;
|
|
||||||
padding: 20px 0;
|
|
||||||
margin: 0 0 20px;
|
|
||||||
}
|
|
||||||
header a small {
|
|
||||||
display: inline;
|
|
||||||
}
|
|
||||||
header ul {
|
|
||||||
position: absolute;
|
|
||||||
right: 50px;
|
|
||||||
top: 52px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media print, screen and (max-width: 720px) {
|
|
||||||
body {
|
|
||||||
word-wrap: break-word;
|
|
||||||
}
|
|
||||||
header {
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
header ul, header p.view {
|
|
||||||
position: static;
|
|
||||||
}
|
|
||||||
pre, code {
|
|
||||||
word-wrap: normal;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media print, screen and (max-width: 480px) {
|
|
||||||
body {
|
|
||||||
padding: 15px;
|
|
||||||
}
|
|
||||||
header ul {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media print {
|
|
||||||
body {
|
|
||||||
padding: 0.4in;
|
|
||||||
font-size: 12pt;
|
|
||||||
color: #444444;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,31 +0,0 @@
|
|||||||
function downloadCsv(csv, filename) {
|
|
||||||
var csvFile = new Blob([csv], {"type": "text/csv"});
|
|
||||||
|
|
||||||
var downloadLink = document.createElement("a");
|
|
||||||
downloadLink.download = filename;
|
|
||||||
downloadLink.href = window.URL.createObjectURL(csvFile);
|
|
||||||
downloadLink.style.display = "none";
|
|
||||||
|
|
||||||
document.body.appendChild(downloadLink);
|
|
||||||
downloadLink.click();
|
|
||||||
}
|
|
||||||
|
|
||||||
function exportTableToCsv(filename) {
|
|
||||||
var table = document.getElementById("result");
|
|
||||||
var rows = table.getElementsByTagName("tr");
|
|
||||||
|
|
||||||
var csv = [];
|
|
||||||
for (var i = 0; i < rows.length; i++) {
|
|
||||||
if (rows[i].style.display === "none")
|
|
||||||
continue;
|
|
||||||
var cols = rows[i].querySelectorAll("td, th");
|
|
||||||
|
|
||||||
var row = [];
|
|
||||||
for (var j = 0; j < cols.length; j++)
|
|
||||||
row.push(cols[j].innerText);
|
|
||||||
|
|
||||||
csv.push(row.join(","));
|
|
||||||
}
|
|
||||||
|
|
||||||
downloadCsv(csv.join("\n"), filename);
|
|
||||||
}
|
|
@ -1,21 +0,0 @@
|
|||||||
function searchTable() {
|
|
||||||
var input = document.getElementById("search");
|
|
||||||
var filter = input.value.toLowerCase();
|
|
||||||
var table = document.getElementById("result");
|
|
||||||
var tr = table.getElementsByTagName("tr");
|
|
||||||
|
|
||||||
// from 1 coz of header
|
|
||||||
for (var i = 1; i < tr.length; i++) {
|
|
||||||
var td = tr[i].getElementsByClassName("include_search");
|
|
||||||
var display = "none";
|
|
||||||
for (var j = 0; j < td.length; j++) {
|
|
||||||
if (td[j].tagName.toLowerCase() === "td") {
|
|
||||||
if (td[j].innerHTML.toLowerCase().indexOf(filter) > -1) {
|
|
||||||
display = "";
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
tr[i].style.display = display;
|
|
||||||
}
|
|
||||||
}
|
|
44
src/main/resources/static/utils.js
Normal file
44
src/main/resources/static/utils.js
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
function createAlert(message, placeholder) {
|
||||||
|
const wrapper = document.createElement('div');
|
||||||
|
wrapper.innerHTML = `<div class="alert alert-danger alert-dismissible" role="alert">${safe(message)}<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button></div>`;
|
||||||
|
placeholder.append(wrapper);
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatPlayerId(obj) {
|
||||||
|
return `${obj.nick} (${obj.job})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCurrentOption(select) {
|
||||||
|
return select.find(":selected")[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPartyId() {
|
||||||
|
const request = new XMLHttpRequest();
|
||||||
|
request.open("HEAD", document.location, false);
|
||||||
|
request.send(null);
|
||||||
|
|
||||||
|
// tuple lol
|
||||||
|
return [
|
||||||
|
request.getResponseHeader("X-Party-Id"),
|
||||||
|
request.getResponseHeader("X-User-Permission") === "get",
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
function requestAlert(jqXHR, errorThrown) {
|
||||||
|
let message;
|
||||||
|
try {
|
||||||
|
message = $.parseJSON(jqXHR.responseText).message;
|
||||||
|
} catch (_) {
|
||||||
|
message = errorThrown;
|
||||||
|
}
|
||||||
|
const alert = $("#alert-placeholder");
|
||||||
|
createAlert(`Error during request: ${message}`, alert);
|
||||||
|
}
|
||||||
|
|
||||||
|
function safe(string) {
|
||||||
|
return String(string)
|
||||||
|
.replace(/&/g, "&")
|
||||||
|
.replace(/</g, "<")
|
||||||
|
.replace(/>/g, ">")
|
||||||
|
.replace(/"/g, """);
|
||||||
|
}
|
@ -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
|
# Basic workflow
|
||||||
|
|
||||||
@ -11,6 +11,8 @@ REST json API description to interact with FFXIVBiS service.
|
|||||||
|
|
||||||
# Limitations
|
# Limitations
|
||||||
|
|
||||||
|
No limitations for the API so far.
|
||||||
|
|
||||||
# Authentication
|
# Authentication
|
||||||
|
|
||||||
For the most party utils service requires user to be authenticated. User permission can be one of `get`, `post` or `admin`.
|
For the most party utils service requires user to be authenticated. User permission can be one of `get`, `post` or `admin`.
|
||||||
|
@ -1,24 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<title>ReDoc</title>
|
|
||||||
<!-- needed for adaptive design -->
|
|
||||||
<meta charset="utf-8"/>
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
||||||
<link href="https://fonts.googleapis.com/css?family=Montserrat:300,400,700|Roboto:300,400,700" rel="stylesheet">
|
|
||||||
|
|
||||||
<!--
|
|
||||||
ReDoc doesn't change outer page styles
|
|
||||||
-->
|
|
||||||
<style>
|
|
||||||
body {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<redoc spec-url='/api-docs/swagger.json'></redoc>
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/redoc@next/bundles/redoc.standalone.js"> </script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (c) 2019 Evgeniy Alekseev.
|
* Copyright (c) 2019-2022 Evgeniy Alekseev.
|
||||||
*
|
*
|
||||||
* This file is part of ffxivbis
|
* This file is part of ffxivbis
|
||||||
* (see https://github.com/arcan1s/ffxivbis).
|
* (see https://github.com/arcan1s/ffxivbis).
|
||||||
@ -8,30 +8,31 @@
|
|||||||
*/
|
*/
|
||||||
package me.arcanis.ffxivbis
|
package me.arcanis.ffxivbis
|
||||||
|
|
||||||
import akka.actor.typed.{Behavior, PostStop, Signal}
|
|
||||||
import akka.actor.typed.scaladsl.{AbstractBehavior, ActorContext, Behaviors}
|
import akka.actor.typed.scaladsl.{AbstractBehavior, ActorContext, Behaviors}
|
||||||
|
import akka.actor.typed.{Behavior, PostStop, Signal}
|
||||||
import akka.http.scaladsl.Http
|
import akka.http.scaladsl.Http
|
||||||
|
import akka.http.scaladsl.server.Route
|
||||||
import akka.stream.Materializer
|
import akka.stream.Materializer
|
||||||
import com.typesafe.scalalogging.StrictLogging
|
import com.typesafe.scalalogging.StrictLogging
|
||||||
import me.arcanis.ffxivbis.http.RootEndpoint
|
import me.arcanis.ffxivbis.http.RootEndpoint
|
||||||
|
import me.arcanis.ffxivbis.service.PartyService
|
||||||
import me.arcanis.ffxivbis.service.bis.BisProvider
|
import me.arcanis.ffxivbis.service.bis.BisProvider
|
||||||
import me.arcanis.ffxivbis.service.{Database, PartyService}
|
import me.arcanis.ffxivbis.service.database.{Database, Migration}
|
||||||
import me.arcanis.ffxivbis.storage.Migration
|
|
||||||
|
|
||||||
import scala.concurrent.ExecutionContext
|
import scala.concurrent.ExecutionContext
|
||||||
|
import scala.jdk.CollectionConverters._
|
||||||
|
import scala.util.{Failure, Success}
|
||||||
|
|
||||||
class Application(context: ActorContext[Nothing])
|
class Application(context: ActorContext[Nothing]) extends AbstractBehavior[Nothing](context) with StrictLogging {
|
||||||
extends AbstractBehavior[Nothing](context) with StrictLogging {
|
|
||||||
|
|
||||||
logger.info("root supervisor started")
|
logger.info("root supervisor started")
|
||||||
startApplication()
|
startApplication()
|
||||||
|
|
||||||
override def onMessage(msg: Nothing): Behavior[Nothing] = Behaviors.unhandled
|
override def onMessage(msg: Nothing): Behavior[Nothing] = Behaviors.unhandled
|
||||||
|
|
||||||
override def onSignal: PartialFunction[Signal, Behavior[Nothing]] = {
|
override def onSignal: PartialFunction[Signal, Behavior[Nothing]] = { case PostStop =>
|
||||||
case PostStop =>
|
logger.info("root supervisor stopped")
|
||||||
logger.info("root supervisor stopped")
|
Behaviors.same
|
||||||
Behaviors.same
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private def startApplication(): Unit = {
|
private def startApplication(): Unit = {
|
||||||
@ -42,14 +43,25 @@ class Application(context: ActorContext[Nothing])
|
|||||||
implicit val executionContext: ExecutionContext = context.system.executionContext
|
implicit val executionContext: ExecutionContext = context.system.executionContext
|
||||||
implicit val materializer: Materializer = Materializer(context)
|
implicit val materializer: Materializer = Materializer(context)
|
||||||
|
|
||||||
Migration(config)
|
Migration(config) match {
|
||||||
|
case Success(result) if result.success =>
|
||||||
|
val bisProvider = context.spawn(BisProvider(), "bis-provider")
|
||||||
|
val storage = context.spawn(Database(), "storage")
|
||||||
|
val party = context.spawn(PartyService(storage), "party")
|
||||||
|
val http = new RootEndpoint(context.system, party, bisProvider)
|
||||||
|
|
||||||
val bisProvider = context.spawn(BisProvider(), "bis-provider")
|
val flow = Route.toFlow(http.routes)(context.system)
|
||||||
val storage = context.spawn(Database(), "storage")
|
Http(context.system).newServerAt(host, port).bindFlow(flow)
|
||||||
val party = context.spawn(PartyService(storage), "party")
|
|
||||||
val http = new RootEndpoint(context.system, party, bisProvider)
|
|
||||||
|
|
||||||
Http()(context.system).newServerAt(host, port).bindFlow(http.route)
|
case Success(result) =>
|
||||||
|
logger.error(s"migration completed with error, executed ${result.migrationsExecuted}")
|
||||||
|
result.migrations.asScala.foreach(o => logger.info(s"=> ${o.description} (${o.executionTime})"))
|
||||||
|
context.system.terminate()
|
||||||
|
|
||||||
|
case Failure(exception) =>
|
||||||
|
logger.error("exception during migration", exception)
|
||||||
|
context.system.terminate()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
23
src/main/scala/me/arcanis/ffxivbis/Configuration.scala
Normal file
23
src/main/scala/me/arcanis/ffxivbis/Configuration.scala
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2019-2022 Evgeniy Alekseev.
|
||||||
|
*
|
||||||
|
* This file is part of ffxivbis
|
||||||
|
* (see https://github.com/arcan1s/ffxivbis).
|
||||||
|
*
|
||||||
|
* License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause
|
||||||
|
*/
|
||||||
|
package me.arcanis.ffxivbis
|
||||||
|
|
||||||
|
import com.typesafe.config.{Config, ConfigFactory, ConfigValueFactory}
|
||||||
|
|
||||||
|
object Configuration {
|
||||||
|
|
||||||
|
def load(): Config = {
|
||||||
|
val root = ConfigFactory.load()
|
||||||
|
root
|
||||||
|
.withValue(
|
||||||
|
"akka.http.server.transparent-head-requests",
|
||||||
|
ConfigValueFactory.fromAnyRef(root.getBoolean("me.arcanis.ffxivbis.web.enable-head-requests"))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (c) 2019 Evgeniy Alekseev.
|
* Copyright (c) 2019-2022 Evgeniy Alekseev.
|
||||||
*
|
*
|
||||||
* This file is part of ffxivbis
|
* This file is part of ffxivbis
|
||||||
* (see https://github.com/arcan1s/ffxivbis).
|
* (see https://github.com/arcan1s/ffxivbis).
|
||||||
@ -9,12 +9,8 @@
|
|||||||
package me.arcanis.ffxivbis
|
package me.arcanis.ffxivbis
|
||||||
|
|
||||||
import akka.actor.typed.ActorSystem
|
import akka.actor.typed.ActorSystem
|
||||||
import com.typesafe.config.ConfigFactory
|
|
||||||
|
|
||||||
object ffxivbis {
|
object ffxivbis extends App {
|
||||||
|
|
||||||
def main(args: Array[String]): Unit = {
|
ActorSystem[Nothing](Application(), "ffxivbis", Configuration.load())
|
||||||
val config = ConfigFactory.load()
|
|
||||||
ActorSystem[Nothing](Application(), "ffxivbis", config)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (c) 2019 Evgeniy Alekseev.
|
* Copyright (c) 2019-2022 Evgeniy Alekseev.
|
||||||
*
|
*
|
||||||
* This file is part of ffxivbis
|
* This file is part of ffxivbis
|
||||||
* (see https://github.com/arcan1s/ffxivbis).
|
* (see https://github.com/arcan1s/ffxivbis).
|
||||||
@ -8,25 +8,20 @@
|
|||||||
*/
|
*/
|
||||||
package me.arcanis.ffxivbis.http
|
package me.arcanis.ffxivbis.http
|
||||||
|
|
||||||
import akka.actor.typed.scaladsl.AskPattern.Askable
|
|
||||||
import akka.actor.typed.{ActorRef, Scheduler}
|
|
||||||
import akka.http.scaladsl.model.headers._
|
import akka.http.scaladsl.model.headers._
|
||||||
import akka.http.scaladsl.server.AuthenticationFailedRejection._
|
import akka.http.scaladsl.server.AuthenticationFailedRejection._
|
||||||
import akka.http.scaladsl.server.Directives._
|
import akka.http.scaladsl.server.Directives._
|
||||||
import akka.http.scaladsl.server._
|
import akka.http.scaladsl.server._
|
||||||
import akka.util.Timeout
|
import me.arcanis.ffxivbis.models.{Permission, User}
|
||||||
import me.arcanis.ffxivbis.messages.{GetUser, Message}
|
|
||||||
import me.arcanis.ffxivbis.models.Permission
|
|
||||||
|
|
||||||
import scala.concurrent.{ExecutionContext, Future}
|
import scala.concurrent.{ExecutionContext, Future}
|
||||||
|
|
||||||
// idea comes from https://synkre.com/bcrypt-for-akka-http-password-encryption/
|
// idea comes from https://synkre.com/bcrypt-for-akka-http-password-encryption/
|
||||||
trait Authorization {
|
trait Authorization {
|
||||||
|
|
||||||
def storage: ActorRef[Message]
|
def auth: AuthorizationProvider
|
||||||
|
|
||||||
def authenticateBasicBCrypt[T](realm: String,
|
def authenticateBasicBCrypt[T](realm: String, authenticate: (String, String) => Future[Option[T]]): Directive1[T] = {
|
||||||
authenticate: (String, String) => Future[Option[T]]): Directive1[T] = {
|
|
||||||
def challenge = HttpChallenges.basic(realm)
|
def challenge = HttpChallenges.basic(realm)
|
||||||
|
|
||||||
extractCredentials.flatMap {
|
extractCredentials.flatMap {
|
||||||
@ -39,22 +34,19 @@ trait Authorization {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
def authenticator(scope: Permission.Value, partyId: String)(username: String, password: String)
|
def authAdmin(partyId: String)(username: String, password: String)(implicit
|
||||||
(implicit executionContext: ExecutionContext, timeout: Timeout, scheduler: Scheduler): Future[Option[String]] =
|
executionContext: ExecutionContext
|
||||||
storage.ask(GetUser(partyId, username, _)).map {
|
): Future[Option[User]] =
|
||||||
case Some(user) if user.verify(password) && user.verityScope(scope) => Some(username)
|
auth.authenticator(Permission.admin, partyId)(username, password)
|
||||||
case _ => None
|
|
||||||
}
|
|
||||||
|
|
||||||
def authAdmin(partyId: String)(username: String, password: String)
|
def authGet(partyId: String)(username: String, password: String)(implicit
|
||||||
(implicit executionContext: ExecutionContext, timeout: Timeout, scheduler: Scheduler): Future[Option[String]] =
|
executionContext: ExecutionContext
|
||||||
authenticator(Permission.admin, partyId)(username, password)
|
): Future[Option[User]] =
|
||||||
|
auth.authenticator(Permission.get, partyId)(username, password)
|
||||||
|
|
||||||
def authGet(partyId: String)(username: String, password: String)
|
def authPost(partyId: String)(username: String, password: String)(implicit
|
||||||
(implicit executionContext: ExecutionContext, timeout: Timeout, scheduler: Scheduler): Future[Option[String]] =
|
executionContext: ExecutionContext
|
||||||
authenticator(Permission.get, partyId)(username, password)
|
): Future[Option[User]] =
|
||||||
|
auth.authenticator(Permission.post, partyId)(username, password)
|
||||||
|
|
||||||
def authPost(partyId: String)(username: String, password: String)
|
|
||||||
(implicit executionContext: ExecutionContext, timeout: Timeout, scheduler: Scheduler): Future[Option[String]] =
|
|
||||||
authenticator(Permission.post, partyId)(username, password)
|
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,64 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2019-2022 Evgeniy Alekseev.
|
||||||
|
*
|
||||||
|
* This file is part of ffxivbis
|
||||||
|
* (see https://github.com/arcan1s/ffxivbis).
|
||||||
|
*
|
||||||
|
* License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause
|
||||||
|
*/
|
||||||
|
package me.arcanis.ffxivbis.http
|
||||||
|
|
||||||
|
import akka.actor.typed.scaladsl.AskPattern.Askable
|
||||||
|
import akka.actor.typed.{ActorRef, Scheduler}
|
||||||
|
import akka.util.Timeout
|
||||||
|
import com.google.common.cache.{CacheBuilder, CacheLoader, LoadingCache}
|
||||||
|
import com.typesafe.config.Config
|
||||||
|
import me.arcanis.ffxivbis.messages.DatabaseMessage.GetUser
|
||||||
|
import me.arcanis.ffxivbis.messages.Message
|
||||||
|
import me.arcanis.ffxivbis.models.{Permission, User}
|
||||||
|
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
import scala.concurrent.{ExecutionContext, Future}
|
||||||
|
|
||||||
|
trait AuthorizationProvider {
|
||||||
|
|
||||||
|
def get(partyId: String, username: String): Future[Option[User]]
|
||||||
|
|
||||||
|
def authenticator[T](scope: Permission.Value, partyId: String)(username: String, password: String)(implicit
|
||||||
|
executionContext: ExecutionContext,
|
||||||
|
extractor: User => T
|
||||||
|
): Future[Option[T]] =
|
||||||
|
get(partyId, username).map {
|
||||||
|
case Some(user) if user.verify(password) && user.verityScope(scope) => Some(extractor(user))
|
||||||
|
case _ => None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
object AuthorizationProvider {
|
||||||
|
|
||||||
|
def apply(config: Config, storage: ActorRef[Message])(implicit
|
||||||
|
timeout: Timeout,
|
||||||
|
scheduler: Scheduler
|
||||||
|
): AuthorizationProvider =
|
||||||
|
new AuthorizationProvider {
|
||||||
|
private val cacheSize = config.getInt("me.arcanis.ffxivbis.web.authorization-cache.cache-size")
|
||||||
|
private val cacheTimeout =
|
||||||
|
config.getDuration("me.arcanis.ffxivbis.web.authorization-cache.cache-timeout", TimeUnit.MILLISECONDS)
|
||||||
|
|
||||||
|
private val cache: LoadingCache[(String, String), Future[Option[User]]] = CacheBuilder
|
||||||
|
.newBuilder()
|
||||||
|
.expireAfterWrite(cacheTimeout, TimeUnit.MILLISECONDS)
|
||||||
|
.maximumSize(cacheSize)
|
||||||
|
.build(
|
||||||
|
new CacheLoader[(String, String), Future[Option[User]]] {
|
||||||
|
override def load(key: (String, String)): Future[Option[User]] = {
|
||||||
|
val (partyId, username) = key
|
||||||
|
storage.ask(GetUser(partyId, username, _))(timeout, scheduler)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
override def get(partyId: String, username: String): Future[Option[User]] =
|
||||||
|
cache.get((partyId, username))
|
||||||
|
}
|
||||||
|
}
|
@ -1,52 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (c) 2019 Evgeniy Alekseev.
|
|
||||||
*
|
|
||||||
* This file is part of ffxivbis
|
|
||||||
* (see https://github.com/arcan1s/ffxivbis).
|
|
||||||
*
|
|
||||||
* License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause
|
|
||||||
*/
|
|
||||||
package me.arcanis.ffxivbis.http
|
|
||||||
|
|
||||||
import akka.actor.typed.scaladsl.AskPattern.Askable
|
|
||||||
import akka.actor.typed.{ActorRef, Scheduler}
|
|
||||||
import akka.util.Timeout
|
|
||||||
import me.arcanis.ffxivbis.http.api.v1.json.ApiAction
|
|
||||||
import me.arcanis.ffxivbis.messages.{AddPieceToBis, GetBiS, Message, RemovePieceFromBiS, RemovePiecesFromBiS}
|
|
||||||
import me.arcanis.ffxivbis.models.{Piece, Player, PlayerId}
|
|
||||||
|
|
||||||
import scala.concurrent.{ExecutionContext, Future}
|
|
||||||
|
|
||||||
trait BiSHelper extends BisProviderHelper {
|
|
||||||
|
|
||||||
def storage: ActorRef[Message]
|
|
||||||
|
|
||||||
def addPieceBiS(playerId: PlayerId, piece: Piece)
|
|
||||||
(implicit timeout: Timeout, scheduler: Scheduler): Future[Unit] =
|
|
||||||
storage.ask(AddPieceToBis(playerId, piece.withJob(playerId.job), _))
|
|
||||||
|
|
||||||
def bis(partyId: String, playerId: Option[PlayerId])
|
|
||||||
(implicit timeout: Timeout, scheduler: Scheduler): Future[Seq[Player]] =
|
|
||||||
storage.ask(GetBiS(partyId, playerId, _))
|
|
||||||
|
|
||||||
def doModifyBiS(action: ApiAction.Value, playerId: PlayerId, piece: Piece)
|
|
||||||
(implicit timeout: Timeout, scheduler: Scheduler): Future[Unit] =
|
|
||||||
action match {
|
|
||||||
case ApiAction.add => addPieceBiS(playerId, piece)
|
|
||||||
case ApiAction.remove => removePieceBiS(playerId, piece)
|
|
||||||
}
|
|
||||||
|
|
||||||
def putBiS(playerId: PlayerId, link: String)
|
|
||||||
(implicit executionContext: ExecutionContext, timeout: Timeout, scheduler: Scheduler): Future[Unit] = {
|
|
||||||
storage.ask(RemovePiecesFromBiS(playerId, _)).flatMap { _ =>
|
|
||||||
downloadBiS(link, playerId.job).flatMap { bis =>
|
|
||||||
Future.traverse(bis.pieces)(addPieceBiS(playerId, _))
|
|
||||||
}.map(_ => ())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
def removePieceBiS(playerId: PlayerId, piece: Piece)
|
|
||||||
(implicit timeout: Timeout, scheduler: Scheduler): Future[Unit] =
|
|
||||||
storage.ask(RemovePieceFromBiS(playerId, piece, _))
|
|
||||||
|
|
||||||
}
|
|
74
src/main/scala/me/arcanis/ffxivbis/http/HttpLog.scala
Normal file
74
src/main/scala/me/arcanis/ffxivbis/http/HttpLog.scala
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2019-2022 Evgeniy Alekseev.
|
||||||
|
*
|
||||||
|
* This file is part of ffxivbis
|
||||||
|
* (see https://github.com/arcan1s/ffxivbis).
|
||||||
|
*
|
||||||
|
* License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause
|
||||||
|
*/
|
||||||
|
package me.arcanis.ffxivbis.http
|
||||||
|
|
||||||
|
import akka.http.scaladsl.model.headers.{`User-Agent`, Authorization, BasicHttpCredentials, Referer}
|
||||||
|
import akka.http.scaladsl.server.Directive0
|
||||||
|
import akka.http.scaladsl.server.Directives.{extractClientIP, extractRequestContext, mapResponse, optionalHeaderValueByType}
|
||||||
|
import com.typesafe.scalalogging.Logger
|
||||||
|
|
||||||
|
import java.time.format.DateTimeFormatter
|
||||||
|
import java.time.{Instant, ZoneId}
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
trait HttpLog {
|
||||||
|
|
||||||
|
private val httpLogger = Logger("http")
|
||||||
|
|
||||||
|
def withHttpLog: Directive0 =
|
||||||
|
extractRequestContext.flatMap { context =>
|
||||||
|
val request = s"${context.request.method.name()} ${context.request.uri.path}"
|
||||||
|
|
||||||
|
extractClientIP.flatMap { maybeRemoteAddr =>
|
||||||
|
val remoteAddr = maybeRemoteAddr.toIP.getOrElse("-")
|
||||||
|
|
||||||
|
optionalHeaderValueByType(Referer).flatMap { maybeReferer =>
|
||||||
|
val referer = maybeReferer.map(_.uri).getOrElse("-")
|
||||||
|
|
||||||
|
optionalHeaderValueByType(`User-Agent`).flatMap { maybeUserAgent =>
|
||||||
|
val userAgent = maybeUserAgent.map(_.products.map(_.toString()).mkString(" ")).getOrElse("-")
|
||||||
|
|
||||||
|
optionalHeaderValueByType(Authorization).flatMap { maybeAuth =>
|
||||||
|
val remoteUser = maybeAuth
|
||||||
|
.map(_.credentials)
|
||||||
|
.collect { case BasicHttpCredentials(username, _) =>
|
||||||
|
username
|
||||||
|
}
|
||||||
|
.getOrElse("-")
|
||||||
|
|
||||||
|
val start = Instant.now.toEpochMilli
|
||||||
|
val timeLocal = HttpLog.httpLogDatetimeFormatter.format(Instant.now)
|
||||||
|
|
||||||
|
mapResponse { response =>
|
||||||
|
val time = (Instant.now.toEpochMilli - start) / 1000.0
|
||||||
|
|
||||||
|
val status = response.status.intValue()
|
||||||
|
val bytesSent = response.entity.getContentLengthOption.getAsLong
|
||||||
|
|
||||||
|
httpLogger.debug(
|
||||||
|
s"""$remoteAddr - $remoteUser [$timeLocal] "$request" $status $bytesSent "$referer" "$userAgent" $time"""
|
||||||
|
)
|
||||||
|
response
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
object HttpLog {
|
||||||
|
|
||||||
|
val httpLogDatetimeFormatter: DateTimeFormatter =
|
||||||
|
DateTimeFormatter
|
||||||
|
.ofPattern("dd/MMM/uuuu:HH:mm:ss xx")
|
||||||
|
.withLocale(Locale.UK)
|
||||||
|
.withZone(ZoneId.systemDefault())
|
||||||
|
}
|
@ -1,48 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (c) 2019 Evgeniy Alekseev.
|
|
||||||
*
|
|
||||||
* This file is part of ffxivbis
|
|
||||||
* (see https://github.com/arcan1s/ffxivbis).
|
|
||||||
*
|
|
||||||
* License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause
|
|
||||||
*/
|
|
||||||
package me.arcanis.ffxivbis.http
|
|
||||||
|
|
||||||
import akka.actor.typed.scaladsl.AskPattern.Askable
|
|
||||||
import akka.actor.typed.{ActorRef, Scheduler}
|
|
||||||
import akka.util.Timeout
|
|
||||||
import me.arcanis.ffxivbis.http.api.v1.json.ApiAction
|
|
||||||
import me.arcanis.ffxivbis.messages.{AddPieceTo, GetLoot, Message, RemovePieceFrom, SuggestLoot}
|
|
||||||
import me.arcanis.ffxivbis.models.{Piece, Player, PlayerId, PlayerIdWithCounters}
|
|
||||||
|
|
||||||
import scala.concurrent.{ExecutionContext, Future}
|
|
||||||
|
|
||||||
trait LootHelper {
|
|
||||||
|
|
||||||
def storage: ActorRef[Message]
|
|
||||||
|
|
||||||
def addPieceLoot(playerId: PlayerId, piece: Piece, isFreeLoot: Boolean)
|
|
||||||
(implicit timeout: Timeout, scheduler: Scheduler): Future[Unit] =
|
|
||||||
storage.ask(
|
|
||||||
AddPieceTo(playerId, piece, isFreeLoot, _))
|
|
||||||
|
|
||||||
def doModifyLoot(action: ApiAction.Value, playerId: PlayerId, piece: Piece, maybeFree: Option[Boolean])
|
|
||||||
(implicit timeout: Timeout, scheduler: Scheduler): 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")
|
|
||||||
}
|
|
||||||
|
|
||||||
def loot(partyId: String, playerId: Option[PlayerId])
|
|
||||||
(implicit timeout: Timeout, scheduler: Scheduler): 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 suggestPiece(partyId: String, piece: Piece)
|
|
||||||
(implicit executionContext: ExecutionContext, timeout: Timeout, scheduler: Scheduler): Future[Seq[PlayerIdWithCounters]] =
|
|
||||||
storage.ask(SuggestLoot(partyId, piece, _)).map(_.result)
|
|
||||||
}
|
|
@ -1,63 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (c) 2019 Evgeniy Alekseev.
|
|
||||||
*
|
|
||||||
* This file is part of ffxivbis
|
|
||||||
* (see https://github.com/arcan1s/ffxivbis).
|
|
||||||
*
|
|
||||||
* License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause
|
|
||||||
*/
|
|
||||||
package me.arcanis.ffxivbis.http
|
|
||||||
|
|
||||||
import akka.actor.typed.{ActorRef, Scheduler}
|
|
||||||
import akka.actor.typed.scaladsl.AskPattern.Askable
|
|
||||||
import akka.util.Timeout
|
|
||||||
import me.arcanis.ffxivbis.http.api.v1.json.ApiAction
|
|
||||||
import me.arcanis.ffxivbis.messages.{AddPieceToBis, AddPlayer, GetParty, GetPartyDescription, GetPlayer, Message, RemovePlayer, UpdateParty}
|
|
||||||
import me.arcanis.ffxivbis.models.{PartyDescription, Player, PlayerId}
|
|
||||||
|
|
||||||
import scala.concurrent.{ExecutionContext, Future}
|
|
||||||
|
|
||||||
trait PlayerHelper extends BisProviderHelper {
|
|
||||||
|
|
||||||
def storage: ActorRef[Message]
|
|
||||||
|
|
||||||
def addPlayer(player: Player)
|
|
||||||
(implicit executionContext: ExecutionContext, timeout: Timeout, scheduler: Scheduler): Future[Unit] =
|
|
||||||
storage.ask(ref => AddPlayer(player, ref)).map { res =>
|
|
||||||
player.link match {
|
|
||||||
case Some(link) =>
|
|
||||||
downloadBiS(link, player.job).map { bis =>
|
|
||||||
bis.pieces.map(piece => storage.ask(AddPieceToBis(player.playerId, piece, _)))
|
|
||||||
}.map(_ => res)
|
|
||||||
case None => Future.successful(res)
|
|
||||||
}
|
|
||||||
}.flatten
|
|
||||||
|
|
||||||
def doModifyPlayer(action: ApiAction.Value, player: Player)
|
|
||||||
(implicit executionContext: ExecutionContext, timeout: Timeout, scheduler: Scheduler): Future[Unit] =
|
|
||||||
action match {
|
|
||||||
case ApiAction.add => addPlayer(player)
|
|
||||||
case ApiAction.remove => removePlayer(player.playerId)
|
|
||||||
}
|
|
||||||
|
|
||||||
def getPartyDescription(partyId: String)
|
|
||||||
(implicit timeout: Timeout, scheduler: Scheduler): Future[PartyDescription] =
|
|
||||||
storage.ask(GetPartyDescription(partyId, _))
|
|
||||||
|
|
||||||
def getPlayers(partyId: String, maybePlayerId: Option[PlayerId])
|
|
||||||
(implicit executionContext: ExecutionContext, timeout: Timeout, scheduler: Scheduler): Future[Seq[Player]] =
|
|
||||||
maybePlayerId match {
|
|
||||||
case Some(playerId) =>
|
|
||||||
storage.ask(GetPlayer(playerId, _)).map(_.toSeq)
|
|
||||||
case None =>
|
|
||||||
storage.ask(GetParty(partyId, _)).map(_.players.values.toSeq)
|
|
||||||
}
|
|
||||||
|
|
||||||
def removePlayer(playerId: PlayerId)
|
|
||||||
(implicit timeout: Timeout, scheduler: Scheduler): Future[Unit] =
|
|
||||||
storage.ask(RemovePlayer(playerId, _))
|
|
||||||
|
|
||||||
def updateDescription(partyDescription: PartyDescription)
|
|
||||||
(implicit executionContext: ExecutionContext, timeout: Timeout, scheduler: Scheduler): Future[Unit] =
|
|
||||||
storage.ask(UpdateParty(partyDescription, _))
|
|
||||||
}
|
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (c) 2019 Evgeniy Alekseev.
|
* Copyright (c) 2019-2022 Evgeniy Alekseev.
|
||||||
*
|
*
|
||||||
* This file is part of ffxivbis
|
* This file is part of ffxivbis
|
||||||
* (see https://github.com/arcan1s/ffxivbis).
|
* (see https://github.com/arcan1s/ffxivbis).
|
||||||
@ -8,68 +8,56 @@
|
|||||||
*/
|
*/
|
||||||
package me.arcanis.ffxivbis.http
|
package me.arcanis.ffxivbis.http
|
||||||
|
|
||||||
import java.time.Instant
|
|
||||||
|
|
||||||
import akka.actor.typed.{ActorRef, ActorSystem, Scheduler}
|
import akka.actor.typed.{ActorRef, ActorSystem, Scheduler}
|
||||||
import akka.http.scaladsl.server.Directives._
|
import akka.http.scaladsl.server.Directives._
|
||||||
import akka.http.scaladsl.server._
|
import akka.http.scaladsl.server._
|
||||||
import akka.util.Timeout
|
import akka.util.Timeout
|
||||||
import com.typesafe.scalalogging.{Logger, StrictLogging}
|
import ch.megard.akka.http.cors.scaladsl.CorsDirectives.cors
|
||||||
|
import com.typesafe.scalalogging.StrictLogging
|
||||||
import me.arcanis.ffxivbis.http.api.v1.RootApiV1Endpoint
|
import me.arcanis.ffxivbis.http.api.v1.RootApiV1Endpoint
|
||||||
import me.arcanis.ffxivbis.http.view.RootView
|
import me.arcanis.ffxivbis.http.view.RootView
|
||||||
import me.arcanis.ffxivbis.messages.{BiSProviderMessage, Message}
|
import me.arcanis.ffxivbis.messages.{BiSProviderMessage, Message}
|
||||||
|
|
||||||
class RootEndpoint(system: ActorSystem[Nothing],
|
class RootEndpoint(system: ActorSystem[Nothing], storage: ActorRef[Message], provider: ActorRef[BiSProviderMessage])
|
||||||
storage: ActorRef[Message],
|
extends StrictLogging
|
||||||
provider: ActorRef[BiSProviderMessage])
|
with HttpLog {
|
||||||
extends StrictLogging {
|
|
||||||
import me.arcanis.ffxivbis.utils.Implicits._
|
import me.arcanis.ffxivbis.utils.Implicits._
|
||||||
|
|
||||||
private val config = system.settings.config
|
private val config = system.settings.config
|
||||||
|
|
||||||
implicit val scheduler: Scheduler = system.scheduler
|
implicit val scheduler: Scheduler = system.scheduler
|
||||||
implicit val timeout: Timeout =
|
implicit val timeout: Timeout = config.getTimeout("me.arcanis.ffxivbis.settings.request-timeout")
|
||||||
config.getDuration("me.arcanis.ffxivbis.settings.request-timeout")
|
|
||||||
|
|
||||||
private val rootApiV1Endpoint: RootApiV1Endpoint = new RootApiV1Endpoint(storage, provider, config)
|
private val auth = AuthorizationProvider(config, storage)
|
||||||
private val rootView: RootView = new RootView(storage, provider)
|
|
||||||
private val swagger: Swagger = new Swagger(config)
|
|
||||||
private val httpLogger = Logger("http")
|
|
||||||
|
|
||||||
private val withHttpLog: Directive0 =
|
private val rootApiV1Endpoint = new RootApiV1Endpoint(storage, auth, provider, config)
|
||||||
extractRequestContext.flatMap { context =>
|
private val rootView = new RootView(auth)
|
||||||
val start = Instant.now.toEpochMilli
|
private val swagger = new Swagger(config)
|
||||||
mapResponse { response =>
|
|
||||||
val time = (Instant.now.toEpochMilli - start) / 1000.0
|
|
||||||
httpLogger.debug(s"""- - [${Instant.now}] "${context.request.method.name()} ${context.request.uri.path}" ${response.status.intValue()} ${response.entity.getContentLengthOption.getAsLong} $time""")
|
|
||||||
response
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
def route: Route =
|
def routes: Route =
|
||||||
withHttpLog {
|
withHttpLog {
|
||||||
apiRoute ~ htmlRoute ~ swagger.routes ~ swaggerUIRoute
|
ignoreTrailingSlash {
|
||||||
}
|
cors() {
|
||||||
|
apiRoutes ~ htmlRoutes ~ swagger.routes ~ swaggerUIRoutes
|
||||||
private def apiRoute: Route =
|
|
||||||
ignoreTrailingSlash {
|
|
||||||
pathPrefix("api") {
|
|
||||||
pathPrefix(Segment) {
|
|
||||||
case "v1" => rootApiV1Endpoint.route
|
|
||||||
case _ => reject
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private def htmlRoute: Route =
|
private def apiRoutes: Route =
|
||||||
ignoreTrailingSlash {
|
pathPrefix("api") {
|
||||||
pathPrefix("static") {
|
pathPrefix(Segment) {
|
||||||
getFromResourceDirectory("static")
|
case "v1" => rootApiV1Endpoint.routes
|
||||||
} ~ rootView.route
|
case _ => reject
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private def swaggerUIRoute: Route =
|
private def htmlRoutes: Route =
|
||||||
path("swagger") {
|
pathPrefix("static") {
|
||||||
getFromResource("swagger/index.html")
|
getFromResourceDirectory("static")
|
||||||
} ~ getFromResourceDirectory("swagger")
|
} ~ rootView.routes
|
||||||
|
|
||||||
|
private def swaggerUIRoutes: Route =
|
||||||
|
path("api-docs") {
|
||||||
|
getFromResource("html/api.html")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (c) 2019 Evgeniy Alekseev.
|
* Copyright (c) 2019-2022 Evgeniy Alekseev.
|
||||||
*
|
*
|
||||||
* This file is part of ffxivbis
|
* This file is part of ffxivbis
|
||||||
* (see https://github.com/arcan1s/ffxivbis).
|
* (see https://github.com/arcan1s/ffxivbis).
|
||||||
@ -14,13 +14,18 @@ import com.typesafe.config.Config
|
|||||||
import io.swagger.v3.oas.models.security.SecurityScheme
|
import io.swagger.v3.oas.models.security.SecurityScheme
|
||||||
|
|
||||||
import scala.io.Source
|
import scala.io.Source
|
||||||
|
import scala.jdk.CollectionConverters._
|
||||||
|
|
||||||
class Swagger(config: Config) extends SwaggerHttpService {
|
class Swagger(config: Config) extends SwaggerHttpService {
|
||||||
|
|
||||||
override val apiClasses: Set[Class[_]] = Set(
|
override val apiClasses: Set[Class[_]] = Set(
|
||||||
classOf[api.v1.BiSEndpoint], classOf[api.v1.LootEndpoint],
|
classOf[api.v1.BiSEndpoint],
|
||||||
classOf[api.v1.PartyEndpoint], classOf[api.v1.PlayerEndpoint],
|
classOf[api.v1.LootEndpoint],
|
||||||
classOf[api.v1.TypesEndpoint], classOf[api.v1.UserEndpoint]
|
classOf[api.v1.PartyEndpoint],
|
||||||
|
classOf[api.v1.PlayerEndpoint],
|
||||||
|
classOf[api.v1.StatusEndpoint],
|
||||||
|
classOf[api.v1.TypesEndpoint],
|
||||||
|
classOf[api.v1.UserEndpoint]
|
||||||
)
|
)
|
||||||
|
|
||||||
override val info: Info = Info(
|
override val info: Info = Info(
|
||||||
@ -32,15 +37,17 @@ class Swagger(config: Config) extends SwaggerHttpService {
|
|||||||
|
|
||||||
override val host: String =
|
override val host: String =
|
||||||
if (config.hasPath("me.arcanis.ffxivbis.web.hostname")) config.getString("me.arcanis.ffxivbis.web.hostname")
|
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")}"
|
||||||
|
|
||||||
|
override val schemes: List[String] = config.getStringList("me.arcanis.ffxivbis.web.schemes").asScala.toList
|
||||||
|
|
||||||
private val basicAuth = new SecurityScheme()
|
private val basicAuth = new SecurityScheme()
|
||||||
.description("basic http auth")
|
.description("basic http auth")
|
||||||
.`type`(SecurityScheme.Type.HTTP)
|
.`type`(SecurityScheme.Type.HTTP)
|
||||||
.in(SecurityScheme.In.HEADER)
|
.in(SecurityScheme.In.HEADER)
|
||||||
.scheme("bearer")
|
.scheme("basic")
|
||||||
override val securitySchemes: Map[String, SecurityScheme] = Map("basic auth" -> basicAuth)
|
override val securitySchemes: Map[String, SecurityScheme] = Map("basic" -> basicAuth)
|
||||||
|
|
||||||
override val unwantedDefinitions: Seq[String] =
|
override val unwantedDefinitions: Seq[String] =
|
||||||
Seq("Function1", "Function1RequestContextFutureRouteResult")
|
Seq("Function1", "Function1RequestContextFutureRouteResult", "SeqLootModel", "SeqPieceModel")
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,16 @@
|
|||||||
|
package me.arcanis.ffxivbis.http
|
||||||
|
|
||||||
|
import scala.collection.immutable.HashSet
|
||||||
|
|
||||||
|
trait ValidatorHelper {
|
||||||
|
|
||||||
|
def isValidString(string: String): Boolean = string.nonEmpty && string.forall(isValidSymbol)
|
||||||
|
|
||||||
|
def isValidSymbol(char: Char): Boolean =
|
||||||
|
char.isLetterOrDigit || ValidatorHelper.VALID_CHARACTERS.contains(char)
|
||||||
|
}
|
||||||
|
|
||||||
|
object ValidatorHelper {
|
||||||
|
|
||||||
|
final val VALID_CHARACTERS = HashSet.from("!@#$%^&*()-_=+;:',./? ")
|
||||||
|
}
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (c) 2019 Evgeniy Alekseev.
|
* Copyright (c) 2019-2022 Evgeniy Alekseev.
|
||||||
*
|
*
|
||||||
* This file is part of ffxivbis
|
* This file is part of ffxivbis
|
||||||
* (see https://github.com/arcan1s/ffxivbis).
|
* (see https://github.com/arcan1s/ffxivbis).
|
||||||
@ -19,43 +19,72 @@ import io.swagger.v3.oas.annotations.parameters.RequestBody
|
|||||||
import io.swagger.v3.oas.annotations.responses.ApiResponse
|
import io.swagger.v3.oas.annotations.responses.ApiResponse
|
||||||
import io.swagger.v3.oas.annotations.security.SecurityRequirement
|
import io.swagger.v3.oas.annotations.security.SecurityRequirement
|
||||||
import io.swagger.v3.oas.annotations.{Operation, Parameter}
|
import io.swagger.v3.oas.annotations.{Operation, Parameter}
|
||||||
import javax.ws.rs._
|
import jakarta.ws.rs._
|
||||||
import me.arcanis.ffxivbis.http.api.v1.json._
|
import me.arcanis.ffxivbis.http.api.v1.json._
|
||||||
import me.arcanis.ffxivbis.http.{Authorization, BiSHelper}
|
import me.arcanis.ffxivbis.http.helpers.BiSHelper
|
||||||
|
import me.arcanis.ffxivbis.http.{Authorization, AuthorizationProvider}
|
||||||
import me.arcanis.ffxivbis.messages.{BiSProviderMessage, Message}
|
import me.arcanis.ffxivbis.messages.{BiSProviderMessage, Message}
|
||||||
import me.arcanis.ffxivbis.models.PlayerId
|
import me.arcanis.ffxivbis.models.PlayerId
|
||||||
|
|
||||||
import scala.util.{Failure, Success}
|
import scala.util.{Failure, Success}
|
||||||
|
|
||||||
@Path("api/v1")
|
@Path("/api/v1")
|
||||||
class BiSEndpoint(override val storage: ActorRef[Message],
|
class BiSEndpoint(
|
||||||
override val provider: ActorRef[BiSProviderMessage])
|
override val storage: ActorRef[Message],
|
||||||
(implicit timeout: Timeout, scheduler: Scheduler)
|
override val provider: ActorRef[BiSProviderMessage],
|
||||||
extends BiSHelper with Authorization with JsonSupport {
|
override val auth: AuthorizationProvider
|
||||||
|
)(implicit
|
||||||
|
timeout: Timeout,
|
||||||
|
scheduler: Scheduler
|
||||||
|
) extends BiSHelper
|
||||||
|
with Authorization
|
||||||
|
with JsonSupport {
|
||||||
|
|
||||||
def route: Route = createBiS ~ getBiS ~ modifyBiS
|
def routes: Route = createBiS ~ getBiS ~ modifyBiS
|
||||||
|
|
||||||
@PUT
|
@PUT
|
||||||
@Path("party/{partyId}/bis")
|
@Path("party/{partyId}/bis")
|
||||||
@Consumes(value = Array("application/json"))
|
@Consumes(value = Array("application/json"))
|
||||||
@Operation(summary = "create best in slot", description = "Create the best in slot set",
|
@Operation(
|
||||||
|
summary = "create best in slot",
|
||||||
|
description = "Create the best in slot set",
|
||||||
parameters = Array(
|
parameters = Array(
|
||||||
new Parameter(name = "partyId", in = ParameterIn.PATH, description = "unique party ID", example = "abcdefgh"),
|
new Parameter(
|
||||||
|
name = "partyId",
|
||||||
|
in = ParameterIn.PATH,
|
||||||
|
description = "unique party ID",
|
||||||
|
example = "o3KicHQPW5b0JcOm5yI3"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
requestBody = new RequestBody(
|
||||||
|
description = "player best in slot description",
|
||||||
|
required = true,
|
||||||
|
content = Array(new Content(schema = new Schema(implementation = classOf[PlayerBiSLinkModel])))
|
||||||
),
|
),
|
||||||
requestBody = new RequestBody(description = "player best in slot description", required = true,
|
|
||||||
content = Array(new Content(schema = new Schema(implementation = classOf[PlayerBiSLinkResponse])))),
|
|
||||||
responses = Array(
|
responses = Array(
|
||||||
new ApiResponse(responseCode = "201", description = "Best in slot set has been created"),
|
new ApiResponse(responseCode = "201", description = "Best in slot set has been created"),
|
||||||
new ApiResponse(responseCode = "400", description = "Invalid parameters were supplied",
|
new ApiResponse(
|
||||||
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))),
|
responseCode = "400",
|
||||||
new ApiResponse(responseCode = "401", description = "Supplied authorization is invalid",
|
description = "Invalid parameters were supplied",
|
||||||
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))),
|
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel])))
|
||||||
new ApiResponse(responseCode = "403", description = "Access is forbidden",
|
),
|
||||||
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))),
|
new ApiResponse(
|
||||||
new ApiResponse(responseCode = "500", description = "Internal server error",
|
responseCode = "401",
|
||||||
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))),
|
description = "Supplied authorization is invalid",
|
||||||
|
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel])))
|
||||||
|
),
|
||||||
|
new ApiResponse(
|
||||||
|
responseCode = "403",
|
||||||
|
description = "Access is forbidden",
|
||||||
|
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel])))
|
||||||
|
),
|
||||||
|
new ApiResponse(
|
||||||
|
responseCode = "500",
|
||||||
|
description = "Internal server error",
|
||||||
|
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel])))
|
||||||
|
),
|
||||||
),
|
),
|
||||||
security = Array(new SecurityRequirement(name = "basic auth", scopes = Array("post"))),
|
security = Array(new SecurityRequirement(name = "basic", scopes = Array("post"))),
|
||||||
tags = Array("best in slot"),
|
tags = Array("best in slot"),
|
||||||
)
|
)
|
||||||
def createBiS: Route =
|
def createBiS: Route =
|
||||||
@ -63,11 +92,10 @@ class BiSEndpoint(override val storage: ActorRef[Message],
|
|||||||
extractExecutionContext { implicit executionContext =>
|
extractExecutionContext { implicit executionContext =>
|
||||||
authenticateBasicBCrypt(s"party $partyId", authPost(partyId)) { _ =>
|
authenticateBasicBCrypt(s"party $partyId", authPost(partyId)) { _ =>
|
||||||
put {
|
put {
|
||||||
entity(as[PlayerBiSLinkResponse]) { bisLink =>
|
entity(as[PlayerBiSLinkModel]) { bisLink =>
|
||||||
val playerId = bisLink.playerId.withPartyId(partyId)
|
val playerId = bisLink.playerId.withPartyId(partyId)
|
||||||
onComplete(putBiS(playerId, bisLink.link)) {
|
onSuccess(putBiS(playerId, bisLink.link)) {
|
||||||
case Success(_) => complete(StatusCodes.Created, HttpEntity.Empty)
|
complete(StatusCodes.Created, HttpEntity.Empty)
|
||||||
case Failure(exception) => throw exception
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -78,25 +106,51 @@ class BiSEndpoint(override val storage: ActorRef[Message],
|
|||||||
@GET
|
@GET
|
||||||
@Path("party/{partyId}/bis")
|
@Path("party/{partyId}/bis")
|
||||||
@Produces(value = Array("application/json"))
|
@Produces(value = Array("application/json"))
|
||||||
@Operation(summary = "get best in slot", description = "Return the best in slot items",
|
@Operation(
|
||||||
|
summary = "get best in slot",
|
||||||
|
description = "Return the best in slot items",
|
||||||
parameters = Array(
|
parameters = Array(
|
||||||
new Parameter(name = "partyId", in = ParameterIn.PATH, description = "unique party ID", example = "abcdefgh"),
|
new Parameter(
|
||||||
new Parameter(name = "nick", in = ParameterIn.QUERY, description = "player nick name to filter", example = "Siuan Sanche"),
|
name = "partyId",
|
||||||
|
in = ParameterIn.PATH,
|
||||||
|
description = "unique party ID",
|
||||||
|
example = "o3KicHQPW5b0JcOm5yI3"
|
||||||
|
),
|
||||||
|
new Parameter(
|
||||||
|
name = "nick",
|
||||||
|
in = ParameterIn.QUERY,
|
||||||
|
description = "player nick name to filter",
|
||||||
|
example = "Siuan Sanche"
|
||||||
|
),
|
||||||
new Parameter(name = "job", in = ParameterIn.QUERY, description = "player job to filter", example = "DNC"),
|
new Parameter(name = "job", in = ParameterIn.QUERY, description = "player job to filter", example = "DNC"),
|
||||||
),
|
),
|
||||||
responses = Array(
|
responses = Array(
|
||||||
new ApiResponse(responseCode = "200", description = "Best in slot",
|
new ApiResponse(
|
||||||
content = Array(new Content(
|
responseCode = "200",
|
||||||
array = new ArraySchema(schema = new Schema(implementation = classOf[PlayerResponse]))
|
description = "Best in slot",
|
||||||
))),
|
content = Array(
|
||||||
new ApiResponse(responseCode = "401", description = "Supplied authorization is invalid",
|
new Content(
|
||||||
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))),
|
array = new ArraySchema(schema = new Schema(implementation = classOf[PlayerModel]))
|
||||||
new ApiResponse(responseCode = "403", description = "Access is forbidden",
|
)
|
||||||
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))),
|
)
|
||||||
new ApiResponse(responseCode = "500", description = "Internal server error",
|
),
|
||||||
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))),
|
new ApiResponse(
|
||||||
|
responseCode = "401",
|
||||||
|
description = "Supplied authorization is invalid",
|
||||||
|
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel])))
|
||||||
|
),
|
||||||
|
new ApiResponse(
|
||||||
|
responseCode = "403",
|
||||||
|
description = "Access is forbidden",
|
||||||
|
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel])))
|
||||||
|
),
|
||||||
|
new ApiResponse(
|
||||||
|
responseCode = "500",
|
||||||
|
description = "Internal server error",
|
||||||
|
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel])))
|
||||||
|
),
|
||||||
),
|
),
|
||||||
security = Array(new SecurityRequirement(name = "basic auth", scopes = Array("get"))),
|
security = Array(new SecurityRequirement(name = "basic", scopes = Array("get"))),
|
||||||
tags = Array("best in slot"),
|
tags = Array("best in slot"),
|
||||||
)
|
)
|
||||||
def getBiS: Route =
|
def getBiS: Route =
|
||||||
@ -106,9 +160,8 @@ class BiSEndpoint(override val storage: ActorRef[Message],
|
|||||||
get {
|
get {
|
||||||
parameters("nick".as[String].?, "job".as[String].?) { (maybeNick, maybeJob) =>
|
parameters("nick".as[String].?, "job".as[String].?) { (maybeNick, maybeJob) =>
|
||||||
val playerId = PlayerId(partyId, maybeNick, maybeJob)
|
val playerId = PlayerId(partyId, maybeNick, maybeJob)
|
||||||
onComplete(bis(partyId, playerId)) {
|
onSuccess(bis(partyId, playerId)) { response =>
|
||||||
case Success(response) => complete(response.map(PlayerResponse.fromPlayer))
|
complete(response.map(PlayerModel.fromPlayer))
|
||||||
case Failure(exception) => throw exception
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -120,24 +173,46 @@ class BiSEndpoint(override val storage: ActorRef[Message],
|
|||||||
@POST
|
@POST
|
||||||
@Path("party/{partyId}/bis")
|
@Path("party/{partyId}/bis")
|
||||||
@Consumes(value = Array("application/json"))
|
@Consumes(value = Array("application/json"))
|
||||||
@Operation(summary = "modify best in slot", description = "Add or remove an item from the best in slot",
|
@Operation(
|
||||||
|
summary = "modify best in slot",
|
||||||
|
description = "Add or remove an item from the best in slot",
|
||||||
parameters = Array(
|
parameters = Array(
|
||||||
new Parameter(name = "partyId", in = ParameterIn.PATH, description = "unique party ID", example = "abcdefgh"),
|
new Parameter(
|
||||||
|
name = "partyId",
|
||||||
|
in = ParameterIn.PATH,
|
||||||
|
description = "unique party ID",
|
||||||
|
example = "o3KicHQPW5b0JcOm5yI3"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
requestBody = new RequestBody(
|
||||||
|
description = "action and piece description",
|
||||||
|
required = true,
|
||||||
|
content = Array(new Content(schema = new Schema(implementation = classOf[PieceActionModel])))
|
||||||
),
|
),
|
||||||
requestBody = new RequestBody(description = "action and piece description", required = true,
|
|
||||||
content = Array(new Content(schema = new Schema(implementation = classOf[PieceActionResponse])))),
|
|
||||||
responses = Array(
|
responses = Array(
|
||||||
new ApiResponse(responseCode = "202", description = "Best in slot set has been modified"),
|
new ApiResponse(responseCode = "202", description = "Best in slot set has been modified"),
|
||||||
new ApiResponse(responseCode = "400", description = "Invalid parameters were supplied",
|
new ApiResponse(
|
||||||
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))),
|
responseCode = "400",
|
||||||
new ApiResponse(responseCode = "401", description = "Supplied authorization is invalid",
|
description = "Invalid parameters were supplied",
|
||||||
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))),
|
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel])))
|
||||||
new ApiResponse(responseCode = "403", description = "Access is forbidden",
|
),
|
||||||
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))),
|
new ApiResponse(
|
||||||
new ApiResponse(responseCode = "500", description = "Internal server error",
|
responseCode = "401",
|
||||||
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))),
|
description = "Supplied authorization is invalid",
|
||||||
|
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel])))
|
||||||
|
),
|
||||||
|
new ApiResponse(
|
||||||
|
responseCode = "403",
|
||||||
|
description = "Access is forbidden",
|
||||||
|
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel])))
|
||||||
|
),
|
||||||
|
new ApiResponse(
|
||||||
|
responseCode = "500",
|
||||||
|
description = "Internal server error",
|
||||||
|
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel])))
|
||||||
|
),
|
||||||
),
|
),
|
||||||
security = Array(new SecurityRequirement(name = "basic auth", scopes = Array("post"))),
|
security = Array(new SecurityRequirement(name = "basic", scopes = Array("post"))),
|
||||||
tags = Array("best in slot"),
|
tags = Array("best in slot"),
|
||||||
)
|
)
|
||||||
def modifyBiS: Route =
|
def modifyBiS: Route =
|
||||||
@ -145,11 +220,10 @@ class BiSEndpoint(override val storage: ActorRef[Message],
|
|||||||
extractExecutionContext { implicit executionContext =>
|
extractExecutionContext { implicit executionContext =>
|
||||||
authenticateBasicBCrypt(s"party $partyId", authPost(partyId)) { _ =>
|
authenticateBasicBCrypt(s"party $partyId", authPost(partyId)) { _ =>
|
||||||
post {
|
post {
|
||||||
entity(as[PieceActionResponse]) { action =>
|
entity(as[PieceActionModel]) { action =>
|
||||||
val playerId = action.playerId.withPartyId(partyId)
|
val playerId = action.playerId.withPartyId(partyId)
|
||||||
onComplete(doModifyBiS(action.action, playerId, action.piece.toPiece)) {
|
onSuccess(doModifyBiS(action.action, playerId, action.piece.toPiece)) {
|
||||||
case Success(_) => complete(StatusCodes.Accepted, HttpEntity.Empty)
|
complete(StatusCodes.Accepted, HttpEntity.Empty)
|
||||||
case Failure(exception) => throw exception
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (c) 2019 Evgeniy Alekseev.
|
* Copyright (c) 2019-2022 Evgeniy Alekseev.
|
||||||
*
|
*
|
||||||
* This file is part of ffxivbis
|
* This file is part of ffxivbis
|
||||||
* (see https://github.com/arcan1s/ffxivbis).
|
* (see https://github.com/arcan1s/ffxivbis).
|
||||||
@ -11,26 +11,46 @@ package me.arcanis.ffxivbis.http.api.v1
|
|||||||
import akka.http.scaladsl.model._
|
import akka.http.scaladsl.model._
|
||||||
import akka.http.scaladsl.server.Directives._
|
import akka.http.scaladsl.server.Directives._
|
||||||
import akka.http.scaladsl.server._
|
import akka.http.scaladsl.server._
|
||||||
|
import ch.megard.akka.http.cors.scaladsl.CorsDirectives.corsRejectionHandler
|
||||||
import com.typesafe.scalalogging.StrictLogging
|
import com.typesafe.scalalogging.StrictLogging
|
||||||
import me.arcanis.ffxivbis.http.api.v1.json._
|
import me.arcanis.ffxivbis.http.api.v1.json._
|
||||||
import spray.json._
|
import spray.json._
|
||||||
|
|
||||||
|
import scala.util.control.NonFatal
|
||||||
|
|
||||||
trait HttpHandler extends StrictLogging { this: JsonSupport =>
|
trait HttpHandler extends StrictLogging { this: JsonSupport =>
|
||||||
|
|
||||||
implicit def exceptionHandler: ExceptionHandler = ExceptionHandler {
|
def exceptionHandler: ExceptionHandler = ExceptionHandler {
|
||||||
case ex: IllegalArgumentException =>
|
case exception: IllegalArgumentException =>
|
||||||
complete(StatusCodes.BadRequest, ErrorResponse(ex.getMessage))
|
complete(StatusCodes.BadRequest, ErrorModel(exception.getMessage))
|
||||||
|
|
||||||
case other: Exception =>
|
case NonFatal(other) =>
|
||||||
logger.error("exception during request completion", other)
|
logger.error("exception during request completion", other)
|
||||||
complete(StatusCodes.InternalServerError, ErrorResponse("unknown server error"))
|
complete(StatusCodes.InternalServerError, ErrorModel("unknown server error"))
|
||||||
}
|
}
|
||||||
|
|
||||||
implicit def rejectionHandler: RejectionHandler =
|
def rejectionHandler: RejectionHandler =
|
||||||
RejectionHandler.default
|
RejectionHandler
|
||||||
|
.newBuilder()
|
||||||
|
.handleAll[MethodRejection] { rejections =>
|
||||||
|
val (methods, names) = rejections.map(r => r.supported -> r.supported.name).unzip
|
||||||
|
|
||||||
|
respondWithHeader(headers.Allow(methods)) {
|
||||||
|
options {
|
||||||
|
complete(StatusCodes.OK, HttpEntity.Empty)
|
||||||
|
} ~
|
||||||
|
complete(
|
||||||
|
StatusCodes.MethodNotAllowed,
|
||||||
|
s"HTTP method not allowed, supported methods: ${names.mkString(", ")}"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.result()
|
||||||
|
.withFallback(corsRejectionHandler)
|
||||||
|
.seal
|
||||||
.mapRejectionResponse {
|
.mapRejectionResponse {
|
||||||
case response @ HttpResponse(_, _, entity: HttpEntity.Strict, _) =>
|
case response @ HttpResponse(_, _, entity: HttpEntity.Strict, _) if entity.data.nonEmpty =>
|
||||||
val message = ErrorResponse(entity.data.utf8String).toJson
|
val message = ErrorModel(entity.data.utf8String).toJson
|
||||||
response.withEntity(HttpEntity(ContentTypes.`application/json`, message.compactPrint))
|
response.withEntity(HttpEntity(ContentTypes.`application/json`, message.compactPrint))
|
||||||
case other => other
|
case other => other
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (c) 2019 Evgeniy Alekseev.
|
* Copyright (c) 2019-2022 Evgeniy Alekseev.
|
||||||
*
|
*
|
||||||
* This file is part of ffxivbis
|
* This file is part of ffxivbis
|
||||||
* (see https://github.com/arcan1s/ffxivbis).
|
* (see https://github.com/arcan1s/ffxivbis).
|
||||||
@ -19,43 +19,74 @@ import io.swagger.v3.oas.annotations.parameters.RequestBody
|
|||||||
import io.swagger.v3.oas.annotations.responses.ApiResponse
|
import io.swagger.v3.oas.annotations.responses.ApiResponse
|
||||||
import io.swagger.v3.oas.annotations.security.SecurityRequirement
|
import io.swagger.v3.oas.annotations.security.SecurityRequirement
|
||||||
import io.swagger.v3.oas.annotations.{Operation, Parameter}
|
import io.swagger.v3.oas.annotations.{Operation, Parameter}
|
||||||
import javax.ws.rs._
|
import jakarta.ws.rs._
|
||||||
import me.arcanis.ffxivbis.http.api.v1.json._
|
import me.arcanis.ffxivbis.http.api.v1.json._
|
||||||
import me.arcanis.ffxivbis.http.{Authorization, LootHelper}
|
import me.arcanis.ffxivbis.http.helpers.LootHelper
|
||||||
|
import me.arcanis.ffxivbis.http.{Authorization, AuthorizationProvider}
|
||||||
import me.arcanis.ffxivbis.messages.Message
|
import me.arcanis.ffxivbis.messages.Message
|
||||||
import me.arcanis.ffxivbis.models.PlayerId
|
import me.arcanis.ffxivbis.models.PlayerId
|
||||||
|
|
||||||
import scala.util.{Failure, Success}
|
import scala.util.{Failure, Success}
|
||||||
|
|
||||||
@Path("api/v1")
|
@Path("/api/v1")
|
||||||
class LootEndpoint(override val storage: ActorRef[Message])
|
class LootEndpoint(override val storage: ActorRef[Message], override val auth: AuthorizationProvider)(implicit
|
||||||
(implicit timeout: Timeout, scheduler: Scheduler)
|
timeout: Timeout,
|
||||||
extends LootHelper with Authorization with JsonSupport with HttpHandler {
|
scheduler: Scheduler
|
||||||
|
) extends LootHelper
|
||||||
|
with Authorization
|
||||||
|
with JsonSupport
|
||||||
|
with HttpHandler {
|
||||||
|
|
||||||
def route: Route = getLoot ~ modifyLoot
|
def routes: Route = getLoot ~ modifyLoot ~ suggestLoot
|
||||||
|
|
||||||
@GET
|
@GET
|
||||||
@Path("party/{partyId}/loot")
|
@Path("party/{partyId}/loot")
|
||||||
@Produces(value = Array("application/json"))
|
@Produces(value = Array("application/json"))
|
||||||
@Operation(summary = "get loot list", description = "Return the looted items",
|
@Operation(
|
||||||
|
summary = "get loot list",
|
||||||
|
description = "Return the looted items",
|
||||||
parameters = Array(
|
parameters = Array(
|
||||||
new Parameter(name = "partyId", in = ParameterIn.PATH, description = "unique party ID", example = "abcdefgh"),
|
new Parameter(
|
||||||
new Parameter(name = "nick", in = ParameterIn.QUERY, description = "player nick name to filter", example = "Siuan Sanche"),
|
name = "partyId",
|
||||||
|
in = ParameterIn.PATH,
|
||||||
|
description = "unique party ID",
|
||||||
|
example = "o3KicHQPW5b0JcOm5yI3"
|
||||||
|
),
|
||||||
|
new Parameter(
|
||||||
|
name = "nick",
|
||||||
|
in = ParameterIn.QUERY,
|
||||||
|
description = "player nick name to filter",
|
||||||
|
example = "Siuan Sanche"
|
||||||
|
),
|
||||||
new Parameter(name = "job", in = ParameterIn.QUERY, description = "player job to filter", example = "DNC"),
|
new Parameter(name = "job", in = ParameterIn.QUERY, description = "player job to filter", example = "DNC"),
|
||||||
),
|
),
|
||||||
responses = Array(
|
responses = Array(
|
||||||
new ApiResponse(responseCode = "200", description = "Loot list",
|
new ApiResponse(
|
||||||
content = Array(new Content(
|
responseCode = "200",
|
||||||
array = new ArraySchema(schema = new Schema(implementation = classOf[PlayerResponse]))
|
description = "Loot list",
|
||||||
))),
|
content = Array(
|
||||||
new ApiResponse(responseCode = "401", description = "Supplied authorization is invalid",
|
new Content(
|
||||||
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))),
|
array = new ArraySchema(schema = new Schema(implementation = classOf[PlayerModel]))
|
||||||
new ApiResponse(responseCode = "403", description = "Access is forbidden",
|
)
|
||||||
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))),
|
)
|
||||||
new ApiResponse(responseCode = "500", description = "Internal server error",
|
),
|
||||||
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))),
|
new ApiResponse(
|
||||||
|
responseCode = "401",
|
||||||
|
description = "Supplied authorization is invalid",
|
||||||
|
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel])))
|
||||||
|
),
|
||||||
|
new ApiResponse(
|
||||||
|
responseCode = "403",
|
||||||
|
description = "Access is forbidden",
|
||||||
|
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel])))
|
||||||
|
),
|
||||||
|
new ApiResponse(
|
||||||
|
responseCode = "500",
|
||||||
|
description = "Internal server error",
|
||||||
|
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel])))
|
||||||
|
),
|
||||||
),
|
),
|
||||||
security = Array(new SecurityRequirement(name = "basic auth", scopes = Array("get"))),
|
security = Array(new SecurityRequirement(name = "basic", scopes = Array("get"))),
|
||||||
tags = Array("loot"),
|
tags = Array("loot"),
|
||||||
)
|
)
|
||||||
def getLoot: Route =
|
def getLoot: Route =
|
||||||
@ -65,9 +96,8 @@ class LootEndpoint(override val storage: ActorRef[Message])
|
|||||||
get {
|
get {
|
||||||
parameters("nick".as[String].?, "job".as[String].?) { (maybeNick, maybeJob) =>
|
parameters("nick".as[String].?, "job".as[String].?) { (maybeNick, maybeJob) =>
|
||||||
val playerId = PlayerId(partyId, maybeNick, maybeJob)
|
val playerId = PlayerId(partyId, maybeNick, maybeJob)
|
||||||
onComplete(loot(partyId, playerId)) {
|
onSuccess(loot(partyId, playerId)) { response =>
|
||||||
case Success(response) => complete(response.map(PlayerResponse.fromPlayer))
|
complete(response.map(PlayerModel.fromPlayer))
|
||||||
case Failure(exception) => throw exception
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -78,24 +108,46 @@ class LootEndpoint(override val storage: ActorRef[Message])
|
|||||||
@POST
|
@POST
|
||||||
@Consumes(value = Array("application/json"))
|
@Consumes(value = Array("application/json"))
|
||||||
@Path("party/{partyId}/loot")
|
@Path("party/{partyId}/loot")
|
||||||
@Operation(summary = "modify loot list", description = "Add or remove an item from the loot list",
|
@Operation(
|
||||||
|
summary = "modify loot list",
|
||||||
|
description = "Add or remove an item from the loot list",
|
||||||
parameters = Array(
|
parameters = Array(
|
||||||
new Parameter(name = "partyId", in = ParameterIn.PATH, description = "unique party ID", example = "abcdefgh"),
|
new Parameter(
|
||||||
|
name = "partyId",
|
||||||
|
in = ParameterIn.PATH,
|
||||||
|
description = "unique party ID",
|
||||||
|
example = "o3KicHQPW5b0JcOm5yI3"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
requestBody = new RequestBody(
|
||||||
|
description = "action and piece description",
|
||||||
|
required = true,
|
||||||
|
content = Array(new Content(schema = new Schema(implementation = classOf[PieceActionModel])))
|
||||||
),
|
),
|
||||||
requestBody = new RequestBody(description = "action and piece description", required = true,
|
|
||||||
content = Array(new Content(schema = new Schema(implementation = classOf[PieceActionResponse])))),
|
|
||||||
responses = Array(
|
responses = Array(
|
||||||
new ApiResponse(responseCode = "202", description = "Loot list has been modified"),
|
new ApiResponse(responseCode = "202", description = "Loot list has been modified"),
|
||||||
new ApiResponse(responseCode = "400", description = "Invalid parameters were supplied",
|
new ApiResponse(
|
||||||
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))),
|
responseCode = "400",
|
||||||
new ApiResponse(responseCode = "401", description = "Supplied authorization is invalid",
|
description = "Invalid parameters were supplied",
|
||||||
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))),
|
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel])))
|
||||||
new ApiResponse(responseCode = "403", description = "Access is forbidden",
|
),
|
||||||
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))),
|
new ApiResponse(
|
||||||
new ApiResponse(responseCode = "500", description = "Internal server error",
|
responseCode = "401",
|
||||||
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))),
|
description = "Supplied authorization is invalid",
|
||||||
|
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel])))
|
||||||
|
),
|
||||||
|
new ApiResponse(
|
||||||
|
responseCode = "403",
|
||||||
|
description = "Access is forbidden",
|
||||||
|
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel])))
|
||||||
|
),
|
||||||
|
new ApiResponse(
|
||||||
|
responseCode = "500",
|
||||||
|
description = "Internal server error",
|
||||||
|
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel])))
|
||||||
|
),
|
||||||
),
|
),
|
||||||
security = Array(new SecurityRequirement(name = "basic auth", scopes = Array("post"))),
|
security = Array(new SecurityRequirement(name = "basic", scopes = Array("post"))),
|
||||||
tags = Array("loot"),
|
tags = Array("loot"),
|
||||||
)
|
)
|
||||||
def modifyLoot: Route =
|
def modifyLoot: Route =
|
||||||
@ -103,11 +155,10 @@ class LootEndpoint(override val storage: ActorRef[Message])
|
|||||||
extractExecutionContext { implicit executionContext =>
|
extractExecutionContext { implicit executionContext =>
|
||||||
authenticateBasicBCrypt(s"party $partyId", authPost(partyId)) { _ =>
|
authenticateBasicBCrypt(s"party $partyId", authPost(partyId)) { _ =>
|
||||||
post {
|
post {
|
||||||
entity(as[PieceActionResponse]) { action =>
|
entity(as[PieceActionModel]) { action =>
|
||||||
val playerId = action.playerId.withPartyId(partyId)
|
val playerId = action.playerId.withPartyId(partyId)
|
||||||
onComplete(doModifyLoot(action.action, playerId, action.piece.toPiece, action.isFreeLoot)) {
|
onSuccess(doModifyLoot(action.action, playerId, action.piece.toPiece, action.isFreeLoot)) {
|
||||||
case Success(_) => complete(StatusCodes.Accepted, HttpEntity.Empty)
|
complete(StatusCodes.Accepted, HttpEntity.Empty)
|
||||||
case Failure(exception) => throw exception
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -119,27 +170,54 @@ class LootEndpoint(override val storage: ActorRef[Message])
|
|||||||
@Path("party/{partyId}/loot")
|
@Path("party/{partyId}/loot")
|
||||||
@Consumes(value = Array("application/json"))
|
@Consumes(value = Array("application/json"))
|
||||||
@Produces(value = Array("application/json"))
|
@Produces(value = Array("application/json"))
|
||||||
@Operation(summary = "suggest loot", description = "Suggest loot piece to party",
|
@Operation(
|
||||||
|
summary = "suggest loot",
|
||||||
|
description = "Suggest loot piece to party",
|
||||||
parameters = Array(
|
parameters = Array(
|
||||||
new Parameter(name = "partyId", in = ParameterIn.PATH, description = "unique party ID", example = "abcdefgh"),
|
new Parameter(
|
||||||
|
name = "partyId",
|
||||||
|
in = ParameterIn.PATH,
|
||||||
|
description = "unique party ID",
|
||||||
|
example = "o3KicHQPW5b0JcOm5yI3"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
requestBody = new RequestBody(
|
||||||
|
description = "piece description",
|
||||||
|
required = true,
|
||||||
|
content = Array(new Content(schema = new Schema(implementation = classOf[PieceModel])))
|
||||||
),
|
),
|
||||||
requestBody = new RequestBody(description = "piece description", required = true,
|
|
||||||
content = Array(new Content(schema = new Schema(implementation = classOf[PieceResponse])))),
|
|
||||||
responses = Array(
|
responses = Array(
|
||||||
new ApiResponse(responseCode = "200", description = "Players with counters ordered by priority to get this item",
|
new ApiResponse(
|
||||||
content = Array(new Content(
|
responseCode = "200",
|
||||||
array = new ArraySchema(schema = new Schema(implementation = classOf[PlayerIdWithCountersResponse])),
|
description = "Players with counters ordered by priority to get this item",
|
||||||
))),
|
content = Array(
|
||||||
new ApiResponse(responseCode = "400", description = "Invalid parameters were supplied",
|
new Content(
|
||||||
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))),
|
array = new ArraySchema(schema = new Schema(implementation = classOf[PlayerIdWithCountersModel])),
|
||||||
new ApiResponse(responseCode = "401", description = "Supplied authorization is invalid",
|
)
|
||||||
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))),
|
)
|
||||||
new ApiResponse(responseCode = "403", description = "Access is forbidden",
|
),
|
||||||
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))),
|
new ApiResponse(
|
||||||
new ApiResponse(responseCode = "500", description = "Internal server error",
|
responseCode = "400",
|
||||||
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))),
|
description = "Invalid parameters were supplied",
|
||||||
|
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel])))
|
||||||
|
),
|
||||||
|
new ApiResponse(
|
||||||
|
responseCode = "401",
|
||||||
|
description = "Supplied authorization is invalid",
|
||||||
|
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel])))
|
||||||
|
),
|
||||||
|
new ApiResponse(
|
||||||
|
responseCode = "403",
|
||||||
|
description = "Access is forbidden",
|
||||||
|
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel])))
|
||||||
|
),
|
||||||
|
new ApiResponse(
|
||||||
|
responseCode = "500",
|
||||||
|
description = "Internal server error",
|
||||||
|
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel])))
|
||||||
|
),
|
||||||
),
|
),
|
||||||
security = Array(new SecurityRequirement(name = "basic auth", scopes = Array("get"))),
|
security = Array(new SecurityRequirement(name = "basic", scopes = Array("get"))),
|
||||||
tags = Array("loot"),
|
tags = Array("loot"),
|
||||||
)
|
)
|
||||||
def suggestLoot: Route =
|
def suggestLoot: Route =
|
||||||
@ -147,10 +225,9 @@ class LootEndpoint(override val storage: ActorRef[Message])
|
|||||||
extractExecutionContext { implicit executionContext =>
|
extractExecutionContext { implicit executionContext =>
|
||||||
authenticateBasicBCrypt(s"party $partyId", authGet(partyId)) { _ =>
|
authenticateBasicBCrypt(s"party $partyId", authGet(partyId)) { _ =>
|
||||||
put {
|
put {
|
||||||
entity(as[PieceResponse]) { piece =>
|
entity(as[PieceModel]) { piece =>
|
||||||
onComplete(suggestPiece(partyId, piece.toPiece)) {
|
onSuccess(suggestPiece(partyId, piece.toPiece)) { response =>
|
||||||
case Success(response) => complete(response.map(PlayerIdWithCountersResponse.fromPlayerId))
|
complete(response.map(PlayerIdWithCountersModel.fromPlayerId))
|
||||||
case Failure(exception) => throw exception
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (c) 2020 Evgeniy Alekseev.
|
* Copyright (c) 2019-2022 Evgeniy Alekseev.
|
||||||
*
|
*
|
||||||
* This file is part of ffxivbis
|
* This file is part of ffxivbis
|
||||||
* (see https://github.com/arcan1s/ffxivbis).
|
* (see https://github.com/arcan1s/ffxivbis).
|
||||||
@ -19,39 +19,66 @@ import io.swagger.v3.oas.annotations.parameters.RequestBody
|
|||||||
import io.swagger.v3.oas.annotations.responses.ApiResponse
|
import io.swagger.v3.oas.annotations.responses.ApiResponse
|
||||||
import io.swagger.v3.oas.annotations.security.SecurityRequirement
|
import io.swagger.v3.oas.annotations.security.SecurityRequirement
|
||||||
import io.swagger.v3.oas.annotations.{Operation, Parameter}
|
import io.swagger.v3.oas.annotations.{Operation, Parameter}
|
||||||
import javax.ws.rs._
|
import jakarta.ws.rs._
|
||||||
import me.arcanis.ffxivbis.http.api.v1.json._
|
import me.arcanis.ffxivbis.http.api.v1.json._
|
||||||
import me.arcanis.ffxivbis.http.{Authorization, PlayerHelper}
|
import me.arcanis.ffxivbis.http.helpers.PlayerHelper
|
||||||
|
import me.arcanis.ffxivbis.http.{Authorization, AuthorizationProvider}
|
||||||
import me.arcanis.ffxivbis.messages.{BiSProviderMessage, Message}
|
import me.arcanis.ffxivbis.messages.{BiSProviderMessage, Message}
|
||||||
|
|
||||||
import scala.util.{Failure, Success}
|
import scala.util.{Failure, Success}
|
||||||
|
|
||||||
@Path("api/v1")
|
@Path("/api/v1")
|
||||||
class PartyEndpoint(override val storage: ActorRef[Message],
|
class PartyEndpoint(
|
||||||
override val provider: ActorRef[BiSProviderMessage])
|
override val storage: ActorRef[Message],
|
||||||
(implicit timeout: Timeout, scheduler: Scheduler)
|
override val provider: ActorRef[BiSProviderMessage],
|
||||||
extends PlayerHelper with Authorization with JsonSupport with HttpHandler {
|
override val auth: AuthorizationProvider
|
||||||
|
)(implicit
|
||||||
|
timeout: Timeout,
|
||||||
|
scheduler: Scheduler
|
||||||
|
) extends PlayerHelper
|
||||||
|
with Authorization
|
||||||
|
with JsonSupport
|
||||||
|
with HttpHandler {
|
||||||
|
|
||||||
def route: Route = getPartyDescription ~ modifyPartyDescription
|
def routes: Route = getPartyDescription ~ modifyPartyDescription
|
||||||
|
|
||||||
@GET
|
@GET
|
||||||
@Path("party/{partyId}/description")
|
@Path("party/{partyId}/description")
|
||||||
@Produces(value = Array("application/json"))
|
@Produces(value = Array("application/json"))
|
||||||
@Operation(summary = "get party description", description = "Return the party description",
|
@Operation(
|
||||||
|
summary = "get party description",
|
||||||
|
description = "Return the party description",
|
||||||
parameters = Array(
|
parameters = Array(
|
||||||
new Parameter(name = "partyId", in = ParameterIn.PATH, description = "unique party ID", example = "abcdefgh"),
|
new Parameter(
|
||||||
|
name = "partyId",
|
||||||
|
in = ParameterIn.PATH,
|
||||||
|
description = "unique party ID",
|
||||||
|
example = "o3KicHQPW5b0JcOm5yI3"
|
||||||
|
),
|
||||||
),
|
),
|
||||||
responses = Array(
|
responses = Array(
|
||||||
new ApiResponse(responseCode = "200", description = "Party description",
|
new ApiResponse(
|
||||||
content = Array(new Content(schema = new Schema(implementation = classOf[PartyDescriptionResponse])))),
|
responseCode = "200",
|
||||||
new ApiResponse(responseCode = "401", description = "Supplied authorization is invalid",
|
description = "Party description",
|
||||||
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))),
|
content = Array(new Content(schema = new Schema(implementation = classOf[PartyDescriptionModel])))
|
||||||
new ApiResponse(responseCode = "403", description = "Access is forbidden",
|
),
|
||||||
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))),
|
new ApiResponse(
|
||||||
new ApiResponse(responseCode = "500", description = "Internal server error",
|
responseCode = "401",
|
||||||
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))),
|
description = "Supplied authorization is invalid",
|
||||||
|
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel])))
|
||||||
|
),
|
||||||
|
new ApiResponse(
|
||||||
|
responseCode = "403",
|
||||||
|
description = "Access is forbidden",
|
||||||
|
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel])))
|
||||||
|
),
|
||||||
|
new ApiResponse(
|
||||||
|
responseCode = "500",
|
||||||
|
description = "Internal server error",
|
||||||
|
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel])))
|
||||||
|
),
|
||||||
),
|
),
|
||||||
security = Array(new SecurityRequirement(name = "basic auth", scopes = Array("get"))),
|
security = Array(new SecurityRequirement(name = "basic", scopes = Array("get"))),
|
||||||
tags = Array("party"),
|
tags = Array("party"),
|
||||||
)
|
)
|
||||||
def getPartyDescription: Route =
|
def getPartyDescription: Route =
|
||||||
@ -59,9 +86,8 @@ class PartyEndpoint(override val storage: ActorRef[Message],
|
|||||||
extractExecutionContext { implicit executionContext =>
|
extractExecutionContext { implicit executionContext =>
|
||||||
authenticateBasicBCrypt(s"party $partyId", authGet(partyId)) { _ =>
|
authenticateBasicBCrypt(s"party $partyId", authGet(partyId)) { _ =>
|
||||||
get {
|
get {
|
||||||
onComplete(getPartyDescription(partyId)) {
|
onSuccess(getPartyDescription(partyId)) { response =>
|
||||||
case Success(response) => complete(PartyDescriptionResponse.fromDescription(response))
|
complete(PartyDescriptionModel.fromDescription(response))
|
||||||
case Failure(exception) => throw exception
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -71,24 +97,46 @@ class PartyEndpoint(override val storage: ActorRef[Message],
|
|||||||
@POST
|
@POST
|
||||||
@Consumes(value = Array("application/json"))
|
@Consumes(value = Array("application/json"))
|
||||||
@Path("party/{partyId}/description")
|
@Path("party/{partyId}/description")
|
||||||
@Operation(summary = "modify party description", description = "Edit party description",
|
@Operation(
|
||||||
|
summary = "modify party description",
|
||||||
|
description = "Edit party description",
|
||||||
parameters = Array(
|
parameters = Array(
|
||||||
new Parameter(name = "partyId", in = ParameterIn.PATH, description = "unique party ID", example = "abcdefgh"),
|
new Parameter(
|
||||||
|
name = "partyId",
|
||||||
|
in = ParameterIn.PATH,
|
||||||
|
description = "unique party ID",
|
||||||
|
example = "o3KicHQPW5b0JcOm5yI3"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
requestBody = new RequestBody(
|
||||||
|
description = "new party description",
|
||||||
|
required = true,
|
||||||
|
content = Array(new Content(schema = new Schema(implementation = classOf[PartyDescriptionModel])))
|
||||||
),
|
),
|
||||||
requestBody = new RequestBody(description = "new party description", required = true,
|
|
||||||
content = Array(new Content(schema = new Schema(implementation = classOf[PartyDescriptionResponse])))),
|
|
||||||
responses = Array(
|
responses = Array(
|
||||||
new ApiResponse(responseCode = "202", description = "Party description has been modified"),
|
new ApiResponse(responseCode = "202", description = "Party description has been modified"),
|
||||||
new ApiResponse(responseCode = "400", description = "Invalid parameters were supplied",
|
new ApiResponse(
|
||||||
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))),
|
responseCode = "400",
|
||||||
new ApiResponse(responseCode = "401", description = "Supplied authorization is invalid",
|
description = "Invalid parameters were supplied",
|
||||||
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))),
|
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel])))
|
||||||
new ApiResponse(responseCode = "403", description = "Access is forbidden",
|
),
|
||||||
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))),
|
new ApiResponse(
|
||||||
new ApiResponse(responseCode = "500", description = "Internal server error",
|
responseCode = "401",
|
||||||
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))),
|
description = "Supplied authorization is invalid",
|
||||||
|
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel])))
|
||||||
|
),
|
||||||
|
new ApiResponse(
|
||||||
|
responseCode = "403",
|
||||||
|
description = "Access is forbidden",
|
||||||
|
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel])))
|
||||||
|
),
|
||||||
|
new ApiResponse(
|
||||||
|
responseCode = "500",
|
||||||
|
description = "Internal server error",
|
||||||
|
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel])))
|
||||||
|
),
|
||||||
),
|
),
|
||||||
security = Array(new SecurityRequirement(name = "basic auth", scopes = Array("post"))),
|
security = Array(new SecurityRequirement(name = "basic", scopes = Array("post"))),
|
||||||
tags = Array("party"),
|
tags = Array("party"),
|
||||||
)
|
)
|
||||||
def modifyPartyDescription: Route =
|
def modifyPartyDescription: Route =
|
||||||
@ -96,11 +144,10 @@ class PartyEndpoint(override val storage: ActorRef[Message],
|
|||||||
extractExecutionContext { implicit executionContext =>
|
extractExecutionContext { implicit executionContext =>
|
||||||
authenticateBasicBCrypt(s"party $partyId", authPost(partyId)) { _ =>
|
authenticateBasicBCrypt(s"party $partyId", authPost(partyId)) { _ =>
|
||||||
post {
|
post {
|
||||||
entity(as[PartyDescriptionResponse]) { partyDescription =>
|
entity(as[PartyDescriptionModel]) { partyDescription =>
|
||||||
val description = partyDescription.copy(partyId = partyId)
|
val description = partyDescription.copy(partyId = partyId)
|
||||||
onComplete(updateDescription(description.toDescription)) {
|
onSuccess(updateDescription(description.toDescription)) {
|
||||||
case Success(_) => complete(StatusCodes.Accepted, HttpEntity.Empty)
|
complete(StatusCodes.Accepted, HttpEntity.Empty)
|
||||||
case Failure(exception) => throw exception
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (c) 2019 Evgeniy Alekseev.
|
* Copyright (c) 2019-2022 Evgeniy Alekseev.
|
||||||
*
|
*
|
||||||
* This file is part of ffxivbis
|
* This file is part of ffxivbis
|
||||||
* (see https://github.com/arcan1s/ffxivbis).
|
* (see https://github.com/arcan1s/ffxivbis).
|
||||||
@ -19,44 +19,78 @@ import io.swagger.v3.oas.annotations.parameters.RequestBody
|
|||||||
import io.swagger.v3.oas.annotations.responses.ApiResponse
|
import io.swagger.v3.oas.annotations.responses.ApiResponse
|
||||||
import io.swagger.v3.oas.annotations.security.SecurityRequirement
|
import io.swagger.v3.oas.annotations.security.SecurityRequirement
|
||||||
import io.swagger.v3.oas.annotations.{Operation, Parameter}
|
import io.swagger.v3.oas.annotations.{Operation, Parameter}
|
||||||
import javax.ws.rs._
|
import jakarta.ws.rs._
|
||||||
import me.arcanis.ffxivbis.http.api.v1.json._
|
import me.arcanis.ffxivbis.http.api.v1.json._
|
||||||
import me.arcanis.ffxivbis.http.{Authorization, PlayerHelper}
|
import me.arcanis.ffxivbis.http.helpers.PlayerHelper
|
||||||
|
import me.arcanis.ffxivbis.http.{Authorization, AuthorizationProvider}
|
||||||
import me.arcanis.ffxivbis.messages.{BiSProviderMessage, Message}
|
import me.arcanis.ffxivbis.messages.{BiSProviderMessage, Message}
|
||||||
import me.arcanis.ffxivbis.models.PlayerId
|
import me.arcanis.ffxivbis.models.PlayerId
|
||||||
|
|
||||||
import scala.util.{Failure, Success}
|
import scala.util.{Failure, Success}
|
||||||
|
|
||||||
@Path("api/v1")
|
@Path("/api/v1")
|
||||||
class PlayerEndpoint(override val storage: ActorRef[Message],
|
class PlayerEndpoint(
|
||||||
override val provider: ActorRef[BiSProviderMessage])
|
override val storage: ActorRef[Message],
|
||||||
(implicit timeout: Timeout, scheduler: Scheduler)
|
override val provider: ActorRef[BiSProviderMessage],
|
||||||
extends PlayerHelper with Authorization with JsonSupport with HttpHandler {
|
override val auth: AuthorizationProvider
|
||||||
|
)(implicit
|
||||||
|
timeout: Timeout,
|
||||||
|
scheduler: Scheduler
|
||||||
|
) extends PlayerHelper
|
||||||
|
with Authorization
|
||||||
|
with JsonSupport
|
||||||
|
with HttpHandler {
|
||||||
|
|
||||||
def route: Route = getParty ~ modifyParty
|
def routes: Route = getParty ~ getPartyStats ~ modifyParty
|
||||||
|
|
||||||
@GET
|
@GET
|
||||||
@Path("party/{partyId}")
|
@Path("party/{partyId}")
|
||||||
@Produces(value = Array("application/json"))
|
@Produces(value = Array("application/json"))
|
||||||
@Operation(summary = "get party", description = "Return the players who belong to the party",
|
@Operation(
|
||||||
|
summary = "get party",
|
||||||
|
description = "Return the players who belong to the party",
|
||||||
parameters = Array(
|
parameters = Array(
|
||||||
new Parameter(name = "partyId", in = ParameterIn.PATH, description = "unique party ID", example = "abcdefgh"),
|
new Parameter(
|
||||||
new Parameter(name = "nick", in = ParameterIn.QUERY, description = "player nick name to filter", example = "Siuan Sanche"),
|
name = "partyId",
|
||||||
|
in = ParameterIn.PATH,
|
||||||
|
description = "unique party ID",
|
||||||
|
example = "o3KicHQPW5b0JcOm5yI3"
|
||||||
|
),
|
||||||
|
new Parameter(
|
||||||
|
name = "nick",
|
||||||
|
in = ParameterIn.QUERY,
|
||||||
|
description = "player nick name to filter",
|
||||||
|
example = "Siuan Sanche"
|
||||||
|
),
|
||||||
new Parameter(name = "job", in = ParameterIn.QUERY, description = "player job to filter", example = "DNC"),
|
new Parameter(name = "job", in = ParameterIn.QUERY, description = "player job to filter", example = "DNC"),
|
||||||
),
|
),
|
||||||
responses = Array(
|
responses = Array(
|
||||||
new ApiResponse(responseCode = "200", description = "Players list",
|
new ApiResponse(
|
||||||
content = Array(new Content(
|
responseCode = "200",
|
||||||
array = new ArraySchema(schema = new Schema(implementation = classOf[PlayerResponse])),
|
description = "Players list",
|
||||||
))),
|
content = Array(
|
||||||
new ApiResponse(responseCode = "401", description = "Supplied authorization is invalid",
|
new Content(
|
||||||
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))),
|
array = new ArraySchema(schema = new Schema(implementation = classOf[PlayerModel])),
|
||||||
new ApiResponse(responseCode = "403", description = "Access is forbidden",
|
)
|
||||||
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))),
|
)
|
||||||
new ApiResponse(responseCode = "500", description = "Internal server error",
|
),
|
||||||
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))),
|
new ApiResponse(
|
||||||
|
responseCode = "401",
|
||||||
|
description = "Supplied authorization is invalid",
|
||||||
|
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel])))
|
||||||
|
),
|
||||||
|
new ApiResponse(
|
||||||
|
responseCode = "403",
|
||||||
|
description = "Access is forbidden",
|
||||||
|
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel])))
|
||||||
|
),
|
||||||
|
new ApiResponse(
|
||||||
|
responseCode = "500",
|
||||||
|
description = "Internal server error",
|
||||||
|
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel])))
|
||||||
|
),
|
||||||
),
|
),
|
||||||
security = Array(new SecurityRequirement(name = "basic auth", scopes = Array("get"))),
|
security = Array(new SecurityRequirement(name = "basic", scopes = Array("get"))),
|
||||||
tags = Array("party"),
|
tags = Array("party"),
|
||||||
)
|
)
|
||||||
def getParty: Route =
|
def getParty: Route =
|
||||||
@ -66,9 +100,74 @@ class PlayerEndpoint(override val storage: ActorRef[Message],
|
|||||||
get {
|
get {
|
||||||
parameters("nick".as[String].?, "job".as[String].?) { (maybeNick, maybeJob) =>
|
parameters("nick".as[String].?, "job".as[String].?) { (maybeNick, maybeJob) =>
|
||||||
val playerId = PlayerId(partyId, maybeNick, maybeJob)
|
val playerId = PlayerId(partyId, maybeNick, maybeJob)
|
||||||
onComplete(getPlayers(partyId, playerId)) {
|
onSuccess(getPlayers(partyId, playerId)) { response =>
|
||||||
case Success(response) => complete(response.map(PlayerResponse.fromPlayer))
|
complete(response.map(PlayerModel.fromPlayer))
|
||||||
case Failure(exception) => throw exception
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("party/{partyId}/stats")
|
||||||
|
@Produces(value = Array("application/json"))
|
||||||
|
@Operation(
|
||||||
|
summary = "get party statistics",
|
||||||
|
description = "Return the party statistics",
|
||||||
|
parameters = Array(
|
||||||
|
new Parameter(
|
||||||
|
name = "partyId",
|
||||||
|
in = ParameterIn.PATH,
|
||||||
|
description = "unique party ID",
|
||||||
|
example = "o3KicHQPW5b0JcOm5yI3"
|
||||||
|
),
|
||||||
|
new Parameter(
|
||||||
|
name = "nick",
|
||||||
|
in = ParameterIn.QUERY,
|
||||||
|
description = "player nick name to filter",
|
||||||
|
example = "Siuan Sanche"
|
||||||
|
),
|
||||||
|
new Parameter(name = "job", in = ParameterIn.QUERY, description = "player job to filter", example = "DNC"),
|
||||||
|
),
|
||||||
|
responses = Array(
|
||||||
|
new ApiResponse(
|
||||||
|
responseCode = "200",
|
||||||
|
description = "Party loot statistics",
|
||||||
|
content = Array(
|
||||||
|
new Content(
|
||||||
|
array = new ArraySchema(schema = new Schema(implementation = classOf[PlayerIdWithCountersModel])),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
new ApiResponse(
|
||||||
|
responseCode = "401",
|
||||||
|
description = "Supplied authorization is invalid",
|
||||||
|
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel])))
|
||||||
|
),
|
||||||
|
new ApiResponse(
|
||||||
|
responseCode = "403",
|
||||||
|
description = "Access is forbidden",
|
||||||
|
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel])))
|
||||||
|
),
|
||||||
|
new ApiResponse(
|
||||||
|
responseCode = "500",
|
||||||
|
description = "Internal server error",
|
||||||
|
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel])))
|
||||||
|
),
|
||||||
|
),
|
||||||
|
security = Array(new SecurityRequirement(name = "basic", scopes = Array("get"))),
|
||||||
|
tags = Array("party"),
|
||||||
|
)
|
||||||
|
def getPartyStats: Route =
|
||||||
|
path("party" / Segment / "stats") { partyId =>
|
||||||
|
extractExecutionContext { implicit executionContext =>
|
||||||
|
authenticateBasicBCrypt(s"party $partyId", authGet(partyId)) { _ =>
|
||||||
|
get {
|
||||||
|
parameters("nick".as[String].?, "job".as[String].?) { (maybeNick, maybeJob) =>
|
||||||
|
val playerId = PlayerId(partyId, maybeNick, maybeJob)
|
||||||
|
onSuccess(getPlayers(partyId, playerId)) { response =>
|
||||||
|
complete(response.map(player => PlayerIdWithCountersModel.fromPlayerId(player.withCounters(None))))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -79,35 +178,58 @@ class PlayerEndpoint(override val storage: ActorRef[Message],
|
|||||||
@POST
|
@POST
|
||||||
@Path("party/{partyId}")
|
@Path("party/{partyId}")
|
||||||
@Consumes(value = Array("application/json"))
|
@Consumes(value = Array("application/json"))
|
||||||
@Operation(summary = "modify party", description = "Add or remove a player from party list",
|
@Operation(
|
||||||
|
summary = "modify party",
|
||||||
|
description = "Add or remove a player from party list",
|
||||||
parameters = Array(
|
parameters = Array(
|
||||||
new Parameter(name = "partyId", in = ParameterIn.PATH, description = "unique party ID", example = "abcdefgh"),
|
new Parameter(
|
||||||
|
name = "partyId",
|
||||||
|
in = ParameterIn.PATH,
|
||||||
|
description = "unique party ID",
|
||||||
|
example = "o3KicHQPW5b0JcOm5yI3"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
requestBody = new RequestBody(
|
||||||
|
description = "player description",
|
||||||
|
required = true,
|
||||||
|
content = Array(new Content(schema = new Schema(implementation = classOf[PlayerActionModel])))
|
||||||
),
|
),
|
||||||
requestBody = new RequestBody(description = "player description", required = true,
|
|
||||||
content = Array(new Content(schema = new Schema(implementation = classOf[PlayerActionResponse])))),
|
|
||||||
responses = Array(
|
responses = Array(
|
||||||
new ApiResponse(responseCode = "202", description = "Party has been modified"),
|
new ApiResponse(responseCode = "202", description = "Party has been modified"),
|
||||||
new ApiResponse(responseCode = "400", description = "Invalid parameters were supplied",
|
new ApiResponse(
|
||||||
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))),
|
responseCode = "400",
|
||||||
new ApiResponse(responseCode = "401", description = "Supplied authorization is invalid",
|
description = "Invalid parameters were supplied",
|
||||||
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))),
|
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel])))
|
||||||
new ApiResponse(responseCode = "403", description = "Access is forbidden",
|
),
|
||||||
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))),
|
new ApiResponse(
|
||||||
new ApiResponse(responseCode = "500", description = "Internal server error",
|
responseCode = "401",
|
||||||
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))),
|
description = "Supplied authorization is invalid",
|
||||||
|
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel])))
|
||||||
|
),
|
||||||
|
new ApiResponse(
|
||||||
|
responseCode = "403",
|
||||||
|
description = "Access is forbidden",
|
||||||
|
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel])))
|
||||||
|
),
|
||||||
|
new ApiResponse(
|
||||||
|
responseCode = "500",
|
||||||
|
description = "Internal server error",
|
||||||
|
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel])))
|
||||||
|
),
|
||||||
),
|
),
|
||||||
security = Array(new SecurityRequirement(name = "basic auth", scopes = Array("post"))),
|
security = Array(new SecurityRequirement(name = "basic", scopes = Array("post"))),
|
||||||
tags = Array("party"),
|
tags = Array("party"),
|
||||||
)
|
)
|
||||||
def modifyParty: Route =
|
def modifyParty: Route =
|
||||||
path("party" / Segment) { partyId =>
|
path("party" / Segment) { partyId =>
|
||||||
extractExecutionContext { implicit executionContext =>
|
extractExecutionContext { implicit executionContext =>
|
||||||
authenticateBasicBCrypt(s"party $partyId", authPost(partyId)) { _ =>
|
authenticateBasicBCrypt(s"party $partyId", authPost(partyId)) { _ =>
|
||||||
entity(as[PlayerActionResponse]) { action =>
|
post {
|
||||||
val player = action.playerId.toPlayer.copy(partyId = partyId)
|
entity(as[PlayerActionModel]) { action =>
|
||||||
onComplete(doModifyPlayer(action.action, player)) {
|
val player = action.playerId.toPlayer.copy(partyId = partyId)
|
||||||
case Success(_) => complete(StatusCodes.Accepted, HttpEntity.Empty)
|
onSuccess(doModifyPlayer(action.action, player)) {
|
||||||
case Failure(exception) => throw exception
|
complete(StatusCodes.Accepted, HttpEntity.Empty)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (c) 2019 Evgeniy Alekseev.
|
* Copyright (c) 2019-2022 Evgeniy Alekseev.
|
||||||
*
|
*
|
||||||
* This file is part of ffxivbis
|
* This file is part of ffxivbis
|
||||||
* (see https://github.com/arcan1s/ffxivbis).
|
* (see https://github.com/arcan1s/ffxivbis).
|
||||||
@ -13,26 +13,34 @@ import akka.http.scaladsl.server.Directives._
|
|||||||
import akka.http.scaladsl.server.Route
|
import akka.http.scaladsl.server.Route
|
||||||
import akka.util.Timeout
|
import akka.util.Timeout
|
||||||
import com.typesafe.config.Config
|
import com.typesafe.config.Config
|
||||||
|
import me.arcanis.ffxivbis.http.AuthorizationProvider
|
||||||
import me.arcanis.ffxivbis.http.api.v1.json.JsonSupport
|
import me.arcanis.ffxivbis.http.api.v1.json.JsonSupport
|
||||||
import me.arcanis.ffxivbis.messages.{BiSProviderMessage, Message}
|
import me.arcanis.ffxivbis.messages.{BiSProviderMessage, Message}
|
||||||
|
|
||||||
class RootApiV1Endpoint(storage: ActorRef[Message],
|
class RootApiV1Endpoint(
|
||||||
provider: ActorRef[BiSProviderMessage],
|
storage: ActorRef[Message],
|
||||||
config: Config)(implicit timeout: Timeout, scheduler: Scheduler)
|
auth: AuthorizationProvider,
|
||||||
extends JsonSupport with HttpHandler {
|
provider: ActorRef[BiSProviderMessage],
|
||||||
|
config: Config
|
||||||
|
)(implicit
|
||||||
|
timeout: Timeout,
|
||||||
|
scheduler: Scheduler
|
||||||
|
) extends JsonSupport
|
||||||
|
with HttpHandler {
|
||||||
|
|
||||||
private val biSEndpoint = new BiSEndpoint(storage, provider)
|
private val biSEndpoint = new BiSEndpoint(storage, provider, auth)
|
||||||
private val lootEndpoint = new LootEndpoint(storage)
|
private val lootEndpoint = new LootEndpoint(storage, auth)
|
||||||
private val partyEndpoint = new PartyEndpoint(storage, provider)
|
private val partyEndpoint = new PartyEndpoint(storage, provider, auth)
|
||||||
private val playerEndpoint = new PlayerEndpoint(storage, provider)
|
private val playerEndpoint = new PlayerEndpoint(storage, provider, auth)
|
||||||
|
private val statusEndpoint = new StatusEndpoint
|
||||||
private val typesEndpoint = new TypesEndpoint(config)
|
private val typesEndpoint = new TypesEndpoint(config)
|
||||||
private val userEndpoint = new UserEndpoint(storage)
|
private val userEndpoint = new UserEndpoint(storage, auth)
|
||||||
|
|
||||||
def route: Route =
|
def routes: Route =
|
||||||
handleExceptions(exceptionHandler) {
|
handleExceptions(exceptionHandler) {
|
||||||
handleRejections(rejectionHandler) {
|
handleRejections(rejectionHandler) {
|
||||||
biSEndpoint.route ~ lootEndpoint.route ~ partyEndpoint.route ~
|
biSEndpoint.routes ~ lootEndpoint.routes ~ partyEndpoint.routes ~ playerEndpoint.routes ~
|
||||||
playerEndpoint.route ~ typesEndpoint.route ~ userEndpoint.route
|
statusEndpoint.routes ~ typesEndpoint.routes ~ userEndpoint.routes
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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 routes: 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),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (c) 2019 Evgeniy Alekseev.
|
* Copyright (c) 2019-2022 Evgeniy Alekseev.
|
||||||
*
|
*
|
||||||
* This file is part of ffxivbis
|
* This file is part of ffxivbis
|
||||||
* (see https://github.com/arcan1s/ffxivbis).
|
* (see https://github.com/arcan1s/ffxivbis).
|
||||||
@ -11,50 +11,101 @@ package me.arcanis.ffxivbis.http.api.v1
|
|||||||
import akka.http.scaladsl.server.Directives._
|
import akka.http.scaladsl.server.Directives._
|
||||||
import akka.http.scaladsl.server._
|
import akka.http.scaladsl.server._
|
||||||
import com.typesafe.config.Config
|
import com.typesafe.config.Config
|
||||||
|
import io.swagger.v3.oas.annotations.Operation
|
||||||
import io.swagger.v3.oas.annotations.media.{ArraySchema, Content, Schema}
|
import io.swagger.v3.oas.annotations.media.{ArraySchema, Content, Schema}
|
||||||
import io.swagger.v3.oas.annotations.responses.ApiResponse
|
import io.swagger.v3.oas.annotations.responses.ApiResponse
|
||||||
import io.swagger.v3.oas.annotations.Operation
|
import jakarta.ws.rs._
|
||||||
import javax.ws.rs._
|
|
||||||
import me.arcanis.ffxivbis.http.api.v1.json._
|
import me.arcanis.ffxivbis.http.api.v1.json._
|
||||||
import me.arcanis.ffxivbis.models.{Job, Party, Permission, Piece, PieceType}
|
import me.arcanis.ffxivbis.models._
|
||||||
|
|
||||||
@Path("api/v1")
|
@Path("/api/v1")
|
||||||
class TypesEndpoint(config: Config) extends JsonSupport {
|
class TypesEndpoint(config: Config) extends JsonSupport {
|
||||||
|
|
||||||
def route: Route = getJobs ~ getPermissions ~ getPieces ~ getPieceTypes ~ getPriority
|
def routes: Route = getAllJobs ~ getJobs ~ getPermissions ~ getPieces ~ getPieceTypes ~ getPriority
|
||||||
|
|
||||||
@GET
|
@GET
|
||||||
@Path("types/jobs")
|
@Path("types/jobs/all")
|
||||||
@Produces(value = Array("application/json"))
|
@Produces(value = Array("application/json"))
|
||||||
@Operation(summary = "jobs list", description = "Returns the available jobs",
|
@Operation(
|
||||||
|
summary = "full jobs list",
|
||||||
|
description = "Returns the available jobs including any job",
|
||||||
responses = Array(
|
responses = Array(
|
||||||
new ApiResponse(responseCode = "200", description = "List of available jobs",
|
new ApiResponse(
|
||||||
content = Array(new Content(
|
responseCode = "200",
|
||||||
array = new ArraySchema(schema = new Schema(implementation = classOf[String]))
|
description = "List of available jobs with AnyJob",
|
||||||
))),
|
content = Array(
|
||||||
new ApiResponse(responseCode = "500", description = "Internal server error",
|
new Content(
|
||||||
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))),
|
array = new ArraySchema(schema = new Schema(implementation = classOf[String]))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
new ApiResponse(
|
||||||
|
responseCode = "500",
|
||||||
|
description = "Internal server error",
|
||||||
|
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel])))
|
||||||
|
),
|
||||||
),
|
),
|
||||||
tags = Array("types"),
|
tags = Array("types"),
|
||||||
)
|
)
|
||||||
def getJobs: Route =
|
def getAllJobs: Route =
|
||||||
path("types" / "jobs") {
|
path("types" / "jobs" / "all") {
|
||||||
get {
|
get {
|
||||||
complete(Job.availableWithAnyJob.map(_.toString))
|
complete(Job.availableWithAnyJob.map(_.toString))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("types/jobs")
|
||||||
|
@Produces(value = Array("application/json"))
|
||||||
|
@Operation(
|
||||||
|
summary = "jobs list",
|
||||||
|
description = "Returns the available jobs",
|
||||||
|
responses = Array(
|
||||||
|
new ApiResponse(
|
||||||
|
responseCode = "200",
|
||||||
|
description = "List of available jobs",
|
||||||
|
content = Array(
|
||||||
|
new Content(
|
||||||
|
array = new ArraySchema(schema = new Schema(implementation = classOf[String]))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
new ApiResponse(
|
||||||
|
responseCode = "500",
|
||||||
|
description = "Internal server error",
|
||||||
|
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel])))
|
||||||
|
),
|
||||||
|
),
|
||||||
|
tags = Array("types"),
|
||||||
|
)
|
||||||
|
def getJobs: Route =
|
||||||
|
path("types" / "jobs") {
|
||||||
|
get {
|
||||||
|
complete(Job.available.map(_.toString))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@GET
|
@GET
|
||||||
@Path("types/permissions")
|
@Path("types/permissions")
|
||||||
@Produces(value = Array("application/json"))
|
@Produces(value = Array("application/json"))
|
||||||
@Operation(summary = "permissions list", description = "Returns the available permissions",
|
@Operation(
|
||||||
|
summary = "permissions list",
|
||||||
|
description = "Returns the available permissions",
|
||||||
responses = Array(
|
responses = Array(
|
||||||
new ApiResponse(responseCode = "200", description = "List of available permissions",
|
new ApiResponse(
|
||||||
content = Array(new Content(
|
responseCode = "200",
|
||||||
array = new ArraySchema(schema = new Schema(implementation = classOf[String]))
|
description = "List of available permissions",
|
||||||
))),
|
content = Array(
|
||||||
new ApiResponse(responseCode = "500", description = "Internal server error",
|
new Content(
|
||||||
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))),
|
array = new ArraySchema(schema = new Schema(implementation = classOf[String]))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
new ApiResponse(
|
||||||
|
responseCode = "500",
|
||||||
|
description = "Internal server error",
|
||||||
|
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel])))
|
||||||
|
),
|
||||||
),
|
),
|
||||||
tags = Array("types"),
|
tags = Array("types"),
|
||||||
)
|
)
|
||||||
@ -68,14 +119,24 @@ class TypesEndpoint(config: Config) extends JsonSupport {
|
|||||||
@GET
|
@GET
|
||||||
@Path("types/pieces")
|
@Path("types/pieces")
|
||||||
@Produces(value = Array("application/json"))
|
@Produces(value = Array("application/json"))
|
||||||
@Operation(summary = "pieces list", description = "Returns the available pieces",
|
@Operation(
|
||||||
|
summary = "pieces list",
|
||||||
|
description = "Returns the available pieces",
|
||||||
responses = Array(
|
responses = Array(
|
||||||
new ApiResponse(responseCode = "200", description = "List of available pieces",
|
new ApiResponse(
|
||||||
content = Array(new Content(
|
responseCode = "200",
|
||||||
array = new ArraySchema(schema = new Schema(implementation = classOf[String]))
|
description = "List of available pieces",
|
||||||
))),
|
content = Array(
|
||||||
new ApiResponse(responseCode = "500", description = "Internal server error",
|
new Content(
|
||||||
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))),
|
array = new ArraySchema(schema = new Schema(implementation = classOf[String]))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
new ApiResponse(
|
||||||
|
responseCode = "500",
|
||||||
|
description = "Internal server error",
|
||||||
|
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel])))
|
||||||
|
),
|
||||||
),
|
),
|
||||||
tags = Array("types"),
|
tags = Array("types"),
|
||||||
)
|
)
|
||||||
@ -89,14 +150,24 @@ class TypesEndpoint(config: Config) extends JsonSupport {
|
|||||||
@GET
|
@GET
|
||||||
@Path("types/pieces/types")
|
@Path("types/pieces/types")
|
||||||
@Produces(value = Array("application/json"))
|
@Produces(value = Array("application/json"))
|
||||||
@Operation(summary = "piece types list", description = "Returns the available piece types",
|
@Operation(
|
||||||
|
summary = "piece types list",
|
||||||
|
description = "Returns the available piece types",
|
||||||
responses = Array(
|
responses = Array(
|
||||||
new ApiResponse(responseCode = "200", description = "List of available piece types",
|
new ApiResponse(
|
||||||
content = Array(new Content(
|
responseCode = "200",
|
||||||
array = new ArraySchema(schema = new Schema(implementation = classOf[String]))
|
description = "List of available piece types",
|
||||||
))),
|
content = Array(
|
||||||
new ApiResponse(responseCode = "500", description = "Internal server error",
|
new Content(
|
||||||
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))),
|
array = new ArraySchema(schema = new Schema(implementation = classOf[String]))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
new ApiResponse(
|
||||||
|
responseCode = "500",
|
||||||
|
description = "Internal server error",
|
||||||
|
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel])))
|
||||||
|
),
|
||||||
),
|
),
|
||||||
tags = Array("types"),
|
tags = Array("types"),
|
||||||
)
|
)
|
||||||
@ -110,14 +181,24 @@ class TypesEndpoint(config: Config) extends JsonSupport {
|
|||||||
@GET
|
@GET
|
||||||
@Path("types/priority")
|
@Path("types/priority")
|
||||||
@Produces(value = Array("application/json"))
|
@Produces(value = Array("application/json"))
|
||||||
@Operation(summary = "priority list", description = "Returns the current priority list",
|
@Operation(
|
||||||
|
summary = "priority list",
|
||||||
|
description = "Returns the current priority list",
|
||||||
responses = Array(
|
responses = Array(
|
||||||
new ApiResponse(responseCode = "200", description = "Priority order",
|
new ApiResponse(
|
||||||
content = Array(new Content(
|
responseCode = "200",
|
||||||
array = new ArraySchema(schema = new Schema(implementation = classOf[String]))
|
description = "Priority order",
|
||||||
))),
|
content = Array(
|
||||||
new ApiResponse(responseCode = "500", description = "Internal server error",
|
new Content(
|
||||||
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))),
|
array = new ArraySchema(schema = new Schema(implementation = classOf[String]))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
new ApiResponse(
|
||||||
|
responseCode = "500",
|
||||||
|
description = "Internal server error",
|
||||||
|
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel])))
|
||||||
|
),
|
||||||
),
|
),
|
||||||
tags = Array("types"),
|
tags = Array("types"),
|
||||||
)
|
)
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (c) 2019 Evgeniy Alekseev.
|
* Copyright (c) 2019-2022 Evgeniy Alekseev.
|
||||||
*
|
*
|
||||||
* This file is part of ffxivbis
|
* This file is part of ffxivbis
|
||||||
* (see https://github.com/arcan1s/ffxivbis).
|
* (see https://github.com/arcan1s/ffxivbis).
|
||||||
@ -19,51 +19,70 @@ import io.swagger.v3.oas.annotations.parameters.RequestBody
|
|||||||
import io.swagger.v3.oas.annotations.responses.ApiResponse
|
import io.swagger.v3.oas.annotations.responses.ApiResponse
|
||||||
import io.swagger.v3.oas.annotations.security.SecurityRequirement
|
import io.swagger.v3.oas.annotations.security.SecurityRequirement
|
||||||
import io.swagger.v3.oas.annotations.{Operation, Parameter}
|
import io.swagger.v3.oas.annotations.{Operation, Parameter}
|
||||||
import javax.ws.rs._
|
import jakarta.ws.rs._
|
||||||
import me.arcanis.ffxivbis.http.api.v1.json._
|
import me.arcanis.ffxivbis.http.api.v1.json._
|
||||||
import me.arcanis.ffxivbis.http.{Authorization, UserHelper}
|
import me.arcanis.ffxivbis.http.helpers.UserHelper
|
||||||
|
import me.arcanis.ffxivbis.http.{Authorization, AuthorizationProvider}
|
||||||
import me.arcanis.ffxivbis.messages.Message
|
import me.arcanis.ffxivbis.messages.Message
|
||||||
import me.arcanis.ffxivbis.models.Permission
|
import me.arcanis.ffxivbis.models.Permission
|
||||||
|
|
||||||
import scala.util.{Failure, Success}
|
import scala.util.{Failure, Success}
|
||||||
|
|
||||||
@Path("api/v1")
|
@Path("/api/v1")
|
||||||
class UserEndpoint(override val storage: ActorRef[Message])
|
class UserEndpoint(override val storage: ActorRef[Message], override val auth: AuthorizationProvider)(implicit
|
||||||
(implicit timeout: Timeout, scheduler: Scheduler)
|
timeout: Timeout,
|
||||||
extends UserHelper with Authorization with JsonSupport {
|
scheduler: Scheduler
|
||||||
|
) extends UserHelper
|
||||||
|
with Authorization
|
||||||
|
with JsonSupport {
|
||||||
|
|
||||||
def route: Route = createParty ~ createUser ~ deleteUser ~ getUsers
|
def routes: Route = createParty ~ createUser ~ deleteUser ~ getUsers ~ getUsersCurrent
|
||||||
|
|
||||||
@PUT
|
@POST
|
||||||
@Path("party")
|
@Path("party")
|
||||||
@Consumes(value = Array("application/json"))
|
@Consumes(value = Array("application/json"))
|
||||||
@Operation(summary = "create new party", description = "Create new party with specified ID",
|
@Operation(
|
||||||
requestBody = new RequestBody(description = "party administrator description", required = true,
|
summary = "create new party",
|
||||||
content = Array(new Content(schema = new Schema(implementation = classOf[UserResponse])))),
|
description = "Create new party with specified ID",
|
||||||
|
requestBody = new RequestBody(
|
||||||
|
description = "party administrator description",
|
||||||
|
required = true,
|
||||||
|
content = Array(new Content(schema = new Schema(implementation = classOf[UserModel])))
|
||||||
|
),
|
||||||
responses = Array(
|
responses = Array(
|
||||||
new ApiResponse(responseCode = "200", description = "Party has been created"),
|
new ApiResponse(
|
||||||
new ApiResponse(responseCode = "400", description = "Invalid parameters were supplied",
|
responseCode = "200",
|
||||||
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))),
|
description = "Party has been created",
|
||||||
new ApiResponse(responseCode = "406", description = "Party with the specified ID already exists",
|
content = Array(new Content(schema = new Schema(implementation = classOf[PartyIdModel])))
|
||||||
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))),
|
),
|
||||||
new ApiResponse(responseCode = "500", description = "Internal server error",
|
new ApiResponse(
|
||||||
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))),
|
responseCode = "400",
|
||||||
|
description = "Invalid parameters were supplied",
|
||||||
|
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel])))
|
||||||
|
),
|
||||||
|
new ApiResponse(
|
||||||
|
responseCode = "406",
|
||||||
|
description = "Party with the specified ID already exists",
|
||||||
|
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel])))
|
||||||
|
),
|
||||||
|
new ApiResponse(
|
||||||
|
responseCode = "500",
|
||||||
|
description = "Internal server error",
|
||||||
|
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel])))
|
||||||
|
),
|
||||||
),
|
),
|
||||||
tags = Array("party"),
|
tags = Array("party"),
|
||||||
)
|
)
|
||||||
def createParty: Route =
|
def createParty: Route =
|
||||||
path("party") {
|
path("party") {
|
||||||
extractExecutionContext { implicit executionContext =>
|
extractExecutionContext { implicit executionContext =>
|
||||||
put {
|
post {
|
||||||
entity(as[UserResponse]) { user =>
|
entity(as[UserModel]) { user =>
|
||||||
onComplete(newPartyId) {
|
onSuccess(newPartyId) { partyId =>
|
||||||
case Success(partyId) =>
|
val admin = user.toUser.copy(partyId = partyId, permission = Permission.admin)
|
||||||
val admin = user.toUser.copy(partyId = partyId, permission = Permission.admin)
|
onSuccess(addUser(admin, isHashedPassword = false)) {
|
||||||
onComplete(addUser(admin, isHashedPassword = false)) {
|
complete(PartyIdModel(partyId))
|
||||||
case Success(_) => complete(PartyIdResponse(partyId))
|
}
|
||||||
case Failure(exception) => throw exception
|
|
||||||
}
|
|
||||||
case Failure(exception) => throw exception
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -73,24 +92,46 @@ class UserEndpoint(override val storage: ActorRef[Message])
|
|||||||
@POST
|
@POST
|
||||||
@Path("party/{partyId}/users")
|
@Path("party/{partyId}/users")
|
||||||
@Consumes(value = Array("application/json"))
|
@Consumes(value = Array("application/json"))
|
||||||
@Operation(summary = "create new user", description = "Add an user to the specified party",
|
@Operation(
|
||||||
|
summary = "create new user",
|
||||||
|
description = "Add an user to the specified party",
|
||||||
parameters = Array(
|
parameters = Array(
|
||||||
new Parameter(name = "partyId", in = ParameterIn.PATH, description = "unique party ID", example = "abcdefgh"),
|
new Parameter(
|
||||||
|
name = "partyId",
|
||||||
|
in = ParameterIn.PATH,
|
||||||
|
description = "unique party ID",
|
||||||
|
example = "o3KicHQPW5b0JcOm5yI3"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
requestBody = new RequestBody(
|
||||||
|
description = "user description",
|
||||||
|
required = true,
|
||||||
|
content = Array(new Content(schema = new Schema(implementation = classOf[UserModel])))
|
||||||
),
|
),
|
||||||
requestBody = new RequestBody(description = "user description", required = true,
|
|
||||||
content = Array(new Content(schema = new Schema(implementation = classOf[UserResponse])))),
|
|
||||||
responses = Array(
|
responses = Array(
|
||||||
new ApiResponse(responseCode = "201", description = "User has been created"),
|
new ApiResponse(responseCode = "201", description = "User has been created"),
|
||||||
new ApiResponse(responseCode = "400", description = "Invalid parameters were supplied",
|
new ApiResponse(
|
||||||
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))),
|
responseCode = "400",
|
||||||
new ApiResponse(responseCode = "401", description = "Supplied authorization is invalid",
|
description = "Invalid parameters were supplied",
|
||||||
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))),
|
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel])))
|
||||||
new ApiResponse(responseCode = "403", description = "Access is forbidden",
|
),
|
||||||
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))),
|
new ApiResponse(
|
||||||
new ApiResponse(responseCode = "500", description = "Internal server error",
|
responseCode = "401",
|
||||||
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))),
|
description = "Supplied authorization is invalid",
|
||||||
|
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel])))
|
||||||
|
),
|
||||||
|
new ApiResponse(
|
||||||
|
responseCode = "403",
|
||||||
|
description = "Access is forbidden",
|
||||||
|
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel])))
|
||||||
|
),
|
||||||
|
new ApiResponse(
|
||||||
|
responseCode = "500",
|
||||||
|
description = "Internal server error",
|
||||||
|
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel])))
|
||||||
|
),
|
||||||
),
|
),
|
||||||
security = Array(new SecurityRequirement(name = "basic auth", scopes = Array("admin"))),
|
security = Array(new SecurityRequirement(name = "basic", scopes = Array("admin"))),
|
||||||
tags = Array("users"),
|
tags = Array("users"),
|
||||||
)
|
)
|
||||||
def createUser: Route =
|
def createUser: Route =
|
||||||
@ -98,11 +139,10 @@ class UserEndpoint(override val storage: ActorRef[Message])
|
|||||||
extractExecutionContext { implicit executionContext =>
|
extractExecutionContext { implicit executionContext =>
|
||||||
authenticateBasicBCrypt(s"party $partyId", authAdmin(partyId)) { _ =>
|
authenticateBasicBCrypt(s"party $partyId", authAdmin(partyId)) { _ =>
|
||||||
post {
|
post {
|
||||||
entity(as[UserResponse]) { user =>
|
entity(as[UserModel]) { user =>
|
||||||
val withPartyId = user.toUser.copy(partyId = partyId)
|
val withPartyId = user.toUser.copy(partyId = partyId)
|
||||||
onComplete(addUser(withPartyId, isHashedPassword = false)) {
|
onSuccess(addUser(withPartyId, isHashedPassword = false)) {
|
||||||
case Success(_) => complete(StatusCodes.Accepted, HttpEntity.Empty)
|
complete(StatusCodes.Accepted, HttpEntity.Empty)
|
||||||
case Failure(exception) => throw exception
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -112,21 +152,37 @@ class UserEndpoint(override val storage: ActorRef[Message])
|
|||||||
|
|
||||||
@DELETE
|
@DELETE
|
||||||
@Path("party/{partyId}/users/{username}")
|
@Path("party/{partyId}/users/{username}")
|
||||||
@Operation(summary = "remove user", description = "Remove an user from the specified party",
|
@Operation(
|
||||||
|
summary = "remove user",
|
||||||
|
description = "Remove an user from the specified party",
|
||||||
parameters = Array(
|
parameters = Array(
|
||||||
new Parameter(name = "partyId", in = ParameterIn.PATH, description = "unique party ID", example = "abcdefgh"),
|
new Parameter(
|
||||||
|
name = "partyId",
|
||||||
|
in = ParameterIn.PATH,
|
||||||
|
description = "unique party ID",
|
||||||
|
example = "o3KicHQPW5b0JcOm5yI3"
|
||||||
|
),
|
||||||
new Parameter(name = "username", in = ParameterIn.PATH, description = "username to remove", example = "siuan"),
|
new Parameter(name = "username", in = ParameterIn.PATH, description = "username to remove", example = "siuan"),
|
||||||
),
|
),
|
||||||
responses = Array(
|
responses = Array(
|
||||||
new ApiResponse(responseCode = "202", description = "User has been removed"),
|
new ApiResponse(responseCode = "202", description = "User has been removed"),
|
||||||
new ApiResponse(responseCode = "401", description = "Supplied authorization is invalid",
|
new ApiResponse(
|
||||||
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))),
|
responseCode = "401",
|
||||||
new ApiResponse(responseCode = "403", description = "Access is forbidden",
|
description = "Supplied authorization is invalid",
|
||||||
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))),
|
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel])))
|
||||||
new ApiResponse(responseCode = "500", description = "Internal server error",
|
),
|
||||||
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))),
|
new ApiResponse(
|
||||||
|
responseCode = "403",
|
||||||
|
description = "Access is forbidden",
|
||||||
|
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel])))
|
||||||
|
),
|
||||||
|
new ApiResponse(
|
||||||
|
responseCode = "500",
|
||||||
|
description = "Internal server error",
|
||||||
|
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel])))
|
||||||
|
),
|
||||||
),
|
),
|
||||||
security = Array(new SecurityRequirement(name = "basic auth", scopes = Array("admin"))),
|
security = Array(new SecurityRequirement(name = "basic", scopes = Array("admin"))),
|
||||||
tags = Array("users"),
|
tags = Array("users"),
|
||||||
)
|
)
|
||||||
def deleteUser: Route =
|
def deleteUser: Route =
|
||||||
@ -134,9 +190,8 @@ class UserEndpoint(override val storage: ActorRef[Message])
|
|||||||
extractExecutionContext { implicit executionContext =>
|
extractExecutionContext { implicit executionContext =>
|
||||||
authenticateBasicBCrypt(s"party $partyId", authAdmin(partyId)) { _ =>
|
authenticateBasicBCrypt(s"party $partyId", authAdmin(partyId)) { _ =>
|
||||||
delete {
|
delete {
|
||||||
onComplete(removeUser(partyId, username)) {
|
onSuccess(removeUser(partyId, username)) {
|
||||||
case Success(_) => complete(StatusCodes.Accepted, HttpEntity.Empty)
|
complete(StatusCodes.Accepted, HttpEntity.Empty)
|
||||||
case Failure(exception) => throw exception
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -146,23 +201,44 @@ class UserEndpoint(override val storage: ActorRef[Message])
|
|||||||
@GET
|
@GET
|
||||||
@Path("party/{partyId}/users")
|
@Path("party/{partyId}/users")
|
||||||
@Produces(value = Array("application/json"))
|
@Produces(value = Array("application/json"))
|
||||||
@Operation(summary = "get users", description = "Return the list of users belong to party",
|
@Operation(
|
||||||
|
summary = "get users",
|
||||||
|
description = "Return the list of users belong to party",
|
||||||
parameters = Array(
|
parameters = Array(
|
||||||
new Parameter(name = "partyId", in = ParameterIn.PATH, description = "unique party ID", example = "abcdefgh"),
|
new Parameter(
|
||||||
|
name = "partyId",
|
||||||
|
in = ParameterIn.PATH,
|
||||||
|
description = "unique party ID",
|
||||||
|
example = "o3KicHQPW5b0JcOm5yI3"
|
||||||
|
),
|
||||||
),
|
),
|
||||||
responses = Array(
|
responses = Array(
|
||||||
new ApiResponse(responseCode = "200", description = "Users list",
|
new ApiResponse(
|
||||||
content = Array(new Content(
|
responseCode = "200",
|
||||||
array = new ArraySchema(schema = new Schema(implementation = classOf[UserResponse])),
|
description = "Users list",
|
||||||
))),
|
content = Array(
|
||||||
new ApiResponse(responseCode = "401", description = "Supplied authorization is invalid",
|
new Content(
|
||||||
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))),
|
array = new ArraySchema(schema = new Schema(implementation = classOf[UserModel])),
|
||||||
new ApiResponse(responseCode = "403", description = "Access is forbidden",
|
)
|
||||||
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))),
|
)
|
||||||
new ApiResponse(responseCode = "500", description = "Internal server error",
|
),
|
||||||
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))),
|
new ApiResponse(
|
||||||
|
responseCode = "401",
|
||||||
|
description = "Supplied authorization is invalid",
|
||||||
|
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel])))
|
||||||
|
),
|
||||||
|
new ApiResponse(
|
||||||
|
responseCode = "403",
|
||||||
|
description = "Access is forbidden",
|
||||||
|
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel])))
|
||||||
|
),
|
||||||
|
new ApiResponse(
|
||||||
|
responseCode = "500",
|
||||||
|
description = "Internal server error",
|
||||||
|
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel])))
|
||||||
|
),
|
||||||
),
|
),
|
||||||
security = Array(new SecurityRequirement(name = "basic auth", scopes = Array("admin"))),
|
security = Array(new SecurityRequirement(name = "basic", scopes = Array("get"))),
|
||||||
tags = Array("users"),
|
tags = Array("users"),
|
||||||
)
|
)
|
||||||
def getUsers: Route =
|
def getUsers: Route =
|
||||||
@ -170,12 +246,61 @@ class UserEndpoint(override val storage: ActorRef[Message])
|
|||||||
extractExecutionContext { implicit executionContext =>
|
extractExecutionContext { implicit executionContext =>
|
||||||
authenticateBasicBCrypt(s"party $partyId", authAdmin(partyId)) { _ =>
|
authenticateBasicBCrypt(s"party $partyId", authAdmin(partyId)) { _ =>
|
||||||
get {
|
get {
|
||||||
onComplete(users(partyId)) {
|
onSuccess(users(partyId)) { response =>
|
||||||
case Success(response) => complete(response.map(UserResponse.fromUser))
|
complete(response.map(UserModel.fromUser))
|
||||||
case Failure(exception) => throw exception
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("party/{partyId}/users/current")
|
||||||
|
@Produces(value = Array("application/json"))
|
||||||
|
@Operation(
|
||||||
|
summary = "get current user",
|
||||||
|
description = "Return the current user descriptor",
|
||||||
|
parameters = Array(
|
||||||
|
new Parameter(
|
||||||
|
name = "partyId",
|
||||||
|
in = ParameterIn.PATH,
|
||||||
|
description = "unique party ID",
|
||||||
|
example = "o3KicHQPW5b0JcOm5yI3"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
responses = Array(
|
||||||
|
new ApiResponse(
|
||||||
|
responseCode = "200",
|
||||||
|
description = "User descriptor",
|
||||||
|
content = Array(new Content(schema = new Schema(implementation = classOf[UserModel])))
|
||||||
|
),
|
||||||
|
new ApiResponse(
|
||||||
|
responseCode = "401",
|
||||||
|
description = "Supplied authorization is invalid",
|
||||||
|
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel])))
|
||||||
|
),
|
||||||
|
new ApiResponse(
|
||||||
|
responseCode = "403",
|
||||||
|
description = "Access is forbidden",
|
||||||
|
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel])))
|
||||||
|
),
|
||||||
|
new ApiResponse(
|
||||||
|
responseCode = "500",
|
||||||
|
description = "Internal server error",
|
||||||
|
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel])))
|
||||||
|
),
|
||||||
|
),
|
||||||
|
security = Array(new SecurityRequirement(name = "basic", scopes = Array("admin"))),
|
||||||
|
tags = Array("users"),
|
||||||
|
)
|
||||||
|
def getUsersCurrent: Route =
|
||||||
|
path("party" / Segment / "users" / "current") { partyId =>
|
||||||
|
extractExecutionContext { implicit executionContext =>
|
||||||
|
authenticateBasicBCrypt(s"party $partyId", authGet(partyId)) { user =>
|
||||||
|
get {
|
||||||
|
complete(UserModel.fromUser(user))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (c) 2019 Evgeniy Alekseev.
|
* Copyright (c) 2019-2022 Evgeniy Alekseev.
|
||||||
*
|
*
|
||||||
* This file is part of ffxivbis
|
* This file is part of ffxivbis
|
||||||
* (see https://github.com/arcan1s/ffxivbis).
|
* (see https://github.com/arcan1s/ffxivbis).
|
||||||
@ -9,5 +9,6 @@
|
|||||||
package me.arcanis.ffxivbis.http.api.v1.json
|
package me.arcanis.ffxivbis.http.api.v1.json
|
||||||
|
|
||||||
object ApiAction extends Enumeration {
|
object ApiAction extends Enumeration {
|
||||||
|
|
||||||
val add, remove = Value
|
val add, remove = Value
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (c) 2019 Evgeniy Alekseev.
|
* Copyright (c) 2019-2022 Evgeniy Alekseev.
|
||||||
*
|
*
|
||||||
* This file is part of ffxivbis
|
* This file is part of ffxivbis
|
||||||
* (see https://github.com/arcan1s/ffxivbis).
|
* (see https://github.com/arcan1s/ffxivbis).
|
||||||
@ -10,5 +10,4 @@ package me.arcanis.ffxivbis.http.api.v1.json
|
|||||||
|
|
||||||
import io.swagger.v3.oas.annotations.media.Schema
|
import io.swagger.v3.oas.annotations.media.Schema
|
||||||
|
|
||||||
case class ErrorResponse(
|
case class ErrorModel(@Schema(description = "error message", required = true) message: String)
|
||||||
@Schema(description = "error message", required = true) message: String)
|
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (c) 2019 Evgeniy Alekseev.
|
* Copyright (c) 2019-2022 Evgeniy Alekseev.
|
||||||
*
|
*
|
||||||
* This file is part of ffxivbis
|
* This file is part of ffxivbis
|
||||||
* (see https://github.com/arcan1s/ffxivbis).
|
* (see https://github.com/arcan1s/ffxivbis).
|
||||||
@ -8,20 +8,20 @@
|
|||||||
*/
|
*/
|
||||||
package me.arcanis.ffxivbis.http.api.v1.json
|
package me.arcanis.ffxivbis.http.api.v1.json
|
||||||
|
|
||||||
import java.time.Instant
|
|
||||||
|
|
||||||
import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport
|
import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport
|
||||||
import me.arcanis.ffxivbis.models.Permission
|
import me.arcanis.ffxivbis.models.Permission
|
||||||
import spray.json._
|
import spray.json._
|
||||||
|
|
||||||
|
import java.time.Instant
|
||||||
|
|
||||||
trait JsonSupport extends SprayJsonSupport with DefaultJsonProtocol {
|
trait JsonSupport extends SprayJsonSupport with DefaultJsonProtocol {
|
||||||
|
|
||||||
private def enumFormat[E <: Enumeration](enum: E): RootJsonFormat[E#Value] =
|
private def enumFormat[E <: Enumeration](enumeration: E): RootJsonFormat[E#Value] =
|
||||||
new RootJsonFormat[E#Value] {
|
new RootJsonFormat[E#Value] {
|
||||||
override def write(obj: E#Value): JsValue = obj.toString.toJson
|
override def write(obj: E#Value): JsValue = obj.toString.toJson
|
||||||
override def read(json: JsValue): E#Value = json match {
|
override def read(json: JsValue): E#Value = json match {
|
||||||
case JsNumber(value) => enum(value.toInt)
|
case JsNumber(value) => enumeration(value.toInt)
|
||||||
case JsString(name) => enum.withName(name)
|
case JsString(name) => enumeration.withName(name)
|
||||||
case other => deserializationError(s"String or number expected, got $other")
|
case other => deserializationError(s"String or number expected, got $other")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -38,17 +38,20 @@ trait JsonSupport extends SprayJsonSupport with DefaultJsonProtocol {
|
|||||||
implicit val actionFormat: RootJsonFormat[ApiAction.Value] = enumFormat(ApiAction)
|
implicit val actionFormat: RootJsonFormat[ApiAction.Value] = enumFormat(ApiAction)
|
||||||
implicit val permissionFormat: RootJsonFormat[Permission.Value] = enumFormat(Permission)
|
implicit val permissionFormat: RootJsonFormat[Permission.Value] = enumFormat(Permission)
|
||||||
|
|
||||||
implicit val errorFormat: RootJsonFormat[ErrorResponse] = jsonFormat1(ErrorResponse.apply)
|
implicit val errorFormat: RootJsonFormat[ErrorModel] = jsonFormat1(ErrorModel.apply)
|
||||||
implicit val partyIdFormat: RootJsonFormat[PartyIdResponse] = jsonFormat1(PartyIdResponse.apply)
|
implicit val partyIdFormat: RootJsonFormat[PartyIdModel] = jsonFormat1(PartyIdModel.apply)
|
||||||
implicit val pieceFormat: RootJsonFormat[PieceResponse] = jsonFormat3(PieceResponse.apply)
|
implicit val pieceFormat: RootJsonFormat[PieceModel] = jsonFormat3(PieceModel.apply)
|
||||||
implicit val lootFormat: RootJsonFormat[LootResponse] = jsonFormat3(LootResponse.apply)
|
implicit val lootFormat: RootJsonFormat[LootModel] = jsonFormat3(LootModel.apply)
|
||||||
implicit val partyDescriptionFormat: RootJsonFormat[PartyDescriptionResponse] = jsonFormat2(PartyDescriptionResponse.apply)
|
implicit val partyDescriptionFormat: RootJsonFormat[PartyDescriptionModel] = jsonFormat2(
|
||||||
implicit val playerFormat: RootJsonFormat[PlayerResponse] = jsonFormat7(PlayerResponse.apply)
|
PartyDescriptionModel.apply
|
||||||
implicit val playerActionFormat: RootJsonFormat[PlayerActionResponse] = jsonFormat2(PlayerActionResponse.apply)
|
)
|
||||||
implicit val playerIdFormat: RootJsonFormat[PlayerIdResponse] = jsonFormat3(PlayerIdResponse.apply)
|
implicit val playerFormat: RootJsonFormat[PlayerModel] = jsonFormat9(PlayerModel.apply)
|
||||||
implicit val pieceActionFormat: RootJsonFormat[PieceActionResponse] = jsonFormat4(PieceActionResponse.apply)
|
implicit val playerActionFormat: RootJsonFormat[PlayerActionModel] = jsonFormat2(PlayerActionModel.apply)
|
||||||
implicit val playerBiSLinkFormat: RootJsonFormat[PlayerBiSLinkResponse] = jsonFormat2(PlayerBiSLinkResponse.apply)
|
implicit val playerIdFormat: RootJsonFormat[PlayerIdModel] = jsonFormat3(PlayerIdModel.apply)
|
||||||
implicit val playerIdWithCountersFormat: RootJsonFormat[PlayerIdWithCountersResponse] =
|
implicit val pieceActionFormat: RootJsonFormat[PieceActionModel] = jsonFormat4(PieceActionModel.apply)
|
||||||
jsonFormat9(PlayerIdWithCountersResponse.apply)
|
implicit val playerBiSLinkFormat: RootJsonFormat[PlayerBiSLinkModel] = jsonFormat2(PlayerBiSLinkModel.apply)
|
||||||
implicit val userFormat: RootJsonFormat[UserResponse] = jsonFormat4(UserResponse.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)
|
||||||
}
|
}
|
||||||
|
@ -1,20 +1,29 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2019-2022 Evgeniy Alekseev.
|
||||||
|
*
|
||||||
|
* This file is part of ffxivbis
|
||||||
|
* (see https://github.com/arcan1s/ffxivbis).
|
||||||
|
*
|
||||||
|
* License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause
|
||||||
|
*/
|
||||||
package me.arcanis.ffxivbis.http.api.v1.json
|
package me.arcanis.ffxivbis.http.api.v1.json
|
||||||
|
|
||||||
import java.time.Instant
|
|
||||||
|
|
||||||
import io.swagger.v3.oas.annotations.media.Schema
|
import io.swagger.v3.oas.annotations.media.Schema
|
||||||
import me.arcanis.ffxivbis.models.Loot
|
import me.arcanis.ffxivbis.models.Loot
|
||||||
|
|
||||||
case class LootResponse(
|
import java.time.Instant
|
||||||
@Schema(description = "looted piece", required = true) piece: PieceResponse,
|
|
||||||
|
case class LootModel(
|
||||||
|
@Schema(description = "looted piece", required = true) piece: PieceModel,
|
||||||
@Schema(description = "loot timestamp", required = true) timestamp: Instant,
|
@Schema(description = "loot timestamp", required = true) timestamp: Instant,
|
||||||
@Schema(description = "is loot free for all", required = true) isFreeLoot: Boolean) {
|
@Schema(description = "is loot free for all", required = true) isFreeLoot: Boolean
|
||||||
|
) {
|
||||||
|
|
||||||
def toLoot: Loot = Loot(-1, piece.toPiece, timestamp, isFreeLoot)
|
def toLoot: Loot = Loot(-1, piece.toPiece, timestamp, isFreeLoot)
|
||||||
}
|
}
|
||||||
|
|
||||||
object LootResponse {
|
object LootModel {
|
||||||
|
|
||||||
def fromLoot(loot: Loot): LootResponse =
|
def fromLoot(loot: Loot): LootModel =
|
||||||
LootResponse(PieceResponse.fromPiece(loot.piece), loot.timestamp, loot.isFreeLoot)
|
LootModel(PieceModel.fromPiece(loot.piece), loot.timestamp, loot.isFreeLoot)
|
||||||
}
|
}
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (c) 2020 Evgeniy Alekseev.
|
* Copyright (c) 2019-2022 Evgeniy Alekseev.
|
||||||
*
|
*
|
||||||
* This file is part of ffxivbis
|
* This file is part of ffxivbis
|
||||||
* (see https://github.com/arcan1s/ffxivbis).
|
* (see https://github.com/arcan1s/ffxivbis).
|
||||||
@ -11,15 +11,18 @@ package me.arcanis.ffxivbis.http.api.v1.json
|
|||||||
import io.swagger.v3.oas.annotations.media.Schema
|
import io.swagger.v3.oas.annotations.media.Schema
|
||||||
import me.arcanis.ffxivbis.models.PartyDescription
|
import me.arcanis.ffxivbis.models.PartyDescription
|
||||||
|
|
||||||
case class PartyDescriptionResponse(
|
case class PartyDescriptionModel(
|
||||||
@Schema(description = "party id", required = true) partyId: String,
|
@Schema(description = "party id", required = true, example = "o3KicHQPW5b0JcOm5yI3") partyId: String,
|
||||||
@Schema(description = "party name") partyAlias: Option[String]) {
|
@Schema(description = "party name") partyAlias: Option[String]
|
||||||
|
) extends Validator {
|
||||||
|
|
||||||
|
require(partyAlias.forall(isValidString), stringMatchError("Party alias"))
|
||||||
|
|
||||||
def toDescription: PartyDescription = PartyDescription(partyId, partyAlias)
|
def toDescription: PartyDescription = PartyDescription(partyId, partyAlias)
|
||||||
}
|
}
|
||||||
|
|
||||||
object PartyDescriptionResponse {
|
object PartyDescriptionModel {
|
||||||
|
|
||||||
def fromDescription(description: PartyDescription): PartyDescriptionResponse =
|
def fromDescription(description: PartyDescription): PartyDescriptionModel =
|
||||||
PartyDescriptionResponse(description.partyId, description.partyAlias)
|
PartyDescriptionModel(description.partyId, description.partyAlias)
|
||||||
}
|
}
|
@ -0,0 +1,15 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2019-2022 Evgeniy Alekseev.
|
||||||
|
*
|
||||||
|
* This file is part of ffxivbis
|
||||||
|
* (see https://github.com/arcan1s/ffxivbis).
|
||||||
|
*
|
||||||
|
* License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause
|
||||||
|
*/
|
||||||
|
package me.arcanis.ffxivbis.http.api.v1.json
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema
|
||||||
|
|
||||||
|
case class PartyIdModel(
|
||||||
|
@Schema(description = "party id", required = true, example = "o3KicHQPW5b0JcOm5yI3") partyId: String
|
||||||
|
)
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (c) 2019 Evgeniy Alekseev.
|
* Copyright (c) 2019-2022 Evgeniy Alekseev.
|
||||||
*
|
*
|
||||||
* This file is part of ffxivbis
|
* This file is part of ffxivbis
|
||||||
* (see https://github.com/arcan1s/ffxivbis).
|
* (see https://github.com/arcan1s/ffxivbis).
|
||||||
@ -10,8 +10,14 @@ package me.arcanis.ffxivbis.http.api.v1.json
|
|||||||
|
|
||||||
import io.swagger.v3.oas.annotations.media.Schema
|
import io.swagger.v3.oas.annotations.media.Schema
|
||||||
|
|
||||||
case class PieceActionResponse(
|
case class PieceActionModel(
|
||||||
@Schema(description = "action to perform", required = true, `type` = "string", allowableValues = Array("add", "remove")) action: ApiAction.Value,
|
@Schema(
|
||||||
@Schema(description = "piece description", required = true) piece: PieceResponse,
|
description = "action to perform",
|
||||||
@Schema(description = "player description", required = true) playerId: PlayerIdResponse,
|
required = true,
|
||||||
@Schema(description = "is piece free to roll or not") isFreeLoot: Option[Boolean])
|
`type` = "string",
|
||||||
|
allowableValues = Array("add", "remove")
|
||||||
|
) action: ApiAction.Value,
|
||||||
|
@Schema(description = "piece description", required = true) piece: PieceModel,
|
||||||
|
@Schema(description = "player description", required = true) playerId: PlayerIdModel,
|
||||||
|
@Schema(description = "is piece free to roll or not", `type` = "boolean") isFreeLoot: Option[Boolean]
|
||||||
|
)
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (c) 2019 Evgeniy Alekseev.
|
* Copyright (c) 2019-2022 Evgeniy Alekseev.
|
||||||
*
|
*
|
||||||
* This file is part of ffxivbis
|
* This file is part of ffxivbis
|
||||||
* (see https://github.com/arcan1s/ffxivbis).
|
* (see https://github.com/arcan1s/ffxivbis).
|
||||||
@ -11,16 +11,17 @@ package me.arcanis.ffxivbis.http.api.v1.json
|
|||||||
import io.swagger.v3.oas.annotations.media.Schema
|
import io.swagger.v3.oas.annotations.media.Schema
|
||||||
import me.arcanis.ffxivbis.models.{Job, Piece, PieceType}
|
import me.arcanis.ffxivbis.models.{Job, Piece, PieceType}
|
||||||
|
|
||||||
case class PieceResponse(
|
case class PieceModel(
|
||||||
@Schema(description = "piece type", required = true) pieceType: String,
|
@Schema(description = "piece type", required = true, example = "Savage") pieceType: String,
|
||||||
@Schema(description = "job name to which piece belong or AnyJob", required = true, example = "DNC") job: String,
|
@Schema(description = "job name to which piece belong or AnyJob", required = true, example = "DNC") job: String,
|
||||||
@Schema(description = "piece name", required = true, example = "body") piece: String) {
|
@Schema(description = "piece name", required = true, example = "body") piece: String
|
||||||
|
) {
|
||||||
|
|
||||||
def toPiece: Piece = Piece(piece, PieceType.withName(pieceType), Job.withName(job))
|
def toPiece: Piece = Piece(piece, PieceType.withName(pieceType), Job.withName(job))
|
||||||
}
|
}
|
||||||
|
|
||||||
object PieceResponse {
|
object PieceModel {
|
||||||
|
|
||||||
def fromPiece(piece: Piece): PieceResponse =
|
def fromPiece(piece: Piece): PieceModel =
|
||||||
PieceResponse(piece.pieceType.toString, piece.job.toString, piece.piece)
|
PieceModel(piece.pieceType.toString, piece.job.toString, piece.piece)
|
||||||
}
|
}
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (c) 2019 Evgeniy Alekseev.
|
* Copyright (c) 2019-2022 Evgeniy Alekseev.
|
||||||
*
|
*
|
||||||
* This file is part of ffxivbis
|
* This file is part of ffxivbis
|
||||||
* (see https://github.com/arcan1s/ffxivbis).
|
* (see https://github.com/arcan1s/ffxivbis).
|
||||||
@ -10,6 +10,13 @@ package me.arcanis.ffxivbis.http.api.v1.json
|
|||||||
|
|
||||||
import io.swagger.v3.oas.annotations.media.Schema
|
import io.swagger.v3.oas.annotations.media.Schema
|
||||||
|
|
||||||
case class PlayerBiSLinkResponse(
|
case class PlayerActionModel(
|
||||||
@Schema(description = "link to player best in slot", required = true, example = "https://ffxiv.ariyala.com/19V5R") link: String,
|
@Schema(
|
||||||
@Schema(description = "player description", required = true) playerId: PlayerIdResponse)
|
description = "action to perform",
|
||||||
|
required = true,
|
||||||
|
`type` = "string",
|
||||||
|
allowableValues = Array("add", "remove"),
|
||||||
|
example = "add"
|
||||||
|
) action: ApiAction.Value,
|
||||||
|
@Schema(description = "player description", required = true) playerId: PlayerModel
|
||||||
|
)
|
@ -1,15 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (c) 2019 Evgeniy Alekseev.
|
|
||||||
*
|
|
||||||
* This file is part of ffxivbis
|
|
||||||
* (see https://github.com/arcan1s/ffxivbis).
|
|
||||||
*
|
|
||||||
* License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause
|
|
||||||
*/
|
|
||||||
package me.arcanis.ffxivbis.http.api.v1.json
|
|
||||||
|
|
||||||
import io.swagger.v3.oas.annotations.media.Schema
|
|
||||||
|
|
||||||
case class PlayerActionResponse(
|
|
||||||
@Schema(description = "action to perform", required = true, `type` = "string", allowableValues = Array("add", "remove"), example = "add") action: ApiAction.Value,
|
|
||||||
@Schema(description = "player description", required = true) playerId: PlayerResponse)
|
|
@ -0,0 +1,23 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2019-2022 Evgeniy Alekseev.
|
||||||
|
*
|
||||||
|
* This file is part of ffxivbis
|
||||||
|
* (see https://github.com/arcan1s/ffxivbis).
|
||||||
|
*
|
||||||
|
* License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause
|
||||||
|
*/
|
||||||
|
package me.arcanis.ffxivbis.http.api.v1.json
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema
|
||||||
|
|
||||||
|
case class PlayerBiSLinkModel(
|
||||||
|
@Schema(
|
||||||
|
description = "link to player best in slot",
|
||||||
|
required = true,
|
||||||
|
example = "https://ffxiv.ariyala.com/19V5R"
|
||||||
|
) link: String,
|
||||||
|
@Schema(description = "player description", required = true) playerId: PlayerIdModel
|
||||||
|
) extends Validator {
|
||||||
|
|
||||||
|
require(isValidString(link), stringMatchError("BiS link"))
|
||||||
|
}
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (c) 2019 Evgeniy Alekseev.
|
* Copyright (c) 2019-2022 Evgeniy Alekseev.
|
||||||
*
|
*
|
||||||
* This file is part of ffxivbis
|
* This file is part of ffxivbis
|
||||||
* (see https://github.com/arcan1s/ffxivbis).
|
* (see https://github.com/arcan1s/ffxivbis).
|
||||||
@ -11,17 +11,22 @@ package me.arcanis.ffxivbis.http.api.v1.json
|
|||||||
import io.swagger.v3.oas.annotations.media.Schema
|
import io.swagger.v3.oas.annotations.media.Schema
|
||||||
import me.arcanis.ffxivbis.models.{Job, PlayerId}
|
import me.arcanis.ffxivbis.models.{Job, PlayerId}
|
||||||
|
|
||||||
case class PlayerIdResponse(
|
case class PlayerIdModel(
|
||||||
@Schema(description = "unique party ID. Required in responses", example = "abcdefgh") partyId: Option[String],
|
@Schema(description = "unique party ID. Required in responses", example = "o3KicHQPW5b0JcOm5yI3") partyId: Option[
|
||||||
|
String
|
||||||
|
],
|
||||||
@Schema(description = "job name", required = true, example = "DNC") job: String,
|
@Schema(description = "job name", required = true, example = "DNC") job: String,
|
||||||
@Schema(description = "player nick name", required = true, example = "Siuan Sanche") nick: String) {
|
@Schema(description = "player nick name", required = true, example = "Siuan Sanche") nick: String
|
||||||
|
) extends Validator {
|
||||||
|
|
||||||
|
require(isValidString(nick), stringMatchError("Player name"))
|
||||||
|
|
||||||
def withPartyId(partyId: String): PlayerId =
|
def withPartyId(partyId: String): PlayerId =
|
||||||
PlayerId(partyId, Job.withName(job), nick)
|
PlayerId(partyId, Job.withName(job), nick)
|
||||||
}
|
}
|
||||||
|
|
||||||
object PlayerIdResponse {
|
object PlayerIdModel {
|
||||||
|
|
||||||
def fromPlayerId(playerId: PlayerId): PlayerIdResponse =
|
def fromPlayerId(playerId: PlayerId): PlayerIdModel =
|
||||||
PlayerIdResponse(Some(playerId.partyId), playerId.job.toString, playerId.nick)
|
PlayerIdModel(Some(playerId.partyId), playerId.job.toString, playerId.nick)
|
||||||
}
|
}
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (c) 2019 Evgeniy Alekseev.
|
* Copyright (c) 2019-2022 Evgeniy Alekseev.
|
||||||
*
|
*
|
||||||
* This file is part of ffxivbis
|
* This file is part of ffxivbis
|
||||||
* (see https://github.com/arcan1s/ffxivbis).
|
* (see https://github.com/arcan1s/ffxivbis).
|
||||||
@ -11,21 +11,22 @@ package me.arcanis.ffxivbis.http.api.v1.json
|
|||||||
import io.swagger.v3.oas.annotations.media.Schema
|
import io.swagger.v3.oas.annotations.media.Schema
|
||||||
import me.arcanis.ffxivbis.models.PlayerIdWithCounters
|
import me.arcanis.ffxivbis.models.PlayerIdWithCounters
|
||||||
|
|
||||||
case class PlayerIdWithCountersResponse(
|
case class PlayerIdWithCountersModel(
|
||||||
@Schema(description = "unique party ID", required = true, example = "abcdefgh") partyId: String,
|
@Schema(description = "unique party ID", required = true, example = "o3KicHQPW5b0JcOm5yI3") partyId: String,
|
||||||
@Schema(description = "job name", required = true, example = "DNC") job: String,
|
@Schema(description = "job name", required = true, example = "DNC") job: String,
|
||||||
@Schema(description = "player nick name", required = true, example = "Siuan Sanche") nick: String,
|
@Schema(description = "player nick name", required = true, example = "Siuan Sanche") nick: String,
|
||||||
@Schema(description = "is piece required by player or not", required = true) isRequired: Boolean,
|
@Schema(description = "is piece required by player or not", required = true) isRequired: Boolean,
|
||||||
@Schema(description = "player loot priority", required = true) priority: Int,
|
@Schema(description = "player loot priority", required = true) priority: Int,
|
||||||
@Schema(description = "count of savage pieces in best in slot", required = true) bisCountTotal: Int,
|
@Schema(description = "count of savage pieces in best in slot", required = true) bisCountTotal: Int,
|
||||||
@Schema(description = "count of looted pieces", required = true) lootCount: Int,
|
@Schema(description = "count of looted pieces of this type", required = true) lootCount: Int,
|
||||||
@Schema(description = "count of looted pieces which are parts of best in slot", required = true) lootCountBiS: Int,
|
@Schema(description = "count of looted pieces which are parts of best in slot", required = true) lootCountBiS: Int,
|
||||||
@Schema(description = "total count of looted pieces", required = true) lootCountTotal: Int)
|
@Schema(description = "total count of looted pieces", required = true) lootCountTotal: Int
|
||||||
|
)
|
||||||
|
|
||||||
object PlayerIdWithCountersResponse {
|
object PlayerIdWithCountersModel {
|
||||||
|
|
||||||
def fromPlayerId(playerIdWithCounters: PlayerIdWithCounters): PlayerIdWithCountersResponse =
|
def fromPlayerId(playerIdWithCounters: PlayerIdWithCounters): PlayerIdWithCountersModel =
|
||||||
PlayerIdWithCountersResponse(
|
PlayerIdWithCountersModel(
|
||||||
playerIdWithCounters.partyId,
|
playerIdWithCounters.partyId,
|
||||||
playerIdWithCounters.job.toString,
|
playerIdWithCounters.job.toString,
|
||||||
playerIdWithCounters.nick,
|
playerIdWithCounters.nick,
|
||||||
@ -34,5 +35,6 @@ object PlayerIdWithCountersResponse {
|
|||||||
playerIdWithCounters.bisCountTotal,
|
playerIdWithCounters.bisCountTotal,
|
||||||
playerIdWithCounters.lootCount,
|
playerIdWithCounters.lootCount,
|
||||||
playerIdWithCounters.lootCountBiS,
|
playerIdWithCounters.lootCountBiS,
|
||||||
playerIdWithCounters.lootCountTotal)
|
playerIdWithCounters.lootCountTotal
|
||||||
|
)
|
||||||
}
|
}
|
@ -0,0 +1,59 @@
|
|||||||
|
/*
|
||||||
|
* 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
|
||||||
|
import me.arcanis.ffxivbis.models.{BiS, Job, Player}
|
||||||
|
|
||||||
|
case class PlayerModel(
|
||||||
|
@Schema(description = "unique party ID", required = true, example = "o3KicHQPW5b0JcOm5yI3") partyId: String,
|
||||||
|
@Schema(description = "job name", required = true, example = "DNC") job: String,
|
||||||
|
@Schema(description = "player nick name", required = true, example = "Siuan Sanche") nick: String,
|
||||||
|
@Schema(description = "pieces in best in slot") bis: Option[Seq[PieceModel]],
|
||||||
|
@Schema(description = "looted pieces") loot: Option[Seq[LootModel]],
|
||||||
|
@Schema(description = "link to best in slot", example = "https://ffxiv.ariyala.com/19V5R") link: Option[String],
|
||||||
|
@Schema(description = "player loot priority", `type` = "number") priority: Option[Int],
|
||||||
|
@Schema(
|
||||||
|
description = "count of looted pieces which are parts of best in slot",
|
||||||
|
`type` = "number"
|
||||||
|
) lootCountBiS: Option[Int],
|
||||||
|
@Schema(description = "total count of looted pieces", `type` = "number") lootCountTotal: Option[Int],
|
||||||
|
) extends Validator {
|
||||||
|
|
||||||
|
require(isValidString(nick), stringMatchError("Player name"))
|
||||||
|
require(link.forall(isValidString), stringMatchError("BiS link"))
|
||||||
|
|
||||||
|
def toPlayer: Player =
|
||||||
|
Player(
|
||||||
|
-1,
|
||||||
|
partyId,
|
||||||
|
Job.withName(job),
|
||||||
|
nick,
|
||||||
|
BiS(bis.getOrElse(Seq.empty).map(_.toPiece)),
|
||||||
|
loot.getOrElse(Seq.empty).map(_.toLoot),
|
||||||
|
link,
|
||||||
|
priority.getOrElse(0)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
object PlayerModel {
|
||||||
|
|
||||||
|
def fromPlayer(player: Player): PlayerModel =
|
||||||
|
PlayerModel(
|
||||||
|
player.partyId,
|
||||||
|
player.job.toString,
|
||||||
|
player.nick,
|
||||||
|
Some(player.bis.pieces.map(PieceModel.fromPiece)),
|
||||||
|
Some(player.loot.map(LootModel.fromLoot)),
|
||||||
|
player.link,
|
||||||
|
Some(player.priority),
|
||||||
|
Some(player.lootCountBiS),
|
||||||
|
Some(player.lootCountTotal),
|
||||||
|
)
|
||||||
|
}
|
@ -1,37 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (c) 2019 Evgeniy Alekseev.
|
|
||||||
*
|
|
||||||
* This file is part of ffxivbis
|
|
||||||
* (see https://github.com/arcan1s/ffxivbis).
|
|
||||||
*
|
|
||||||
* License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause
|
|
||||||
*/
|
|
||||||
package me.arcanis.ffxivbis.http.api.v1.json
|
|
||||||
|
|
||||||
import io.swagger.v3.oas.annotations.media.Schema
|
|
||||||
import me.arcanis.ffxivbis.models.{BiS, Job, Player}
|
|
||||||
|
|
||||||
case class PlayerResponse(
|
|
||||||
@Schema(description = "unique party ID", required = true, example = "abcdefgh") partyId: String,
|
|
||||||
@Schema(description = "job name", required = true, example = "DNC") job: String,
|
|
||||||
@Schema(description = "player nick name", required = true, example = "Siuan Sanche") nick: String,
|
|
||||||
@Schema(description = "pieces in best in slot") bis: Option[Seq[PieceResponse]],
|
|
||||||
@Schema(description = "looted pieces") loot: Option[Seq[LootResponse]],
|
|
||||||
@Schema(description = "link to best in slot", example = "https://ffxiv.ariyala.com/19V5R") link: Option[String],
|
|
||||||
@Schema(description = "player loot priority", `type` = "number") priority: Option[Int]) {
|
|
||||||
|
|
||||||
def toPlayer: Player =
|
|
||||||
Player(-1, partyId, Job.withName(job), nick,
|
|
||||||
BiS(bis.getOrElse(Seq.empty).map(_.toPiece)),
|
|
||||||
loot.getOrElse(Seq.empty).map(_.toLoot),
|
|
||||||
link, priority.getOrElse(0))
|
|
||||||
}
|
|
||||||
|
|
||||||
object PlayerResponse {
|
|
||||||
|
|
||||||
def fromPlayer(player: Player): PlayerResponse =
|
|
||||||
PlayerResponse(player.partyId, player.job.toString, player.nick,
|
|
||||||
Some(player.bis.pieces.map(PieceResponse.fromPiece)),
|
|
||||||
Some(player.loot.map(LootResponse.fromLoot)),
|
|
||||||
player.link, Some(player.priority))
|
|
||||||
}
|
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (c) 2019 Evgeniy Alekseev.
|
* Copyright (c) 2019-2022 Evgeniy Alekseev.
|
||||||
*
|
*
|
||||||
* This file is part of ffxivbis
|
* This file is part of ffxivbis
|
||||||
* (see https://github.com/arcan1s/ffxivbis).
|
* (see https://github.com/arcan1s/ffxivbis).
|
||||||
@ -10,5 +10,4 @@ package me.arcanis.ffxivbis.http.api.v1.json
|
|||||||
|
|
||||||
import io.swagger.v3.oas.annotations.media.Schema
|
import io.swagger.v3.oas.annotations.media.Schema
|
||||||
|
|
||||||
case class PartyIdResponse(
|
case class StatusModel(@Schema(description = "server version") version: Option[String])
|
||||||
@Schema(description = "party id", required = true) partyId: String)
|
|
@ -0,0 +1,41 @@
|
|||||||
|
/*
|
||||||
|
* 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
|
||||||
|
import me.arcanis.ffxivbis.models.{Permission, User}
|
||||||
|
|
||||||
|
case class UserModel(
|
||||||
|
@Schema(description = "unique party ID", required = true, example = "o3KicHQPW5b0JcOm5yI3") partyId: String,
|
||||||
|
@Schema(description = "username to login to party", required = true, example = "siuan") username: String,
|
||||||
|
@Schema(description = "password to login to party, required for user editing", example = "pa55w0rd") password: Option[
|
||||||
|
String
|
||||||
|
],
|
||||||
|
@Schema(
|
||||||
|
description = "user permission",
|
||||||
|
defaultValue = "get",
|
||||||
|
`type` = "string",
|
||||||
|
allowableValues = Array("get", "post", "admin")
|
||||||
|
) permission: Option[Permission.Value] = None
|
||||||
|
) extends Validator {
|
||||||
|
|
||||||
|
require(isValidString(username), stringMatchError("Username"))
|
||||||
|
require(password.forall(_.nonEmpty), "Password must not be empty")
|
||||||
|
|
||||||
|
def toUser: User =
|
||||||
|
password.fold(throw new IllegalArgumentException("Password must noot be empty"))(
|
||||||
|
User(partyId, username, _, permission.getOrElse(Permission.get))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
object UserModel {
|
||||||
|
|
||||||
|
def fromUser(user: User): UserModel =
|
||||||
|
UserModel(user.partyId, user.username, None, Some(user.permission))
|
||||||
|
}
|
@ -1,28 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (c) 2019 Evgeniy Alekseev.
|
|
||||||
*
|
|
||||||
* This file is part of ffxivbis
|
|
||||||
* (see https://github.com/arcan1s/ffxivbis).
|
|
||||||
*
|
|
||||||
* License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause
|
|
||||||
*/
|
|
||||||
package me.arcanis.ffxivbis.http.api.v1.json
|
|
||||||
|
|
||||||
import io.swagger.v3.oas.annotations.media.Schema
|
|
||||||
import me.arcanis.ffxivbis.models.{Permission, User}
|
|
||||||
|
|
||||||
case class UserResponse(
|
|
||||||
@Schema(description = "unique party ID", required = true, example = "abcdefgh") partyId: String,
|
|
||||||
@Schema(description = "username to login to party", required = true, example = "siuan") username: String,
|
|
||||||
@Schema(description = "password to login to party", required = true, example = "pa55w0rd") password: String,
|
|
||||||
@Schema(description = "user permission", defaultValue = "get", allowableValues = Array("get", "post", "admin")) permission: Option[Permission.Value] = None) {
|
|
||||||
|
|
||||||
def toUser: User =
|
|
||||||
User(partyId, username, password, permission.getOrElse(Permission.get))
|
|
||||||
}
|
|
||||||
|
|
||||||
object UserResponse {
|
|
||||||
|
|
||||||
def fromUser(user: User): UserResponse =
|
|
||||||
UserResponse(user.partyId, user.username, "", Some(user.permission))
|
|
||||||
}
|
|
@ -0,0 +1,9 @@
|
|||||||
|
package me.arcanis.ffxivbis.http.api.v1.json
|
||||||
|
|
||||||
|
import me.arcanis.ffxivbis.http.ValidatorHelper
|
||||||
|
|
||||||
|
trait Validator extends ValidatorHelper {
|
||||||
|
|
||||||
|
def stringMatchError(what: String): String =
|
||||||
|
s"$what must contain only letters or digits or one of (${ValidatorHelper.VALID_CHARACTERS.mkString(", ")})"
|
||||||
|
}
|
@ -0,0 +1,61 @@
|
|||||||
|
/*
|
||||||
|
* 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
|
||||||
|
import akka.actor.typed.{ActorRef, Scheduler}
|
||||||
|
import akka.util.Timeout
|
||||||
|
import me.arcanis.ffxivbis.http.api.v1.json.ApiAction
|
||||||
|
import me.arcanis.ffxivbis.messages.DatabaseMessage._
|
||||||
|
import me.arcanis.ffxivbis.messages.Message
|
||||||
|
import me.arcanis.ffxivbis.models.{Piece, Player, PlayerId}
|
||||||
|
|
||||||
|
import scala.concurrent.{ExecutionContext, Future}
|
||||||
|
|
||||||
|
trait BiSHelper extends BisProviderHelper {
|
||||||
|
|
||||||
|
def storage: ActorRef[Message]
|
||||||
|
|
||||||
|
def addPieceBiS(playerId: PlayerId, piece: Piece)(implicit timeout: Timeout, scheduler: Scheduler): Future[Unit] =
|
||||||
|
storage.ask(AddPieceToBis(playerId, piece.withJob(playerId.job), _))
|
||||||
|
|
||||||
|
def bis(partyId: String, playerId: Option[PlayerId])(implicit
|
||||||
|
timeout: Timeout,
|
||||||
|
scheduler: Scheduler
|
||||||
|
): Future[Seq[Player]] =
|
||||||
|
storage.ask(GetBiS(partyId, playerId, _))
|
||||||
|
|
||||||
|
def doModifyBiS(action: ApiAction.Value, playerId: PlayerId, piece: Piece)(implicit
|
||||||
|
timeout: Timeout,
|
||||||
|
scheduler: Scheduler
|
||||||
|
): Future[Unit] =
|
||||||
|
action match {
|
||||||
|
case ApiAction.add => addPieceBiS(playerId, piece)
|
||||||
|
case ApiAction.remove => removePieceBiS(playerId, piece)
|
||||||
|
}
|
||||||
|
|
||||||
|
def putBiS(playerId: PlayerId, link: String)(implicit
|
||||||
|
executionContext: ExecutionContext,
|
||||||
|
timeout: Timeout,
|
||||||
|
scheduler: Scheduler
|
||||||
|
): Future[Unit] =
|
||||||
|
storage
|
||||||
|
.ask(RemovePiecesFromBiS(playerId, _))
|
||||||
|
.flatMap { _ =>
|
||||||
|
downloadBiS(link, playerId.job)
|
||||||
|
.flatMap { bis =>
|
||||||
|
Future.traverse(bis.pieces)(addPieceBiS(playerId, _))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.flatMap(_ => storage.ask(UpdateBiSLink(playerId, link, _)))
|
||||||
|
|
||||||
|
def removePieceBiS(playerId: PlayerId, piece: Piece)(implicit timeout: Timeout, scheduler: Scheduler): Future[Unit] =
|
||||||
|
storage.ask(RemovePieceFromBiS(playerId, piece, _))
|
||||||
|
|
||||||
|
}
|
@ -1,17 +1,18 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (c) 2019 Evgeniy Alekseev.
|
* Copyright (c) 2019-2022 Evgeniy Alekseev.
|
||||||
*
|
*
|
||||||
* This file is part of ffxivbis
|
* This file is part of ffxivbis
|
||||||
* (see https://github.com/arcan1s/ffxivbis).
|
* (see https://github.com/arcan1s/ffxivbis).
|
||||||
*
|
*
|
||||||
* License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause
|
* License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause
|
||||||
*/
|
*/
|
||||||
package me.arcanis.ffxivbis.http
|
package me.arcanis.ffxivbis.http.helpers
|
||||||
|
|
||||||
import akka.actor.typed.{ActorRef, Scheduler}
|
|
||||||
import akka.actor.typed.scaladsl.AskPattern.Askable
|
import akka.actor.typed.scaladsl.AskPattern.Askable
|
||||||
|
import akka.actor.typed.{ActorRef, Scheduler}
|
||||||
import akka.util.Timeout
|
import akka.util.Timeout
|
||||||
import me.arcanis.ffxivbis.messages.{BiSProviderMessage, DownloadBiS}
|
import me.arcanis.ffxivbis.messages.BiSProviderMessage
|
||||||
|
import me.arcanis.ffxivbis.messages.BiSProviderMessage._
|
||||||
import me.arcanis.ffxivbis.models.{BiS, Job}
|
import me.arcanis.ffxivbis.models.{BiS, Job}
|
||||||
|
|
||||||
import scala.concurrent.Future
|
import scala.concurrent.Future
|
||||||
@ -20,7 +21,6 @@ trait BisProviderHelper {
|
|||||||
|
|
||||||
def provider: ActorRef[BiSProviderMessage]
|
def provider: ActorRef[BiSProviderMessage]
|
||||||
|
|
||||||
def downloadBiS(link: String, job: Job.Job)
|
def downloadBiS(link: String, job: Job)(implicit timeout: Timeout, scheduler: Scheduler): Future[BiS] =
|
||||||
(implicit timeout: Timeout, scheduler: Scheduler): Future[BiS] =
|
|
||||||
provider.ask(DownloadBiS(link, job, _))
|
provider.ask(DownloadBiS(link, job, _))
|
||||||
}
|
}
|
@ -0,0 +1,59 @@
|
|||||||
|
/*
|
||||||
|
* 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
|
||||||
|
import akka.actor.typed.{ActorRef, Scheduler}
|
||||||
|
import akka.util.Timeout
|
||||||
|
import me.arcanis.ffxivbis.http.api.v1.json.ApiAction
|
||||||
|
import me.arcanis.ffxivbis.messages.DatabaseMessage._
|
||||||
|
import me.arcanis.ffxivbis.messages.Message
|
||||||
|
import me.arcanis.ffxivbis.models.{Piece, Player, PlayerId, PlayerIdWithCounters}
|
||||||
|
|
||||||
|
import scala.concurrent.{ExecutionContext, Future}
|
||||||
|
|
||||||
|
trait LootHelper {
|
||||||
|
|
||||||
|
def storage: ActorRef[Message]
|
||||||
|
|
||||||
|
def addPieceLoot(playerId: PlayerId, piece: Piece, isFreeLoot: Boolean)(implicit
|
||||||
|
timeout: Timeout,
|
||||||
|
scheduler: Scheduler
|
||||||
|
): Future[Unit] =
|
||||||
|
storage.ask(AddPieceTo(playerId, piece, isFreeLoot, _))
|
||||||
|
|
||||||
|
def doModifyLoot(action: ApiAction.Value, playerId: PlayerId, piece: Piece, maybeFree: Option[Boolean])(implicit
|
||||||
|
timeout: Timeout,
|
||||||
|
scheduler: Scheduler
|
||||||
|
): Future[Unit] =
|
||||||
|
(action, maybeFree) match {
|
||||||
|
case (ApiAction.add, Some(isFreeLoot)) => addPieceLoot(playerId, piece, isFreeLoot)
|
||||||
|
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
|
||||||
|
timeout: Timeout,
|
||||||
|
scheduler: Scheduler
|
||||||
|
): Future[Seq[Player]] =
|
||||||
|
storage.ask(GetLoot(partyId, playerId, _))
|
||||||
|
|
||||||
|
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,
|
||||||
|
timeout: Timeout,
|
||||||
|
scheduler: Scheduler
|
||||||
|
): Future[Seq[PlayerIdWithCounters]] =
|
||||||
|
storage.ask(SuggestLoot(partyId, piece, _)).map(_.result)
|
||||||
|
}
|
@ -0,0 +1,75 @@
|
|||||||
|
/*
|
||||||
|
* 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
|
||||||
|
import akka.actor.typed.{ActorRef, Scheduler}
|
||||||
|
import akka.util.Timeout
|
||||||
|
import me.arcanis.ffxivbis.http.api.v1.json.ApiAction
|
||||||
|
import me.arcanis.ffxivbis.messages.DatabaseMessage._
|
||||||
|
import me.arcanis.ffxivbis.messages.Message
|
||||||
|
import me.arcanis.ffxivbis.models.{PartyDescription, Player, PlayerId}
|
||||||
|
|
||||||
|
import scala.concurrent.{ExecutionContext, Future}
|
||||||
|
|
||||||
|
trait PlayerHelper extends BisProviderHelper {
|
||||||
|
|
||||||
|
def storage: ActorRef[Message]
|
||||||
|
|
||||||
|
def addPlayer(
|
||||||
|
player: Player
|
||||||
|
)(implicit executionContext: ExecutionContext, timeout: Timeout, scheduler: Scheduler): Future[Unit] =
|
||||||
|
storage
|
||||||
|
.ask(ref => AddPlayer(player, ref))
|
||||||
|
.map { _ =>
|
||||||
|
player.link.map(_.trim).filter(_.nonEmpty) match {
|
||||||
|
case Some(link) =>
|
||||||
|
downloadBiS(link, player.job)
|
||||||
|
.map { bis =>
|
||||||
|
bis.pieces.map(piece => storage.ask(AddPieceToBis(player.playerId, piece, _)))
|
||||||
|
}
|
||||||
|
.flatMap(_ => storage.ask(UpdateBiSLink(player.playerId, link, _)))
|
||||||
|
case None => Future.successful(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.flatten
|
||||||
|
|
||||||
|
def doModifyPlayer(action: ApiAction.Value, player: Player)(implicit
|
||||||
|
executionContext: ExecutionContext,
|
||||||
|
timeout: Timeout,
|
||||||
|
scheduler: Scheduler
|
||||||
|
): Future[Unit] =
|
||||||
|
action match {
|
||||||
|
case ApiAction.add => addPlayer(player)
|
||||||
|
case ApiAction.remove => removePlayer(player.playerId)
|
||||||
|
}
|
||||||
|
|
||||||
|
def getPartyDescription(partyId: String)(implicit timeout: Timeout, scheduler: Scheduler): Future[PartyDescription] =
|
||||||
|
storage.ask(GetPartyDescription(partyId, _))
|
||||||
|
|
||||||
|
def getPlayers(partyId: String, maybePlayerId: Option[PlayerId])(implicit
|
||||||
|
executionContext: ExecutionContext,
|
||||||
|
timeout: Timeout,
|
||||||
|
scheduler: Scheduler
|
||||||
|
): Future[Seq[Player]] =
|
||||||
|
maybePlayerId match {
|
||||||
|
case Some(playerId) =>
|
||||||
|
storage.ask(GetPlayer(playerId, _)).map(_.toSeq)
|
||||||
|
case None =>
|
||||||
|
storage.ask(GetParty(partyId, _)).map(_.players.values.toSeq)
|
||||||
|
}
|
||||||
|
|
||||||
|
def removePlayer(playerId: PlayerId)(implicit timeout: Timeout, scheduler: Scheduler): Future[Unit] =
|
||||||
|
storage.ask(RemovePlayer(playerId, _))
|
||||||
|
|
||||||
|
def updateDescription(
|
||||||
|
partyDescription: PartyDescription
|
||||||
|
)(implicit executionContext: ExecutionContext, timeout: Timeout, scheduler: Scheduler): Future[Unit] =
|
||||||
|
storage.ask(UpdateParty(partyDescription, _))
|
||||||
|
}
|
@ -1,17 +1,19 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (c) 2019 Evgeniy Alekseev.
|
* Copyright (c) 2019-2022 Evgeniy Alekseev.
|
||||||
*
|
*
|
||||||
* This file is part of ffxivbis
|
* This file is part of ffxivbis
|
||||||
* (see https://github.com/arcan1s/ffxivbis).
|
* (see https://github.com/arcan1s/ffxivbis).
|
||||||
*
|
*
|
||||||
* License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause
|
* License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause
|
||||||
*/
|
*/
|
||||||
package me.arcanis.ffxivbis.http
|
package me.arcanis.ffxivbis.http.helpers
|
||||||
|
|
||||||
import akka.actor.typed.scaladsl.AskPattern.Askable
|
import akka.actor.typed.scaladsl.AskPattern.Askable
|
||||||
import akka.actor.typed.{ActorRef, Scheduler}
|
import akka.actor.typed.{ActorRef, Scheduler}
|
||||||
import akka.util.Timeout
|
import akka.util.Timeout
|
||||||
import me.arcanis.ffxivbis.messages.{AddUser, DeleteUser, GetNewPartyId, GetUser, GetUsers, Message}
|
import me.arcanis.ffxivbis.messages.ControlMessage.GetNewPartyId
|
||||||
|
import me.arcanis.ffxivbis.messages.DatabaseMessage._
|
||||||
|
import me.arcanis.ffxivbis.messages.Message
|
||||||
import me.arcanis.ffxivbis.models.User
|
import me.arcanis.ffxivbis.models.User
|
||||||
|
|
||||||
import scala.concurrent.Future
|
import scala.concurrent.Future
|
||||||
@ -20,22 +22,18 @@ trait UserHelper {
|
|||||||
|
|
||||||
def storage: ActorRef[Message]
|
def storage: ActorRef[Message]
|
||||||
|
|
||||||
def addUser(user: User, isHashedPassword: Boolean)
|
def addUser(user: User, isHashedPassword: Boolean)(implicit timeout: Timeout, scheduler: Scheduler): Future[Unit] =
|
||||||
(implicit timeout: Timeout, scheduler: Scheduler): Future[Unit] =
|
|
||||||
storage.ask(AddUser(user, isHashedPassword, _))
|
storage.ask(AddUser(user, isHashedPassword, _))
|
||||||
|
|
||||||
def newPartyId(implicit timeout: Timeout, scheduler: Scheduler): Future[String] =
|
def newPartyId(implicit timeout: Timeout, scheduler: Scheduler): Future[String] =
|
||||||
storage.ask(GetNewPartyId)
|
storage.ask(GetNewPartyId)
|
||||||
|
|
||||||
def user(partyId: String, username: String)
|
def user(partyId: String, username: String)(implicit timeout: Timeout, scheduler: Scheduler): Future[Option[User]] =
|
||||||
(implicit timeout: Timeout, scheduler: Scheduler): Future[Option[User]] =
|
|
||||||
storage.ask(GetUser(partyId, username, _))
|
storage.ask(GetUser(partyId, username, _))
|
||||||
|
|
||||||
def users(partyId: String)
|
def users(partyId: String)(implicit timeout: Timeout, scheduler: Scheduler): Future[Seq[User]] =
|
||||||
(implicit timeout: Timeout, scheduler: Scheduler): Future[Seq[User]] =
|
|
||||||
storage.ask(GetUsers(partyId, _))
|
storage.ask(GetUsers(partyId, _))
|
||||||
|
|
||||||
def removeUser(partyId: String, username: String)
|
def removeUser(partyId: String, username: String)(implicit timeout: Timeout, scheduler: Scheduler): Future[Unit] =
|
||||||
(implicit timeout: Timeout, scheduler: Scheduler): Future[Unit] =
|
|
||||||
storage.ask(DeleteUser(partyId, username, _))
|
storage.ask(DeleteUser(partyId, username, _))
|
||||||
}
|
}
|
@ -1,71 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (c) 2019 Evgeniy Alekseev.
|
|
||||||
*
|
|
||||||
* This file is part of ffxivbis
|
|
||||||
* (see https://github.com/arcan1s/ffxivbis).
|
|
||||||
*
|
|
||||||
* License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause
|
|
||||||
*/
|
|
||||||
package me.arcanis.ffxivbis.http.view
|
|
||||||
|
|
||||||
import akka.actor.typed.{ActorRef, Scheduler}
|
|
||||||
import akka.http.scaladsl.model.StatusCodes
|
|
||||||
import akka.http.scaladsl.server.Directives._
|
|
||||||
import akka.http.scaladsl.server._
|
|
||||||
import akka.util.Timeout
|
|
||||||
import me.arcanis.ffxivbis.http.{Authorization, PlayerHelper}
|
|
||||||
import me.arcanis.ffxivbis.messages.{BiSProviderMessage, Message}
|
|
||||||
|
|
||||||
import scala.util.{Failure, Success}
|
|
||||||
|
|
||||||
class BasePartyView(override val storage: ActorRef[Message],
|
|
||||||
override val provider: ActorRef[BiSProviderMessage])
|
|
||||||
(implicit timeout: Timeout, scheduler: Scheduler)
|
|
||||||
extends PlayerHelper with Authorization {
|
|
||||||
|
|
||||||
def route: Route = getIndex
|
|
||||||
|
|
||||||
def getIndex: Route =
|
|
||||||
path("party" / Segment) { partyId =>
|
|
||||||
extractExecutionContext { implicit executionContext =>
|
|
||||||
authenticateBasicBCrypt(s"party $partyId", authGet(partyId)) { _ =>
|
|
||||||
get {
|
|
||||||
onComplete(getPartyDescription(partyId)) {
|
|
||||||
case Success(description) =>
|
|
||||||
complete(StatusCodes.OK, RootView.toHtml(BasePartyView.template(partyId, description.alias)))
|
|
||||||
case Failure(exception) => throw exception
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
object BasePartyView {
|
|
||||||
import scalatags.Text
|
|
||||||
import scalatags.Text.all._
|
|
||||||
import scalatags.Text.tags2.{title => titleTag}
|
|
||||||
|
|
||||||
def root(partyId: String): Text.TypedTag[String] =
|
|
||||||
a(href:=s"/party/$partyId", title:="root")("root")
|
|
||||||
|
|
||||||
def template(partyId: String, alias: String): String =
|
|
||||||
"<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01//EN\" \"http://www.w3.org/TR/html4/strict.dtd\">" +
|
|
||||||
html(lang:="en",
|
|
||||||
head(
|
|
||||||
titleTag(s"Party $alias"),
|
|
||||||
link(rel:="stylesheet", `type`:="text/css", href:="/static/styles.css")
|
|
||||||
),
|
|
||||||
|
|
||||||
body(
|
|
||||||
h2(s"Party $alias"),
|
|
||||||
br,
|
|
||||||
h2(a(href:=s"/party/$partyId/players", title:="party")("party")),
|
|
||||||
h2(a(href:=s"/party/$partyId/bis", title:="bis management")("best in slot")),
|
|
||||||
h2(a(href:=s"/party/$partyId/loot", title:="loot management")("loot")),
|
|
||||||
h2(a(href:=s"/party/$partyId/suggest", title:="suggest loot")("suggest")),
|
|
||||||
hr,
|
|
||||||
h2(a(href:=s"/party/$partyId/users", title:="user management")("users"))
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
@ -1,155 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (c) 2019 Evgeniy Alekseev.
|
|
||||||
*
|
|
||||||
* This file is part of ffxivbis
|
|
||||||
* (see https://github.com/arcan1s/ffxivbis).
|
|
||||||
*
|
|
||||||
* License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause
|
|
||||||
*/
|
|
||||||
package me.arcanis.ffxivbis.http.view
|
|
||||||
|
|
||||||
import akka.actor.typed.{ActorRef, Scheduler}
|
|
||||||
import akka.http.scaladsl.model.StatusCodes
|
|
||||||
import akka.http.scaladsl.server.Directives._
|
|
||||||
import akka.http.scaladsl.server._
|
|
||||||
import akka.util.Timeout
|
|
||||||
import me.arcanis.ffxivbis.http.{Authorization, BiSHelper}
|
|
||||||
import me.arcanis.ffxivbis.messages.{BiSProviderMessage, Message}
|
|
||||||
import me.arcanis.ffxivbis.models.{Piece, PieceType, Player, PlayerId}
|
|
||||||
|
|
||||||
import scala.concurrent.{ExecutionContext, Future}
|
|
||||||
import scala.util.Try
|
|
||||||
|
|
||||||
class BiSView(override val storage: ActorRef[Message],
|
|
||||||
override val provider: ActorRef[BiSProviderMessage])
|
|
||||||
(implicit timeout: Timeout, scheduler: Scheduler)
|
|
||||||
extends BiSHelper with Authorization {
|
|
||||||
|
|
||||||
def route: Route = getBiS ~ modifyBiS
|
|
||||||
|
|
||||||
def getBiS: Route =
|
|
||||||
path("party" / Segment / "bis") { partyId: String =>
|
|
||||||
extractExecutionContext { implicit executionContext =>
|
|
||||||
authenticateBasicBCrypt(s"party $partyId", authGet(partyId)) { _ =>
|
|
||||||
get {
|
|
||||||
complete {
|
|
||||||
bis(partyId, None).map { players =>
|
|
||||||
BiSView.template(partyId, players, None)
|
|
||||||
}.map { text =>
|
|
||||||
(StatusCodes.OK, RootView.toHtml(text))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
def modifyBiS: Route =
|
|
||||||
path("party" / Segment / "bis") { partyId: String =>
|
|
||||||
extractExecutionContext { implicit executionContext =>
|
|
||||||
authenticateBasicBCrypt(s"party $partyId", authPost(partyId)) { _ =>
|
|
||||||
post {
|
|
||||||
formFields("player".as[String], "piece".as[String].?, "piece_type".as[String].?, "link".as[String].?, "action".as[String]) {
|
|
||||||
(player, maybePiece, maybePieceType, maybeLink, action) =>
|
|
||||||
onComplete(modifyBiSCall(partyId, player, maybePiece, maybePieceType, maybeLink, action)) { _ =>
|
|
||||||
redirect(s"/party/$partyId/bis", StatusCodes.Found)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private def modifyBiSCall(partyId: String, player: String,
|
|
||||||
maybePiece: Option[String], maybePieceType: Option[String],
|
|
||||||
maybeLink: Option[String], action: String)
|
|
||||||
(implicit executionContext: ExecutionContext, timeout: Timeout): Future[Unit] = {
|
|
||||||
def getPiece(playerId: PlayerId, piece: String, pieceType: String) =
|
|
||||||
Try(Piece(piece, PieceType.withName(pieceType), playerId.job)).toOption
|
|
||||||
|
|
||||||
def bisAction(playerId: PlayerId, piece: String, pieceType: String)(fn: Piece => Future[Unit]) =
|
|
||||||
getPiece(playerId, piece, pieceType) match {
|
|
||||||
case Some(item) => fn(item).map(_ => ())
|
|
||||||
case _ => Future.failed(new Error(s"Could not construct piece from `$piece ($pieceType)`"))
|
|
||||||
}
|
|
||||||
|
|
||||||
PlayerId(partyId, player) match {
|
|
||||||
case Some(playerId) => (maybePiece, maybePieceType, action, maybeLink) match {
|
|
||||||
case (Some(piece), Some(pieceType), "add", _) =>
|
|
||||||
bisAction(playerId, piece, pieceType)(addPieceBiS(playerId, _))
|
|
||||||
case (Some(piece), Some(pieceType), "remove", _) =>
|
|
||||||
bisAction(playerId, piece, pieceType)(removePieceBiS(playerId, _))
|
|
||||||
case (_, _, "create", Some(link)) => putBiS(playerId, link).map(_ => ())
|
|
||||||
case _ => Future.failed(new Error(s"Could not perform $action"))
|
|
||||||
}
|
|
||||||
case _ => Future.failed(new Error(s"Could not construct player id from `$player`"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
object BiSView {
|
|
||||||
import scalatags.Text.all._
|
|
||||||
import scalatags.Text.tags2.{title => titleTag}
|
|
||||||
|
|
||||||
def template(partyId: String, party: Seq[Player], error: Option[String]): String =
|
|
||||||
"<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01//EN\" \"http://www.w3.org/TR/html4/strict.dtd\">" +
|
|
||||||
html(lang:="en",
|
|
||||||
head(
|
|
||||||
titleTag("Best in slot"),
|
|
||||||
link(rel:="stylesheet", `type`:="text/css", href:="/static/styles.css")
|
|
||||||
),
|
|
||||||
|
|
||||||
body(
|
|
||||||
h2("Best in slot"),
|
|
||||||
|
|
||||||
ErrorView.template(error),
|
|
||||||
SearchLineView.template,
|
|
||||||
|
|
||||||
form(action:=s"/party/$partyId/bis", method:="post")(
|
|
||||||
select(name:="player", id:="player", title:="player")
|
|
||||||
(for (player <- party) yield option(player.playerId.toString)),
|
|
||||||
select(name:="piece", id:="piece", title:="piece")
|
|
||||||
(for (piece <- Piece.available) yield option(piece)),
|
|
||||||
select(name:="piece_type", id:="piece_type", title:="piece type")
|
|
||||||
(for (pieceType <- PieceType.available) yield option(pieceType.toString)),
|
|
||||||
input(name:="action", id:="action", `type`:="hidden", value:="add"),
|
|
||||||
input(name:="add", id:="add", `type`:="submit", value:="add")
|
|
||||||
),
|
|
||||||
|
|
||||||
form(action:=s"/party/$partyId/bis", method:="post")(
|
|
||||||
select(name:="player", id:="player", title:="player")
|
|
||||||
(for (player <- party) yield option(player.playerId.toString)),
|
|
||||||
input(name:="link", id:="link", placeholder:="player bis link", title:="link", `type`:="text"),
|
|
||||||
input(name:="action", id:="action", `type`:="hidden", value:="create"),
|
|
||||||
input(name:="add", id:="add", `type`:="submit", value:="add")
|
|
||||||
),
|
|
||||||
|
|
||||||
table(id:="result")(
|
|
||||||
tr(
|
|
||||||
th("player"),
|
|
||||||
th("piece"),
|
|
||||||
th("piece type"),
|
|
||||||
th("")
|
|
||||||
),
|
|
||||||
for (player <- party; piece <- player.bis.pieces) yield tr(
|
|
||||||
td(`class`:="include_search")(player.playerId.toString),
|
|
||||||
td(`class`:="include_search")(piece.piece),
|
|
||||||
td(piece.pieceType.toString),
|
|
||||||
td(
|
|
||||||
form(action:=s"/party/$partyId/bis", method:="post")(
|
|
||||||
input(name:="player", id:="player", `type`:="hidden", value:=player.playerId.toString),
|
|
||||||
input(name:="piece", id:="piece", `type`:="hidden", value:=piece.piece),
|
|
||||||
input(name:="piece_type", id:="piece_type", `type`:="hidden", value:=piece.pieceType.toString),
|
|
||||||
input(name:="action", id:="action", `type`:="hidden", value:="remove"),
|
|
||||||
input(name:="remove", id:="remove", `type`:="submit", value:="x")
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
),
|
|
||||||
|
|
||||||
ExportToCSVView.template,
|
|
||||||
BasePartyView.root(partyId),
|
|
||||||
script(src:="/static/table_search.js", `type`:="text/javascript")
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
@ -1,20 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (c) 2019 Evgeniy Alekseev.
|
|
||||||
*
|
|
||||||
* This file is part of ffxivbis
|
|
||||||
* (see https://github.com/arcan1s/ffxivbis).
|
|
||||||
*
|
|
||||||
* License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause
|
|
||||||
*/
|
|
||||||
package me.arcanis.ffxivbis.http.view
|
|
||||||
|
|
||||||
import scalatags.Text
|
|
||||||
import scalatags.Text.all._
|
|
||||||
|
|
||||||
object ErrorView {
|
|
||||||
|
|
||||||
def template(error: Option[String]): Text.TypedTag[String] = error match {
|
|
||||||
case Some(text) => p(id:="error", s"Error occurs: $text")
|
|
||||||
case None => p("")
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,21 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (c) 2019 Evgeniy Alekseev.
|
|
||||||
*
|
|
||||||
* This file is part of ffxivbis
|
|
||||||
* (see https://github.com/arcan1s/ffxivbis).
|
|
||||||
*
|
|
||||||
* License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause
|
|
||||||
*/
|
|
||||||
package me.arcanis.ffxivbis.http.view
|
|
||||||
|
|
||||||
import scalatags.Text
|
|
||||||
import scalatags.Text.all._
|
|
||||||
|
|
||||||
object ExportToCSVView {
|
|
||||||
|
|
||||||
def template: Text.TypedTag[String] =
|
|
||||||
div(
|
|
||||||
button(onclick:="exportTableToCsv('result.csv')")("Export to CSV"),
|
|
||||||
script(src:="/static/table_export.js", `type`:="text/javascript")
|
|
||||||
)
|
|
||||||
}
|
|
@ -1,94 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (c) 2019 Evgeniy Alekseev.
|
|
||||||
*
|
|
||||||
* This file is part of ffxivbis
|
|
||||||
* (see https://github.com/arcan1s/ffxivbis).
|
|
||||||
*
|
|
||||||
* License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause
|
|
||||||
*/
|
|
||||||
package me.arcanis.ffxivbis.http.view
|
|
||||||
|
|
||||||
import akka.actor.typed.{ActorRef, Scheduler}
|
|
||||||
import akka.http.scaladsl.model.StatusCodes
|
|
||||||
import akka.http.scaladsl.server.Directives._
|
|
||||||
import akka.http.scaladsl.server._
|
|
||||||
import akka.util.Timeout
|
|
||||||
import me.arcanis.ffxivbis.http.{PlayerHelper, UserHelper}
|
|
||||||
import me.arcanis.ffxivbis.messages.{BiSProviderMessage, Message}
|
|
||||||
import me.arcanis.ffxivbis.models.{PartyDescription, Permission, User}
|
|
||||||
|
|
||||||
import scala.concurrent.Future
|
|
||||||
import scala.util.{Failure, Success}
|
|
||||||
|
|
||||||
class IndexView(override val storage: ActorRef[Message],
|
|
||||||
override val provider: ActorRef[BiSProviderMessage])
|
|
||||||
(implicit timeout: Timeout, scheduler: Scheduler)
|
|
||||||
extends PlayerHelper with UserHelper {
|
|
||||||
|
|
||||||
def route: Route = createParty ~ getIndex
|
|
||||||
|
|
||||||
def createParty: Route =
|
|
||||||
path("party") {
|
|
||||||
extractExecutionContext { implicit executionContext =>
|
|
||||||
post {
|
|
||||||
formFields("username".as[String], "password".as[String], "alias".as[String].?) { (username, password, maybeAlias) =>
|
|
||||||
onComplete {
|
|
||||||
newPartyId.flatMap { partyId =>
|
|
||||||
val user = User(partyId, username, password, Permission.admin)
|
|
||||||
addUser(user, isHashedPassword = false).flatMap { _ =>
|
|
||||||
if (maybeAlias.getOrElse("").isEmpty) Future.successful(partyId)
|
|
||||||
else updateDescription(PartyDescription(partyId, maybeAlias)).map(_ => partyId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} {
|
|
||||||
case Success(partyId) => redirect(s"/party/$partyId", StatusCodes.Found)
|
|
||||||
case Failure(exception) => throw exception
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
def getIndex: Route =
|
|
||||||
pathEndOrSingleSlash {
|
|
||||||
get {
|
|
||||||
parameters("partyId".as[String].?) {
|
|
||||||
case Some(partyId) => redirect(s"/party/$partyId", StatusCodes.Found)
|
|
||||||
case _ => complete(StatusCodes.OK, RootView.toHtml(IndexView.template))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
object IndexView {
|
|
||||||
import scalatags.Text.all._
|
|
||||||
import scalatags.Text.tags2.{title => titleTag}
|
|
||||||
|
|
||||||
def template: String =
|
|
||||||
"<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01//EN\" \"http://www.w3.org/TR/html4/strict.dtd\">" +
|
|
||||||
html(
|
|
||||||
head(
|
|
||||||
titleTag("FFXIV loot helper"),
|
|
||||||
link(rel:="stylesheet", `type`:="text/css", href:="/static/styles.css")
|
|
||||||
),
|
|
||||||
|
|
||||||
body(
|
|
||||||
form(action:=s"party", method:="post")(
|
|
||||||
label("create a new party"),
|
|
||||||
input(name:="alias", id:="alias", placeholder:="party alias", title:="alias", `type`:="text"),
|
|
||||||
input(name:="username", id:="username", placeholder:="username", title:="username", `type`:="text"),
|
|
||||||
input(name:="password", id:="password", placeholder:="password", title:="password", `type`:="password"),
|
|
||||||
input(name:="add", id:="add", `type`:="submit", value:="add")
|
|
||||||
),
|
|
||||||
|
|
||||||
br,
|
|
||||||
|
|
||||||
form(action:="/", method:="get")(
|
|
||||||
label("already have party?"),
|
|
||||||
input(name:="partyId", id:="partyId", placeholder:="party id", title:="party id", `type`:="text"),
|
|
||||||
input(name:="go", id:="go", `type`:="submit", value:="go")
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -1,140 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (c) 2019 Evgeniy Alekseev.
|
|
||||||
*
|
|
||||||
* This file is part of ffxivbis
|
|
||||||
* (see https://github.com/arcan1s/ffxivbis).
|
|
||||||
*
|
|
||||||
* License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause
|
|
||||||
*/
|
|
||||||
package me.arcanis.ffxivbis.http.view
|
|
||||||
|
|
||||||
import akka.actor.typed.{ActorRef, Scheduler}
|
|
||||||
import akka.http.scaladsl.model.StatusCodes
|
|
||||||
import akka.http.scaladsl.server.Directives._
|
|
||||||
import akka.http.scaladsl.server.Route
|
|
||||||
import akka.util.Timeout
|
|
||||||
import me.arcanis.ffxivbis.http.{Authorization, LootHelper}
|
|
||||||
import me.arcanis.ffxivbis.messages.Message
|
|
||||||
import me.arcanis.ffxivbis.models.{Job, Piece, PieceType, PlayerIdWithCounters}
|
|
||||||
|
|
||||||
import scala.concurrent.{ExecutionContext, Future}
|
|
||||||
import scala.util.{Failure, Success, Try}
|
|
||||||
|
|
||||||
class LootSuggestView(override val storage: ActorRef[Message])
|
|
||||||
(implicit timeout: Timeout, scheduler: Scheduler)
|
|
||||||
extends LootHelper with Authorization {
|
|
||||||
|
|
||||||
def route: Route = getIndex ~ suggestLoot
|
|
||||||
|
|
||||||
def getIndex: Route =
|
|
||||||
path("party" / Segment / "suggest") { partyId: String =>
|
|
||||||
extractExecutionContext { implicit executionContext =>
|
|
||||||
authenticateBasicBCrypt(s"party $partyId", authGet(partyId)) { _ =>
|
|
||||||
get {
|
|
||||||
complete {
|
|
||||||
val text = LootSuggestView.template(partyId, Seq.empty, None, false, None)
|
|
||||||
(StatusCodes.OK, RootView.toHtml(text))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
def suggestLoot: Route =
|
|
||||||
path("party" / Segment / "suggest") { partyId: String =>
|
|
||||||
extractExecutionContext { implicit executionContext =>
|
|
||||||
authenticateBasicBCrypt(s"party $partyId", authGet(partyId)) { _ =>
|
|
||||||
post {
|
|
||||||
formFields("piece".as[String], "job".as[String], "piece_type".as[String], "free_loot".as[String].?) {
|
|
||||||
(piece, job, pieceType, maybeFreeLoot) =>
|
|
||||||
import me.arcanis.ffxivbis.utils.Implicits._
|
|
||||||
|
|
||||||
val maybePiece = Try(Piece(piece, PieceType.withName(pieceType), Job.withName(job))).toOption
|
|
||||||
|
|
||||||
onComplete(suggestLootCall(partyId, maybePiece)) {
|
|
||||||
case Success(players) =>
|
|
||||||
val text = LootSuggestView.template(partyId, players, maybePiece, maybeFreeLoot, None)
|
|
||||||
complete(StatusCodes.OK, RootView.toHtml(text))
|
|
||||||
case Failure(exception) =>
|
|
||||||
val text = LootSuggestView.template(partyId, Seq.empty, None, false, Some(exception.getMessage))
|
|
||||||
complete(StatusCodes.OK, RootView.toHtml(text))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private def suggestLootCall(partyId: String, maybePiece: Option[Piece])
|
|
||||||
(implicit executionContext: ExecutionContext, timeout: Timeout): Future[Seq[PlayerIdWithCounters]] =
|
|
||||||
maybePiece match {
|
|
||||||
case Some(piece) => suggestPiece(partyId, piece)
|
|
||||||
case _ => Future.failed(new Error(s"Could not construct piece from `$maybePiece`"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
object LootSuggestView {
|
|
||||||
import scalatags.Text.all._
|
|
||||||
import scalatags.Text.tags2.{title => titleTag}
|
|
||||||
|
|
||||||
def template(partyId: String, party: Seq[PlayerIdWithCounters], piece: Option[Piece],
|
|
||||||
isFreeLoot: Boolean, error: Option[String]): String =
|
|
||||||
"<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01//EN\" \"http://www.w3.org/TR/html4/strict.dtd\">" +
|
|
||||||
html(lang:="en",
|
|
||||||
head(
|
|
||||||
titleTag("Suggest loot"),
|
|
||||||
link(rel:="stylesheet", `type`:="text/css", href:="/static/styles.css")
|
|
||||||
),
|
|
||||||
|
|
||||||
body(
|
|
||||||
h2("Suggest loot"),
|
|
||||||
|
|
||||||
ErrorView.template(error),
|
|
||||||
SearchLineView.template,
|
|
||||||
|
|
||||||
form(action:=s"/party/$partyId/suggest", method:="post")(
|
|
||||||
select(name:="piece", id:="piece", title:="piece")
|
|
||||||
(for (piece <- Piece.available) yield option(piece)),
|
|
||||||
select(name:="job", id:="job", title:="job")
|
|
||||||
(for (job <- Job.availableWithAnyJob) yield option(job.toString)),
|
|
||||||
select(name:="piece_type", id:="piece_type", title:="piece type")
|
|
||||||
(for (pieceType <- PieceType.available) yield option(pieceType.toString)),
|
|
||||||
input(name:="free_loot", id:="free_loot", title:="is free loot", `type`:="checkbox"),
|
|
||||||
label(`for`:="free_loot")("is free loot"),
|
|
||||||
input(name:="suggest", id:="suggest", `type`:="submit", value:="suggest")
|
|
||||||
),
|
|
||||||
|
|
||||||
table(id:="result")(
|
|
||||||
tr(
|
|
||||||
th("player"),
|
|
||||||
th("is required"),
|
|
||||||
th("these pieces looted"),
|
|
||||||
th("total bis pieces looted"),
|
|
||||||
th("total pieces looted"),
|
|
||||||
th("")
|
|
||||||
),
|
|
||||||
for (player <- party) yield tr(
|
|
||||||
td(`class`:="include_search")(player.playerId.toString),
|
|
||||||
td(player.isRequiredToString),
|
|
||||||
td(player.lootCount),
|
|
||||||
td(player.lootCountBiS),
|
|
||||||
td(player.lootCountTotal),
|
|
||||||
td(
|
|
||||||
form(action:=s"/party/$partyId/loot", method:="post")(
|
|
||||||
input(name:="player", id:="player", `type`:="hidden", value:=player.playerId.toString),
|
|
||||||
input(name:="piece", id:="piece", `type`:="hidden", value:=piece.map(_.piece).getOrElse("")),
|
|
||||||
input(name:="piece_type", id:="piece_type", `type`:="hidden", value:=piece.map(_.pieceType.toString).getOrElse("")),
|
|
||||||
input(name:="free_loot", id:="free_loot", `type`:="hidden", value:=(if (isFreeLoot) "yes" else "no")),
|
|
||||||
input(name:="action", id:="action", `type`:="hidden", value:="add"),
|
|
||||||
input(name:="add", id:="add", `type`:="submit", value:="add")
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
),
|
|
||||||
|
|
||||||
ExportToCSVView.template,
|
|
||||||
BasePartyView.root(partyId),
|
|
||||||
script(src:="/static/table_search.js", `type`:="text/javascript")
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
@ -1,147 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (c) 2019 Evgeniy Alekseev.
|
|
||||||
*
|
|
||||||
* This file is part of ffxivbis
|
|
||||||
* (see https://github.com/arcan1s/ffxivbis).
|
|
||||||
*
|
|
||||||
* License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause
|
|
||||||
*/
|
|
||||||
package me.arcanis.ffxivbis.http.view
|
|
||||||
|
|
||||||
import akka.actor.typed.{ActorRef, Scheduler}
|
|
||||||
import akka.http.scaladsl.model.StatusCodes
|
|
||||||
import akka.http.scaladsl.server.Directives._
|
|
||||||
import akka.http.scaladsl.server.Route
|
|
||||||
import akka.util.Timeout
|
|
||||||
import me.arcanis.ffxivbis.http.{Authorization, LootHelper}
|
|
||||||
import me.arcanis.ffxivbis.messages.Message
|
|
||||||
import me.arcanis.ffxivbis.models.{Piece, PieceType, Player, PlayerId}
|
|
||||||
|
|
||||||
import scala.concurrent.{ExecutionContext, Future}
|
|
||||||
import scala.util.Try
|
|
||||||
|
|
||||||
class LootView(override val storage: ActorRef[Message])
|
|
||||||
(implicit timeout: Timeout, scheduler: Scheduler)
|
|
||||||
extends LootHelper with Authorization {
|
|
||||||
|
|
||||||
def route: Route = getLoot ~ modifyLoot
|
|
||||||
|
|
||||||
def getLoot: Route =
|
|
||||||
path("party" / Segment / "loot") { partyId: String =>
|
|
||||||
extractExecutionContext { implicit executionContext =>
|
|
||||||
authenticateBasicBCrypt(s"party $partyId", authGet(partyId)) { _ =>
|
|
||||||
get {
|
|
||||||
complete {
|
|
||||||
loot(partyId, None).map { players =>
|
|
||||||
LootView.template(partyId, players, None)
|
|
||||||
}.map { text =>
|
|
||||||
(StatusCodes.OK, RootView.toHtml(text))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
def modifyLoot: Route =
|
|
||||||
path("party" / Segment / "loot") { partyId: String =>
|
|
||||||
extractExecutionContext { implicit executionContext =>
|
|
||||||
authenticateBasicBCrypt(s"party $partyId", authPost(partyId)) { _ =>
|
|
||||||
post {
|
|
||||||
formFields("player".as[String], "piece".as[String], "piece_type".as[String], "action".as[String], "free_loot".as[String].?) {
|
|
||||||
(player, piece, pieceType, action, isFreeLoot) =>
|
|
||||||
onComplete(modifyLootCall(partyId, player, piece, pieceType, isFreeLoot, action)) { _ =>
|
|
||||||
redirect(s"/party/$partyId/loot", StatusCodes.Found)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private def modifyLootCall(partyId: String, player: String, maybePiece: String,
|
|
||||||
maybePieceType: String, maybeFreeLoot: Option[String],
|
|
||||||
action: String)
|
|
||||||
(implicit executionContext: ExecutionContext, timeout: Timeout): Future[Unit] = {
|
|
||||||
import me.arcanis.ffxivbis.utils.Implicits._
|
|
||||||
|
|
||||||
def getPiece(playerId: PlayerId) =
|
|
||||||
Try(Piece(maybePiece, PieceType.withName(maybePieceType), playerId.job)).toOption
|
|
||||||
|
|
||||||
PlayerId(partyId, player) match {
|
|
||||||
case Some(playerId) => (getPiece(playerId), action) match {
|
|
||||||
case (Some(piece), "add") => addPieceLoot(playerId, piece, maybeFreeLoot).map(_ => ())
|
|
||||||
case (Some(piece), "remove") => removePieceLoot(playerId, piece).map(_ => ())
|
|
||||||
case _ => Future.failed(new Error(s"Could not construct piece from `$maybePiece ($maybePieceType)`"))
|
|
||||||
}
|
|
||||||
case _ => Future.failed(new Error(s"Could not construct player id from `$player`"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
object LootView {
|
|
||||||
import scalatags.Text.all._
|
|
||||||
import scalatags.Text.tags2.{title => titleTag}
|
|
||||||
|
|
||||||
def template(partyId: String, party: Seq[Player], error: Option[String]): String =
|
|
||||||
"<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01//EN\" \"http://www.w3.org/TR/html4/strict.dtd\">" +
|
|
||||||
html(lang:="en",
|
|
||||||
head(
|
|
||||||
titleTag("Loot"),
|
|
||||||
link(rel:="stylesheet", `type`:="text/css", href:="/static/styles.css")
|
|
||||||
),
|
|
||||||
|
|
||||||
body(
|
|
||||||
h2("Loot"),
|
|
||||||
|
|
||||||
ErrorView.template(error),
|
|
||||||
SearchLineView.template,
|
|
||||||
|
|
||||||
form(action:=s"/party/$partyId/loot", method:="post")(
|
|
||||||
select(name:="player", id:="player", title:="player")
|
|
||||||
(for (player <- party) yield option(player.playerId.toString)),
|
|
||||||
select(name:="piece", id:="piece", title:="piece")
|
|
||||||
(for (piece <- Piece.available) yield option(piece)),
|
|
||||||
select(name:="piece_type", id:="piece_type", title:="piece type")
|
|
||||||
(for (pieceType <- PieceType.available) yield option(pieceType.toString)),
|
|
||||||
input(name:="free_loot", id:="free_loot", title:="is free loot", `type`:="checkbox"),
|
|
||||||
label(`for`:="free_loot")("is free loot"),
|
|
||||||
input(name:="action", id:="action", `type`:="hidden", value:="add"),
|
|
||||||
input(name:="add", id:="add", `type`:="submit", value:="add")
|
|
||||||
),
|
|
||||||
|
|
||||||
table(id:="result")(
|
|
||||||
tr(
|
|
||||||
th("player"),
|
|
||||||
th("piece"),
|
|
||||||
th("piece type"),
|
|
||||||
th("is free loot"),
|
|
||||||
th("timestamp"),
|
|
||||||
th("")
|
|
||||||
),
|
|
||||||
for (player <- party; loot <- player.loot) yield tr(
|
|
||||||
td(`class`:="include_search")(player.playerId.toString),
|
|
||||||
td(`class`:="include_search")(loot.piece.piece),
|
|
||||||
td(loot.piece.pieceType.toString),
|
|
||||||
td(loot.isFreeLootToString),
|
|
||||||
td(loot.timestamp.toString),
|
|
||||||
td(
|
|
||||||
form(action:=s"/party/$partyId/loot", method:="post")(
|
|
||||||
input(name:="player", id:="player", `type`:="hidden", value:=player.playerId.toString),
|
|
||||||
input(name:="piece", id:="piece", `type`:="hidden", value:=loot.piece.piece),
|
|
||||||
input(name:="piece_type", id:="piece_type", `type`:="hidden", value:=loot.piece.pieceType.toString),
|
|
||||||
input(name:="free_loot", id:="free_loot", `type`:="hidden", value:=loot.isFreeLootToString),
|
|
||||||
input(name:="action", id:="action", `type`:="hidden", value:="remove"),
|
|
||||||
input(name:="remove", id:="remove", `type`:="submit", value:="x")
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
),
|
|
||||||
|
|
||||||
ExportToCSVView.template,
|
|
||||||
BasePartyView.root(partyId),
|
|
||||||
script(src:="/static/table_search.js", `type`:="text/javascript")
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
}
|
|
@ -1,137 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (c) 2019 Evgeniy Alekseev.
|
|
||||||
*
|
|
||||||
* This file is part of ffxivbis
|
|
||||||
* (see https://github.com/arcan1s/ffxivbis).
|
|
||||||
*
|
|
||||||
* License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause
|
|
||||||
*/
|
|
||||||
package me.arcanis.ffxivbis.http.view
|
|
||||||
|
|
||||||
import akka.actor.typed.{ActorRef, Scheduler}
|
|
||||||
import akka.http.scaladsl.model.StatusCodes
|
|
||||||
import akka.http.scaladsl.server.Directives._
|
|
||||||
import akka.http.scaladsl.server.Route
|
|
||||||
import akka.util.Timeout
|
|
||||||
import me.arcanis.ffxivbis.http.{Authorization, PlayerHelper}
|
|
||||||
import me.arcanis.ffxivbis.messages.{BiSProviderMessage, Message}
|
|
||||||
import me.arcanis.ffxivbis.models._
|
|
||||||
|
|
||||||
import scala.concurrent.{ExecutionContext, Future}
|
|
||||||
|
|
||||||
class PlayerView(override val storage: ActorRef[Message],
|
|
||||||
override val provider: ActorRef[BiSProviderMessage])
|
|
||||||
(implicit timeout: Timeout, scheduler: Scheduler)
|
|
||||||
extends PlayerHelper with Authorization {
|
|
||||||
|
|
||||||
def route: Route = getParty ~ modifyParty
|
|
||||||
|
|
||||||
def getParty: Route =
|
|
||||||
path("party" / Segment / "players") { partyId: String =>
|
|
||||||
extractExecutionContext { implicit executionContext =>
|
|
||||||
authenticateBasicBCrypt(s"party $partyId", authGet(partyId)) { _ =>
|
|
||||||
get {
|
|
||||||
complete {
|
|
||||||
getPlayers(partyId, None).map { players =>
|
|
||||||
PlayerView.template(partyId, players.map(_.withCounters(None)), None)
|
|
||||||
}.map { text =>
|
|
||||||
(StatusCodes.OK, RootView.toHtml(text))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
def modifyParty: Route =
|
|
||||||
path("party" / Segment / "players") { partyId: String =>
|
|
||||||
extractExecutionContext { implicit executionContext =>
|
|
||||||
authenticateBasicBCrypt(s"party $partyId", authPost(partyId)) { _ =>
|
|
||||||
post {
|
|
||||||
formFields("nick".as[String], "job".as[String], "priority".as[Int].?, "link".as[String].?, "action".as[String]) {
|
|
||||||
(nick, job, maybePriority, maybeLink, action) =>
|
|
||||||
onComplete(modifyPartyCall(partyId, nick, job, maybePriority, maybeLink, action)) { _ =>
|
|
||||||
redirect(s"/party/$partyId/players", StatusCodes.Found)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private def modifyPartyCall(partyId: String, nick: String, job: String,
|
|
||||||
maybePriority: Option[Int], maybeLink: Option[String],
|
|
||||||
action: String)
|
|
||||||
(implicit executionContext: ExecutionContext, timeout: Timeout): Future[Unit] = {
|
|
||||||
def maybePlayerId = PlayerId(partyId, Some(nick), Some(job))
|
|
||||||
def player(playerId: PlayerId) =
|
|
||||||
Player(-1, partyId, playerId.job, playerId.nick, BiS.empty, Seq.empty, maybeLink, maybePriority.getOrElse(0))
|
|
||||||
|
|
||||||
(action, maybePlayerId) match {
|
|
||||||
case ("add", Some(playerId)) => addPlayer(player(playerId)).map(_ => ())
|
|
||||||
case ("remove", Some(playerId)) => removePlayer(playerId).map(_ => ())
|
|
||||||
case _ => Future.failed(new Error(s"Could not perform $action with $nick ($job)"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
object PlayerView {
|
|
||||||
import scalatags.Text.all._
|
|
||||||
import scalatags.Text.tags2.{title => titleTag}
|
|
||||||
|
|
||||||
def template(partyId: String, party: Seq[PlayerIdWithCounters], error: Option[String]): String =
|
|
||||||
"<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01//EN\" \"http://www.w3.org/TR/html4/strict.dtd\">" +
|
|
||||||
html(lang:="en",
|
|
||||||
head(
|
|
||||||
titleTag("Party"),
|
|
||||||
link(rel:="stylesheet", `type`:="text/css", href:="/static/styles.css")
|
|
||||||
),
|
|
||||||
|
|
||||||
body(
|
|
||||||
h2("Party"),
|
|
||||||
|
|
||||||
ErrorView.template(error),
|
|
||||||
SearchLineView.template,
|
|
||||||
|
|
||||||
form(action:=s"/party/$partyId/players", method:="post")(
|
|
||||||
input(name:="nick", id:="nick", placeholder:="nick", title:="nick", `type`:="nick"),
|
|
||||||
select(name:="job", id:="job", title:="job")
|
|
||||||
(for (job <- Job.available) yield option(job.toString)),
|
|
||||||
input(name:="link", id:="link", placeholder:="player bis link", title:="link", `type`:="text"),
|
|
||||||
input(name:="prioiry", id:="priority", placeholder:="priority", title:="priority", `type`:="number", value:="0"),
|
|
||||||
input(name:="action", id:="action", `type`:="hidden", value:="add"),
|
|
||||||
input(name:="add", id:="add", `type`:="submit", value:="add")
|
|
||||||
),
|
|
||||||
|
|
||||||
table(id:="result")(
|
|
||||||
tr(
|
|
||||||
th("nick"),
|
|
||||||
th("job"),
|
|
||||||
th("total bis pieces looted"),
|
|
||||||
th("total pieces looted"),
|
|
||||||
th("priority"),
|
|
||||||
th("")
|
|
||||||
),
|
|
||||||
for (player <- party) yield tr(
|
|
||||||
td(`class`:="include_search")(player.nick),
|
|
||||||
td(`class`:="include_search")(player.job.toString),
|
|
||||||
td(player.lootCountBiS),
|
|
||||||
td(player.lootCountTotal),
|
|
||||||
td(player.priority),
|
|
||||||
td(
|
|
||||||
form(action:=s"/party/$partyId/players", method:="post")(
|
|
||||||
input(name:="nick", id:="nick", `type`:="hidden", value:=player.nick),
|
|
||||||
input(name:="job", id:="job", `type`:="hidden", value:=player.job.toString),
|
|
||||||
input(name:="action", id:="action", `type`:="hidden", value:="remove"),
|
|
||||||
input(name:="remove", id:="remove", `type`:="submit", value:="x")
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
),
|
|
||||||
|
|
||||||
ExportToCSVView.template,
|
|
||||||
BasePartyView.root(partyId),
|
|
||||||
script(src:="/static/table_search.js", `type`:="text/javascript")
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (c) 2019 Evgeniy Alekseev.
|
* Copyright (c) 2019-2022 Evgeniy Alekseev.
|
||||||
*
|
*
|
||||||
* This file is part of ffxivbis
|
* This file is part of ffxivbis
|
||||||
* (see https://github.com/arcan1s/ffxivbis).
|
* (see https://github.com/arcan1s/ffxivbis).
|
||||||
@ -8,33 +8,73 @@
|
|||||||
*/
|
*/
|
||||||
package me.arcanis.ffxivbis.http.view
|
package me.arcanis.ffxivbis.http.view
|
||||||
|
|
||||||
import akka.actor.typed.{ActorRef, Scheduler}
|
import akka.http.scaladsl.model.headers.RawHeader
|
||||||
import akka.http.scaladsl.model.{ContentTypes, HttpEntity}
|
|
||||||
import akka.http.scaladsl.server.Directives._
|
import akka.http.scaladsl.server.Directives._
|
||||||
import akka.http.scaladsl.server.Route
|
import akka.http.scaladsl.server.Route
|
||||||
import akka.util.Timeout
|
import me.arcanis.ffxivbis.http.{Authorization, AuthorizationProvider}
|
||||||
import me.arcanis.ffxivbis.messages.{BiSProviderMessage, Message}
|
|
||||||
|
|
||||||
class RootView(storage: ActorRef[Message],
|
class RootView(override val auth: AuthorizationProvider) extends Authorization {
|
||||||
provider: ActorRef[BiSProviderMessage])
|
|
||||||
(implicit timeout: Timeout, scheduler: Scheduler) {
|
|
||||||
|
|
||||||
private val basePartyView = new BasePartyView(storage, provider)
|
def routes: Route = getBiS ~ getIndex ~ getLoot ~ getParty ~ getUsers
|
||||||
private val indexView = new IndexView(storage, provider)
|
|
||||||
|
|
||||||
private val biSView = new BiSView(storage, provider)
|
def getBiS: Route =
|
||||||
private val lootView = new LootView(storage)
|
path("party" / Segment / "bis") { partyId: String =>
|
||||||
private val lootSuggestView = new LootSuggestView(storage)
|
extractExecutionContext { implicit executionContext =>
|
||||||
private val playerView = new PlayerView(storage, provider)
|
authenticateBasicBCrypt(s"party $partyId", authGet(partyId)) { user =>
|
||||||
private val userView = new UserView(storage)
|
respondWithHeaders(
|
||||||
|
RawHeader("X-Party-Id", partyId),
|
||||||
|
RawHeader("X-User-Permission", user.permission.toString)
|
||||||
|
) {
|
||||||
|
getFromResource("html/bis.html")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
def route: Route =
|
def getIndex: Route =
|
||||||
basePartyView.route ~ indexView.route ~
|
pathEndOrSingleSlash {
|
||||||
biSView.route ~ lootView.route ~ lootSuggestView.route ~ playerView.route ~ userView.route
|
getFromResource("html/index.html")
|
||||||
}
|
}
|
||||||
|
|
||||||
object RootView {
|
def getLoot: Route =
|
||||||
|
path("party" / Segment / "loot") { partyId: String =>
|
||||||
def toHtml(template: String): HttpEntity.Strict =
|
extractExecutionContext { implicit executionContext =>
|
||||||
HttpEntity(ContentTypes.`text/html(UTF-8)`, template)
|
authenticateBasicBCrypt(s"party $partyId", authGet(partyId)) { user =>
|
||||||
|
respondWithHeaders(
|
||||||
|
RawHeader("X-Party-Id", partyId),
|
||||||
|
RawHeader("X-User-Permission", user.permission.toString)
|
||||||
|
) {
|
||||||
|
getFromResource("html/loot.html")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def getParty: Route =
|
||||||
|
path("party" / Segment) { partyId: String =>
|
||||||
|
extractExecutionContext { implicit executionContext =>
|
||||||
|
authenticateBasicBCrypt(s"party $partyId", authGet(partyId)) { user =>
|
||||||
|
respondWithHeaders(
|
||||||
|
RawHeader("X-Party-Id", partyId),
|
||||||
|
RawHeader("X-User-Permission", user.permission.toString)
|
||||||
|
) {
|
||||||
|
getFromResource("html/party.html")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def getUsers: Route =
|
||||||
|
path("party" / Segment / "users") { partyId: String =>
|
||||||
|
extractExecutionContext { implicit executionContext =>
|
||||||
|
authenticateBasicBCrypt(s"party $partyId", authAdmin(partyId)) { user =>
|
||||||
|
respondWithHeaders(
|
||||||
|
RawHeader("X-Party-Id", partyId),
|
||||||
|
RawHeader("X-User-Permission", user.permission.toString)
|
||||||
|
) {
|
||||||
|
getFromResource("html/users.html")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,23 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (c) 2019 Evgeniy Alekseev.
|
|
||||||
*
|
|
||||||
* This file is part of ffxivbis
|
|
||||||
* (see https://github.com/arcan1s/ffxivbis).
|
|
||||||
*
|
|
||||||
* License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause
|
|
||||||
*/
|
|
||||||
package me.arcanis.ffxivbis.http.view
|
|
||||||
|
|
||||||
import scalatags.Text
|
|
||||||
import scalatags.Text.all._
|
|
||||||
|
|
||||||
object SearchLineView {
|
|
||||||
|
|
||||||
def template: Text.TypedTag[String] =
|
|
||||||
div(
|
|
||||||
input(
|
|
||||||
`type`:="text", id:="search", onkeyup:="searchTable()",
|
|
||||||
placeholder:="search for data", title:="search"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
@ -1,130 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (c) 2019 Evgeniy Alekseev.
|
|
||||||
*
|
|
||||||
* This file is part of ffxivbis
|
|
||||||
* (see https://github.com/arcan1s/ffxivbis).
|
|
||||||
*
|
|
||||||
* License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause
|
|
||||||
*/
|
|
||||||
package me.arcanis.ffxivbis.http.view
|
|
||||||
|
|
||||||
import akka.actor.typed.{ActorRef, Scheduler}
|
|
||||||
import akka.http.scaladsl.model.StatusCodes
|
|
||||||
import akka.http.scaladsl.server.Directives._
|
|
||||||
import akka.http.scaladsl.server.Route
|
|
||||||
import akka.util.Timeout
|
|
||||||
import me.arcanis.ffxivbis.http.{Authorization, UserHelper}
|
|
||||||
import me.arcanis.ffxivbis.messages.Message
|
|
||||||
import me.arcanis.ffxivbis.models.{Permission, User}
|
|
||||||
|
|
||||||
import scala.concurrent.{ExecutionContext, Future}
|
|
||||||
import scala.util.Try
|
|
||||||
|
|
||||||
class UserView(override val storage: ActorRef[Message])
|
|
||||||
(implicit timeout: Timeout, scheduler: Scheduler)
|
|
||||||
extends UserHelper with Authorization {
|
|
||||||
|
|
||||||
def route: Route = getUsers ~ modifyUsers
|
|
||||||
|
|
||||||
def getUsers: Route =
|
|
||||||
path("party" / Segment / "users") { partyId: String =>
|
|
||||||
extractExecutionContext { implicit executionContext =>
|
|
||||||
authenticateBasicBCrypt(s"party $partyId", authAdmin(partyId)) { _ =>
|
|
||||||
get {
|
|
||||||
complete {
|
|
||||||
users(partyId).map { users =>
|
|
||||||
UserView.template(partyId, users, None)
|
|
||||||
}.map { text =>
|
|
||||||
(StatusCodes.OK, RootView.toHtml(text))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
def modifyUsers: Route =
|
|
||||||
path("party" / Segment / "users") { partyId: String =>
|
|
||||||
extractExecutionContext { implicit executionContext =>
|
|
||||||
authenticateBasicBCrypt(s"party $partyId", authAdmin(partyId)) { _ =>
|
|
||||||
post {
|
|
||||||
formFields("username".as[String], "password".as[String].?, "permission".as[String].?, "action".as[String]) {
|
|
||||||
(username, maybePassword, maybePermission, action) =>
|
|
||||||
onComplete(modifyUsersCall(partyId, username, maybePassword, maybePermission, action)) {
|
|
||||||
case _ => redirect(s"/party/$partyId/users", StatusCodes.Found)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private def modifyUsersCall(partyId: String, username: String,
|
|
||||||
maybePassword: Option[String], maybePermission: Option[String],
|
|
||||||
action: String)
|
|
||||||
(implicit executionContext: ExecutionContext, timeout: Timeout): Future[Unit] = {
|
|
||||||
def permission: Option[Permission.Value] =
|
|
||||||
maybePermission.flatMap(p => Try(Permission.withName(p)).toOption)
|
|
||||||
|
|
||||||
action match {
|
|
||||||
case "add" => (maybePassword, permission) match {
|
|
||||||
case (Some(password), Some(permission)) => addUser(User(partyId, username, password, permission), isHashedPassword = false).map(_ => ())
|
|
||||||
case _ => Future.failed(new Error(s"Could not construct permission/password from `$maybePermission`/`$maybePassword`"))
|
|
||||||
}
|
|
||||||
case "remove" => removeUser(partyId, username).map(_ => ())
|
|
||||||
case _ => Future.failed(new Error(s"Could not perform $action"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
object UserView {
|
|
||||||
import scalatags.Text.all._
|
|
||||||
import scalatags.Text.tags2.{title => titleTag}
|
|
||||||
|
|
||||||
def template(partyId: String, users: Seq[User], error: Option[String]) =
|
|
||||||
"<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01//EN\" \"http://www.w3.org/TR/html4/strict.dtd\">" +
|
|
||||||
html(lang:="en",
|
|
||||||
head(
|
|
||||||
titleTag("Users"),
|
|
||||||
link(rel:="stylesheet", `type`:="text/css", href:="/static/styles.css")
|
|
||||||
),
|
|
||||||
|
|
||||||
body(
|
|
||||||
h2("Users"),
|
|
||||||
|
|
||||||
ErrorView.template(error),
|
|
||||||
SearchLineView.template,
|
|
||||||
|
|
||||||
form(action:=s"/party/$partyId/users", method:="post")(
|
|
||||||
input(name:="username", id:="username", placeholder:="username", title:="username", `type`:="text"),
|
|
||||||
input(name:="password", id:="password", placeholder:="password", title:="password", `type`:="password"),
|
|
||||||
select(name:="permission", id:="permission", title:="permission")(option("get"), option("post")),
|
|
||||||
input(name:="action", id:="action", `type`:="hidden", value:="add"),
|
|
||||||
input(name:="add", id:="add", `type`:="submit", value:="add")
|
|
||||||
),
|
|
||||||
|
|
||||||
table(id:="result")(
|
|
||||||
tr(
|
|
||||||
th("username"),
|
|
||||||
th("permission"),
|
|
||||||
th("")
|
|
||||||
),
|
|
||||||
for (user <- users) yield tr(
|
|
||||||
td(`class`:="include_search")(user.username),
|
|
||||||
td(user.permission.toString),
|
|
||||||
td(
|
|
||||||
form(action:=s"/party/$partyId/users", method:="post")(
|
|
||||||
input(name:="username", id:="username", `type`:="hidden", value:=user.username.toString),
|
|
||||||
input(name:="action", id:="action", `type`:="hidden", value:="remove"),
|
|
||||||
input(name:="remove", id:="remove", `type`:="submit", value:="x")
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
),
|
|
||||||
|
|
||||||
ExportToCSVView.template,
|
|
||||||
BasePartyView.root(partyId),
|
|
||||||
script(src:="/static/table_search.js", `type`:="text/javascript")
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
@ -1,3 +1,11 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2019-2022 Evgeniy Alekseev.
|
||||||
|
*
|
||||||
|
* This file is part of ffxivbis
|
||||||
|
* (see https://github.com/arcan1s/ffxivbis).
|
||||||
|
*
|
||||||
|
* License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause
|
||||||
|
*/
|
||||||
package me.arcanis.ffxivbis.messages
|
package me.arcanis.ffxivbis.messages
|
||||||
|
|
||||||
import akka.actor.typed.ActorRef
|
import akka.actor.typed.ActorRef
|
||||||
@ -5,4 +13,10 @@ import me.arcanis.ffxivbis.models.{BiS, Job}
|
|||||||
|
|
||||||
sealed trait BiSProviderMessage
|
sealed trait BiSProviderMessage
|
||||||
|
|
||||||
case class DownloadBiS(link: String, job: Job.Job, replyTo: ActorRef[BiS]) extends BiSProviderMessage
|
object BiSProviderMessage {
|
||||||
|
|
||||||
|
case class DownloadBiS(link: String, job: Job, replyTo: ActorRef[BiS]) extends BiSProviderMessage {
|
||||||
|
|
||||||
|
require(link.nonEmpty && link.trim == link, "Link must be not empty and contain no spaces")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,10 +0,0 @@
|
|||||||
package me.arcanis.ffxivbis.messages
|
|
||||||
|
|
||||||
import akka.actor.typed.ActorRef
|
|
||||||
import me.arcanis.ffxivbis.models.Party
|
|
||||||
|
|
||||||
case class ForgetParty(partyId: String) extends Message
|
|
||||||
|
|
||||||
case class GetNewPartyId(replyTo: ActorRef[String]) extends Message
|
|
||||||
|
|
||||||
case class StoreParty(partyId: String, party: Party) extends Message
|
|
@ -0,0 +1,23 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2019-2022 Evgeniy Alekseev.
|
||||||
|
*
|
||||||
|
* This file is part of ffxivbis
|
||||||
|
* (see https://github.com/arcan1s/ffxivbis).
|
||||||
|
*
|
||||||
|
* License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause
|
||||||
|
*/
|
||||||
|
package me.arcanis.ffxivbis.messages
|
||||||
|
|
||||||
|
import akka.actor.typed.ActorRef
|
||||||
|
import me.arcanis.ffxivbis.models.Party
|
||||||
|
|
||||||
|
sealed trait ControlMessage extends Message
|
||||||
|
|
||||||
|
object ControlMessage {
|
||||||
|
|
||||||
|
case class ForgetParty(partyId: String) extends ControlMessage
|
||||||
|
|
||||||
|
case class GetNewPartyId(replyTo: ActorRef[String]) extends ControlMessage
|
||||||
|
|
||||||
|
case class StoreParty(partyId: String, party: Party) extends ControlMessage
|
||||||
|
}
|
@ -1,77 +1,131 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2019-2022 Evgeniy Alekseev.
|
||||||
|
*
|
||||||
|
* This file is part of ffxivbis
|
||||||
|
* (see https://github.com/arcan1s/ffxivbis).
|
||||||
|
*
|
||||||
|
* License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause
|
||||||
|
*/
|
||||||
package me.arcanis.ffxivbis.messages
|
package me.arcanis.ffxivbis.messages
|
||||||
|
|
||||||
import akka.actor.typed.{ActorRef, Behavior}
|
import akka.actor.typed.ActorRef
|
||||||
import me.arcanis.ffxivbis.models.{Party, PartyDescription, Piece, Player, PlayerId, User}
|
import me.arcanis.ffxivbis.models._
|
||||||
import me.arcanis.ffxivbis.service.LootSelector
|
import me.arcanis.ffxivbis.service.LootSelector
|
||||||
|
|
||||||
sealed trait DatabaseMessage extends Message {
|
sealed trait DatabaseMessage extends Message {
|
||||||
|
|
||||||
def partyId: String
|
def partyId: String
|
||||||
|
|
||||||
|
def isReadOnly: Boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
object DatabaseMessage {
|
object DatabaseMessage {
|
||||||
|
|
||||||
type Handler = PartialFunction[DatabaseMessage, Behavior[DatabaseMessage]]
|
// bis handler
|
||||||
|
trait BisDatabaseMessage extends DatabaseMessage
|
||||||
|
|
||||||
|
case class AddPieceToBis(playerId: PlayerId, piece: Piece, replyTo: ActorRef[Unit]) extends BisDatabaseMessage {
|
||||||
|
override val partyId: String = playerId.partyId
|
||||||
|
override val isReadOnly: Boolean = false
|
||||||
|
}
|
||||||
|
|
||||||
|
case class GetBiS(partyId: String, playerId: Option[PlayerId], replyTo: ActorRef[Seq[Player]])
|
||||||
|
extends BisDatabaseMessage {
|
||||||
|
override val isReadOnly: Boolean = true
|
||||||
|
}
|
||||||
|
|
||||||
|
case class RemovePieceFromBiS(playerId: PlayerId, piece: Piece, replyTo: ActorRef[Unit]) extends BisDatabaseMessage {
|
||||||
|
override val partyId: String = playerId.partyId
|
||||||
|
override val isReadOnly: Boolean = false
|
||||||
|
}
|
||||||
|
|
||||||
|
case class RemovePiecesFromBiS(playerId: PlayerId, replyTo: ActorRef[Unit]) extends BisDatabaseMessage {
|
||||||
|
override val partyId: String = playerId.partyId
|
||||||
|
override val isReadOnly: Boolean = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// loot handler
|
||||||
|
trait LootDatabaseMessage extends DatabaseMessage
|
||||||
|
|
||||||
|
case class AddPieceTo(playerId: PlayerId, piece: Piece, isFreeLoot: Boolean, replyTo: ActorRef[Unit])
|
||||||
|
extends LootDatabaseMessage {
|
||||||
|
override val partyId: String = playerId.partyId
|
||||||
|
override val isReadOnly: Boolean = false
|
||||||
|
}
|
||||||
|
|
||||||
|
case class GetLoot(partyId: String, playerId: Option[PlayerId], replyTo: ActorRef[Seq[Player]])
|
||||||
|
extends LootDatabaseMessage {
|
||||||
|
override val isReadOnly: Boolean = true
|
||||||
|
}
|
||||||
|
|
||||||
|
case class RemovePieceFrom(playerId: PlayerId, piece: Piece, isFreeLoot: Boolean, replyTo: ActorRef[Unit])
|
||||||
|
extends LootDatabaseMessage {
|
||||||
|
override val partyId: String = playerId.partyId
|
||||||
|
override val isReadOnly: Boolean = false
|
||||||
|
}
|
||||||
|
|
||||||
|
case class SuggestLoot(partyId: String, piece: Piece, replyTo: ActorRef[LootSelector.LootSelectorResult])
|
||||||
|
extends LootDatabaseMessage {
|
||||||
|
override val isReadOnly: Boolean = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// party handler
|
||||||
|
trait PartyDatabaseMessage extends DatabaseMessage
|
||||||
|
|
||||||
|
case class AddPlayer(player: Player, replyTo: ActorRef[Unit]) extends PartyDatabaseMessage {
|
||||||
|
override val partyId: String = player.partyId
|
||||||
|
override val isReadOnly: Boolean = false
|
||||||
|
}
|
||||||
|
|
||||||
|
case class GetParty(partyId: String, replyTo: ActorRef[Party]) extends PartyDatabaseMessage {
|
||||||
|
override val isReadOnly: Boolean = true
|
||||||
|
}
|
||||||
|
|
||||||
|
case class GetPartyDescription(partyId: String, replyTo: ActorRef[PartyDescription]) extends PartyDatabaseMessage {
|
||||||
|
override val isReadOnly: Boolean = true
|
||||||
|
}
|
||||||
|
|
||||||
|
case class GetPlayer(playerId: PlayerId, replyTo: ActorRef[Option[Player]]) extends PartyDatabaseMessage {
|
||||||
|
override val partyId: String = playerId.partyId
|
||||||
|
override val isReadOnly: Boolean = true
|
||||||
|
}
|
||||||
|
|
||||||
|
case class RemovePlayer(playerId: PlayerId, replyTo: ActorRef[Unit]) extends PartyDatabaseMessage {
|
||||||
|
override val partyId: String = playerId.partyId
|
||||||
|
override val isReadOnly: Boolean = false
|
||||||
|
}
|
||||||
|
|
||||||
|
case class UpdateBiSLink(playerId: PlayerId, link: String, actorRef: ActorRef[Unit]) extends PartyDatabaseMessage {
|
||||||
|
override val partyId: String = playerId.partyId
|
||||||
|
override val isReadOnly: Boolean = false
|
||||||
|
}
|
||||||
|
|
||||||
|
case class UpdateParty(partyDescription: PartyDescription, replyTo: ActorRef[Unit]) extends PartyDatabaseMessage {
|
||||||
|
override val partyId: String = partyDescription.partyId
|
||||||
|
override val isReadOnly: Boolean = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// user handler
|
||||||
|
trait UserDatabaseMessage extends DatabaseMessage
|
||||||
|
|
||||||
|
case class AddUser(user: User, isHashedPassword: Boolean, replyTo: ActorRef[Unit]) extends UserDatabaseMessage {
|
||||||
|
override val partyId: String = user.partyId
|
||||||
|
override val isReadOnly: Boolean = false
|
||||||
|
}
|
||||||
|
|
||||||
|
case class DeleteUser(partyId: String, username: String, replyTo: ActorRef[Unit]) extends UserDatabaseMessage {
|
||||||
|
override val isReadOnly: Boolean = true
|
||||||
|
}
|
||||||
|
|
||||||
|
case class Exists(partyId: String, replyTo: ActorRef[Boolean]) extends UserDatabaseMessage {
|
||||||
|
override val isReadOnly: Boolean = true
|
||||||
|
}
|
||||||
|
|
||||||
|
case class GetUser(partyId: String, username: String, replyTo: ActorRef[Option[User]]) extends UserDatabaseMessage {
|
||||||
|
override val isReadOnly: Boolean = true
|
||||||
|
}
|
||||||
|
|
||||||
|
case class GetUsers(partyId: String, replyTo: ActorRef[Seq[User]]) extends UserDatabaseMessage {
|
||||||
|
override val isReadOnly: Boolean = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// bis handler
|
|
||||||
case class AddPieceToBis(playerId: PlayerId, piece: Piece, replyTo: ActorRef[Unit]) extends DatabaseMessage {
|
|
||||||
override def partyId: String = playerId.partyId
|
|
||||||
}
|
|
||||||
|
|
||||||
case class GetBiS(partyId: String, playerId: Option[PlayerId], replyTo: ActorRef[Seq[Player]]) extends DatabaseMessage
|
|
||||||
|
|
||||||
case class RemovePieceFromBiS(playerId: PlayerId, piece: Piece, replyTo: ActorRef[Unit]) extends DatabaseMessage {
|
|
||||||
override def partyId: String = playerId.partyId
|
|
||||||
}
|
|
||||||
|
|
||||||
case class RemovePiecesFromBiS(playerId: PlayerId, replyTo: ActorRef[Unit]) extends DatabaseMessage {
|
|
||||||
override def partyId: String = playerId.partyId
|
|
||||||
}
|
|
||||||
|
|
||||||
// loot handler
|
|
||||||
case class AddPieceTo(playerId: PlayerId, piece: Piece, isFreeLoot: Boolean, replyTo: ActorRef[Unit]) extends DatabaseMessage {
|
|
||||||
override def partyId: String = playerId.partyId
|
|
||||||
}
|
|
||||||
|
|
||||||
case class GetLoot(partyId: String, playerId: Option[PlayerId], replyTo: ActorRef[Seq[Player]]) extends DatabaseMessage
|
|
||||||
|
|
||||||
case class RemovePieceFrom(playerId: PlayerId, piece: Piece, replyTo: ActorRef[Unit]) extends DatabaseMessage {
|
|
||||||
override def partyId: String = playerId.partyId
|
|
||||||
}
|
|
||||||
|
|
||||||
case class SuggestLoot(partyId: String, piece: Piece, replyTo: ActorRef[LootSelector.LootSelectorResult]) extends DatabaseMessage
|
|
||||||
|
|
||||||
// party handler
|
|
||||||
case class AddPlayer(player: Player, replyTo: ActorRef[Unit]) extends DatabaseMessage {
|
|
||||||
override def partyId: String = player.partyId
|
|
||||||
}
|
|
||||||
|
|
||||||
case class GetParty(partyId: String, replyTo: ActorRef[Party]) extends DatabaseMessage
|
|
||||||
|
|
||||||
case class GetPartyDescription(partyId: String, replyTo: ActorRef[PartyDescription]) extends DatabaseMessage
|
|
||||||
|
|
||||||
case class GetPlayer(playerId: PlayerId, replyTo: ActorRef[Option[Player]]) extends DatabaseMessage {
|
|
||||||
override def partyId: String = playerId.partyId
|
|
||||||
}
|
|
||||||
|
|
||||||
case class RemovePlayer(playerId: PlayerId, replyTo: ActorRef[Unit]) extends DatabaseMessage {
|
|
||||||
override def partyId: String = playerId.partyId
|
|
||||||
}
|
|
||||||
|
|
||||||
case class UpdateParty(partyDescription: PartyDescription, replyTo: ActorRef[Unit]) extends DatabaseMessage {
|
|
||||||
override def partyId: String = partyDescription.partyId
|
|
||||||
}
|
|
||||||
|
|
||||||
// user handler
|
|
||||||
case class AddUser(user: User, isHashedPassword: Boolean, replyTo: ActorRef[Unit]) extends DatabaseMessage {
|
|
||||||
override def partyId: String = user.partyId
|
|
||||||
}
|
|
||||||
|
|
||||||
case class DeleteUser(partyId: String, username: String, replyTo: ActorRef[Unit]) extends DatabaseMessage
|
|
||||||
|
|
||||||
case class Exists(partyId: String, replyTo: ActorRef[Boolean]) extends DatabaseMessage
|
|
||||||
|
|
||||||
case class GetUser(partyId: String, username: String, replyTo: ActorRef[Option[User]]) extends DatabaseMessage
|
|
||||||
|
|
||||||
case class GetUsers(partyId: String, replyTo: ActorRef[Seq[User]]) extends DatabaseMessage
|
|
@ -1,3 +1,11 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2019-2022 Evgeniy Alekseev.
|
||||||
|
*
|
||||||
|
* This file is part of ffxivbis
|
||||||
|
* (see https://github.com/arcan1s/ffxivbis).
|
||||||
|
*
|
||||||
|
* License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause
|
||||||
|
*/
|
||||||
package me.arcanis.ffxivbis.messages
|
package me.arcanis.ffxivbis.messages
|
||||||
|
|
||||||
import akka.actor.typed.Behavior
|
import akka.actor.typed.Behavior
|
||||||
@ -5,5 +13,6 @@ import akka.actor.typed.Behavior
|
|||||||
trait Message
|
trait Message
|
||||||
|
|
||||||
object Message {
|
object Message {
|
||||||
|
|
||||||
type Handler = PartialFunction[Message, Behavior[Message]]
|
type Handler = PartialFunction[Message, Behavior[Message]]
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (c) 2019 Evgeniy Alekseev.
|
* Copyright (c) 2019-2022 Evgeniy Alekseev.
|
||||||
*
|
*
|
||||||
* This file is part of ffxivbis
|
* This file is part of ffxivbis
|
||||||
* (see https://github.com/arcan1s/ffxivbis).
|
* (see https://github.com/arcan1s/ffxivbis).
|
||||||
@ -11,23 +11,27 @@ package me.arcanis.ffxivbis.models
|
|||||||
case class BiS(pieces: Seq[Piece]) {
|
case class BiS(pieces: Seq[Piece]) {
|
||||||
|
|
||||||
def hasPiece(piece: Piece): Boolean = piece match {
|
def hasPiece(piece: Piece): Boolean = piece match {
|
||||||
case upgrade: PieceUpgrade => upgrades.contains(upgrade)
|
case upgrade: Piece.PieceUpgrade => upgrades.contains(upgrade)
|
||||||
case _ => pieces.contains(piece)
|
case _ => pieces.contains(piece)
|
||||||
}
|
}
|
||||||
|
|
||||||
def upgrades: Map[PieceUpgrade, Int] =
|
def upgrades: Map[Piece.PieceUpgrade, Int] =
|
||||||
pieces.groupBy(_.upgrade).foldLeft(Map.empty[PieceUpgrade, Int]) {
|
pieces
|
||||||
case (acc, (Some(k), v)) => acc + (k -> v.size)
|
.groupBy(_.upgrade)
|
||||||
case (acc, _) => acc
|
.foldLeft(Map.empty[Piece.PieceUpgrade, Int]) {
|
||||||
} withDefaultValue 0
|
case (acc, (Some(k), v)) => acc + (k -> v.size)
|
||||||
|
case (acc, _) => acc
|
||||||
|
}
|
||||||
|
.withDefaultValue(0)
|
||||||
|
|
||||||
def withPiece(piece: Piece): BiS = copy(pieces :+ piece)
|
def withPiece(piece: Piece): BiS = copy(pieces :+ piece)
|
||||||
|
|
||||||
def withoutPiece(piece: Piece): BiS = copy(pieces.filterNot(_.strictEqual(piece)))
|
def withoutPiece(piece: Piece): BiS = copy(pieces.filterNot(_.strictEqual(piece)))
|
||||||
|
|
||||||
override def equals(obj: Any): Boolean = {
|
override def equals(obj: Any): Boolean = {
|
||||||
def comparePieces(left: Seq[Piece], right: Seq[Piece]): Boolean =
|
def comparePieces(left: Seq[Piece], right: Seq[Piece]): Boolean =
|
||||||
left.groupBy(identity).view.mapValues(_.size).forall {
|
left.groupBy(identity).view.mapValues(_.size).forall { case (key, count) =>
|
||||||
case (key, count) => right.count(_.strictEqual(key)) == count
|
right.count(_.strictEqual(key)) == count
|
||||||
}
|
}
|
||||||
|
|
||||||
obj match {
|
obj match {
|
||||||
@ -39,5 +43,5 @@ case class BiS(pieces: Seq[Piece]) {
|
|||||||
|
|
||||||
object BiS {
|
object BiS {
|
||||||
|
|
||||||
def empty: BiS = BiS(Seq.empty)
|
val empty: BiS = BiS(Seq.empty)
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (c) 2019 Evgeniy Alekseev.
|
* Copyright (c) 2019-2022 Evgeniy Alekseev.
|
||||||
*
|
*
|
||||||
* This file is part of ffxivbis
|
* This file is part of ffxivbis
|
||||||
* (see https://github.com/arcan1s/ffxivbis).
|
* (see https://github.com/arcan1s/ffxivbis).
|
||||||
@ -8,6 +8,26 @@
|
|||||||
*/
|
*/
|
||||||
package me.arcanis.ffxivbis.models
|
package me.arcanis.ffxivbis.models
|
||||||
|
|
||||||
|
sealed trait Job extends Equals {
|
||||||
|
|
||||||
|
def leftSide: Job.LeftSide
|
||||||
|
|
||||||
|
def rightSide: Job.RightSide
|
||||||
|
|
||||||
|
// conversion to string to avoid recursion
|
||||||
|
override def canEqual(that: Any): Boolean = that.isInstanceOf[Job]
|
||||||
|
|
||||||
|
override def equals(obj: Any): Boolean = {
|
||||||
|
def equality(objRepr: String): Boolean = objRepr match {
|
||||||
|
case _ if objRepr == Job.AnyJob.toString => true
|
||||||
|
case _ if this.toString == Job.AnyJob.toString => true
|
||||||
|
case _ => this.toString == objRepr
|
||||||
|
}
|
||||||
|
|
||||||
|
canEqual(obj) && equality(obj.toString)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
object Job {
|
object Job {
|
||||||
|
|
||||||
sealed trait RightSide
|
sealed trait RightSide
|
||||||
@ -26,49 +46,38 @@ object Job {
|
|||||||
object BodyTanks extends LeftSide
|
object BodyTanks extends LeftSide
|
||||||
object BodyRanges extends LeftSide
|
object BodyRanges extends LeftSide
|
||||||
|
|
||||||
sealed trait Job extends Equals {
|
|
||||||
|
|
||||||
def leftSide: LeftSide
|
|
||||||
def rightSide: RightSide
|
|
||||||
|
|
||||||
// conversion to string to avoid recursion
|
|
||||||
override def canEqual(that: Any): Boolean = that.isInstanceOf[Job]
|
|
||||||
|
|
||||||
override def equals(obj: Any): Boolean = {
|
|
||||||
def equality(objRepr: String): Boolean = objRepr match {
|
|
||||||
case _ if objRepr == AnyJob.toString => true
|
|
||||||
case _ if this.toString == AnyJob.toString => true
|
|
||||||
case _ => this.toString == objRepr
|
|
||||||
}
|
|
||||||
|
|
||||||
canEqual(obj) && equality(obj.toString)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
case object AnyJob extends Job {
|
case object AnyJob extends Job {
|
||||||
val leftSide: LeftSide = null
|
override val leftSide: LeftSide = null
|
||||||
val rightSide: RightSide = null
|
override val rightSide: RightSide = null
|
||||||
}
|
}
|
||||||
|
|
||||||
trait Casters extends Job {
|
trait Casters extends Job {
|
||||||
val leftSide: LeftSide = BodyCasters
|
override val leftSide: LeftSide = BodyCasters
|
||||||
val rightSide: RightSide = AccessoriesInt
|
override val rightSide: RightSide = AccessoriesInt
|
||||||
}
|
}
|
||||||
trait Healers extends Job {
|
trait Healers extends Job {
|
||||||
val leftSide: LeftSide = BodyHealers
|
override val leftSide: LeftSide = BodyHealers
|
||||||
val rightSide: RightSide = AccessoriesMnd
|
override val rightSide: RightSide = AccessoriesMnd
|
||||||
}
|
}
|
||||||
trait Mnks extends Job {
|
trait Mnks extends Job {
|
||||||
val leftSide: LeftSide = BodyMnks
|
override val leftSide: LeftSide = BodyMnks
|
||||||
val rightSide: RightSide = AccessoriesStr
|
override val rightSide: RightSide = AccessoriesStr
|
||||||
|
}
|
||||||
|
trait Drgs extends Job {
|
||||||
|
override val leftSide: LeftSide = BodyDrgs
|
||||||
|
override val rightSide: RightSide = AccessoriesStr
|
||||||
|
}
|
||||||
|
trait Nins extends Job {
|
||||||
|
override val leftSide: LeftSide = BodyNins
|
||||||
|
override val rightSide: RightSide = AccessoriesDex
|
||||||
}
|
}
|
||||||
trait Tanks extends Job {
|
trait Tanks extends Job {
|
||||||
val leftSide: LeftSide = BodyTanks
|
override val leftSide: LeftSide = BodyTanks
|
||||||
val rightSide: RightSide = AccessoriesVit
|
override val rightSide: RightSide = AccessoriesVit
|
||||||
}
|
}
|
||||||
trait Ranges extends Job {
|
trait Ranges extends Job {
|
||||||
val leftSide: LeftSide = BodyRanges
|
override val leftSide: LeftSide = BodyRanges
|
||||||
val rightSide: RightSide = AccessoriesDex
|
override val rightSide: RightSide = AccessoriesDex
|
||||||
}
|
}
|
||||||
|
|
||||||
case object PLD extends Tanks
|
case object PLD extends Tanks
|
||||||
@ -79,17 +88,14 @@ object Job {
|
|||||||
case object WHM extends Healers
|
case object WHM extends Healers
|
||||||
case object SCH extends Healers
|
case object SCH extends Healers
|
||||||
case object AST extends Healers
|
case object AST extends Healers
|
||||||
|
case object SGE extends Healers
|
||||||
|
|
||||||
case object MNK extends Mnks
|
case object MNK extends Mnks
|
||||||
case object DRG extends Job {
|
case object DRG extends Drgs
|
||||||
val leftSide: LeftSide = BodyDrgs
|
case object RPR extends Drgs
|
||||||
val rightSide: RightSide = AccessoriesStr
|
case object NIN extends Nins
|
||||||
}
|
|
||||||
case object NIN extends Job {
|
|
||||||
val leftSide: LeftSide = BodyNins
|
|
||||||
val rightSide: RightSide = AccessoriesDex
|
|
||||||
}
|
|
||||||
case object SAM extends Mnks
|
case object SAM extends Mnks
|
||||||
|
case object VPR extends Mnks
|
||||||
|
|
||||||
case object BRD extends Ranges
|
case object BRD extends Ranges
|
||||||
case object MCH extends Ranges
|
case object MCH extends Ranges
|
||||||
@ -98,12 +104,13 @@ object Job {
|
|||||||
case object BLM extends Casters
|
case object BLM extends Casters
|
||||||
case object SMN extends Casters
|
case object SMN extends Casters
|
||||||
case object RDM extends Casters
|
case object RDM extends Casters
|
||||||
|
case object PCT extends Casters
|
||||||
|
|
||||||
lazy val available: Seq[Job] =
|
val available: Seq[Job] =
|
||||||
Seq(PLD, WAR, DRK, GNB, WHM, SCH, AST, MNK, DRG, NIN, SAM, BRD, MCH, DNC, BLM, SMN, RDM)
|
Seq(PLD, WAR, DRK, GNB, WHM, SCH, AST, SGE, MNK, DRG, RPR, NIN, SAM, VPR, BRD, MCH, DNC, BLM, SMN, RDM, PCT)
|
||||||
lazy val availableWithAnyJob: Seq[Job] = available.prepended(AnyJob)
|
val availableWithAnyJob: Seq[Job] = available.prepended(AnyJob)
|
||||||
|
|
||||||
def withName(job: String): Job.Job =
|
def withName(job: String): Job =
|
||||||
availableWithAnyJob.find(_.toString.equalsIgnoreCase(job)) match {
|
availableWithAnyJob.find(_.toString.equalsIgnoreCase(job)) match {
|
||||||
case Some(value) => value
|
case Some(value) => value
|
||||||
case None if job.isEmpty => AnyJob
|
case None if job.isEmpty => AnyJob
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (c) 2019 Evgeniy Alekseev.
|
* Copyright (c) 2019-2022 Evgeniy Alekseev.
|
||||||
*
|
*
|
||||||
* This file is part of ffxivbis
|
* This file is part of ffxivbis
|
||||||
* (see https://github.com/arcan1s/ffxivbis).
|
* (see https://github.com/arcan1s/ffxivbis).
|
||||||
@ -12,5 +12,5 @@ import java.time.Instant
|
|||||||
|
|
||||||
case class Loot(playerId: Long, piece: Piece, timestamp: Instant, isFreeLoot: Boolean) {
|
case class Loot(playerId: Long, piece: Piece, timestamp: Instant, isFreeLoot: Boolean) {
|
||||||
|
|
||||||
def isFreeLootToString: String = if (isFreeLoot) "yes" else "no"
|
lazy val isFreeLootToInt: Int = if (isFreeLoot) 1 else 0
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (c) 2019 Evgeniy Alekseev.
|
* Copyright (c) 2019-2022 Evgeniy Alekseev.
|
||||||
*
|
*
|
||||||
* This file is part of ffxivbis
|
* This file is part of ffxivbis
|
||||||
* (see https://github.com/arcan1s/ffxivbis).
|
* (see https://github.com/arcan1s/ffxivbis).
|
||||||
@ -14,19 +14,23 @@ import me.arcanis.ffxivbis.service.LootSelector
|
|||||||
|
|
||||||
import scala.jdk.CollectionConverters._
|
import scala.jdk.CollectionConverters._
|
||||||
import scala.util.Random
|
import scala.util.Random
|
||||||
|
import scala.util.control.NonFatal
|
||||||
|
|
||||||
case class Party(partyDescription: PartyDescription, rules: Seq[String], players: Map[PlayerId, Player])
|
case class Party(partyDescription: PartyDescription, rules: Seq[String], players: Map[PlayerId, Player])
|
||||||
extends StrictLogging {
|
extends StrictLogging {
|
||||||
|
|
||||||
require(players.keys.forall(_.partyId == partyDescription.partyId), "party id must be same")
|
require(players.keys.forall(_.partyId == partyDescription.partyId), "party id must be same")
|
||||||
|
|
||||||
def getPlayers: Seq[Player] = players.values.toSeq
|
def getPlayers: Seq[Player] = players.values.toSeq
|
||||||
|
|
||||||
def player(playerId: PlayerId): Option[Player] = players.get(playerId)
|
def player(playerId: PlayerId): Option[Player] = players.get(playerId)
|
||||||
|
|
||||||
def withPlayer(player: Player): Party =
|
def withPlayer(player: Player): Party =
|
||||||
try {
|
try {
|
||||||
require(player.partyId == partyDescription.partyId, "player must belong to this party")
|
require(player.partyId == partyDescription.partyId, "player must belong to this party")
|
||||||
copy(players = players + (player.playerId -> player))
|
copy(players = players + (player.playerId -> player))
|
||||||
} catch {
|
} catch {
|
||||||
case exception: Exception =>
|
case NonFatal(exception) =>
|
||||||
logger.error("cannot add player", exception)
|
logger.error("cannot add player", exception)
|
||||||
this
|
this
|
||||||
}
|
}
|
||||||
@ -37,15 +41,19 @@ case class Party(partyDescription: PartyDescription, rules: Seq[String], players
|
|||||||
|
|
||||||
object Party {
|
object Party {
|
||||||
|
|
||||||
def apply(party: PartyDescription, config: Config,
|
def apply(
|
||||||
players: Map[Long, Player], bis: Seq[Loot], loot: Seq[Loot]): Party = {
|
party: PartyDescription,
|
||||||
|
config: Config,
|
||||||
|
players: Map[Long, Player],
|
||||||
|
bis: Seq[Loot],
|
||||||
|
loot: Seq[Loot]
|
||||||
|
): Party = {
|
||||||
val bisByPlayer = bis.groupBy(_.playerId).view.mapValues(piece => BiS(piece.map(_.piece)))
|
val bisByPlayer = bis.groupBy(_.playerId).view.mapValues(piece => BiS(piece.map(_.piece)))
|
||||||
val lootByPlayer = loot.groupBy(_.playerId).view
|
val lootByPlayer = loot.groupBy(_.playerId).view
|
||||||
val playersWithItems = players.foldLeft(Map.empty[PlayerId, Player]) {
|
val playersWithItems = players.foldLeft(Map.empty[PlayerId, Player]) { case (acc, (playerId, player)) =>
|
||||||
case (acc, (playerId, player)) =>
|
acc + (player.playerId -> player
|
||||||
acc + (player.playerId -> player
|
.withBiS(bisByPlayer.get(playerId))
|
||||||
.withBiS(bisByPlayer.get(playerId))
|
.withLoot(lootByPlayer.getOrElse(playerId, Seq.empty)))
|
||||||
.withLoot(lootByPlayer.getOrElse(playerId, Seq.empty)))
|
|
||||||
}
|
}
|
||||||
Party(party, getRules(config), playersWithItems)
|
Party(party, getRules(config), playersWithItems)
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (c) 2020 Evgeniy Alekseev.
|
* Copyright (c) 2019-2022 Evgeniy Alekseev.
|
||||||
*
|
*
|
||||||
* This file is part of ffxivbis
|
* This file is part of ffxivbis
|
||||||
* (see https://github.com/arcan1s/ffxivbis).
|
* (see https://github.com/arcan1s/ffxivbis).
|
||||||
@ -8,12 +8,9 @@
|
|||||||
*/
|
*/
|
||||||
package me.arcanis.ffxivbis.models
|
package me.arcanis.ffxivbis.models
|
||||||
|
|
||||||
case class PartyDescription(partyId: String, partyAlias: Option[String]) {
|
case class PartyDescription(partyId: String, partyAlias: Option[String])
|
||||||
|
|
||||||
def alias: String = partyAlias.getOrElse(partyId)
|
|
||||||
}
|
|
||||||
|
|
||||||
object PartyDescription {
|
object PartyDescription {
|
||||||
|
|
||||||
def empty(partyId: String): PartyDescription = PartyDescription(partyId, None)
|
def empty(partyId: String): PartyDescription = PartyDescription(partyId, None)
|
||||||
}
|
}
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user