mirror of
https://github.com/arcan1s/ffxivbis.git
synced 2025-07-07 02:45:52 +00:00
Compare commits
23 Commits
Author | SHA1 | Date | |
---|---|---|---|
a6991a0a91 | |||
5ec372be87 | |||
bcdc88fa2c | |||
53b42a6fa8 | |||
99ed2705a2 | |||
0ed9e92441 | |||
1866a1bb12 | |||
08f7f4571e | |||
d9cbb6cf00 | |||
df8e09f02c | |||
df1f28c7ef | |||
8d516cdb15 | |||
2e16a8c1fa | |||
25b05aa289 | |||
534ed98459 | |||
0171b229a1 | |||
10c107d2c2 | |||
16ce0bf61c | |||
1e6064e081 | |||
92e2c1d383 | |||
5eae1d46a2 | |||
eb24019965 | |||
173ea9079f |
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: 8
|
||||||
|
- name: create dist
|
||||||
|
run: sbt -v 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: 8
|
||||||
|
- name: run tests
|
||||||
|
run: sbt -v +test
|
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
|
|
@ -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
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
name := "ffxivbis"
|
name := "ffxivbis"
|
||||||
|
|
||||||
scalaVersion := "2.13.1"
|
scalaVersion := "2.13.6"
|
||||||
|
|
||||||
scalacOptions ++= Seq("-deprecation", "-feature")
|
scalacOptions ++= Seq("-deprecation", "-feature")
|
||||||
|
|
||||||
|
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,19 +1,33 @@
|
|||||||
libraryDependencies += "ch.qos.logback" % "logback-classic" % "1.2.3"
|
val AkkaVersion = "2.6.17"
|
||||||
libraryDependencies += "com.typesafe.scala-logging" %% "scala-logging" % "3.9.2"
|
val AkkaHttpVersion = "10.2.7"
|
||||||
|
val ScalaTestVersion = "3.2.10"
|
||||||
|
val SlickVersion = "3.3.3"
|
||||||
|
|
||||||
libraryDependencies += "com.typesafe.akka" %% "akka-http" % "10.1.10"
|
libraryDependencies += "ch.qos.logback" % "logback-classic" % "1.2.9"
|
||||||
libraryDependencies += "com.typesafe.akka" %% "akka-http-spray-json" % "10.1.10"
|
libraryDependencies += "com.typesafe.scala-logging" %% "scala-logging" % "3.9.4"
|
||||||
libraryDependencies += "com.typesafe.akka" %% "akka-stream" % "2.5.23"
|
|
||||||
libraryDependencies += "com.github.swagger-akka-http" %% "swagger-akka-http" % "2.0.4"
|
|
||||||
libraryDependencies += "javax.ws.rs" % "javax.ws.rs-api" % "2.1.1"
|
|
||||||
|
|
||||||
libraryDependencies += "io.spray" %% "spray-json" % "1.3.5"
|
libraryDependencies += "com.typesafe.akka" %% "akka-http" % AkkaHttpVersion
|
||||||
libraryDependencies += "com.lihaoyi" %% "scalatags" % "0.7.0"
|
libraryDependencies += "com.typesafe.akka" %% "akka-http-spray-json" % AkkaHttpVersion
|
||||||
|
libraryDependencies += "com.typesafe.akka" %% "akka-actor-typed" % AkkaVersion
|
||||||
|
libraryDependencies += "com.typesafe.akka" %% "akka-stream" % AkkaVersion
|
||||||
|
libraryDependencies += "com.github.swagger-akka-http" %% "swagger-akka-http" % "2.6.0"
|
||||||
|
libraryDependencies += "jakarta.platform" % "jakarta.jakartaee-web-api" % "9.1.0"
|
||||||
|
|
||||||
libraryDependencies += "com.typesafe.slick" %% "slick" % "3.3.2"
|
libraryDependencies += "io.spray" %% "spray-json" % "1.3.6"
|
||||||
libraryDependencies += "com.typesafe.slick" %% "slick-hikaricp" % "3.3.2"
|
libraryDependencies += "com.lihaoyi" %% "scalatags" % "0.9.2"
|
||||||
libraryDependencies += "org.flywaydb" % "flyway-core" % "6.0.6"
|
|
||||||
libraryDependencies += "org.xerial" % "sqlite-jdbc" % "3.28.0"
|
|
||||||
libraryDependencies += "org.postgresql" % "postgresql" % "9.3-1104-jdbc4"
|
|
||||||
|
|
||||||
libraryDependencies += "org.mindrot" % "jbcrypt" % "0.3m"
|
libraryDependencies += "com.typesafe.slick" %% "slick" % SlickVersion
|
||||||
|
libraryDependencies += "com.typesafe.slick" %% "slick-hikaricp" % SlickVersion
|
||||||
|
libraryDependencies += "org.flywaydb" % "flyway-core" % "8.2.2"
|
||||||
|
libraryDependencies += "org.xerial" % "sqlite-jdbc" % "3.36.0.3"
|
||||||
|
libraryDependencies += "org.postgresql" % "postgresql" % "42.3.1"
|
||||||
|
|
||||||
|
libraryDependencies += "org.mindrot" % "jbcrypt" % "0.4"
|
||||||
|
|
||||||
|
// testing
|
||||||
|
libraryDependencies += "org.scalactic" %% "scalactic" % ScalaTestVersion % "test"
|
||||||
|
libraryDependencies += "org.scalatest" %% "scalatest" % ScalaTestVersion % "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-http-testkit" % AkkaHttpVersion % "test"
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.14.6")
|
addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.14.6")
|
||||||
|
addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.4.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")
|
addSbtPlugin("net.virtual-void" % "sbt-dependency-graph" % "0.10.0-RC1")
|
||||||
|
@ -0,0 +1,5 @@
|
|||||||
|
create table parties (
|
||||||
|
player_id bigserial unique,
|
||||||
|
party_name text not null,
|
||||||
|
party_alias text);
|
||||||
|
create unique index parties_party_name_idx on parties(party_name);
|
@ -0,0 +1,17 @@
|
|||||||
|
-- loot
|
||||||
|
alter table loot add column piece_type text;
|
||||||
|
|
||||||
|
update loot set piece_type = 'Tome' where is_tome = 1;
|
||||||
|
update loot set piece_type = 'Savage' where is_tome = 0;
|
||||||
|
|
||||||
|
alter table loot alter column piece_type set not null;
|
||||||
|
alter table loot drop column is_tome;
|
||||||
|
|
||||||
|
-- bis
|
||||||
|
alter table bis add column piece_type text;
|
||||||
|
|
||||||
|
update bis set piece_type = 'Tome' where is_tome = 1;
|
||||||
|
update bis set piece_type = 'Savage' where is_tome = 0;
|
||||||
|
|
||||||
|
alter table bis alter column piece_type set not null;
|
||||||
|
alter table bis drop column is_tome;
|
@ -0,0 +1 @@
|
|||||||
|
alter table loot add column is_free_loot integer not null default 0;
|
@ -0,0 +1,2 @@
|
|||||||
|
drop index bis_piece_player_id_idx;
|
||||||
|
create index bis_piece_type_player_id_idx on bis(player_id, piece, piece_type);
|
@ -0,0 +1 @@
|
|||||||
|
alter table parties rename column player_id to party_id;
|
@ -0,0 +1,5 @@
|
|||||||
|
create table parties (
|
||||||
|
player_id integer primary key autoincrement,
|
||||||
|
party_name text not null,
|
||||||
|
party_alias text);
|
||||||
|
create unique index parties_party_name_idx on parties(party_name);
|
@ -0,0 +1,42 @@
|
|||||||
|
-- loot
|
||||||
|
alter table loot add column piece_type text;
|
||||||
|
|
||||||
|
update loot set piece_type = 'Tome' where is_tome = 1;
|
||||||
|
update loot set piece_type = 'Savage' where is_tome = 0;
|
||||||
|
|
||||||
|
create table loot_new (
|
||||||
|
loot_id integer primary key autoincrement,
|
||||||
|
player_id integer not null,
|
||||||
|
created integer not null,
|
||||||
|
piece text not null,
|
||||||
|
piece_type text not null,
|
||||||
|
job text not null,
|
||||||
|
foreign key (player_id) references players(player_id) on delete cascade);
|
||||||
|
insert into loot_new select loot_id, player_id, created, piece, piece_type, job from loot;
|
||||||
|
|
||||||
|
drop index loot_owner_idx;
|
||||||
|
drop table loot;
|
||||||
|
|
||||||
|
alter table loot_new rename to loot;
|
||||||
|
create index loot_owner_idx on loot(player_id);
|
||||||
|
|
||||||
|
-- bis
|
||||||
|
alter table bis add column piece_type text;
|
||||||
|
|
||||||
|
update bis set piece_type = 'Tome' where is_tome = 1;
|
||||||
|
update bis set piece_type = 'Savage' where is_tome = 0;
|
||||||
|
|
||||||
|
create table bis_new (
|
||||||
|
player_id integer not null,
|
||||||
|
created integer not null,
|
||||||
|
piece text not null,
|
||||||
|
piece_type text not null,
|
||||||
|
job text not null,
|
||||||
|
foreign key (player_id) references players(player_id) on delete cascade);
|
||||||
|
insert into bis_new select player_id, created, piece, piece_type, job from bis;
|
||||||
|
|
||||||
|
drop index bis_piece_player_id_idx;
|
||||||
|
drop table bis;
|
||||||
|
|
||||||
|
alter table bis_new rename to bis;
|
||||||
|
create unique index bis_piece_player_id_idx on bis(player_id, piece);
|
20
src/main/resources/db/migration/sqlite/V5_0__Free_loot.sql
Normal file
20
src/main/resources/db/migration/sqlite/V5_0__Free_loot.sql
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
alter table loot add column is_free_loot integer;
|
||||||
|
|
||||||
|
update loot set is_free_loot = 0;
|
||||||
|
|
||||||
|
create table loot_new (
|
||||||
|
loot_id integer primary key autoincrement,
|
||||||
|
player_id integer not null,
|
||||||
|
created integer not null,
|
||||||
|
piece text not null,
|
||||||
|
piece_type text not null,
|
||||||
|
job text not null,
|
||||||
|
is_free_loot integer not null,
|
||||||
|
foreign key (player_id) references players(player_id) on delete cascade);
|
||||||
|
insert into loot_new select loot_id, player_id, created, piece, piece_type, job, is_free_loot from loot;
|
||||||
|
|
||||||
|
drop index loot_owner_idx;
|
||||||
|
drop table loot;
|
||||||
|
|
||||||
|
alter table loot_new rename to loot;
|
||||||
|
create index loot_owner_idx on loot(player_id);
|
@ -0,0 +1,2 @@
|
|||||||
|
drop index bis_piece_player_id_idx;
|
||||||
|
create index bis_piece_type_player_id_idx on bis(player_id, piece, piece_type);
|
@ -0,0 +1,11 @@
|
|||||||
|
create table parties_new (
|
||||||
|
party_id integer primary key autoincrement,
|
||||||
|
party_name text not null,
|
||||||
|
party_alias text);
|
||||||
|
insert into parties_new select player_id, party_name, party_alias from parties;
|
||||||
|
|
||||||
|
drop index parties_party_name_idx;
|
||||||
|
drop table parties;
|
||||||
|
|
||||||
|
alter table parties_new rename to parties;
|
||||||
|
create unique index parties_party_name_idx on parties(party_name);
|
@ -1,7 +1,7 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<title>ReDoc</title>
|
<title>FFXIV loot tracker API</title>
|
||||||
<!-- needed for adaptive design -->
|
<!-- needed for adaptive design -->
|
||||||
<meta charset="utf-8"/>
|
<meta charset="utf-8"/>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
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
@ -8,9 +8,10 @@
|
|||||||
</root>
|
</root>
|
||||||
|
|
||||||
<logger name="me.arcanis.ffxivbis" level="DEBUG" />
|
<logger name="me.arcanis.ffxivbis" level="DEBUG" />
|
||||||
<logger name="http" level="DEBUG">
|
<logger name="http" level="DEBUG" additivity="false">
|
||||||
<appender-ref ref="http" />
|
<appender-ref ref="http" />
|
||||||
</logger>
|
</logger>
|
||||||
<logger name="slick" level="INFO" />
|
<logger name="slick" level="INFO" />
|
||||||
|
<logger name="org.flywaydb.core.internal" level="INFO" />
|
||||||
|
|
||||||
</configuration>
|
</configuration>
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
me.arcanis.ffxivbis {
|
me.arcanis.ffxivbis {
|
||||||
ariyala {
|
|
||||||
# ariyala base url, string, required
|
bis-provider {
|
||||||
ariyala-url = "https://ffxiv.ariyala.com"
|
include "item_data.json"
|
||||||
|
|
||||||
# xivapi base url, string, required
|
# xivapi base url, string, required
|
||||||
xivapi-url = "https://xivapi.com"
|
xivapi-url = "https://xivapi.com"
|
||||||
# xivapi developer key, string, optional
|
# xivapi developer key, string, optional
|
||||||
@ -56,12 +57,6 @@ me.arcanis.ffxivbis {
|
|||||||
port = 8000
|
port = 8000
|
||||||
# hostname to use in docs, if not set host:port will be used
|
# hostname to use in docs, if not set host:port will be used
|
||||||
#hostname = "127.0.0.1:8000"
|
#hostname = "127.0.0.1:8000"
|
||||||
|
|
||||||
# rate limits
|
|
||||||
limits {
|
|
||||||
intetval = 1m
|
|
||||||
max-count = 60
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
default-dispatcher {
|
default-dispatcher {
|
||||||
|
@ -8,45 +8,66 @@
|
|||||||
*/
|
*/
|
||||||
package me.arcanis.ffxivbis
|
package me.arcanis.ffxivbis
|
||||||
|
|
||||||
import akka.actor.{Actor, Props}
|
import akka.actor.typed.{Behavior, PostStop, Signal}
|
||||||
|
import akka.actor.typed.scaladsl.{AbstractBehavior, ActorContext, Behaviors}
|
||||||
import akka.http.scaladsl.Http
|
import akka.http.scaladsl.Http
|
||||||
import akka.stream.ActorMaterializer
|
import akka.http.scaladsl.server.Route
|
||||||
|
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.impl.DatabaseImpl
|
import me.arcanis.ffxivbis.service.bis.BisProvider
|
||||||
import me.arcanis.ffxivbis.service.{Ariyala, PartyService}
|
import me.arcanis.ffxivbis.service.database.Database
|
||||||
|
import me.arcanis.ffxivbis.service.PartyService
|
||||||
import me.arcanis.ffxivbis.storage.Migration
|
import me.arcanis.ffxivbis.storage.Migration
|
||||||
|
|
||||||
import scala.concurrent.duration.Duration
|
import scala.concurrent.ExecutionContext
|
||||||
import scala.concurrent.{Await, ExecutionContext}
|
import scala.jdk.CollectionConverters._
|
||||||
import scala.util.{Failure, Success}
|
import scala.util.{Failure, Success}
|
||||||
|
|
||||||
class Application extends Actor with StrictLogging {
|
class Application(context: ActorContext[Nothing]) extends AbstractBehavior[Nothing](context) with StrictLogging {
|
||||||
implicit private val executionContext: ExecutionContext = context.system.dispatcher
|
|
||||||
implicit private val materializer: ActorMaterializer = ActorMaterializer()
|
|
||||||
|
|
||||||
private val config = context.system.settings.config
|
logger.info("root supervisor started")
|
||||||
private val host = config.getString("me.arcanis.ffxivbis.web.host")
|
startApplication()
|
||||||
private val port = config.getInt("me.arcanis.ffxivbis.web.port")
|
|
||||||
|
|
||||||
override def receive: Receive = Actor.emptyBehavior
|
override def onMessage(msg: Nothing): Behavior[Nothing] = Behaviors.unhandled
|
||||||
|
|
||||||
Migration(config).onComplete {
|
override def onSignal: PartialFunction[Signal, Behavior[Nothing]] = { case PostStop =>
|
||||||
case Success(_) =>
|
logger.info("root supervisor stopped")
|
||||||
val ariyala = context.system.actorOf(Ariyala.props, "ariyala")
|
Behaviors.same
|
||||||
val storage = context.system.actorOf(DatabaseImpl.props, "storage")
|
}
|
||||||
val party = context.system.actorOf(PartyService.props(storage), "party")
|
|
||||||
val http = new RootEndpoint(context.system, party, ariyala)
|
|
||||||
|
|
||||||
logger.info(s"start server at $host:$port")
|
private def startApplication(): Unit = {
|
||||||
val bind = Http()(context.system).bindAndHandle(http.route, host, port)
|
val config = context.system.settings.config
|
||||||
Await.result(context.system.whenTerminated, Duration.Inf)
|
val host = config.getString("me.arcanis.ffxivbis.web.host")
|
||||||
bind.foreach(_.unbind())
|
val port = config.getInt("me.arcanis.ffxivbis.web.port")
|
||||||
|
|
||||||
case Failure(exception) => throw exception
|
implicit val executionContext: ExecutionContext = context.system.executionContext
|
||||||
|
implicit val materializer: Materializer = Materializer(context)
|
||||||
|
|
||||||
|
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 flow = Route.toFlow(http.route)(context.system)
|
||||||
|
Http(context.system).newServerAt(host, port).bindFlow(flow)
|
||||||
|
|
||||||
|
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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
object Application {
|
object Application {
|
||||||
def props: Props = Props(new Application)
|
|
||||||
|
def apply(): Behavior[Nothing] =
|
||||||
|
Behaviors.setup[Nothing](context => new Application(context))
|
||||||
}
|
}
|
||||||
|
@ -8,13 +8,13 @@
|
|||||||
*/
|
*/
|
||||||
package me.arcanis.ffxivbis
|
package me.arcanis.ffxivbis
|
||||||
|
|
||||||
import akka.actor.ActorSystem
|
import akka.actor.typed.ActorSystem
|
||||||
import com.typesafe.config.ConfigFactory
|
import com.typesafe.config.ConfigFactory
|
||||||
|
|
||||||
object ffxivbis {
|
object ffxivbis {
|
||||||
|
|
||||||
def main(args: Array[String]): Unit = {
|
def main(args: Array[String]): Unit = {
|
||||||
val config = ConfigFactory.load()
|
val config = ConfigFactory.load()
|
||||||
val actorSystem = ActorSystem("ffxivbis", config)
|
ActorSystem[Nothing](Application(), "ffxivbis", config)
|
||||||
actorSystem.actorOf(Application.props, "ffxivbis")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,24 +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.ActorRef
|
|
||||||
import akka.pattern.ask
|
|
||||||
import akka.util.Timeout
|
|
||||||
import me.arcanis.ffxivbis.models.{BiS, Job}
|
|
||||||
import me.arcanis.ffxivbis.service.Ariyala
|
|
||||||
|
|
||||||
import scala.concurrent.{ExecutionContext, Future}
|
|
||||||
|
|
||||||
class AriyalaHelper(ariyala: ActorRef) {
|
|
||||||
|
|
||||||
def downloadBiS(link: String, job: Job.Job)
|
|
||||||
(implicit executionContext: ExecutionContext, timeout: Timeout): Future[BiS] =
|
|
||||||
(ariyala ? Ariyala.GetBiS(link, job)).mapTo[BiS]
|
|
||||||
}
|
|
@ -8,25 +8,24 @@
|
|||||||
*/
|
*/
|
||||||
package me.arcanis.ffxivbis.http
|
package me.arcanis.ffxivbis.http
|
||||||
|
|
||||||
import akka.actor.ActorRef
|
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.pattern.ask
|
|
||||||
import akka.util.Timeout
|
import akka.util.Timeout
|
||||||
import me.arcanis.ffxivbis.models.{Permission, User}
|
import me.arcanis.ffxivbis.messages.{GetUser, Message}
|
||||||
import me.arcanis.ffxivbis.service.impl.DatabaseUserHandler
|
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
|
def storage: ActorRef[Message]
|
||||||
|
|
||||||
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,23 +38,34 @@ trait Authorization {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
def authenticator(scope: Permission.Value)(partyId: String)
|
def authenticator(scope: Permission.Value, partyId: String)(username: String, password: String)(implicit
|
||||||
(username: String, password: String)
|
executionContext: ExecutionContext,
|
||||||
(implicit executionContext: ExecutionContext, timeout: Timeout): Future[Option[String]] =
|
timeout: Timeout,
|
||||||
(storage ? DatabaseUserHandler.GetUser(partyId, username)).mapTo[Option[User]].map {
|
scheduler: Scheduler
|
||||||
|
): Future[Option[String]] =
|
||||||
|
storage.ask(GetUser(partyId, username, _)).map {
|
||||||
case Some(user) if user.verify(password) && user.verityScope(scope) => Some(username)
|
case Some(user) if user.verify(password) && user.verityScope(scope) => Some(username)
|
||||||
case _ => None
|
case _ => None
|
||||||
}
|
}
|
||||||
|
|
||||||
def authAdmin(partyId: String)(username: String, password: String)
|
def authAdmin(partyId: String)(username: String, password: String)(implicit
|
||||||
(implicit executionContext: ExecutionContext, timeout: Timeout): Future[Option[String]] =
|
executionContext: ExecutionContext,
|
||||||
authenticator(Permission.admin)(partyId)(username, password)
|
timeout: Timeout,
|
||||||
|
scheduler: Scheduler
|
||||||
|
): Future[Option[String]] =
|
||||||
|
authenticator(Permission.admin, partyId)(username, password)
|
||||||
|
|
||||||
def authGet(partyId: String)(username: String, password: String)
|
def authGet(partyId: String)(username: String, password: String)(implicit
|
||||||
(implicit executionContext: ExecutionContext, timeout: Timeout): Future[Option[String]] =
|
executionContext: ExecutionContext,
|
||||||
authenticator(Permission.get)(partyId)(username, password)
|
timeout: Timeout,
|
||||||
|
scheduler: Scheduler
|
||||||
|
): Future[Option[String]] =
|
||||||
|
authenticator(Permission.get, partyId)(username, password)
|
||||||
|
|
||||||
def authPost(partyId: String)(username: String, password: String)
|
def authPost(partyId: String)(username: String, password: String)(implicit
|
||||||
(implicit executionContext: ExecutionContext, timeout: Timeout): Future[Option[String]] =
|
executionContext: ExecutionContext,
|
||||||
authenticator(Permission.post)(partyId)(username, password)
|
timeout: Timeout,
|
||||||
|
scheduler: Scheduler
|
||||||
|
): Future[Option[String]] =
|
||||||
|
authenticator(Permission.post, partyId)(username, password)
|
||||||
}
|
}
|
||||||
|
@ -8,40 +8,51 @@
|
|||||||
*/
|
*/
|
||||||
package me.arcanis.ffxivbis.http
|
package me.arcanis.ffxivbis.http
|
||||||
|
|
||||||
import akka.actor.ActorRef
|
import akka.actor.typed.scaladsl.AskPattern.Askable
|
||||||
import akka.pattern.ask
|
import akka.actor.typed.{ActorRef, Scheduler}
|
||||||
import akka.util.Timeout
|
import akka.util.Timeout
|
||||||
import me.arcanis.ffxivbis.http.api.v1.json.ApiAction
|
import me.arcanis.ffxivbis.http.api.v1.json.ApiAction
|
||||||
|
import me.arcanis.ffxivbis.messages.{AddPieceToBis, GetBiS, Message, RemovePieceFromBiS, RemovePiecesFromBiS}
|
||||||
import me.arcanis.ffxivbis.models.{Piece, Player, PlayerId}
|
import me.arcanis.ffxivbis.models.{Piece, Player, PlayerId}
|
||||||
import me.arcanis.ffxivbis.service.impl.DatabaseBiSHandler
|
|
||||||
|
|
||||||
import scala.concurrent.{ExecutionContext, Future}
|
import scala.concurrent.{ExecutionContext, Future}
|
||||||
|
|
||||||
class BiSHelper(storage: ActorRef, ariyala: ActorRef) extends AriyalaHelper(ariyala) {
|
trait BiSHelper extends BisProviderHelper {
|
||||||
|
|
||||||
def addPieceBiS(playerId: PlayerId, piece: Piece)
|
def storage: ActorRef[Message]
|
||||||
(implicit executionContext: ExecutionContext, timeout: Timeout): Future[Int] =
|
|
||||||
(storage ? DatabaseBiSHandler.AddPieceToBis(playerId, piece.withJob(playerId.job))).mapTo[Int]
|
|
||||||
|
|
||||||
def bis(partyId: String, playerId: Option[PlayerId])
|
def addPieceBiS(playerId: PlayerId, piece: Piece)(implicit timeout: Timeout, scheduler: Scheduler): Future[Unit] =
|
||||||
(implicit executionContext: ExecutionContext, timeout: Timeout): Future[Seq[Player]] =
|
storage.ask(AddPieceToBis(playerId, piece.withJob(playerId.job), _))
|
||||||
(storage ? DatabaseBiSHandler.GetBiS(partyId, playerId)).mapTo[Seq[Player]]
|
|
||||||
|
|
||||||
def doModifyBiS(action: ApiAction.Value, playerId: PlayerId, piece: Piece)
|
def bis(partyId: String, playerId: Option[PlayerId])(implicit
|
||||||
(implicit executionContext: ExecutionContext, timeout: Timeout): Future[Int] =
|
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 {
|
action match {
|
||||||
case ApiAction.add => addPieceBiS(playerId, piece)
|
case ApiAction.add => addPieceBiS(playerId, piece)
|
||||||
case ApiAction.remove => removePieceBiS(playerId, piece)
|
case ApiAction.remove => removePieceBiS(playerId, piece)
|
||||||
}
|
}
|
||||||
|
|
||||||
def putBiS(playerId: PlayerId, link: String)
|
def putBiS(playerId: PlayerId, link: String)(implicit
|
||||||
(implicit executionContext: ExecutionContext, timeout: Timeout): Future[Unit] =
|
executionContext: ExecutionContext,
|
||||||
downloadBiS(link, playerId.job).flatMap { bis =>
|
timeout: Timeout,
|
||||||
Future.traverse(bis.pieces)(addPieceBiS(playerId, _))
|
scheduler: Scheduler
|
||||||
}.map(_ => ())
|
): 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)
|
def removePieceBiS(playerId: PlayerId, piece: Piece)(implicit timeout: Timeout, scheduler: Scheduler): Future[Unit] =
|
||||||
(implicit executionContext: ExecutionContext, timeout: Timeout): Future[Int] =
|
storage.ask(RemovePieceFromBiS(playerId, piece, _))
|
||||||
(storage ? DatabaseBiSHandler.RemovePieceFromBiS(playerId, piece)).mapTo[Int]
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,25 @@
|
|||||||
|
/*
|
||||||
|
* 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.messages.{BiSProviderMessage, DownloadBiS}
|
||||||
|
import me.arcanis.ffxivbis.models.{BiS, Job}
|
||||||
|
|
||||||
|
import scala.concurrent.Future
|
||||||
|
|
||||||
|
trait BisProviderHelper {
|
||||||
|
|
||||||
|
def provider: ActorRef[BiSProviderMessage]
|
||||||
|
|
||||||
|
def downloadBiS(link: String, job: Job.Job)(implicit timeout: Timeout, scheduler: Scheduler): Future[BiS] =
|
||||||
|
provider.ask(DownloadBiS(link, job, _))
|
||||||
|
}
|
@ -8,38 +8,48 @@
|
|||||||
*/
|
*/
|
||||||
package me.arcanis.ffxivbis.http
|
package me.arcanis.ffxivbis.http
|
||||||
|
|
||||||
import akka.actor.ActorRef
|
import akka.actor.typed.scaladsl.AskPattern.Askable
|
||||||
import akka.pattern.ask
|
import akka.actor.typed.{ActorRef, Scheduler}
|
||||||
import akka.util.Timeout
|
import akka.util.Timeout
|
||||||
import me.arcanis.ffxivbis.http.api.v1.json.ApiAction
|
import me.arcanis.ffxivbis.http.api.v1.json.ApiAction
|
||||||
|
import me.arcanis.ffxivbis.messages.{AddPieceTo, GetLoot, Message, RemovePieceFrom, SuggestLoot}
|
||||||
import me.arcanis.ffxivbis.models.{Piece, Player, PlayerId, PlayerIdWithCounters}
|
import me.arcanis.ffxivbis.models.{Piece, Player, PlayerId, PlayerIdWithCounters}
|
||||||
import me.arcanis.ffxivbis.service.LootSelector.LootSelectorResult
|
|
||||||
import me.arcanis.ffxivbis.service.impl.DatabaseLootHandler
|
|
||||||
|
|
||||||
import scala.concurrent.{ExecutionContext, Future}
|
import scala.concurrent.{ExecutionContext, Future}
|
||||||
|
|
||||||
class LootHelper(storage: ActorRef) {
|
trait LootHelper {
|
||||||
|
|
||||||
def addPieceLoot(playerId: PlayerId, piece: Piece)
|
def storage: ActorRef[Message]
|
||||||
(implicit executionContext: ExecutionContext, timeout: Timeout): Future[Int] =
|
|
||||||
(storage ? DatabaseLootHandler.AddPieceTo(playerId, piece)).mapTo[Int]
|
|
||||||
|
|
||||||
def doModifyLoot(action: ApiAction.Value, playerId: PlayerId, piece: Piece)
|
def addPieceLoot(playerId: PlayerId, piece: Piece, isFreeLoot: Boolean)(implicit
|
||||||
(implicit executionContext: ExecutionContext, timeout: Timeout): Future[Int] =
|
timeout: Timeout,
|
||||||
action match {
|
scheduler: Scheduler
|
||||||
case ApiAction.add => addPieceLoot(playerId, piece)
|
): Future[Unit] =
|
||||||
case ApiAction.remove => removePieceLoot(playerId, piece)
|
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])
|
def loot(partyId: String, playerId: Option[PlayerId])(implicit
|
||||||
(implicit executionContext: ExecutionContext, timeout: Timeout): Future[Seq[Player]] =
|
timeout: Timeout,
|
||||||
(storage ? DatabaseLootHandler.GetLoot(partyId, playerId)).mapTo[Seq[Player]]
|
scheduler: Scheduler
|
||||||
|
): Future[Seq[Player]] =
|
||||||
|
storage.ask(GetLoot(partyId, playerId, _))
|
||||||
|
|
||||||
def removePieceLoot(playerId: PlayerId, piece: Piece)
|
def removePieceLoot(playerId: PlayerId, piece: Piece)(implicit timeout: Timeout, scheduler: Scheduler): Future[Unit] =
|
||||||
(implicit executionContext: ExecutionContext, timeout: Timeout): Future[Int] =
|
storage.ask(RemovePieceFrom(playerId, piece, _))
|
||||||
(storage ? DatabaseLootHandler.RemovePieceFrom(playerId, piece)).mapTo[Int]
|
|
||||||
|
|
||||||
def suggestPiece(partyId: String, piece: Piece)
|
def suggestPiece(partyId: String, piece: Piece)(implicit
|
||||||
(implicit executionContext: ExecutionContext, timeout: Timeout): Future[Seq[PlayerIdWithCounters]] =
|
executionContext: ExecutionContext,
|
||||||
(storage ? DatabaseLootHandler.SuggestLoot(partyId, piece)).mapTo[LootSelectorResult].map(_.result)
|
timeout: Timeout,
|
||||||
|
scheduler: Scheduler
|
||||||
|
): Future[Seq[PlayerIdWithCounters]] =
|
||||||
|
storage.ask(SuggestLoot(partyId, piece, _)).map(_.result)
|
||||||
}
|
}
|
||||||
|
@ -8,46 +8,67 @@
|
|||||||
*/
|
*/
|
||||||
package me.arcanis.ffxivbis.http
|
package me.arcanis.ffxivbis.http
|
||||||
|
|
||||||
import akka.actor.ActorRef
|
import akka.actor.typed.{ActorRef, Scheduler}
|
||||||
import akka.pattern.ask
|
import akka.actor.typed.scaladsl.AskPattern.Askable
|
||||||
import akka.util.Timeout
|
import akka.util.Timeout
|
||||||
import me.arcanis.ffxivbis.http.api.v1.json.ApiAction
|
import me.arcanis.ffxivbis.http.api.v1.json.ApiAction
|
||||||
import me.arcanis.ffxivbis.models.{Party, Player, PlayerId}
|
import me.arcanis.ffxivbis.messages.{AddPieceToBis, AddPlayer, GetParty, GetPartyDescription, GetPlayer, Message, RemovePlayer, UpdateParty}
|
||||||
import me.arcanis.ffxivbis.service.impl.{DatabaseBiSHandler, DatabasePartyHandler}
|
import me.arcanis.ffxivbis.models.{PartyDescription, Player, PlayerId}
|
||||||
|
|
||||||
import scala.concurrent.{ExecutionContext, Future}
|
import scala.concurrent.{ExecutionContext, Future}
|
||||||
|
|
||||||
class PlayerHelper(storage: ActorRef, ariyala: ActorRef) extends AriyalaHelper(ariyala) {
|
trait PlayerHelper extends BisProviderHelper {
|
||||||
|
|
||||||
def addPlayer(player: Player)
|
def storage: ActorRef[Message]
|
||||||
(implicit executionContext: ExecutionContext, timeout: Timeout): Future[Int] =
|
|
||||||
(storage ? DatabasePartyHandler.AddPlayer(player)).mapTo[Int].map { res =>
|
def addPlayer(
|
||||||
player.link match {
|
player: Player
|
||||||
case Some(link) =>
|
)(implicit executionContext: ExecutionContext, timeout: Timeout, scheduler: Scheduler): Future[Unit] =
|
||||||
downloadBiS(link, player.job).map { bis =>
|
storage
|
||||||
bis.pieces.map(storage ? DatabaseBiSHandler.AddPieceToBis(player.playerId, _))
|
.ask(ref => AddPlayer(player, ref))
|
||||||
}.map(_ => res)
|
.map { res =>
|
||||||
case None => Future.successful(res)
|
player.link.map(_.trim).filter(_.nonEmpty) match {
|
||||||
|
case Some(link) =>
|
||||||
|
downloadBiS(link, player.job)
|
||||||
|
.map { bis =>
|
||||||
|
bis.pieces.map(piece => storage.ask(AddPieceToBis(player.playerId, piece, _)))
|
||||||
|
}
|
||||||
|
.map(_ => res)
|
||||||
|
case None => Future.successful(res)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}.flatten
|
.flatten
|
||||||
|
|
||||||
def doModifyPlayer(action: ApiAction.Value, player: Player)
|
def doModifyPlayer(action: ApiAction.Value, player: Player)(implicit
|
||||||
(implicit executionContext: ExecutionContext, timeout: Timeout): Future[Int] =
|
executionContext: ExecutionContext,
|
||||||
|
timeout: Timeout,
|
||||||
|
scheduler: Scheduler
|
||||||
|
): Future[Unit] =
|
||||||
action match {
|
action match {
|
||||||
case ApiAction.add => addPlayer(player)
|
case ApiAction.add => addPlayer(player)
|
||||||
case ApiAction.remove => removePlayer(player.playerId)
|
case ApiAction.remove => removePlayer(player.playerId)
|
||||||
}
|
}
|
||||||
|
|
||||||
def getPlayers(partyId: String, maybePlayerId: Option[PlayerId])
|
def getPartyDescription(partyId: String)(implicit timeout: Timeout, scheduler: Scheduler): Future[PartyDescription] =
|
||||||
(implicit executionContext: ExecutionContext, timeout: Timeout): Future[Seq[Player]] =
|
storage.ask(GetPartyDescription(partyId, _))
|
||||||
|
|
||||||
|
def getPlayers(partyId: String, maybePlayerId: Option[PlayerId])(implicit
|
||||||
|
executionContext: ExecutionContext,
|
||||||
|
timeout: Timeout,
|
||||||
|
scheduler: Scheduler
|
||||||
|
): Future[Seq[Player]] =
|
||||||
maybePlayerId match {
|
maybePlayerId match {
|
||||||
case Some(playerId) =>
|
case Some(playerId) =>
|
||||||
(storage ? DatabasePartyHandler.GetPlayer(playerId)).mapTo[Option[Player]].map(_.toSeq)
|
storage.ask(GetPlayer(playerId, _)).map(_.toSeq)
|
||||||
case None =>
|
case None =>
|
||||||
(storage ? DatabasePartyHandler.GetParty(partyId)).mapTo[Party].map(_.players.values.toSeq)
|
storage.ask(GetParty(partyId, _)).map(_.players.values.toSeq)
|
||||||
}
|
}
|
||||||
|
|
||||||
def removePlayer(playerId: PlayerId)
|
def removePlayer(playerId: PlayerId)(implicit timeout: Timeout, scheduler: Scheduler): Future[Unit] =
|
||||||
(implicit executionContext: ExecutionContext, timeout: Timeout): Future[Int] =
|
storage.ask(RemovePlayer(playerId, _))
|
||||||
(storage ? DatabasePartyHandler.RemovePlayer(playerId)).mapTo[Int]
|
|
||||||
|
def updateDescription(
|
||||||
|
partyDescription: PartyDescription
|
||||||
|
)(implicit executionContext: ExecutionContext, timeout: Timeout, scheduler: Scheduler): Future[Unit] =
|
||||||
|
storage.ask(UpdateParty(partyDescription, _))
|
||||||
}
|
}
|
||||||
|
@ -10,25 +10,27 @@ package me.arcanis.ffxivbis.http
|
|||||||
|
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
|
|
||||||
import akka.actor.{ActorRef, ActorSystem}
|
import akka.actor.typed.{ActorRef, ActorSystem, Scheduler}
|
||||||
import akka.http.scaladsl.server.Directives._
|
import akka.http.scaladsl.server.Directives._
|
||||||
import akka.http.scaladsl.server._
|
import akka.http.scaladsl.server._
|
||||||
import akka.util.Timeout
|
import akka.util.Timeout
|
||||||
import com.typesafe.scalalogging.{Logger, StrictLogging}
|
import com.typesafe.scalalogging.{Logger, 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}
|
||||||
|
|
||||||
class RootEndpoint(system: ActorSystem, storage: ActorRef, ariyala: ActorRef)
|
class RootEndpoint(system: ActorSystem[Nothing], storage: ActorRef[Message], provider: ActorRef[BiSProviderMessage])
|
||||||
extends StrictLogging {
|
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 timeout: Timeout =
|
implicit val timeout: Timeout =
|
||||||
config.getDuration("me.arcanis.ffxivbis.settings.request-timeout")
|
config.getDuration("me.arcanis.ffxivbis.settings.request-timeout")
|
||||||
|
|
||||||
private val rootApiV1Endpoint: RootApiV1Endpoint = new RootApiV1Endpoint(storage, ariyala, config)
|
private val rootApiV1Endpoint: RootApiV1Endpoint = new RootApiV1Endpoint(storage, provider, config)
|
||||||
private val rootView: RootView = new RootView(storage, ariyala)
|
private val rootView: RootView = new RootView(storage, provider)
|
||||||
private val swagger: Swagger = new Swagger(config)
|
private val swagger: Swagger = new Swagger(config)
|
||||||
private val httpLogger = Logger("http")
|
private val httpLogger = Logger("http")
|
||||||
|
|
||||||
@ -37,35 +39,36 @@ class RootEndpoint(system: ActorSystem, storage: ActorRef, ariyala: ActorRef)
|
|||||||
val start = Instant.now.toEpochMilli
|
val start = Instant.now.toEpochMilli
|
||||||
mapResponse { response =>
|
mapResponse { response =>
|
||||||
val time = (Instant.now.toEpochMilli - start) / 1000.0
|
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""")
|
httpLogger.debug(
|
||||||
|
s"""- - [${Instant.now}] "${context.request.method.name()} ${context.request.uri.path}" ${response.status
|
||||||
|
.intValue()} ${response.entity.getContentLengthOption.getAsLong} $time"""
|
||||||
|
)
|
||||||
response
|
response
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
def route: Route =
|
def route: Route =
|
||||||
withHttpLog {
|
withHttpLog {
|
||||||
apiRoute ~ htmlRoute ~ swagger.routes ~ swaggerUIRoute
|
ignoreTrailingSlash {
|
||||||
|
apiRoute ~ htmlRoute ~ swagger.routes ~ swaggerUIRoute
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private def apiRoute: Route =
|
private def apiRoute: Route =
|
||||||
ignoreTrailingSlash {
|
pathPrefix("api") {
|
||||||
pathPrefix("api") {
|
pathPrefix(Segment) {
|
||||||
pathPrefix(Segment) {
|
case "v1" => rootApiV1Endpoint.route
|
||||||
case "v1" => rootApiV1Endpoint.route
|
case _ => reject
|
||||||
case _ => reject
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private def htmlRoute: Route =
|
private def htmlRoute: Route =
|
||||||
ignoreTrailingSlash {
|
pathPrefix("static") {
|
||||||
pathPrefix("static") {
|
getFromResourceDirectory("static")
|
||||||
getFromResourceDirectory("static")
|
} ~ rootView.route
|
||||||
} ~ rootView.route
|
|
||||||
}
|
|
||||||
|
|
||||||
private def swaggerUIRoute: Route =
|
private def swaggerUIRoute: Route =
|
||||||
path("swagger") {
|
path("swagger") {
|
||||||
getFromResource("swagger/index.html")
|
getFromResource("html/swagger.html")
|
||||||
} ~ getFromResourceDirectory("swagger")
|
}
|
||||||
}
|
}
|
||||||
|
@ -16,9 +16,13 @@ import io.swagger.v3.oas.models.security.SecurityScheme
|
|||||||
import scala.io.Source
|
import scala.io.Source
|
||||||
|
|
||||||
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.PlayerEndpoint], classOf[api.v1.TypesEndpoint],
|
classOf[api.v1.LootEndpoint],
|
||||||
|
classOf[api.v1.PartyEndpoint],
|
||||||
|
classOf[api.v1.PlayerEndpoint],
|
||||||
|
classOf[api.v1.TypesEndpoint],
|
||||||
classOf[api.v1.UserEndpoint]
|
classOf[api.v1.UserEndpoint]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -8,33 +8,30 @@
|
|||||||
*/
|
*/
|
||||||
package me.arcanis.ffxivbis.http
|
package me.arcanis.ffxivbis.http
|
||||||
|
|
||||||
import akka.actor.ActorRef
|
import akka.actor.typed.scaladsl.AskPattern.Askable
|
||||||
import akka.pattern.ask
|
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.models.User
|
import me.arcanis.ffxivbis.models.User
|
||||||
import me.arcanis.ffxivbis.service.PartyService
|
|
||||||
import me.arcanis.ffxivbis.service.impl.DatabaseUserHandler
|
|
||||||
|
|
||||||
import scala.concurrent.{ExecutionContext, Future}
|
import scala.concurrent.Future
|
||||||
|
|
||||||
class UserHelper(storage: ActorRef) {
|
trait UserHelper {
|
||||||
|
|
||||||
def addUser(user: User, isHashedPassword: Boolean)
|
def storage: ActorRef[Message]
|
||||||
(implicit executionContext: ExecutionContext, timeout: Timeout): Future[Int] =
|
|
||||||
(storage ? DatabaseUserHandler.AddUser(user, isHashedPassword)).mapTo[Int]
|
|
||||||
|
|
||||||
def newPartyId(implicit executionContext: ExecutionContext, timeout: Timeout): Future[String] =
|
def addUser(user: User, isHashedPassword: Boolean)(implicit timeout: Timeout, scheduler: Scheduler): Future[Unit] =
|
||||||
(storage ? PartyService.GetNewPartyId).mapTo[String]
|
storage.ask(AddUser(user, isHashedPassword, _))
|
||||||
|
|
||||||
def user(partyId: String, username: String)
|
def newPartyId(implicit timeout: Timeout, scheduler: Scheduler): Future[String] =
|
||||||
(implicit executionContext: ExecutionContext, timeout: Timeout): Future[Option[User]] =
|
storage.ask(GetNewPartyId)
|
||||||
(storage ? DatabaseUserHandler.GetUser(partyId, username)).mapTo[Option[User]]
|
|
||||||
|
|
||||||
def users(partyId: String)
|
def user(partyId: String, username: String)(implicit timeout: Timeout, scheduler: Scheduler): Future[Option[User]] =
|
||||||
(implicit executionContext: ExecutionContext, timeout: Timeout): Future[Seq[User]] =
|
storage.ask(GetUser(partyId, username, _))
|
||||||
(storage ? DatabaseUserHandler.GetUsers(partyId)).mapTo[Seq[User]]
|
|
||||||
|
|
||||||
def removeUser(partyId: String, username: String)
|
def users(partyId: String)(implicit timeout: Timeout, scheduler: Scheduler): Future[Seq[User]] =
|
||||||
(implicit executionContext: ExecutionContext, timeout: Timeout): Future[Int] =
|
storage.ask(GetUsers(partyId, _))
|
||||||
(storage ? DatabaseUserHandler.DeleteUser(partyId, username)).mapTo[Int]
|
|
||||||
|
def removeUser(partyId: String, username: String)(implicit timeout: Timeout, scheduler: Scheduler): Future[Unit] =
|
||||||
|
storage.ask(DeleteUser(partyId, username, _))
|
||||||
}
|
}
|
||||||
|
@ -8,7 +8,7 @@
|
|||||||
*/
|
*/
|
||||||
package me.arcanis.ffxivbis.http.api.v1
|
package me.arcanis.ffxivbis.http.api.v1
|
||||||
|
|
||||||
import akka.actor.ActorRef
|
import akka.actor.typed.{ActorRef, Scheduler}
|
||||||
import akka.http.scaladsl.model.{HttpEntity, StatusCodes}
|
import akka.http.scaladsl.model.{HttpEntity, StatusCodes}
|
||||||
import akka.http.scaladsl.server.Directives._
|
import akka.http.scaladsl.server.Directives._
|
||||||
import akka.http.scaladsl.server._
|
import akka.http.scaladsl.server._
|
||||||
@ -19,38 +19,60 @@ 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.{Authorization, BiSHelper}
|
||||||
|
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, ariyala: ActorRef)(implicit timeout: Timeout)
|
class BiSEndpoint(override val storage: ActorRef[Message], override val provider: ActorRef[BiSProviderMessage])(implicit
|
||||||
extends BiSHelper(storage, ariyala) with Authorization with JsonSupport {
|
timeout: Timeout,
|
||||||
|
scheduler: Scheduler
|
||||||
|
) extends BiSHelper
|
||||||
|
with Authorization
|
||||||
|
with JsonSupport {
|
||||||
|
|
||||||
def route: Route = createBiS ~ getBiS ~ modifyBiS
|
def route: 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 = "abcdefgh"),
|
||||||
),
|
),
|
||||||
requestBody = new RequestBody(description = "player best in slot description", required = true,
|
requestBody = new RequestBody(
|
||||||
content = Array(new Content(schema = new Schema(implementation = classOf[PlayerBiSLinkResponse])))),
|
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[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 = "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[ErrorResponse])))
|
||||||
|
),
|
||||||
|
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])))
|
||||||
|
),
|
||||||
),
|
),
|
||||||
security = Array(new SecurityRequirement(name = "basic auth", scopes = Array("post"))),
|
security = Array(new SecurityRequirement(name = "basic auth", scopes = Array("post"))),
|
||||||
tags = Array("best in slot"),
|
tags = Array("best in slot"),
|
||||||
@ -75,23 +97,44 @@ class BiSEndpoint(override val storage: ActorRef, ariyala: ActorRef)(implicit ti
|
|||||||
@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(name = "partyId", in = ParameterIn.PATH, description = "unique party ID", example = "abcdefgh"),
|
||||||
new Parameter(name = "nick", in = ParameterIn.QUERY, description = "player nick name to filter", example = "Siuan Sanche"),
|
new Parameter(
|
||||||
|
name = "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[PlayerResponse]))
|
||||||
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[ErrorResponse])))
|
||||||
|
),
|
||||||
|
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])))
|
||||||
|
),
|
||||||
),
|
),
|
||||||
security = Array(new SecurityRequirement(name = "basic auth", scopes = Array("get"))),
|
security = Array(new SecurityRequirement(name = "basic auth", scopes = Array("get"))),
|
||||||
tags = Array("best in slot"),
|
tags = Array("best in slot"),
|
||||||
@ -117,22 +160,39 @@ class BiSEndpoint(override val storage: ActorRef, ariyala: ActorRef)(implicit ti
|
|||||||
@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 = "abcdefgh"),
|
||||||
),
|
),
|
||||||
requestBody = new RequestBody(description = "action and piece description", required = true,
|
requestBody = new RequestBody(
|
||||||
content = Array(new Content(schema = new Schema(implementation = classOf[PieceActionResponse])))),
|
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[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 = "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[ErrorResponse])))
|
||||||
|
),
|
||||||
|
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])))
|
||||||
|
),
|
||||||
),
|
),
|
||||||
security = Array(new SecurityRequirement(name = "basic auth", scopes = Array("post"))),
|
security = Array(new SecurityRequirement(name = "basic auth", scopes = Array("post"))),
|
||||||
tags = Array("best in slot"),
|
tags = Array("best in slot"),
|
||||||
|
@ -17,7 +17,7 @@ import spray.json._
|
|||||||
|
|
||||||
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 ex: IllegalArgumentException =>
|
||||||
complete(StatusCodes.BadRequest, ErrorResponse(ex.getMessage))
|
complete(StatusCodes.BadRequest, ErrorResponse(ex.getMessage))
|
||||||
|
|
||||||
@ -26,12 +26,12 @@ trait HttpHandler extends StrictLogging { this: JsonSupport =>
|
|||||||
complete(StatusCodes.InternalServerError, ErrorResponse("unknown server error"))
|
complete(StatusCodes.InternalServerError, ErrorResponse("unknown server error"))
|
||||||
}
|
}
|
||||||
|
|
||||||
implicit def rejectionHandler: RejectionHandler =
|
def rejectionHandler: RejectionHandler =
|
||||||
RejectionHandler.default
|
RejectionHandler.default
|
||||||
.mapRejectionResponse {
|
.mapRejectionResponse {
|
||||||
case response @ HttpResponse(_, _, entity: HttpEntity.Strict, _) =>
|
case response @ HttpResponse(_, _, entity: HttpEntity.Strict, _) =>
|
||||||
val message = ErrorResponse(entity.data.utf8String).toJson
|
val message = ErrorResponse(entity.data.utf8String).toJson
|
||||||
response.copy(entity = HttpEntity(ContentTypes.`application/json`, message.compactPrint))
|
response.withEntity(HttpEntity(ContentTypes.`application/json`, message.compactPrint))
|
||||||
case other => other
|
case other => other
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,7 +8,7 @@
|
|||||||
*/
|
*/
|
||||||
package me.arcanis.ffxivbis.http.api.v1
|
package me.arcanis.ffxivbis.http.api.v1
|
||||||
|
|
||||||
import akka.actor.ActorRef
|
import akka.actor.typed.{ActorRef, Scheduler}
|
||||||
import akka.http.scaladsl.model.{HttpEntity, StatusCodes}
|
import akka.http.scaladsl.model.{HttpEntity, StatusCodes}
|
||||||
import akka.http.scaladsl.server.Directives._
|
import akka.http.scaladsl.server.Directives._
|
||||||
import akka.http.scaladsl.server._
|
import akka.http.scaladsl.server._
|
||||||
@ -19,39 +19,64 @@ 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.{Authorization, LootHelper}
|
||||||
|
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)(implicit timeout: Timeout)
|
class LootEndpoint(override val storage: ActorRef[Message])(implicit timeout: Timeout, scheduler: Scheduler)
|
||||||
extends LootHelper(storage) with Authorization with JsonSupport with HttpHandler {
|
extends LootHelper
|
||||||
|
with Authorization
|
||||||
|
with JsonSupport
|
||||||
|
with HttpHandler {
|
||||||
|
|
||||||
def route: Route = getLoot ~ modifyLoot
|
def route: Route = getLoot ~ modifyLoot
|
||||||
|
|
||||||
@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(name = "partyId", in = ParameterIn.PATH, description = "unique party ID", example = "abcdefgh"),
|
||||||
new Parameter(name = "nick", in = ParameterIn.QUERY, description = "player nick name to filter", example = "Siuan Sanche"),
|
new Parameter(
|
||||||
|
name = "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[PlayerResponse]))
|
||||||
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[ErrorResponse])))
|
||||||
|
),
|
||||||
|
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])))
|
||||||
|
),
|
||||||
),
|
),
|
||||||
security = Array(new SecurityRequirement(name = "basic auth", scopes = Array("get"))),
|
security = Array(new SecurityRequirement(name = "basic auth", scopes = Array("get"))),
|
||||||
tags = Array("loot"),
|
tags = Array("loot"),
|
||||||
@ -76,22 +101,39 @@ class LootEndpoint(override val storage: ActorRef)(implicit timeout: Timeout)
|
|||||||
@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 = "abcdefgh"),
|
||||||
),
|
),
|
||||||
requestBody = new RequestBody(description = "action and piece description", required = true,
|
requestBody = new RequestBody(
|
||||||
content = Array(new Content(schema = new Schema(implementation = classOf[PieceActionResponse])))),
|
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[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 = "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[ErrorResponse])))
|
||||||
|
),
|
||||||
|
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])))
|
||||||
|
),
|
||||||
),
|
),
|
||||||
security = Array(new SecurityRequirement(name = "basic auth", scopes = Array("post"))),
|
security = Array(new SecurityRequirement(name = "basic auth", scopes = Array("post"))),
|
||||||
tags = Array("loot"),
|
tags = Array("loot"),
|
||||||
@ -103,7 +145,7 @@ class LootEndpoint(override val storage: ActorRef)(implicit timeout: Timeout)
|
|||||||
post {
|
post {
|
||||||
entity(as[PieceActionResponse]) { action =>
|
entity(as[PieceActionResponse]) { action =>
|
||||||
val playerId = action.playerId.withPartyId(partyId)
|
val playerId = action.playerId.withPartyId(partyId)
|
||||||
onComplete(doModifyLoot(action.action, playerId, action.piece.toPiece)) {
|
onComplete(doModifyLoot(action.action, playerId, action.piece.toPiece, action.isFreeLoot)) {
|
||||||
case Success(_) => complete(StatusCodes.Accepted, HttpEntity.Empty)
|
case Success(_) => complete(StatusCodes.Accepted, HttpEntity.Empty)
|
||||||
case Failure(exception) => throw exception
|
case Failure(exception) => throw exception
|
||||||
}
|
}
|
||||||
@ -117,25 +159,47 @@ class LootEndpoint(override val storage: ActorRef)(implicit timeout: Timeout)
|
|||||||
@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 = "abcdefgh"),
|
||||||
),
|
),
|
||||||
requestBody = new RequestBody(description = "piece description", required = true,
|
requestBody = new RequestBody(
|
||||||
content = Array(new Content(schema = new Schema(implementation = classOf[PieceResponse])))),
|
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[PlayerIdWithCountersResponse])),
|
||||||
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[ErrorResponse])))
|
||||||
|
),
|
||||||
|
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(
|
||||||
|
responseCode = "500",
|
||||||
|
description = "Internal server error",
|
||||||
|
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))
|
||||||
|
),
|
||||||
),
|
),
|
||||||
security = Array(new SecurityRequirement(name = "basic auth", scopes = Array("get"))),
|
security = Array(new SecurityRequirement(name = "basic auth", scopes = Array("get"))),
|
||||||
tags = Array("loot"),
|
tags = Array("loot"),
|
||||||
|
@ -0,0 +1,145 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2020 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.actor.typed.{ActorRef, Scheduler}
|
||||||
|
import akka.http.scaladsl.model.{HttpEntity, StatusCodes}
|
||||||
|
import akka.http.scaladsl.server.Directives._
|
||||||
|
import akka.http.scaladsl.server._
|
||||||
|
import akka.util.Timeout
|
||||||
|
import io.swagger.v3.oas.annotations.enums.ParameterIn
|
||||||
|
import io.swagger.v3.oas.annotations.media.{Content, Schema}
|
||||||
|
import io.swagger.v3.oas.annotations.parameters.RequestBody
|
||||||
|
import io.swagger.v3.oas.annotations.responses.ApiResponse
|
||||||
|
import io.swagger.v3.oas.annotations.security.SecurityRequirement
|
||||||
|
import io.swagger.v3.oas.annotations.{Operation, Parameter}
|
||||||
|
import jakarta.ws.rs._
|
||||||
|
import me.arcanis.ffxivbis.http.api.v1.json._
|
||||||
|
import me.arcanis.ffxivbis.http.{Authorization, PlayerHelper}
|
||||||
|
import me.arcanis.ffxivbis.messages.{BiSProviderMessage, Message}
|
||||||
|
|
||||||
|
import scala.util.{Failure, Success}
|
||||||
|
|
||||||
|
@Path("/api/v1")
|
||||||
|
class PartyEndpoint(override val storage: ActorRef[Message], override val provider: ActorRef[BiSProviderMessage])(
|
||||||
|
implicit
|
||||||
|
timeout: Timeout,
|
||||||
|
scheduler: Scheduler
|
||||||
|
) extends PlayerHelper
|
||||||
|
with Authorization
|
||||||
|
with JsonSupport
|
||||||
|
with HttpHandler {
|
||||||
|
|
||||||
|
def route: Route = getPartyDescription ~ modifyPartyDescription
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("party/{partyId}/description")
|
||||||
|
@Produces(value = Array("application/json"))
|
||||||
|
@Operation(
|
||||||
|
summary = "get party description",
|
||||||
|
description = "Return the party description",
|
||||||
|
parameters = Array(
|
||||||
|
new Parameter(name = "partyId", in = ParameterIn.PATH, description = "unique party ID", example = "abcdefgh"),
|
||||||
|
),
|
||||||
|
responses = Array(
|
||||||
|
new ApiResponse(
|
||||||
|
responseCode = "200",
|
||||||
|
description = "Party description",
|
||||||
|
content = Array(new Content(schema = new Schema(implementation = classOf[PartyDescriptionResponse])))
|
||||||
|
),
|
||||||
|
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(
|
||||||
|
responseCode = "500",
|
||||||
|
description = "Internal server error",
|
||||||
|
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))
|
||||||
|
),
|
||||||
|
),
|
||||||
|
security = Array(new SecurityRequirement(name = "basic auth", scopes = Array("get"))),
|
||||||
|
tags = Array("party"),
|
||||||
|
)
|
||||||
|
def getPartyDescription: Route =
|
||||||
|
path("party" / Segment / "description") { partyId =>
|
||||||
|
extractExecutionContext { implicit executionContext =>
|
||||||
|
authenticateBasicBCrypt(s"party $partyId", authGet(partyId)) { _ =>
|
||||||
|
get {
|
||||||
|
onComplete(getPartyDescription(partyId)) {
|
||||||
|
case Success(response) => complete(PartyDescriptionResponse.fromDescription(response))
|
||||||
|
case Failure(exception) => throw exception
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Consumes(value = Array("application/json"))
|
||||||
|
@Path("party/{partyId}/description")
|
||||||
|
@Operation(
|
||||||
|
summary = "modify party description",
|
||||||
|
description = "Edit party description",
|
||||||
|
parameters = Array(
|
||||||
|
new Parameter(name = "partyId", in = ParameterIn.PATH, description = "unique party ID", example = "abcdefgh"),
|
||||||
|
),
|
||||||
|
requestBody = new RequestBody(
|
||||||
|
description = "new party description",
|
||||||
|
required = true,
|
||||||
|
content = Array(new Content(schema = new Schema(implementation = classOf[PartyDescriptionResponse])))
|
||||||
|
),
|
||||||
|
responses = Array(
|
||||||
|
new ApiResponse(responseCode = "202", description = "Party description has been modified"),
|
||||||
|
new ApiResponse(
|
||||||
|
responseCode = "400",
|
||||||
|
description = "Invalid parameters were supplied",
|
||||||
|
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[ErrorResponse])))
|
||||||
|
),
|
||||||
|
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])))
|
||||||
|
),
|
||||||
|
),
|
||||||
|
security = Array(new SecurityRequirement(name = "basic auth", scopes = Array("post"))),
|
||||||
|
tags = Array("party"),
|
||||||
|
)
|
||||||
|
def modifyPartyDescription: Route =
|
||||||
|
path("party" / Segment / "description") { partyId =>
|
||||||
|
extractExecutionContext { implicit executionContext =>
|
||||||
|
authenticateBasicBCrypt(s"party $partyId", authPost(partyId)) { _ =>
|
||||||
|
post {
|
||||||
|
entity(as[PartyDescriptionResponse]) { partyDescription =>
|
||||||
|
val description = partyDescription.copy(partyId = partyId)
|
||||||
|
onComplete(updateDescription(description.toDescription)) {
|
||||||
|
case Success(_) => complete(StatusCodes.Accepted, HttpEntity.Empty)
|
||||||
|
case Failure(exception) => throw exception
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -8,7 +8,7 @@
|
|||||||
*/
|
*/
|
||||||
package me.arcanis.ffxivbis.http.api.v1
|
package me.arcanis.ffxivbis.http.api.v1
|
||||||
|
|
||||||
import akka.actor.ActorRef
|
import akka.actor.typed.{ActorRef, Scheduler}
|
||||||
import akka.http.scaladsl.model.{HttpEntity, StatusCodes}
|
import akka.http.scaladsl.model.{HttpEntity, StatusCodes}
|
||||||
import akka.http.scaladsl.server.Directives._
|
import akka.http.scaladsl.server.Directives._
|
||||||
import akka.http.scaladsl.server._
|
import akka.http.scaladsl.server._
|
||||||
@ -19,39 +19,67 @@ 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.{Authorization, PlayerHelper}
|
||||||
|
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, ariyala: ActorRef)(implicit timeout: Timeout)
|
class PlayerEndpoint(override val storage: ActorRef[Message], override val provider: ActorRef[BiSProviderMessage])(
|
||||||
extends PlayerHelper(storage, ariyala) with Authorization with JsonSupport with HttpHandler {
|
implicit
|
||||||
|
timeout: Timeout,
|
||||||
|
scheduler: Scheduler
|
||||||
|
) extends PlayerHelper
|
||||||
|
with Authorization
|
||||||
|
with JsonSupport
|
||||||
|
with HttpHandler {
|
||||||
|
|
||||||
def route: Route = getParty ~ modifyParty
|
def route: Route = getParty ~ 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(name = "partyId", in = ParameterIn.PATH, description = "unique party ID", example = "abcdefgh"),
|
||||||
new Parameter(name = "nick", in = ParameterIn.QUERY, description = "player nick name to filter", example = "Siuan Sanche"),
|
new Parameter(
|
||||||
|
name = "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[PlayerResponse])),
|
||||||
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[ErrorResponse])))
|
||||||
|
),
|
||||||
|
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])))
|
||||||
|
),
|
||||||
),
|
),
|
||||||
security = Array(new SecurityRequirement(name = "basic auth", scopes = Array("get"))),
|
security = Array(new SecurityRequirement(name = "basic auth", scopes = Array("get"))),
|
||||||
tags = Array("party"),
|
tags = Array("party"),
|
||||||
@ -76,22 +104,39 @@ class PlayerEndpoint(override val storage: ActorRef, ariyala: ActorRef)(implicit
|
|||||||
@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 = "abcdefgh"),
|
||||||
),
|
),
|
||||||
requestBody = new RequestBody(description = "player description", required = true,
|
requestBody = new RequestBody(
|
||||||
content = Array(new Content(schema = new Schema(implementation = classOf[PlayerActionResponse])))),
|
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[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 = "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[ErrorResponse])))
|
||||||
|
),
|
||||||
|
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])))
|
||||||
|
),
|
||||||
),
|
),
|
||||||
security = Array(new SecurityRequirement(name = "basic auth", scopes = Array("post"))),
|
security = Array(new SecurityRequirement(name = "basic auth", scopes = Array("post"))),
|
||||||
tags = Array("party"),
|
tags = Array("party"),
|
||||||
|
@ -8,28 +8,32 @@
|
|||||||
*/
|
*/
|
||||||
package me.arcanis.ffxivbis.http.api.v1
|
package me.arcanis.ffxivbis.http.api.v1
|
||||||
|
|
||||||
import akka.actor.ActorRef
|
import akka.actor.typed.{ActorRef, Scheduler}
|
||||||
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 akka.util.Timeout
|
||||||
import com.typesafe.config.Config
|
import com.typesafe.config.Config
|
||||||
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}
|
||||||
|
|
||||||
class RootApiV1Endpoint(storage: ActorRef, ariyala: ActorRef, config: Config)
|
class RootApiV1Endpoint(storage: ActorRef[Message], provider: ActorRef[BiSProviderMessage], config: Config)(implicit
|
||||||
(implicit timeout: Timeout)
|
timeout: Timeout,
|
||||||
extends JsonSupport with HttpHandler {
|
scheduler: Scheduler
|
||||||
|
) extends JsonSupport
|
||||||
|
with HttpHandler {
|
||||||
|
|
||||||
private val biSEndpoint = new BiSEndpoint(storage, ariyala)
|
private val biSEndpoint = new BiSEndpoint(storage, provider)
|
||||||
private val lootEndpoint = new LootEndpoint(storage)
|
private val lootEndpoint = new LootEndpoint(storage)
|
||||||
private val playerEndpoint = new PlayerEndpoint(storage, ariyala)
|
private val partyEndpoint = new PartyEndpoint(storage, provider)
|
||||||
|
private val playerEndpoint = new PlayerEndpoint(storage, provider)
|
||||||
private val typesEndpoint = new TypesEndpoint(config)
|
private val typesEndpoint = new TypesEndpoint(config)
|
||||||
private val userEndpoint = new UserEndpoint(storage)
|
private val userEndpoint = new UserEndpoint(storage)
|
||||||
|
|
||||||
def route: Route =
|
def route: Route =
|
||||||
handleExceptions(exceptionHandler) {
|
handleExceptions(exceptionHandler) {
|
||||||
handleRejections(rejectionHandler) {
|
handleRejections(rejectionHandler) {
|
||||||
biSEndpoint.route ~ lootEndpoint.route ~ playerEndpoint.route ~
|
biSEndpoint.route ~ lootEndpoint.route ~ partyEndpoint.route ~
|
||||||
typesEndpoint.route ~ userEndpoint.route
|
playerEndpoint.route ~ typesEndpoint.route ~ userEndpoint.route
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -14,26 +14,36 @@ import com.typesafe.config.Config
|
|||||||
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 io.swagger.v3.oas.annotations.Operation
|
||||||
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.models.{Job, Party, Permission, Piece}
|
import me.arcanis.ffxivbis.models.{Job, Party, Permission, Piece, PieceType}
|
||||||
|
|
||||||
@Path("api/v1")
|
@Path("/api/v1")
|
||||||
class TypesEndpoint(config: Config) extends JsonSupport {
|
class TypesEndpoint(config: Config) extends JsonSupport {
|
||||||
|
|
||||||
def route: Route = getJobs ~ getPermissions ~ getPieces ~ getPriority
|
def route: Route = getJobs ~ getPermissions ~ getPieces ~ getPieceTypes ~ getPriority
|
||||||
|
|
||||||
@GET
|
@GET
|
||||||
@Path("types/jobs")
|
@Path("types/jobs")
|
||||||
@Produces(value = Array("application/json"))
|
@Produces(value = Array("application/json"))
|
||||||
@Operation(summary = "jobs list", description = "Returns the available jobs",
|
@Operation(
|
||||||
|
summary = "jobs list",
|
||||||
|
description = "Returns the available jobs",
|
||||||
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",
|
||||||
))),
|
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[ErrorResponse])))
|
||||||
|
),
|
||||||
),
|
),
|
||||||
tags = Array("types"),
|
tags = Array("types"),
|
||||||
)
|
)
|
||||||
@ -47,14 +57,24 @@ class TypesEndpoint(config: Config) extends JsonSupport {
|
|||||||
@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[ErrorResponse])))
|
||||||
|
),
|
||||||
),
|
),
|
||||||
tags = Array("types"),
|
tags = Array("types"),
|
||||||
)
|
)
|
||||||
@ -68,14 +88,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[ErrorResponse])))
|
||||||
|
),
|
||||||
),
|
),
|
||||||
tags = Array("types"),
|
tags = Array("types"),
|
||||||
)
|
)
|
||||||
@ -86,17 +116,58 @@ class TypesEndpoint(config: Config) extends JsonSupport {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("types/pieces/types")
|
||||||
|
@Produces(value = Array("application/json"))
|
||||||
|
@Operation(
|
||||||
|
summary = "piece types list",
|
||||||
|
description = "Returns the available piece types",
|
||||||
|
responses = Array(
|
||||||
|
new ApiResponse(
|
||||||
|
responseCode = "200",
|
||||||
|
description = "List of available piece types",
|
||||||
|
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[ErrorResponse])))
|
||||||
|
),
|
||||||
|
),
|
||||||
|
tags = Array("types"),
|
||||||
|
)
|
||||||
|
def getPieceTypes: Route =
|
||||||
|
path("types" / "pieces" / "types") {
|
||||||
|
get {
|
||||||
|
complete(PieceType.available.map(_.toString))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@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[ErrorResponse])))
|
||||||
|
),
|
||||||
),
|
),
|
||||||
tags = Array("types"),
|
tags = Array("types"),
|
||||||
)
|
)
|
||||||
|
@ -8,7 +8,7 @@
|
|||||||
*/
|
*/
|
||||||
package me.arcanis.ffxivbis.http.api.v1
|
package me.arcanis.ffxivbis.http.api.v1
|
||||||
|
|
||||||
import akka.actor.ActorRef
|
import akka.actor.typed.{ActorRef, Scheduler}
|
||||||
import akka.http.scaladsl.model.{HttpEntity, StatusCodes}
|
import akka.http.scaladsl.model.{HttpEntity, StatusCodes}
|
||||||
import akka.http.scaladsl.server.Directives._
|
import akka.http.scaladsl.server.Directives._
|
||||||
import akka.http.scaladsl.server._
|
import akka.http.scaladsl.server._
|
||||||
@ -19,33 +19,50 @@ 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.{Authorization, UserHelper}
|
||||||
|
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)(implicit timeout: Timeout)
|
class UserEndpoint(override val storage: ActorRef[Message])(implicit timeout: Timeout, scheduler: Scheduler)
|
||||||
extends UserHelper(storage) with Authorization with JsonSupport {
|
extends UserHelper
|
||||||
|
with Authorization
|
||||||
|
with JsonSupport {
|
||||||
|
|
||||||
def route: Route = createParty ~ createUser ~ deleteUser ~ getUsers
|
def route: Route = createParty ~ createUser ~ deleteUser ~ getUsers
|
||||||
|
|
||||||
@PUT
|
@PUT
|
||||||
@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[UserResponse])))
|
||||||
|
),
|
||||||
responses = Array(
|
responses = Array(
|
||||||
new ApiResponse(responseCode = "200", description = "Party has been created"),
|
new ApiResponse(responseCode = "200", description = "Party 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 = "406", description = "Party with the specified ID already exists",
|
description = "Invalid parameters were supplied",
|
||||||
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))),
|
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 = "406",
|
||||||
|
description = "Party with the specified ID already exists",
|
||||||
|
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])))
|
||||||
|
),
|
||||||
),
|
),
|
||||||
tags = Array("party"),
|
tags = Array("party"),
|
||||||
)
|
)
|
||||||
@ -71,22 +88,39 @@ class UserEndpoint(override val storage: ActorRef)(implicit timeout: Timeout)
|
|||||||
@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 = "abcdefgh"),
|
||||||
),
|
),
|
||||||
requestBody = new RequestBody(description = "user description", required = true,
|
requestBody = new RequestBody(
|
||||||
content = Array(new Content(schema = new Schema(implementation = classOf[UserResponse])))),
|
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[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 = "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[ErrorResponse])))
|
||||||
|
),
|
||||||
|
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])))
|
||||||
|
),
|
||||||
),
|
),
|
||||||
security = Array(new SecurityRequirement(name = "basic auth", scopes = Array("admin"))),
|
security = Array(new SecurityRequirement(name = "basic auth", scopes = Array("admin"))),
|
||||||
tags = Array("users"),
|
tags = Array("users"),
|
||||||
@ -110,19 +144,30 @@ class UserEndpoint(override val storage: ActorRef)(implicit timeout: Timeout)
|
|||||||
|
|
||||||
@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 = "abcdefgh"),
|
||||||
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[ErrorResponse])))
|
||||||
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[ErrorResponse])))
|
||||||
|
),
|
||||||
|
new ApiResponse(
|
||||||
|
responseCode = "500",
|
||||||
|
description = "Internal server error",
|
||||||
|
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))
|
||||||
|
),
|
||||||
),
|
),
|
||||||
security = Array(new SecurityRequirement(name = "basic auth", scopes = Array("admin"))),
|
security = Array(new SecurityRequirement(name = "basic auth", scopes = Array("admin"))),
|
||||||
tags = Array("users"),
|
tags = Array("users"),
|
||||||
@ -144,21 +189,37 @@ class UserEndpoint(override val storage: ActorRef)(implicit timeout: Timeout)
|
|||||||
@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 = "abcdefgh"),
|
||||||
),
|
),
|
||||||
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[UserResponse])),
|
||||||
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[ErrorResponse])))
|
||||||
|
),
|
||||||
|
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])))
|
||||||
|
),
|
||||||
),
|
),
|
||||||
security = Array(new SecurityRequirement(name = "basic auth", scopes = Array("admin"))),
|
security = Array(new SecurityRequirement(name = "basic auth", scopes = Array("admin"))),
|
||||||
tags = Array("users"),
|
tags = Array("users"),
|
||||||
|
@ -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 ErrorResponse(@Schema(description = "error message", required = true) message: String)
|
||||||
@Schema(description = "error message", required = true) message: String)
|
|
||||||
|
@ -8,6 +8,8 @@
|
|||||||
*/
|
*/
|
||||||
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._
|
||||||
@ -24,16 +26,29 @@ trait JsonSupport extends SprayJsonSupport with DefaultJsonProtocol {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
implicit val instantFormat: RootJsonFormat[Instant] = new RootJsonFormat[Instant] {
|
||||||
|
override def write(obj: Instant): JsValue = obj.toString.toJson
|
||||||
|
override def read(json: JsValue): Instant = json match {
|
||||||
|
case JsNumber(value) => Instant.ofEpochMilli(value.toLongExact)
|
||||||
|
case JsString(value) => Instant.parse(value)
|
||||||
|
case other => deserializationError(s"String or number expected, got $other")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
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[ErrorResponse] = jsonFormat1(ErrorResponse.apply)
|
||||||
implicit val partyIdFormat: RootJsonFormat[PartyIdResponse] = jsonFormat1(PartyIdResponse.apply)
|
implicit val partyIdFormat: RootJsonFormat[PartyIdResponse] = jsonFormat1(PartyIdResponse.apply)
|
||||||
implicit val pieceFormat: RootJsonFormat[PieceResponse] = jsonFormat3(PieceResponse.apply)
|
implicit val pieceFormat: RootJsonFormat[PieceResponse] = jsonFormat3(PieceResponse.apply)
|
||||||
|
implicit val lootFormat: RootJsonFormat[LootResponse] = jsonFormat3(LootResponse.apply)
|
||||||
|
implicit val partyDescriptionFormat: RootJsonFormat[PartyDescriptionResponse] = jsonFormat2(
|
||||||
|
PartyDescriptionResponse.apply
|
||||||
|
)
|
||||||
implicit val playerFormat: RootJsonFormat[PlayerResponse] = jsonFormat7(PlayerResponse.apply)
|
implicit val playerFormat: RootJsonFormat[PlayerResponse] = jsonFormat7(PlayerResponse.apply)
|
||||||
implicit val playerActionFormat: RootJsonFormat[PlayerActionResponse] = jsonFormat2(PlayerActionResponse.apply)
|
implicit val playerActionFormat: RootJsonFormat[PlayerActionResponse] = jsonFormat2(PlayerActionResponse.apply)
|
||||||
implicit val playerIdFormat: RootJsonFormat[PlayerIdResponse] = jsonFormat3(PlayerIdResponse.apply)
|
implicit val playerIdFormat: RootJsonFormat[PlayerIdResponse] = jsonFormat3(PlayerIdResponse.apply)
|
||||||
implicit val pieceActionFormat: RootJsonFormat[PieceActionResponse] = jsonFormat3(PieceActionResponse.apply)
|
implicit val pieceActionFormat: RootJsonFormat[PieceActionResponse] = jsonFormat4(PieceActionResponse.apply)
|
||||||
implicit val playerBiSLinkFormat: RootJsonFormat[PlayerBiSLinkResponse] = jsonFormat2(PlayerBiSLinkResponse.apply)
|
implicit val playerBiSLinkFormat: RootJsonFormat[PlayerBiSLinkResponse] = jsonFormat2(PlayerBiSLinkResponse.apply)
|
||||||
implicit val playerIdWithCountersFormat: RootJsonFormat[PlayerIdWithCountersResponse] =
|
implicit val playerIdWithCountersFormat: RootJsonFormat[PlayerIdWithCountersResponse] =
|
||||||
jsonFormat9(PlayerIdWithCountersResponse.apply)
|
jsonFormat9(PlayerIdWithCountersResponse.apply)
|
||||||
|
@ -0,0 +1,21 @@
|
|||||||
|
package me.arcanis.ffxivbis.http.api.v1.json
|
||||||
|
|
||||||
|
import java.time.Instant
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema
|
||||||
|
import me.arcanis.ffxivbis.models.Loot
|
||||||
|
|
||||||
|
case class LootResponse(
|
||||||
|
@Schema(description = "looted piece", required = true) piece: PieceResponse,
|
||||||
|
@Schema(description = "loot timestamp", required = true) timestamp: Instant,
|
||||||
|
@Schema(description = "is loot free for all", required = true) isFreeLoot: Boolean
|
||||||
|
) {
|
||||||
|
|
||||||
|
def toLoot: Loot = Loot(-1, piece.toPiece, timestamp, isFreeLoot)
|
||||||
|
}
|
||||||
|
|
||||||
|
object LootResponse {
|
||||||
|
|
||||||
|
def fromLoot(loot: Loot): LootResponse =
|
||||||
|
LootResponse(PieceResponse.fromPiece(loot.piece), loot.timestamp, loot.isFreeLoot)
|
||||||
|
}
|
@ -0,0 +1,26 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2020 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.PartyDescription
|
||||||
|
|
||||||
|
case class PartyDescriptionResponse(
|
||||||
|
@Schema(description = "party id", required = true) partyId: String,
|
||||||
|
@Schema(description = "party name") partyAlias: Option[String]
|
||||||
|
) {
|
||||||
|
|
||||||
|
def toDescription: PartyDescription = PartyDescription(partyId, partyAlias)
|
||||||
|
}
|
||||||
|
|
||||||
|
object PartyDescriptionResponse {
|
||||||
|
|
||||||
|
def fromDescription(description: PartyDescription): PartyDescriptionResponse =
|
||||||
|
PartyDescriptionResponse(description.partyId, description.partyAlias)
|
||||||
|
}
|
@ -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 PartyIdResponse(@Schema(description = "party id", required = true) partyId: String)
|
||||||
@Schema(description = "party id", required = true) partyId: String)
|
|
||||||
|
@ -11,6 +11,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 PieceActionResponse(
|
case class PieceActionResponse(
|
||||||
@Schema(description = "action to perform", required = true, `type` = "string", allowableValues = Array("add", "remove")) action: ApiAction.Value,
|
@Schema(
|
||||||
|
description = "action to perform",
|
||||||
|
required = true,
|
||||||
|
`type` = "string",
|
||||||
|
allowableValues = Array("add", "remove")
|
||||||
|
) action: ApiAction.Value,
|
||||||
@Schema(description = "piece description", required = true) piece: PieceResponse,
|
@Schema(description = "piece description", required = true) piece: PieceResponse,
|
||||||
@Schema(description = "player description", required = true) playerId: PlayerIdResponse)
|
@Schema(description = "player description", required = true) playerId: PlayerIdResponse,
|
||||||
|
@Schema(description = "is piece free to roll or not") isFreeLoot: Option[Boolean]
|
||||||
|
)
|
||||||
|
@ -9,16 +9,19 @@
|
|||||||
package me.arcanis.ffxivbis.http.api.v1.json
|
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}
|
import me.arcanis.ffxivbis.models.{Job, Piece, PieceType}
|
||||||
|
|
||||||
case class PieceResponse(
|
case class PieceResponse(
|
||||||
@Schema(description = "is piece tome gear", required = true) isTome: Boolean,
|
@Schema(description = "piece type", required = true) 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, isTome, Job.withName(job))
|
) {
|
||||||
|
|
||||||
|
def toPiece: Piece = Piece(piece, PieceType.withName(pieceType), Job.withName(job))
|
||||||
}
|
}
|
||||||
|
|
||||||
object PieceResponse {
|
object PieceResponse {
|
||||||
|
|
||||||
def fromPiece(piece: Piece): PieceResponse =
|
def fromPiece(piece: Piece): PieceResponse =
|
||||||
PieceResponse(piece.isTome, piece.job.toString, piece.piece)
|
PieceResponse(piece.pieceType.toString, piece.job.toString, piece.piece)
|
||||||
}
|
}
|
||||||
|
@ -11,5 +11,12 @@ package me.arcanis.ffxivbis.http.api.v1.json
|
|||||||
import io.swagger.v3.oas.annotations.media.Schema
|
import io.swagger.v3.oas.annotations.media.Schema
|
||||||
|
|
||||||
case class PlayerActionResponse(
|
case class PlayerActionResponse(
|
||||||
@Schema(description = "action to perform", required = true, `type` = "string", allowableValues = Array("add", "remove"), example = "add") action: ApiAction.Value,
|
@Schema(
|
||||||
@Schema(description = "player description", required = true) playerId: PlayerResponse)
|
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
|
||||||
|
)
|
||||||
|
@ -11,5 +11,10 @@ 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 PlayerBiSLinkResponse(
|
||||||
@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 = "link to player best in slot",
|
||||||
|
required = true,
|
||||||
|
example = "https://ffxiv.ariyala.com/19V5R"
|
||||||
|
) link: String,
|
||||||
|
@Schema(description = "player description", required = true) playerId: PlayerIdResponse
|
||||||
|
)
|
||||||
|
@ -14,12 +14,15 @@ import me.arcanis.ffxivbis.models.{Job, PlayerId}
|
|||||||
case class PlayerIdResponse(
|
case class PlayerIdResponse(
|
||||||
@Schema(description = "unique party ID. Required in responses", example = "abcdefgh") partyId: Option[String],
|
@Schema(description = "unique party ID. Required in responses", example = "abcdefgh") partyId: Option[String],
|
||||||
@Schema(description = "job name", required = true, example = "DNC") job: String,
|
@Schema(description = "job name", required = true, example = "DNC") job: String,
|
||||||
@Schema(description = "player nick name", required = true, example = "Siuan Sanche") nick: String) {
|
@Schema(description = "player nick name", required = true, example = "Siuan Sanche") nick: String
|
||||||
|
) {
|
||||||
|
|
||||||
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 PlayerIdResponse {
|
||||||
|
|
||||||
def fromPlayerId(playerId: PlayerId): PlayerIdResponse =
|
def fromPlayerId(playerId: PlayerId): PlayerIdResponse =
|
||||||
PlayerIdResponse(Some(playerId.partyId), playerId.job.toString, playerId.nick)
|
PlayerIdResponse(Some(playerId.partyId), playerId.job.toString, playerId.nick)
|
||||||
}
|
}
|
||||||
|
@ -20,9 +20,11 @@ case class PlayerIdWithCountersResponse(
|
|||||||
@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", 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 PlayerIdWithCountersResponse {
|
||||||
|
|
||||||
def fromPlayerId(playerIdWithCounters: PlayerIdWithCounters): PlayerIdWithCountersResponse =
|
def fromPlayerId(playerIdWithCounters: PlayerIdWithCounters): PlayerIdWithCountersResponse =
|
||||||
PlayerIdWithCountersResponse(
|
PlayerIdWithCountersResponse(
|
||||||
playerIdWithCounters.partyId,
|
playerIdWithCounters.partyId,
|
||||||
@ -33,5 +35,6 @@ object PlayerIdWithCountersResponse {
|
|||||||
playerIdWithCounters.bisCountTotal,
|
playerIdWithCounters.bisCountTotal,
|
||||||
playerIdWithCounters.lootCount,
|
playerIdWithCounters.lootCount,
|
||||||
playerIdWithCounters.lootCountBiS,
|
playerIdWithCounters.lootCountBiS,
|
||||||
playerIdWithCounters.lootCountTotal)
|
playerIdWithCounters.lootCountTotal
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
@ -16,18 +16,34 @@ case class PlayerResponse(
|
|||||||
@Schema(description = "job name", required = true, example = "DNC") job: String,
|
@Schema(description = "job name", required = true, example = "DNC") job: String,
|
||||||
@Schema(description = "player nick name", required = true, example = "Siuan Sanche") nick: String,
|
@Schema(description = "player nick name", required = true, example = "Siuan Sanche") nick: String,
|
||||||
@Schema(description = "pieces in best in slot") bis: Option[Seq[PieceResponse]],
|
@Schema(description = "pieces in best in slot") bis: Option[Seq[PieceResponse]],
|
||||||
@Schema(description = "looted pieces") loot: 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 = "link to best in slot", example = "https://ffxiv.ariyala.com/19V5R") link: Option[String],
|
||||||
@Schema(description = "player loot priority", `type` = "number") priority: Option[Int]) {
|
@Schema(description = "player loot priority", `type` = "number") priority: Option[Int]
|
||||||
|
) {
|
||||||
|
|
||||||
def toPlayer: Player =
|
def toPlayer: Player =
|
||||||
Player(partyId, Job.withName(job), nick,
|
Player(
|
||||||
BiS(bis.getOrElse(Seq.empty).map(_.toPiece)), loot.getOrElse(Seq.empty).map(_.toPiece),
|
-1,
|
||||||
link, priority.getOrElse(0))
|
partyId,
|
||||||
|
Job.withName(job),
|
||||||
|
nick,
|
||||||
|
BiS(bis.getOrElse(Seq.empty).map(_.toPiece)),
|
||||||
|
loot.getOrElse(Seq.empty).map(_.toLoot),
|
||||||
|
link,
|
||||||
|
priority.getOrElse(0)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
object PlayerResponse {
|
object PlayerResponse {
|
||||||
|
|
||||||
def fromPlayer(player: Player): PlayerResponse =
|
def fromPlayer(player: Player): PlayerResponse =
|
||||||
PlayerResponse(player.partyId, player.job.toString, player.nick,
|
PlayerResponse(
|
||||||
Some(player.bis.pieces.map(PieceResponse.fromPiece)), Some(player.loot.map(PieceResponse.fromPiece)),
|
player.partyId,
|
||||||
player.link, Some(player.priority))
|
player.job.toString,
|
||||||
|
player.nick,
|
||||||
|
Some(player.bis.pieces.map(PieceResponse.fromPiece)),
|
||||||
|
Some(player.loot.map(LootResponse.fromLoot)),
|
||||||
|
player.link,
|
||||||
|
Some(player.priority)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
@ -15,12 +15,19 @@ case class UserResponse(
|
|||||||
@Schema(description = "unique party ID", required = true, example = "abcdefgh") partyId: String,
|
@Schema(description = "unique party ID", required = true, example = "abcdefgh") partyId: String,
|
||||||
@Schema(description = "username to login to party", required = true, example = "siuan") username: String,
|
@Schema(description = "username to login to party", required = true, example = "siuan") username: String,
|
||||||
@Schema(description = "password to login to party", required = true, example = "pa55w0rd") password: String,
|
@Schema(description = "password to login to party", required = true, example = "pa55w0rd") password: String,
|
||||||
@Schema(description = "user permission", defaultValue = "get", allowableValues = Array("get", "post", "admin")) permission: Option[Permission.Value] = None) {
|
@Schema(
|
||||||
|
description = "user permission",
|
||||||
|
defaultValue = "get",
|
||||||
|
allowableValues = Array("get", "post", "admin")
|
||||||
|
) permission: Option[Permission.Value] = None
|
||||||
|
) {
|
||||||
|
|
||||||
def toUser: User =
|
def toUser: User =
|
||||||
User(partyId, username, password, permission.getOrElse(Permission.get))
|
User(partyId, username, password, permission.getOrElse(Permission.get))
|
||||||
}
|
}
|
||||||
|
|
||||||
object UserResponse {
|
object UserResponse {
|
||||||
|
|
||||||
def fromUser(user: User): UserResponse =
|
def fromUser(user: User): UserResponse =
|
||||||
UserResponse(user.partyId, user.username, "", Some(user.permission))
|
UserResponse(user.partyId, user.username, "", Some(user.permission))
|
||||||
}
|
}
|
||||||
|
@ -8,15 +8,22 @@
|
|||||||
*/
|
*/
|
||||||
package me.arcanis.ffxivbis.http.view
|
package me.arcanis.ffxivbis.http.view
|
||||||
|
|
||||||
import akka.actor.ActorRef
|
import akka.actor.typed.{ActorRef, Scheduler}
|
||||||
import akka.http.scaladsl.model.StatusCodes
|
import akka.http.scaladsl.model.StatusCodes
|
||||||
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 me.arcanis.ffxivbis.http.Authorization
|
import me.arcanis.ffxivbis.http.{Authorization, PlayerHelper}
|
||||||
|
import me.arcanis.ffxivbis.messages.{BiSProviderMessage, Message}
|
||||||
|
|
||||||
class BasePartyView(override val storage: ActorRef)(implicit timeout: Timeout)
|
import scala.util.{Failure, Success}
|
||||||
extends Authorization {
|
|
||||||
|
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 route: Route = getIndex
|
||||||
|
|
||||||
@ -25,8 +32,10 @@ class BasePartyView(override val storage: ActorRef)(implicit timeout: Timeout)
|
|||||||
extractExecutionContext { implicit executionContext =>
|
extractExecutionContext { implicit executionContext =>
|
||||||
authenticateBasicBCrypt(s"party $partyId", authGet(partyId)) { _ =>
|
authenticateBasicBCrypt(s"party $partyId", authGet(partyId)) { _ =>
|
||||||
get {
|
get {
|
||||||
complete {
|
onComplete(getPartyDescription(partyId)) {
|
||||||
(StatusCodes.OK, RootView.toHtml(BasePartyView.template(partyId)))
|
case Success(description) =>
|
||||||
|
complete(StatusCodes.OK, RootView.toHtml(BasePartyView.template(partyId, description.alias)))
|
||||||
|
case Failure(exception) => throw exception
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -40,25 +49,25 @@ object BasePartyView {
|
|||||||
import scalatags.Text.tags2.{title => titleTag}
|
import scalatags.Text.tags2.{title => titleTag}
|
||||||
|
|
||||||
def root(partyId: String): Text.TypedTag[String] =
|
def root(partyId: String): Text.TypedTag[String] =
|
||||||
a(href:=s"/party/$partyId", title:="root")("root")
|
a(href := s"/party/$partyId", title := "root")("root")
|
||||||
|
|
||||||
def template(partyId: String): String =
|
def template(partyId: String, alias: String): String =
|
||||||
"<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01//EN\" \"http://www.w3.org/TR/html4/strict.dtd\">" +
|
"<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01//EN\" \"http://www.w3.org/TR/html4/strict.dtd\">" +
|
||||||
html(lang:="en",
|
html(
|
||||||
|
lang := "en",
|
||||||
head(
|
head(
|
||||||
titleTag(s"Party $partyId"),
|
titleTag(s"Party $alias"),
|
||||||
link(rel:="stylesheet", `type`:="text/css", href:="/static/styles.css")
|
link(rel := "stylesheet", `type` := "text/css", href := "/static/styles.css")
|
||||||
),
|
),
|
||||||
|
|
||||||
body(
|
body(
|
||||||
h2(s"Party $partyId"),
|
h2(s"Party $alias"),
|
||||||
br,
|
br,
|
||||||
h2(a(href:=s"/party/$partyId/players", title:="party")("party")),
|
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/bis", title := "bis management")("best in slot")),
|
||||||
h2(a(href:=s"/party/$partyId/loot", title:="loot management")("loot")),
|
h2(a(href := s"/party/$partyId/loot", title := "loot management")("loot")),
|
||||||
h2(a(href:=s"/party/$partyId/suggest", title:="suggest loot")("suggest")),
|
h2(a(href := s"/party/$partyId/suggest", title := "suggest loot")("suggest")),
|
||||||
hr,
|
hr,
|
||||||
h2(a(href:=s"/party/$partyId/users", title:="user management")("users"))
|
h2(a(href := s"/party/$partyId/users", title := "user management")("users"))
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -8,19 +8,23 @@
|
|||||||
*/
|
*/
|
||||||
package me.arcanis.ffxivbis.http.view
|
package me.arcanis.ffxivbis.http.view
|
||||||
|
|
||||||
import akka.actor.ActorRef
|
import akka.actor.typed.{ActorRef, Scheduler}
|
||||||
import akka.http.scaladsl.model.StatusCodes
|
import akka.http.scaladsl.model.StatusCodes
|
||||||
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 me.arcanis.ffxivbis.http.{Authorization, BiSHelper}
|
import me.arcanis.ffxivbis.http.{Authorization, BiSHelper}
|
||||||
import me.arcanis.ffxivbis.models.{Piece, Player, PlayerId}
|
import me.arcanis.ffxivbis.messages.{BiSProviderMessage, Message}
|
||||||
|
import me.arcanis.ffxivbis.models.{Piece, PieceType, Player, PlayerId}
|
||||||
|
|
||||||
import scala.concurrent.{ExecutionContext, Future}
|
import scala.concurrent.{ExecutionContext, Future}
|
||||||
import scala.util.Try
|
import scala.util.Try
|
||||||
|
|
||||||
class BiSView(override val storage: ActorRef, ariyala: ActorRef)(implicit timeout: Timeout)
|
class BiSView(override val storage: ActorRef[Message], override val provider: ActorRef[BiSProviderMessage])(implicit
|
||||||
extends BiSHelper(storage, ariyala) with Authorization {
|
timeout: Timeout,
|
||||||
|
scheduler: Scheduler
|
||||||
|
) extends BiSHelper
|
||||||
|
with Authorization {
|
||||||
|
|
||||||
def route: Route = getBiS ~ modifyBiS
|
def route: Route = getBiS ~ modifyBiS
|
||||||
|
|
||||||
@ -30,11 +34,13 @@ class BiSView(override val storage: ActorRef, ariyala: ActorRef)(implicit timeou
|
|||||||
authenticateBasicBCrypt(s"party $partyId", authGet(partyId)) { _ =>
|
authenticateBasicBCrypt(s"party $partyId", authGet(partyId)) { _ =>
|
||||||
get {
|
get {
|
||||||
complete {
|
complete {
|
||||||
bis(partyId, None).map { players =>
|
bis(partyId, None)
|
||||||
BiSView.template(partyId, players, None)
|
.map { players =>
|
||||||
}.map { text =>
|
BiSView.template(partyId, players, None)
|
||||||
(StatusCodes.OK, RootView.toHtml(text))
|
}
|
||||||
}
|
.map { text =>
|
||||||
|
(StatusCodes.OK, RootView.toHtml(text))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -46,39 +52,49 @@ class BiSView(override val storage: ActorRef, ariyala: ActorRef)(implicit timeou
|
|||||||
extractExecutionContext { implicit executionContext =>
|
extractExecutionContext { implicit executionContext =>
|
||||||
authenticateBasicBCrypt(s"party $partyId", authPost(partyId)) { _ =>
|
authenticateBasicBCrypt(s"party $partyId", authPost(partyId)) { _ =>
|
||||||
post {
|
post {
|
||||||
formFields("player".as[String], "piece".as[String].?, "is_tome".as[String].?, "link".as[String].?, "action".as[String]) {
|
formFields(
|
||||||
(player, maybePiece, maybeIsTome, maybeLink, action) =>
|
"player".as[String],
|
||||||
onComplete(modifyBiSCall(partyId, player, maybePiece, maybeIsTome, maybeLink, action)) {
|
"piece".as[String].?,
|
||||||
case _ => redirect(s"/party/$partyId/bis", StatusCodes.Found)
|
"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,
|
private def modifyBiSCall(
|
||||||
maybePiece: Option[String], maybeIsTome: Option[String],
|
partyId: String,
|
||||||
maybeLink: Option[String], action: String)
|
player: String,
|
||||||
(implicit executionContext: ExecutionContext, timeout: Timeout): Future[Unit] = {
|
maybePiece: Option[String],
|
||||||
import me.arcanis.ffxivbis.utils.Implicits._
|
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 getPiece(playerId: PlayerId, piece: String) =
|
def bisAction(playerId: PlayerId, piece: String, pieceType: String)(fn: Piece => Future[Unit]) =
|
||||||
Try(Piece(piece, maybeIsTome, playerId.job)).toOption
|
getPiece(playerId, piece, pieceType) match {
|
||||||
|
case Some(item) => fn(item)
|
||||||
|
case _ => Future.failed(new Error(s"Could not construct piece from `$piece ($pieceType)`"))
|
||||||
|
}
|
||||||
|
|
||||||
PlayerId(partyId, player) match {
|
PlayerId(partyId, player) match {
|
||||||
case Some(playerId) => (maybePiece, action, maybeLink) match {
|
case Some(playerId) =>
|
||||||
case (Some(piece), "add", _) => getPiece(playerId, piece) match {
|
(maybePiece, maybePieceType, action, maybeLink.map(_.trim).filter(_.nonEmpty)) match {
|
||||||
case Some(item) => addPieceBiS(playerId, item).map(_ => ())
|
case (Some(piece), Some(pieceType), "add", _) =>
|
||||||
case _ => Future.failed(new Error(s"Could not construct piece from `$piece`"))
|
bisAction(playerId, piece, pieceType)(addPieceBiS(playerId, _))
|
||||||
|
case (Some(piece), Some(pieceType), "remove", _) =>
|
||||||
|
bisAction(playerId, piece, pieceType)(removePieceBiS(playerId, _))
|
||||||
|
case (_, _, "create", Some(link)) => putBiS(playerId, link)
|
||||||
|
case _ => Future.failed(new Error(s"Could not perform $action"))
|
||||||
}
|
}
|
||||||
case (Some(piece), "remove", _) => getPiece(playerId, piece) match {
|
|
||||||
case Some(item) => removePieceBiS(playerId, item).map(_ => ())
|
|
||||||
case _ => Future.failed(new Error(s"Could not construct piece from `$piece`"))
|
|
||||||
}
|
|
||||||
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`"))
|
case _ => Future.failed(new Error(s"Could not construct player id from `$player`"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -90,63 +106,68 @@ object BiSView {
|
|||||||
|
|
||||||
def template(partyId: String, party: Seq[Player], error: Option[String]): String =
|
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\">" +
|
"<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01//EN\" \"http://www.w3.org/TR/html4/strict.dtd\">" +
|
||||||
html(lang:="en",
|
html(
|
||||||
|
lang := "en",
|
||||||
head(
|
head(
|
||||||
titleTag("Best in slot"),
|
titleTag("Best in slot"),
|
||||||
link(rel:="stylesheet", `type`:="text/css", href:="/static/styles.css")
|
link(rel := "stylesheet", `type` := "text/css", href := "/static/styles.css")
|
||||||
),
|
),
|
||||||
|
|
||||||
body(
|
body(
|
||||||
h2("Best in slot"),
|
h2("Best in slot"),
|
||||||
|
|
||||||
ErrorView.template(error),
|
ErrorView.template(error),
|
||||||
SearchLineView.template,
|
SearchLineView.template,
|
||||||
|
form(action := s"/party/$partyId/bis", method := "post")(
|
||||||
form(action:=s"/party/$partyId/bis", method:="post")(
|
select(name := "player", id := "player", title := "player")(
|
||||||
select(name:="player", id:="player", title:="player")
|
for (player <- party) yield option(player.playerId.toString)
|
||||||
(for (player <- party) yield option(player.playerId.toString)),
|
),
|
||||||
select(name:="piece", id:="piece", title:="piece")
|
select(name := "piece", id := "piece", title := "piece")(
|
||||||
(for (piece <- Piece.available) yield option(piece)),
|
for (piece <- Piece.available) yield option(piece)
|
||||||
input(name:="is_tome", id:="is_tome", title:="is tome", `type`:="checkbox"),
|
),
|
||||||
label(`for`:="is_tome")("is tome gear"),
|
select(name := "piece_type", id := "piece_type", title := "piece type")(
|
||||||
input(name:="action", id:="action", `type`:="hidden", value:="add"),
|
for (pieceType <- PieceType.available) yield option(pieceType.toString)
|
||||||
input(name:="add", id:="add", `type`:="submit", value:="add")
|
),
|
||||||
|
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")(
|
||||||
form(action:="/bis", method:="post")(
|
select(name := "player", id := "player", title := "player")(
|
||||||
select(name:="player", id:="player", title:="player")
|
for (player <- party) yield option(player.playerId.toString)
|
||||||
(for (player <- party) yield option(player.playerId.toString)),
|
),
|
||||||
input(name:="link", id:="link", placeholder:="player bis link", title:="link", `type`:="text"),
|
input(name := "link", id := "link", placeholder := "player bis link", title := "link", `type` := "text"),
|
||||||
input(name:="action", id:="action", `type`:="hidden", value:="create"),
|
input(name := "action", id := "action", `type` := "hidden", value := "create"),
|
||||||
input(name:="add", id:="add", `type`:="submit", value:="add")
|
input(name := "add", id := "add", `type` := "submit", value := "add")
|
||||||
),
|
),
|
||||||
|
table(id := "result")(
|
||||||
table(id:="result")(
|
|
||||||
tr(
|
tr(
|
||||||
th("player"),
|
th("player"),
|
||||||
th("piece"),
|
th("piece"),
|
||||||
th("is tome"),
|
th("piece type"),
|
||||||
th("")
|
th("")
|
||||||
),
|
),
|
||||||
for (player <- party; piece <- player.bis.pieces) yield tr(
|
for (player <- party; piece <- player.bis.pieces)
|
||||||
td(`class`:="include_search")(player.playerId.toString),
|
yield tr(
|
||||||
td(`class`:="include_search")(piece.piece),
|
td(`class` := "include_search")(player.playerId.toString),
|
||||||
td(piece.isTomeToString),
|
td(`class` := "include_search")(piece.piece),
|
||||||
td(
|
td(piece.pieceType.toString),
|
||||||
form(action:=s"/party/$partyId/bis", method:="post")(
|
td(
|
||||||
input(name:="player", id:="player", `type`:="hidden", value:=player.playerId.toString),
|
form(action := s"/party/$partyId/bis", method := "post")(
|
||||||
input(name:="piece", id:="piece", `type`:="hidden", value:=piece.piece),
|
input(name := "player", id := "player", `type` := "hidden", value := player.playerId.toString),
|
||||||
input(name:="is_tome", id:="is_tome", `type`:="hidden", value:=piece.isTomeToString),
|
input(name := "piece", id := "piece", `type` := "hidden", value := piece.piece),
|
||||||
input(name:="action", id:="action", `type`:="hidden", value:="remove"),
|
input(
|
||||||
input(name:="remove", id:="remove", `type`:="submit", value:="x")
|
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,
|
ExportToCSVView.template,
|
||||||
BasePartyView.root(partyId),
|
BasePartyView.root(partyId),
|
||||||
script(src:="/static/table_search.js", `type`:="text/javascript")
|
script(src := "/static/table_search.js", `type` := "text/javascript")
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -12,8 +12,9 @@ import scalatags.Text
|
|||||||
import scalatags.Text.all._
|
import scalatags.Text.all._
|
||||||
|
|
||||||
object ErrorView {
|
object ErrorView {
|
||||||
|
|
||||||
def template(error: Option[String]): Text.TypedTag[String] = error match {
|
def template(error: Option[String]): Text.TypedTag[String] = error match {
|
||||||
case Some(text) => p(id:="error", s"Error occurs: $text")
|
case Some(text) => p(id := "error", s"Error occurs: $text")
|
||||||
case None => p("")
|
case None => p("")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -12,9 +12,10 @@ import scalatags.Text
|
|||||||
import scalatags.Text.all._
|
import scalatags.Text.all._
|
||||||
|
|
||||||
object ExportToCSVView {
|
object ExportToCSVView {
|
||||||
|
|
||||||
def template: Text.TypedTag[String] =
|
def template: Text.TypedTag[String] =
|
||||||
div(
|
div(
|
||||||
button(onclick:="exportTableToCsv('result.csv')")("Export to CSV"),
|
button(onclick := "exportTableToCsv('result.csv')")("Export to CSV"),
|
||||||
script(src:="/static/table_export.js", `type`:="text/javascript")
|
script(src := "/static/table_export.js", `type` := "text/javascript")
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -8,18 +8,23 @@
|
|||||||
*/
|
*/
|
||||||
package me.arcanis.ffxivbis.http.view
|
package me.arcanis.ffxivbis.http.view
|
||||||
|
|
||||||
import akka.actor.ActorRef
|
import akka.actor.typed.{ActorRef, Scheduler}
|
||||||
import akka.http.scaladsl.model.StatusCodes
|
import akka.http.scaladsl.model.StatusCodes
|
||||||
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 me.arcanis.ffxivbis.http.UserHelper
|
import me.arcanis.ffxivbis.http.{PlayerHelper, UserHelper}
|
||||||
import me.arcanis.ffxivbis.models.{Permission, User}
|
import me.arcanis.ffxivbis.messages.{BiSProviderMessage, Message}
|
||||||
|
import me.arcanis.ffxivbis.models.{PartyDescription, Permission, User}
|
||||||
|
|
||||||
|
import scala.concurrent.Future
|
||||||
import scala.util.{Failure, Success}
|
import scala.util.{Failure, Success}
|
||||||
|
|
||||||
class IndexView(storage: ActorRef)(implicit timeout: Timeout)
|
class IndexView(override val storage: ActorRef[Message], override val provider: ActorRef[BiSProviderMessage])(implicit
|
||||||
extends UserHelper(storage) {
|
timeout: Timeout,
|
||||||
|
scheduler: Scheduler
|
||||||
|
) extends PlayerHelper
|
||||||
|
with UserHelper {
|
||||||
|
|
||||||
def route: Route = createParty ~ getIndex
|
def route: Route = createParty ~ getIndex
|
||||||
|
|
||||||
@ -27,15 +32,20 @@ class IndexView(storage: ActorRef)(implicit timeout: Timeout)
|
|||||||
path("party") {
|
path("party") {
|
||||||
extractExecutionContext { implicit executionContext =>
|
extractExecutionContext { implicit executionContext =>
|
||||||
post {
|
post {
|
||||||
formFields("username".as[String], "password".as[String]) { (username, password) =>
|
formFields("username".as[String], "password".as[String], "alias".as[String].?) {
|
||||||
onComplete(newPartyId) {
|
(username, password, maybeAlias) =>
|
||||||
case Success(partyId) =>
|
onComplete {
|
||||||
val user = User(partyId, username, password, Permission.admin)
|
newPartyId.flatMap { partyId =>
|
||||||
onComplete(addUser(user, isHashedPassword = false)) {
|
val user = User(partyId, username, password, Permission.admin)
|
||||||
case _ => redirect(s"/party/$partyId", StatusCodes.Found)
|
addUser(user, isHashedPassword = false).flatMap { _ =>
|
||||||
|
if (maybeAlias.getOrElse("").isEmpty) Future.successful(partyId)
|
||||||
|
else updateDescription(PartyDescription(partyId, maybeAlias)).map(_ => partyId)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
case Failure(exception) => throw exception
|
} {
|
||||||
}
|
case Success(partyId) => redirect(s"/party/$partyId", StatusCodes.Found)
|
||||||
|
case Failure(exception) => throw exception
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -61,25 +71,34 @@ object IndexView {
|
|||||||
html(
|
html(
|
||||||
head(
|
head(
|
||||||
titleTag("FFXIV loot helper"),
|
titleTag("FFXIV loot helper"),
|
||||||
link(rel:="stylesheet", `type`:="text/css", href:="/static/styles.css")
|
link(rel := "stylesheet", `type` := "text/css", href := "/static/styles.css")
|
||||||
),
|
),
|
||||||
|
|
||||||
body(
|
body(
|
||||||
form(action:=s"party", method:="post")(
|
form(action := s"party", method := "post")(
|
||||||
label("create a new party"),
|
label("create a new party"),
|
||||||
input(name:="username", id:="username", placeholder:="username", title:="username", `type`:="text"),
|
input(name := "alias", id := "alias", placeholder := "party alias", title := "alias", `type` := "text"),
|
||||||
input(name:="password", id:="password", placeholder:="password", title:="password", `type`:="password"),
|
input(
|
||||||
input(name:="add", id:="add", `type`:="submit", value:="add")
|
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,
|
br,
|
||||||
|
form(action := "/", method := "get")(
|
||||||
form(action:="/", method:="get")(
|
|
||||||
label("already have party?"),
|
label("already have party?"),
|
||||||
input(name:="partyId", id:="partyId", placeholder:="party id", title:="party id", `type`:="text"),
|
input(name := "partyId", id := "partyId", placeholder := "party id", title := "party id", `type` := "text"),
|
||||||
input(name:="go", id:="go", `type`:="submit", value:="go")
|
input(name := "go", id := "go", `type` := "submit", value := "go")
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -8,19 +8,21 @@
|
|||||||
*/
|
*/
|
||||||
package me.arcanis.ffxivbis.http.view
|
package me.arcanis.ffxivbis.http.view
|
||||||
|
|
||||||
import akka.actor.ActorRef
|
import akka.actor.typed.{ActorRef, Scheduler}
|
||||||
import akka.http.scaladsl.model.StatusCodes
|
import akka.http.scaladsl.model.StatusCodes
|
||||||
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 akka.util.Timeout
|
||||||
import me.arcanis.ffxivbis.http.{Authorization, LootHelper}
|
import me.arcanis.ffxivbis.http.{Authorization, LootHelper}
|
||||||
import me.arcanis.ffxivbis.models.{Job, Piece, PlayerIdWithCounters}
|
import me.arcanis.ffxivbis.messages.Message
|
||||||
|
import me.arcanis.ffxivbis.models.{Job, Piece, PieceType, PlayerIdWithCounters}
|
||||||
|
|
||||||
import scala.concurrent.{ExecutionContext, Future}
|
import scala.concurrent.{ExecutionContext, Future}
|
||||||
import scala.util.{Failure, Success, Try}
|
import scala.util.{Failure, Success, Try}
|
||||||
|
|
||||||
class LootSuggestView(override val storage: ActorRef)(implicit timeout: Timeout)
|
class LootSuggestView(override val storage: ActorRef[Message])(implicit timeout: Timeout, scheduler: Scheduler)
|
||||||
extends LootHelper(storage) with Authorization {
|
extends LootHelper
|
||||||
|
with Authorization {
|
||||||
|
|
||||||
def route: Route = getIndex ~ suggestLoot
|
def route: Route = getIndex ~ suggestLoot
|
||||||
|
|
||||||
@ -30,7 +32,7 @@ class LootSuggestView(override val storage: ActorRef)(implicit timeout: Timeout)
|
|||||||
authenticateBasicBCrypt(s"party $partyId", authGet(partyId)) { _ =>
|
authenticateBasicBCrypt(s"party $partyId", authGet(partyId)) { _ =>
|
||||||
get {
|
get {
|
||||||
complete {
|
complete {
|
||||||
val text = LootSuggestView.template(partyId, Seq.empty, None, None)
|
val text = LootSuggestView.template(partyId, Seq.empty, None, false, None)
|
||||||
(StatusCodes.OK, RootView.toHtml(text))
|
(StatusCodes.OK, RootView.toHtml(text))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -43,26 +45,30 @@ class LootSuggestView(override val storage: ActorRef)(implicit timeout: Timeout)
|
|||||||
extractExecutionContext { implicit executionContext =>
|
extractExecutionContext { implicit executionContext =>
|
||||||
authenticateBasicBCrypt(s"party $partyId", authGet(partyId)) { _ =>
|
authenticateBasicBCrypt(s"party $partyId", authGet(partyId)) { _ =>
|
||||||
post {
|
post {
|
||||||
formFields("piece".as[String], "job".as[String], "is_tome".as[String].?) { (piece, job, maybeTome) =>
|
formFields("piece".as[String], "job".as[String], "piece_type".as[String], "free_loot".as[String].?) {
|
||||||
import me.arcanis.ffxivbis.utils.Implicits._
|
(piece, job, pieceType, maybeFreeLoot) =>
|
||||||
val maybePiece = Try(Piece(piece, maybeTome, Job.withName(job))).toOption
|
import me.arcanis.ffxivbis.utils.Implicits._
|
||||||
|
|
||||||
onComplete(suggestLootCall(partyId, maybePiece)) {
|
val maybePiece = Try(Piece(piece, PieceType.withName(pieceType), Job.withName(job))).toOption
|
||||||
case Success(players) =>
|
|
||||||
val text = LootSuggestView.template(partyId, players, maybePiece, None)
|
onComplete(suggestLootCall(partyId, maybePiece)) {
|
||||||
complete(StatusCodes.OK, RootView.toHtml(text))
|
case Success(players) =>
|
||||||
case Failure(exception) =>
|
val text = LootSuggestView.template(partyId, players, maybePiece, maybeFreeLoot, None)
|
||||||
val text = LootSuggestView.template(partyId, Seq.empty, maybePiece, Some(exception.getMessage))
|
complete(StatusCodes.OK, RootView.toHtml(text))
|
||||||
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])
|
private def suggestLootCall(partyId: String, maybePiece: Option[Piece])(implicit
|
||||||
(implicit executionContext: ExecutionContext, timeout: Timeout): Future[Seq[PlayerIdWithCounters]] =
|
executionContext: ExecutionContext,
|
||||||
|
timeout: Timeout
|
||||||
|
): Future[Seq[PlayerIdWithCounters]] =
|
||||||
maybePiece match {
|
maybePiece match {
|
||||||
case Some(piece) => suggestPiece(partyId, piece)
|
case Some(piece) => suggestPiece(partyId, piece)
|
||||||
case _ => Future.failed(new Error(s"Could not construct piece from `$maybePiece`"))
|
case _ => Future.failed(new Error(s"Could not construct piece from `$maybePiece`"))
|
||||||
@ -73,31 +79,40 @@ object LootSuggestView {
|
|||||||
import scalatags.Text.all._
|
import scalatags.Text.all._
|
||||||
import scalatags.Text.tags2.{title => titleTag}
|
import scalatags.Text.tags2.{title => titleTag}
|
||||||
|
|
||||||
def template(partyId: String, party: Seq[PlayerIdWithCounters], piece: Option[Piece], error: Option[String]): String =
|
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\">" +
|
"<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01//EN\" \"http://www.w3.org/TR/html4/strict.dtd\">" +
|
||||||
html(lang:="en",
|
html(
|
||||||
|
lang := "en",
|
||||||
head(
|
head(
|
||||||
titleTag("Suggest loot"),
|
titleTag("Suggest loot"),
|
||||||
link(rel:="stylesheet", `type`:="text/css", href:="/static/styles.css")
|
link(rel := "stylesheet", `type` := "text/css", href := "/static/styles.css")
|
||||||
),
|
),
|
||||||
|
|
||||||
body(
|
body(
|
||||||
h2("Suggest loot"),
|
h2("Suggest loot"),
|
||||||
|
for (part <- piece) yield p(s"Piece ${part.piece} (${part.pieceType})"),
|
||||||
ErrorView.template(error),
|
ErrorView.template(error),
|
||||||
SearchLineView.template,
|
SearchLineView.template,
|
||||||
|
form(action := s"/party/$partyId/suggest", method := "post")(
|
||||||
form(action:=s"/party/$partyId/suggest", method:="post")(
|
select(name := "piece", id := "piece", title := "piece")(
|
||||||
select(name:="piece", id:="piece", title:="piece")
|
for (piece <- Piece.available) yield option(piece)
|
||||||
(for (piece <- Piece.available) yield option(piece)),
|
),
|
||||||
select(name:="job", id:="job", title:="job")
|
select(name := "job", id := "job", title := "job")(
|
||||||
(for (job <- Job.availableWithAnyJob) yield option(job.toString)),
|
for (job <- Job.availableWithAnyJob) yield option(job.toString)
|
||||||
input(name:="is_tome", id:="is_tome", title:="is tome", `type`:="checkbox"),
|
),
|
||||||
label(`for`:="is_tome")("is tome gear"),
|
select(name := "piece_type", id := "piece_type", title := "piece type")(
|
||||||
input(name:="suggest", id:="suggest", `type`:="submit", value:="suggest")
|
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")(
|
||||||
table(id:="result")(
|
|
||||||
tr(
|
tr(
|
||||||
th("player"),
|
th("player"),
|
||||||
th("is required"),
|
th("is required"),
|
||||||
@ -106,27 +121,43 @@ object LootSuggestView {
|
|||||||
th("total pieces looted"),
|
th("total pieces looted"),
|
||||||
th("")
|
th("")
|
||||||
),
|
),
|
||||||
for (player <- party) yield tr(
|
for (player <- party)
|
||||||
td(`class`:="include_search")(player.playerId.toString),
|
yield tr(
|
||||||
td(player.isRequiredToString),
|
td(`class` := "include_search")(player.playerId.toString),
|
||||||
td(player.lootCount),
|
td(player.isRequiredToString),
|
||||||
td(player.lootCountBiS),
|
td(player.lootCount),
|
||||||
td(player.lootCountTotal),
|
td(player.lootCountBiS),
|
||||||
td(
|
td(player.lootCountTotal),
|
||||||
form(action:=s"/party/$partyId/loot", method:="post")(
|
td(
|
||||||
input(name:="player", id:="player", `type`:="hidden", value:=player.playerId.toString),
|
form(action := s"/party/$partyId/loot", method := "post")(
|
||||||
input(name:="piece", id:="piece", `type`:="hidden", value:=piece.map(_.piece).getOrElse("")),
|
input(name := "player", id := "player", `type` := "hidden", value := player.playerId.toString),
|
||||||
input(name:="is_tome", id:="is_tome", `type`:="hidden", value:=piece.map(_.isTomeToString).getOrElse("")),
|
input(
|
||||||
input(name:="action", id:="action", `type`:="hidden", value:="add"),
|
name := "piece",
|
||||||
input(name:="add", id:="add", `type`:="submit", value:="add")
|
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,
|
ExportToCSVView.template,
|
||||||
BasePartyView.root(partyId),
|
BasePartyView.root(partyId),
|
||||||
script(src:="/static/table_search.js", `type`:="text/javascript")
|
script(src := "/static/table_search.js", `type` := "text/javascript")
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -8,19 +8,21 @@
|
|||||||
*/
|
*/
|
||||||
package me.arcanis.ffxivbis.http.view
|
package me.arcanis.ffxivbis.http.view
|
||||||
|
|
||||||
import akka.actor.ActorRef
|
import akka.actor.typed.{ActorRef, Scheduler}
|
||||||
import akka.http.scaladsl.model.StatusCodes
|
import akka.http.scaladsl.model.StatusCodes
|
||||||
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 akka.util.Timeout
|
||||||
import me.arcanis.ffxivbis.http.{Authorization, LootHelper}
|
import me.arcanis.ffxivbis.http.{Authorization, LootHelper}
|
||||||
import me.arcanis.ffxivbis.models.{Piece, Player, PlayerId}
|
import me.arcanis.ffxivbis.messages.Message
|
||||||
|
import me.arcanis.ffxivbis.models.{Piece, PieceType, Player, PlayerId}
|
||||||
|
|
||||||
import scala.concurrent.{ExecutionContext, Future}
|
import scala.concurrent.{ExecutionContext, Future}
|
||||||
import scala.util.Try
|
import scala.util.Try
|
||||||
|
|
||||||
class LootView (override val storage: ActorRef)(implicit timeout: Timeout)
|
class LootView(override val storage: ActorRef[Message])(implicit timeout: Timeout, scheduler: Scheduler)
|
||||||
extends LootHelper(storage) with Authorization {
|
extends LootHelper
|
||||||
|
with Authorization {
|
||||||
|
|
||||||
def route: Route = getLoot ~ modifyLoot
|
def route: Route = getLoot ~ modifyLoot
|
||||||
|
|
||||||
@ -30,11 +32,13 @@ class LootView (override val storage: ActorRef)(implicit timeout: Timeout)
|
|||||||
authenticateBasicBCrypt(s"party $partyId", authGet(partyId)) { _ =>
|
authenticateBasicBCrypt(s"party $partyId", authGet(partyId)) { _ =>
|
||||||
get {
|
get {
|
||||||
complete {
|
complete {
|
||||||
loot(partyId, None).map { players =>
|
loot(partyId, None)
|
||||||
LootView.template(partyId, players, None)
|
.map { players =>
|
||||||
}.map { text =>
|
LootView.template(partyId, players, None)
|
||||||
(StatusCodes.OK, RootView.toHtml(text))
|
}
|
||||||
}
|
.map { text =>
|
||||||
|
(StatusCodes.OK, RootView.toHtml(text))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -46,32 +50,42 @@ class LootView (override val storage: ActorRef)(implicit timeout: Timeout)
|
|||||||
extractExecutionContext { implicit executionContext =>
|
extractExecutionContext { implicit executionContext =>
|
||||||
authenticateBasicBCrypt(s"party $partyId", authPost(partyId)) { _ =>
|
authenticateBasicBCrypt(s"party $partyId", authPost(partyId)) { _ =>
|
||||||
post {
|
post {
|
||||||
formFields("player".as[String], "piece".as[String], "is_tome".as[String].?, "action".as[String]) {
|
formFields(
|
||||||
(player, maybePiece, maybeIsTome, action) =>
|
"player".as[String],
|
||||||
onComplete(modifyLootCall(partyId, player, maybePiece, maybeIsTome, action)) {
|
"piece".as[String],
|
||||||
case _ => redirect(s"/party/$partyId/loot", StatusCodes.Found)
|
"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,
|
private def modifyLootCall(
|
||||||
maybePiece: String, maybeIsTome: Option[String],
|
partyId: String,
|
||||||
action: String)
|
player: String,
|
||||||
(implicit executionContext: ExecutionContext, timeout: Timeout): Future[Unit] = {
|
maybePiece: String,
|
||||||
|
maybePieceType: String,
|
||||||
|
maybeFreeLoot: Option[String],
|
||||||
|
action: String
|
||||||
|
)(implicit executionContext: ExecutionContext, timeout: Timeout): Future[Unit] = {
|
||||||
import me.arcanis.ffxivbis.utils.Implicits._
|
import me.arcanis.ffxivbis.utils.Implicits._
|
||||||
|
|
||||||
def getPiece(playerId: PlayerId) =
|
def getPiece(playerId: PlayerId) =
|
||||||
Try(Piece(maybePiece, maybeIsTome, playerId.job)).toOption
|
Try(Piece(maybePiece, PieceType.withName(maybePieceType), playerId.job)).toOption
|
||||||
|
|
||||||
PlayerId(partyId, player) match {
|
PlayerId(partyId, player) match {
|
||||||
case Some(playerId) => (getPiece(playerId), action) match {
|
case Some(playerId) =>
|
||||||
case (Some(piece), "add") => addPieceLoot(playerId, piece).map(_ => ())
|
(getPiece(playerId), action) match {
|
||||||
case (Some(piece), "remove") => removePieceLoot(playerId, piece).map(_ => ())
|
case (Some(piece), "add") => addPieceLoot(playerId, piece, maybeFreeLoot)
|
||||||
case _ => Future.failed(new Error(s"Could not construct piece from `$maybePiece`"))
|
case (Some(piece), "remove") => removePieceLoot(playerId, piece)
|
||||||
}
|
case _ => Future.failed(new Error(s"Could not construct piece from `$maybePiece ($maybePieceType)`"))
|
||||||
|
}
|
||||||
case _ => Future.failed(new Error(s"Could not construct player id from `$player`"))
|
case _ => Future.failed(new Error(s"Could not construct player id from `$player`"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -83,55 +97,67 @@ object LootView {
|
|||||||
|
|
||||||
def template(partyId: String, party: Seq[Player], error: Option[String]): String =
|
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\">" +
|
"<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01//EN\" \"http://www.w3.org/TR/html4/strict.dtd\">" +
|
||||||
html(lang:="en",
|
html(
|
||||||
|
lang := "en",
|
||||||
head(
|
head(
|
||||||
titleTag("Loot"),
|
titleTag("Loot"),
|
||||||
link(rel:="stylesheet", `type`:="text/css", href:="/static/styles.css")
|
link(rel := "stylesheet", `type` := "text/css", href := "/static/styles.css")
|
||||||
),
|
),
|
||||||
|
|
||||||
body(
|
body(
|
||||||
h2("Loot"),
|
h2("Loot"),
|
||||||
|
|
||||||
ErrorView.template(error),
|
ErrorView.template(error),
|
||||||
SearchLineView.template,
|
SearchLineView.template,
|
||||||
|
form(action := s"/party/$partyId/loot", method := "post")(
|
||||||
form(action:=s"/party/$partyId/loot", method:="post")(
|
select(name := "player", id := "player", title := "player")(
|
||||||
select(name:="player", id:="player", title:="player")
|
for (player <- party) yield option(player.playerId.toString)
|
||||||
(for (player <- party) yield option(player.playerId.toString)),
|
),
|
||||||
select(name:="piece", id:="piece", title:="piece")
|
select(name := "piece", id := "piece", title := "piece")(
|
||||||
(for (piece <- Piece.available) yield option(piece)),
|
for (piece <- Piece.available) yield option(piece)
|
||||||
input(name:="is_tome", id:="is_tome", title:="is tome", `type`:="checkbox"),
|
),
|
||||||
label(`for`:="is_tome")("is tome gear"),
|
select(name := "piece_type", id := "piece_type", title := "piece type")(
|
||||||
input(name:="action", id:="action", `type`:="hidden", value:="add"),
|
for (pieceType <- PieceType.available) yield option(pieceType.toString)
|
||||||
input(name:="add", id:="add", `type`:="submit", value:="add")
|
),
|
||||||
|
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")(
|
||||||
table(id:="result")(
|
|
||||||
tr(
|
tr(
|
||||||
th("player"),
|
th("player"),
|
||||||
th("piece"),
|
th("piece"),
|
||||||
th("is tome"),
|
th("piece type"),
|
||||||
|
th("is free loot"),
|
||||||
|
th("timestamp"),
|
||||||
th("")
|
th("")
|
||||||
),
|
),
|
||||||
for (player <- party; piece <- player.loot) yield tr(
|
for (player <- party; loot <- player.loot)
|
||||||
td(`class`:="include_search")(player.playerId.toString),
|
yield tr(
|
||||||
td(`class`:="include_search")(piece.piece),
|
td(`class` := "include_search")(player.playerId.toString),
|
||||||
td(piece.isTomeToString),
|
td(`class` := "include_search")(loot.piece.piece),
|
||||||
td(
|
td(loot.piece.pieceType.toString),
|
||||||
form(action:=s"/party/$partyId/loot", method:="post")(
|
td(loot.isFreeLootToString),
|
||||||
input(name:="player", id:="player", `type`:="hidden", value:=player.playerId.toString),
|
td(loot.timestamp.toString),
|
||||||
input(name:="piece", id:="piece", `type`:="hidden", value:=piece.piece),
|
td(
|
||||||
input(name:="is_tome", id:="is_tome", `type`:="hidden", value:=piece.isTomeToString),
|
form(action := s"/party/$partyId/loot", method := "post")(
|
||||||
input(name:="action", id:="action", `type`:="hidden", value:="remove"),
|
input(name := "player", id := "player", `type` := "hidden", value := player.playerId.toString),
|
||||||
input(name:="remove", id:="remove", `type`:="submit", value:="x")
|
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,
|
ExportToCSVView.template,
|
||||||
BasePartyView.root(partyId),
|
BasePartyView.root(partyId),
|
||||||
script(src:="/static/table_search.js", `type`:="text/javascript")
|
script(src := "/static/table_search.js", `type` := "text/javascript")
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -8,18 +8,22 @@
|
|||||||
*/
|
*/
|
||||||
package me.arcanis.ffxivbis.http.view
|
package me.arcanis.ffxivbis.http.view
|
||||||
|
|
||||||
import akka.actor.ActorRef
|
import akka.actor.typed.{ActorRef, Scheduler}
|
||||||
import akka.http.scaladsl.model.StatusCodes
|
import akka.http.scaladsl.model.StatusCodes
|
||||||
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 akka.util.Timeout
|
||||||
import me.arcanis.ffxivbis.http.{Authorization, PlayerHelper}
|
import me.arcanis.ffxivbis.http.{Authorization, PlayerHelper}
|
||||||
|
import me.arcanis.ffxivbis.messages.{BiSProviderMessage, Message}
|
||||||
import me.arcanis.ffxivbis.models._
|
import me.arcanis.ffxivbis.models._
|
||||||
|
|
||||||
import scala.concurrent.{ExecutionContext, Future}
|
import scala.concurrent.{ExecutionContext, Future}
|
||||||
|
|
||||||
class PlayerView(override val storage: ActorRef, ariyala: ActorRef)(implicit timeout: Timeout)
|
class PlayerView(override val storage: ActorRef[Message], override val provider: ActorRef[BiSProviderMessage])(implicit
|
||||||
extends PlayerHelper(storage, ariyala) with Authorization {
|
timeout: Timeout,
|
||||||
|
scheduler: Scheduler
|
||||||
|
) extends PlayerHelper
|
||||||
|
with Authorization {
|
||||||
|
|
||||||
def route: Route = getParty ~ modifyParty
|
def route: Route = getParty ~ modifyParty
|
||||||
|
|
||||||
@ -29,11 +33,13 @@ class PlayerView(override val storage: ActorRef, ariyala: ActorRef)(implicit tim
|
|||||||
authenticateBasicBCrypt(s"party $partyId", authGet(partyId)) { _ =>
|
authenticateBasicBCrypt(s"party $partyId", authGet(partyId)) { _ =>
|
||||||
get {
|
get {
|
||||||
complete {
|
complete {
|
||||||
getPlayers(partyId, None).map { players =>
|
getPlayers(partyId, None)
|
||||||
PlayerView.template(partyId, players.map(_.withCounters(None)), None)
|
.map { players =>
|
||||||
}.map { text =>
|
PlayerView.template(partyId, players.map(_.withCounters(None)), None)
|
||||||
(StatusCodes.OK, RootView.toHtml(text))
|
}
|
||||||
}
|
.map { text =>
|
||||||
|
(StatusCodes.OK, RootView.toHtml(text))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -45,28 +51,37 @@ class PlayerView(override val storage: ActorRef, ariyala: ActorRef)(implicit tim
|
|||||||
extractExecutionContext { implicit executionContext =>
|
extractExecutionContext { implicit executionContext =>
|
||||||
authenticateBasicBCrypt(s"party $partyId", authPost(partyId)) { _ =>
|
authenticateBasicBCrypt(s"party $partyId", authPost(partyId)) { _ =>
|
||||||
post {
|
post {
|
||||||
formFields("nick".as[String], "job".as[String], "priority".as[Int].?, "link".as[String].?, "action".as[String]) {
|
formFields(
|
||||||
(nick, job, maybePriority, maybeLink, action) =>
|
"nick".as[String],
|
||||||
onComplete(modifyPartyCall(partyId, nick, job, maybePriority, maybeLink, action)) {
|
"job".as[String],
|
||||||
case _ => redirect(s"/party/$partyId/players", StatusCodes.Found)
|
"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,
|
private def modifyPartyCall(
|
||||||
maybePriority: Option[Int], maybeLink: Option[String],
|
partyId: String,
|
||||||
action: String)
|
nick: String,
|
||||||
(implicit executionContext: ExecutionContext, timeout: Timeout): Future[Unit] = {
|
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 maybePlayerId = PlayerId(partyId, Some(nick), Some(job))
|
||||||
def player(playerId: PlayerId) =
|
def player(playerId: PlayerId) =
|
||||||
Player(partyId, playerId.job, playerId.nick, BiS(), Seq.empty, maybeLink, maybePriority.getOrElse(0))
|
Player(-1, partyId, playerId.job, playerId.nick, BiS.empty, Seq.empty, maybeLink, maybePriority.getOrElse(0))
|
||||||
|
|
||||||
(action, maybePlayerId) match {
|
(action, maybePlayerId) match {
|
||||||
case ("add", Some(playerId)) => addPlayer(player(playerId)).map(_ => ())
|
case ("add", Some(playerId)) => addPlayer(player(playerId))
|
||||||
case ("remove", Some(playerId)) => removePlayer(playerId).map(_ => ())
|
case ("remove", Some(playerId)) => removePlayer(playerId)
|
||||||
case _ => Future.failed(new Error(s"Could not perform $action with $nick ($job)"))
|
case _ => Future.failed(new Error(s"Could not perform $action with $nick ($job)"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -78,29 +93,32 @@ object PlayerView {
|
|||||||
|
|
||||||
def template(partyId: String, party: Seq[PlayerIdWithCounters], error: Option[String]): String =
|
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\">" +
|
"<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01//EN\" \"http://www.w3.org/TR/html4/strict.dtd\">" +
|
||||||
html(lang:="en",
|
html(
|
||||||
|
lang := "en",
|
||||||
head(
|
head(
|
||||||
titleTag("Party"),
|
titleTag("Party"),
|
||||||
link(rel:="stylesheet", `type`:="text/css", href:="/static/styles.css")
|
link(rel := "stylesheet", `type` := "text/css", href := "/static/styles.css")
|
||||||
),
|
),
|
||||||
|
|
||||||
body(
|
body(
|
||||||
h2("Party"),
|
h2("Party"),
|
||||||
|
|
||||||
ErrorView.template(error),
|
ErrorView.template(error),
|
||||||
SearchLineView.template,
|
SearchLineView.template,
|
||||||
|
form(action := s"/party/$partyId/players", method := "post")(
|
||||||
form(action:=s"/party/$partyId/players", method:="post")(
|
input(name := "nick", id := "nick", placeholder := "nick", title := "nick", `type` := "nick"),
|
||||||
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)),
|
||||||
select(name:="job", id:="job", title:="job")
|
input(name := "link", id := "link", placeholder := "player bis link", title := "link", `type` := "text"),
|
||||||
(for (job <- Job.available) yield option(job.toString)),
|
input(
|
||||||
input(name:="link", id:="link", placeholder:="player bis link", title:="link", `type`:="text"),
|
name := "prioiry",
|
||||||
input(name:="prioiry", id:="priority", placeholder:="priority", title:="priority", `type`:="number", value:="0"),
|
id := "priority",
|
||||||
input(name:="action", id:="action", `type`:="hidden", value:="add"),
|
placeholder := "priority",
|
||||||
input(name:="add", id:="add", `type`:="submit", value:="add")
|
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")(
|
||||||
table(id:="result")(
|
|
||||||
tr(
|
tr(
|
||||||
th("nick"),
|
th("nick"),
|
||||||
th("job"),
|
th("job"),
|
||||||
@ -109,26 +127,26 @@ object PlayerView {
|
|||||||
th("priority"),
|
th("priority"),
|
||||||
th("")
|
th("")
|
||||||
),
|
),
|
||||||
for (player <- party) yield tr(
|
for (player <- party)
|
||||||
td(`class`:="include_search")(player.nick),
|
yield tr(
|
||||||
td(`class`:="include_search")(player.job.toString),
|
td(`class` := "include_search")(player.nick),
|
||||||
td(player.lootCountBiS),
|
td(`class` := "include_search")(player.job.toString),
|
||||||
td(player.lootCountTotal),
|
td(player.lootCountBiS),
|
||||||
td(player.priority),
|
td(player.lootCountTotal),
|
||||||
td(
|
td(player.priority),
|
||||||
form(action:=s"/party/$partyId/players", method:="post")(
|
td(
|
||||||
input(name:="nick", id:="nick", `type`:="hidden", value:=player.nick),
|
form(action := s"/party/$partyId/players", method := "post")(
|
||||||
input(name:="job", id:="job", `type`:="hidden", value:=player.job.toString),
|
input(name := "nick", id := "nick", `type` := "hidden", value := player.nick),
|
||||||
input(name:="action", id:="action", `type`:="hidden", value:="remove"),
|
input(name := "job", id := "job", `type` := "hidden", value := player.job.toString),
|
||||||
input(name:="remove", id:="remove", `type`:="submit", value:="x")
|
input(name := "action", id := "action", `type` := "hidden", value := "remove"),
|
||||||
|
input(name := "remove", id := "remove", `type` := "submit", value := "x")
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
|
||||||
),
|
),
|
||||||
|
|
||||||
ExportToCSVView.template,
|
ExportToCSVView.template,
|
||||||
BasePartyView.root(partyId),
|
BasePartyView.root(partyId),
|
||||||
script(src:="/static/table_search.js", `type`:="text/javascript")
|
script(src := "/static/table_search.js", `type` := "text/javascript")
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -8,29 +8,34 @@
|
|||||||
*/
|
*/
|
||||||
package me.arcanis.ffxivbis.http.view
|
package me.arcanis.ffxivbis.http.view
|
||||||
|
|
||||||
import akka.actor.ActorRef
|
import akka.actor.typed.{ActorRef, Scheduler}
|
||||||
import akka.http.scaladsl.model.{ContentTypes, HttpEntity}
|
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 akka.util.Timeout
|
||||||
|
import me.arcanis.ffxivbis.messages.{BiSProviderMessage, Message}
|
||||||
|
|
||||||
class RootView(storage: ActorRef, ariyala: ActorRef)(implicit timeout: Timeout) {
|
class RootView(storage: ActorRef[Message], provider: ActorRef[BiSProviderMessage])(implicit
|
||||||
|
timeout: Timeout,
|
||||||
|
scheduler: Scheduler
|
||||||
|
) {
|
||||||
|
|
||||||
private val basePartyView = new BasePartyView(storage)
|
private val basePartyView = new BasePartyView(storage, provider)
|
||||||
private val indexView = new IndexView(storage)
|
private val indexView = new IndexView(storage, provider)
|
||||||
|
|
||||||
private val biSView = new BiSView(storage, ariyala)
|
private val biSView = new BiSView(storage, provider)
|
||||||
private val lootView = new LootView(storage)
|
private val lootView = new LootView(storage)
|
||||||
private val lootSuggestView = new LootSuggestView(storage)
|
private val lootSuggestView = new LootSuggestView(storage)
|
||||||
private val playerView = new PlayerView(storage, ariyala)
|
private val playerView = new PlayerView(storage, provider)
|
||||||
private val userView = new UserView(storage)
|
private val userView = new UserView(storage)
|
||||||
|
|
||||||
def route: Route =
|
def route: Route =
|
||||||
basePartyView.route ~ indexView.route ~
|
basePartyView.route ~ indexView.route ~
|
||||||
biSView.route ~ lootView.route ~ lootSuggestView.route ~ playerView.route ~ userView.route
|
biSView.route ~ lootView.route ~ lootSuggestView.route ~ playerView.route ~ userView.route
|
||||||
}
|
}
|
||||||
|
|
||||||
object RootView {
|
object RootView {
|
||||||
|
|
||||||
def toHtml(template: String): HttpEntity.Strict =
|
def toHtml(template: String): HttpEntity.Strict =
|
||||||
HttpEntity(ContentTypes.`text/html(UTF-8)`, template)
|
HttpEntity(ContentTypes.`text/html(UTF-8)`, template)
|
||||||
}
|
}
|
||||||
|
@ -12,11 +12,15 @@ import scalatags.Text
|
|||||||
import scalatags.Text.all._
|
import scalatags.Text.all._
|
||||||
|
|
||||||
object SearchLineView {
|
object SearchLineView {
|
||||||
|
|
||||||
def template: Text.TypedTag[String] =
|
def template: Text.TypedTag[String] =
|
||||||
div(
|
div(
|
||||||
input(
|
input(
|
||||||
`type`:="text", id:="search", onkeyup:="searchTable()",
|
`type` := "text",
|
||||||
placeholder:="search for data", title:="search"
|
id := "search",
|
||||||
|
onkeyup := "searchTable()",
|
||||||
|
placeholder := "search for data",
|
||||||
|
title := "search"
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -8,19 +8,21 @@
|
|||||||
*/
|
*/
|
||||||
package me.arcanis.ffxivbis.http.view
|
package me.arcanis.ffxivbis.http.view
|
||||||
|
|
||||||
import akka.actor.ActorRef
|
import akka.actor.typed.{ActorRef, Scheduler}
|
||||||
import akka.http.scaladsl.model.StatusCodes
|
import akka.http.scaladsl.model.StatusCodes
|
||||||
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 akka.util.Timeout
|
||||||
import me.arcanis.ffxivbis.http.{Authorization, UserHelper}
|
import me.arcanis.ffxivbis.http.{Authorization, UserHelper}
|
||||||
|
import me.arcanis.ffxivbis.messages.Message
|
||||||
import me.arcanis.ffxivbis.models.{Permission, User}
|
import me.arcanis.ffxivbis.models.{Permission, User}
|
||||||
|
|
||||||
import scala.concurrent.{ExecutionContext, Future}
|
import scala.concurrent.{ExecutionContext, Future}
|
||||||
import scala.util.Try
|
import scala.util.Try
|
||||||
|
|
||||||
class UserView(override val storage: ActorRef)(implicit timeout: Timeout)
|
class UserView(override val storage: ActorRef[Message])(implicit timeout: Timeout, scheduler: Scheduler)
|
||||||
extends UserHelper(storage) with Authorization {
|
extends UserHelper
|
||||||
|
with Authorization {
|
||||||
|
|
||||||
def route: Route = getUsers ~ modifyUsers
|
def route: Route = getUsers ~ modifyUsers
|
||||||
|
|
||||||
@ -30,11 +32,13 @@ class UserView(override val storage: ActorRef)(implicit timeout: Timeout)
|
|||||||
authenticateBasicBCrypt(s"party $partyId", authAdmin(partyId)) { _ =>
|
authenticateBasicBCrypt(s"party $partyId", authAdmin(partyId)) { _ =>
|
||||||
get {
|
get {
|
||||||
complete {
|
complete {
|
||||||
users(partyId).map { users =>
|
users(partyId)
|
||||||
UserView.template(partyId, users, None)
|
.map { users =>
|
||||||
}.map { text =>
|
UserView.template(partyId, users, None)
|
||||||
(StatusCodes.OK, RootView.toHtml(text))
|
}
|
||||||
}
|
.map { text =>
|
||||||
|
(StatusCodes.OK, RootView.toHtml(text))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -48,8 +52,8 @@ class UserView(override val storage: ActorRef)(implicit timeout: Timeout)
|
|||||||
post {
|
post {
|
||||||
formFields("username".as[String], "password".as[String].?, "permission".as[String].?, "action".as[String]) {
|
formFields("username".as[String], "password".as[String].?, "permission".as[String].?, "action".as[String]) {
|
||||||
(username, maybePassword, maybePermission, action) =>
|
(username, maybePassword, maybePermission, action) =>
|
||||||
onComplete(modifyUsersCall(partyId, username, maybePassword, maybePermission, action)) {
|
onComplete(modifyUsersCall(partyId, username, maybePassword, maybePermission, action)) { case _ =>
|
||||||
case _ => redirect(s"/party/$partyId/users", StatusCodes.Found)
|
redirect(s"/party/$partyId/users", StatusCodes.Found)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -57,19 +61,27 @@ class UserView(override val storage: ActorRef)(implicit timeout: Timeout)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private def modifyUsersCall(partyId: String, username: String,
|
private def modifyUsersCall(
|
||||||
maybePassword: Option[String], maybePermission: Option[String],
|
partyId: String,
|
||||||
action: String)
|
username: String,
|
||||||
(implicit executionContext: ExecutionContext, timeout: Timeout): Future[Unit] = {
|
maybePassword: Option[String],
|
||||||
|
maybePermission: Option[String],
|
||||||
|
action: String
|
||||||
|
)(implicit executionContext: ExecutionContext, timeout: Timeout): Future[Unit] = {
|
||||||
def permission: Option[Permission.Value] =
|
def permission: Option[Permission.Value] =
|
||||||
maybePermission.flatMap(p => Try(Permission.withName(p)).toOption)
|
maybePermission.flatMap(p => Try(Permission.withName(p)).toOption)
|
||||||
|
|
||||||
action match {
|
action match {
|
||||||
case "add" => (maybePassword, permission) match {
|
case "add" =>
|
||||||
case (Some(password), Some(permission)) => addUser(User(partyId, username, password, permission), isHashedPassword = false).map(_ => ())
|
(maybePassword, permission) match {
|
||||||
case _ => Future.failed(new Error(s"Could not construct permission/password from `$maybePermission`/`$maybePassword`"))
|
case (Some(password), Some(permission)) =>
|
||||||
}
|
addUser(User(partyId, username, password, permission), isHashedPassword = false)
|
||||||
case "remove" => removeUser(partyId, username).map(_ => ())
|
case _ =>
|
||||||
|
Future.failed(
|
||||||
|
new Error(s"Could not construct permission/password from `$maybePermission`/`$maybePassword`")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
case "remove" => removeUser(partyId, username)
|
||||||
case _ => Future.failed(new Error(s"Could not perform $action"))
|
case _ => Future.failed(new Error(s"Could not perform $action"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -81,48 +93,57 @@ object UserView {
|
|||||||
|
|
||||||
def template(partyId: String, users: Seq[User], error: Option[String]) =
|
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\">" +
|
"<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01//EN\" \"http://www.w3.org/TR/html4/strict.dtd\">" +
|
||||||
html(lang:="en",
|
html(
|
||||||
|
lang := "en",
|
||||||
head(
|
head(
|
||||||
titleTag("Users"),
|
titleTag("Users"),
|
||||||
link(rel:="stylesheet", `type`:="text/css", href:="/static/styles.css")
|
link(rel := "stylesheet", `type` := "text/css", href := "/static/styles.css")
|
||||||
),
|
),
|
||||||
|
|
||||||
body(
|
body(
|
||||||
h2("Users"),
|
h2("Users"),
|
||||||
|
|
||||||
ErrorView.template(error),
|
ErrorView.template(error),
|
||||||
SearchLineView.template,
|
SearchLineView.template,
|
||||||
|
form(action := s"/party/$partyId/users", method := "post")(
|
||||||
form(action:=s"/party/$partyId/users", method:="post")(
|
input(
|
||||||
input(name:="username", id:="username", placeholder:="username", title:="username", `type`:="text"),
|
name := "username",
|
||||||
input(name:="password", id:="password", placeholder:="password", title:="password", `type`:="password"),
|
id := "username",
|
||||||
select(name:="permission", id:="permission", title:="permission")(option("get"), option("post")),
|
placeholder := "username",
|
||||||
input(name:="action", id:="action", `type`:="hidden", value:="add"),
|
title := "username",
|
||||||
input(name:="add", id:="add", `type`:="submit", value:="add")
|
`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")(
|
||||||
table(id:="result")(
|
|
||||||
tr(
|
tr(
|
||||||
th("username"),
|
th("username"),
|
||||||
th("permission"),
|
th("permission"),
|
||||||
th("")
|
th("")
|
||||||
),
|
),
|
||||||
for (user <- users) yield tr(
|
for (user <- users)
|
||||||
td(`class`:="include_search")(user.username),
|
yield tr(
|
||||||
td(user.permission.toString),
|
td(`class` := "include_search")(user.username),
|
||||||
td(
|
td(user.permission.toString),
|
||||||
form(action:=s"/party/$partyId/users", method:="post")(
|
td(
|
||||||
input(name:="username", id:="username", `type`:="hidden", value:=user.username.toString),
|
form(action := s"/party/$partyId/users", method := "post")(
|
||||||
input(name:="action", id:="action", `type`:="hidden", value:="remove"),
|
input(name := "username", id := "username", `type` := "hidden", value := user.username.toString),
|
||||||
input(name:="remove", id:="remove", `type`:="submit", value:="x")
|
input(name := "action", id := "action", `type` := "hidden", value := "remove"),
|
||||||
|
input(name := "remove", id := "remove", `type` := "submit", value := "x")
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
|
||||||
),
|
),
|
||||||
|
|
||||||
ExportToCSVView.template,
|
ExportToCSVView.template,
|
||||||
BasePartyView.root(partyId),
|
BasePartyView.root(partyId),
|
||||||
script(src:="/static/table_search.js", `type`:="text/javascript")
|
script(src := "/static/table_search.js", `type` := "text/javascript")
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,11 @@
|
|||||||
|
package me.arcanis.ffxivbis.messages
|
||||||
|
|
||||||
|
import akka.actor.typed.ActorRef
|
||||||
|
import me.arcanis.ffxivbis.models.{BiS, Job}
|
||||||
|
|
||||||
|
sealed trait BiSProviderMessage
|
||||||
|
|
||||||
|
case class DownloadBiS(link: String, job: Job.Job, replyTo: ActorRef[BiS]) extends BiSProviderMessage {
|
||||||
|
|
||||||
|
require(link.nonEmpty && link.trim == link, "Link must be not empty and contain no spaces")
|
||||||
|
}
|
@ -0,0 +1,10 @@
|
|||||||
|
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,79 @@
|
|||||||
|
package me.arcanis.ffxivbis.messages
|
||||||
|
|
||||||
|
import akka.actor.typed.{ActorRef, Behavior}
|
||||||
|
import me.arcanis.ffxivbis.models.{Party, PartyDescription, Piece, Player, PlayerId, User}
|
||||||
|
import me.arcanis.ffxivbis.service.LootSelector
|
||||||
|
|
||||||
|
sealed trait DatabaseMessage extends Message {
|
||||||
|
|
||||||
|
def partyId: String
|
||||||
|
}
|
||||||
|
|
||||||
|
object DatabaseMessage {
|
||||||
|
|
||||||
|
type Handler = PartialFunction[DatabaseMessage, Behavior[DatabaseMessage]]
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
@ -0,0 +1,9 @@
|
|||||||
|
package me.arcanis.ffxivbis.messages
|
||||||
|
|
||||||
|
import akka.actor.typed.Behavior
|
||||||
|
|
||||||
|
trait Message
|
||||||
|
|
||||||
|
object Message {
|
||||||
|
type Handler = PartialFunction[Message, Behavior[Message]]
|
||||||
|
}
|
@ -8,21 +8,7 @@
|
|||||||
*/
|
*/
|
||||||
package me.arcanis.ffxivbis.models
|
package me.arcanis.ffxivbis.models
|
||||||
|
|
||||||
case class BiS(weapon: Option[Piece],
|
case class BiS(pieces: Seq[Piece]) {
|
||||||
head: Option[Piece],
|
|
||||||
body: Option[Piece],
|
|
||||||
hands: Option[Piece],
|
|
||||||
waist: Option[Piece],
|
|
||||||
legs: Option[Piece],
|
|
||||||
feet: Option[Piece],
|
|
||||||
ears: Option[Piece],
|
|
||||||
neck: Option[Piece],
|
|
||||||
wrist: Option[Piece],
|
|
||||||
leftRing: Option[Piece],
|
|
||||||
rightRing: Option[Piece]) {
|
|
||||||
|
|
||||||
val pieces: Seq[Piece] =
|
|
||||||
Seq(weapon, head, body, hands, waist, legs, feet, ears, neck, wrist, leftRing, rightRing).flatten
|
|
||||||
|
|
||||||
def hasPiece(piece: Piece): Boolean = piece match {
|
def hasPiece(piece: Piece): Boolean = piece match {
|
||||||
case upgrade: PieceUpgrade => upgrades.contains(upgrade)
|
case upgrade: PieceUpgrade => upgrades.contains(upgrade)
|
||||||
@ -30,51 +16,31 @@ case class BiS(weapon: Option[Piece],
|
|||||||
}
|
}
|
||||||
|
|
||||||
def upgrades: Map[PieceUpgrade, Int] =
|
def upgrades: Map[PieceUpgrade, Int] =
|
||||||
pieces.groupBy(_.upgrade).foldLeft(Map.empty[PieceUpgrade, Int]) {
|
pieces
|
||||||
case (acc, (Some(k), v)) => acc + (k -> v.length)
|
.groupBy(_.upgrade)
|
||||||
case (acc, _) => acc
|
.foldLeft(Map.empty[PieceUpgrade, Int]) {
|
||||||
} withDefaultValue 0
|
case (acc, (Some(k), v)) => acc + (k -> v.size)
|
||||||
|
case (acc, _) => acc
|
||||||
|
}
|
||||||
|
.withDefaultValue(0)
|
||||||
|
|
||||||
def withPiece(piece: Piece): BiS = copyWithPiece(piece.piece, Some(piece))
|
def withPiece(piece: Piece): BiS = copy(pieces :+ piece)
|
||||||
def withoutPiece(piece: Piece): BiS = copyWithPiece(piece.piece, None)
|
def withoutPiece(piece: Piece): BiS = copy(pieces.filterNot(_.strictEqual(piece)))
|
||||||
|
|
||||||
private def copyWithPiece(name: String, piece: Option[Piece]): BiS = {
|
override def equals(obj: Any): Boolean = {
|
||||||
val params = Map(
|
def comparePieces(left: Seq[Piece], right: Seq[Piece]): Boolean =
|
||||||
"weapon" -> weapon,
|
left.groupBy(identity).view.mapValues(_.size).forall { case (key, count) =>
|
||||||
"head" -> head,
|
right.count(_.strictEqual(key)) == count
|
||||||
"body" -> body,
|
}
|
||||||
"hands" -> hands,
|
|
||||||
"waist" -> waist,
|
obj match {
|
||||||
"legs" -> legs,
|
case left: BiS => comparePieces(left.pieces, pieces)
|
||||||
"feet" -> feet,
|
case _ => false
|
||||||
"ears" -> ears,
|
}
|
||||||
"neck" -> neck,
|
|
||||||
"wrist" -> wrist,
|
|
||||||
"left ring" -> leftRing,
|
|
||||||
"right ring" -> rightRing
|
|
||||||
) + (name -> piece)
|
|
||||||
BiS(params)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
object BiS {
|
object BiS {
|
||||||
def apply(data: Map[String, Option[Piece]]): BiS =
|
|
||||||
BiS(
|
|
||||||
data.get("weapon").flatten,
|
|
||||||
data.get("head").flatten,
|
|
||||||
data.get("body").flatten,
|
|
||||||
data.get("hands").flatten,
|
|
||||||
data.get("waist").flatten,
|
|
||||||
data.get("legs").flatten,
|
|
||||||
data.get("feet").flatten,
|
|
||||||
data.get("ears").flatten,
|
|
||||||
data.get("neck").flatten,
|
|
||||||
data.get("wrist").flatten,
|
|
||||||
data.get("left ring").flatten,
|
|
||||||
data.get("right ring").flatten)
|
|
||||||
|
|
||||||
def apply(): BiS = BiS(Seq.empty)
|
def empty: BiS = BiS(Seq.empty)
|
||||||
|
|
||||||
def apply(pieces: Seq[Piece]): BiS =
|
|
||||||
BiS(pieces.map(piece => piece.piece -> Some(piece)).toMap)
|
|
||||||
}
|
}
|
||||||
|
@ -9,6 +9,7 @@
|
|||||||
package me.arcanis.ffxivbis.models
|
package me.arcanis.ffxivbis.models
|
||||||
|
|
||||||
object Job {
|
object Job {
|
||||||
|
|
||||||
sealed trait RightSide
|
sealed trait RightSide
|
||||||
object AccessoriesDex extends RightSide
|
object AccessoriesDex extends RightSide
|
||||||
object AccessoriesInt extends RightSide
|
object AccessoriesInt extends RightSide
|
||||||
@ -25,13 +26,15 @@ object Job {
|
|||||||
object BodyTanks extends LeftSide
|
object BodyTanks extends LeftSide
|
||||||
object BodyRanges extends LeftSide
|
object BodyRanges extends LeftSide
|
||||||
|
|
||||||
sealed trait Job {
|
sealed trait Job extends Equals {
|
||||||
|
|
||||||
def leftSide: LeftSide
|
def leftSide: LeftSide
|
||||||
def rightSide: RightSide
|
def rightSide: RightSide
|
||||||
|
|
||||||
// conversion to string to avoid recursion
|
// conversion to string to avoid recursion
|
||||||
|
override def canEqual(that: Any): Boolean = that.isInstanceOf[Job]
|
||||||
|
|
||||||
override def equals(obj: Any): Boolean = {
|
override def equals(obj: Any): Boolean = {
|
||||||
def canEqual(obj: Any): Boolean = obj.isInstanceOf[Job]
|
|
||||||
def equality(objRepr: String): Boolean = objRepr match {
|
def equality(objRepr: String): Boolean = objRepr match {
|
||||||
case _ if objRepr == AnyJob.toString => true
|
case _ if objRepr == AnyJob.toString => true
|
||||||
case _ if this.toString == AnyJob.toString => true
|
case _ if this.toString == AnyJob.toString => true
|
||||||
@ -59,6 +62,10 @@ object Job {
|
|||||||
val leftSide: LeftSide = BodyMnks
|
val leftSide: LeftSide = BodyMnks
|
||||||
val rightSide: RightSide = AccessoriesStr
|
val rightSide: RightSide = AccessoriesStr
|
||||||
}
|
}
|
||||||
|
trait Drgs extends Job {
|
||||||
|
val leftSide: LeftSide = BodyDrgs
|
||||||
|
val rightSide: RightSide = AccessoriesStr
|
||||||
|
}
|
||||||
trait Tanks extends Job {
|
trait Tanks extends Job {
|
||||||
val leftSide: LeftSide = BodyTanks
|
val leftSide: LeftSide = BodyTanks
|
||||||
val rightSide: RightSide = AccessoriesVit
|
val rightSide: RightSide = AccessoriesVit
|
||||||
@ -76,12 +83,11 @@ 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 Job {
|
case object NIN extends Job {
|
||||||
val leftSide: LeftSide = BodyNins
|
val leftSide: LeftSide = BodyNins
|
||||||
val rightSide: RightSide = AccessoriesDex
|
val rightSide: RightSide = AccessoriesDex
|
||||||
@ -97,13 +103,13 @@ object Job {
|
|||||||
case object RDM extends Casters
|
case object RDM extends Casters
|
||||||
|
|
||||||
lazy val available: Seq[Job] =
|
lazy 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, BRD, MCH, DNC, BLM, SMN, RDM)
|
||||||
lazy val availableWithAnyJob: Seq[Job] = available.prepended(AnyJob)
|
lazy val availableWithAnyJob: Seq[Job] = available.prepended(AnyJob)
|
||||||
|
|
||||||
def withName(job: String): Job.Job =
|
def withName(job: String): Job.Job =
|
||||||
availableWithAnyJob.find(_.toString.equalsIgnoreCase(job.toUpperCase)) 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
|
||||||
case _ => throw new IllegalArgumentException("Invalid or unknown job")
|
case _ => throw new IllegalArgumentException(s"Invalid or unknown job $job")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,4 +8,9 @@
|
|||||||
*/
|
*/
|
||||||
package me.arcanis.ffxivbis.models
|
package me.arcanis.ffxivbis.models
|
||||||
|
|
||||||
case class Loot(playerId: Long, piece: Piece)
|
import java.time.Instant
|
||||||
|
|
||||||
|
case class Loot(playerId: Long, piece: Piece, timestamp: Instant, isFreeLoot: Boolean) {
|
||||||
|
|
||||||
|
def isFreeLootToString: String = if (isFreeLoot) "yes" else "no"
|
||||||
|
}
|
||||||
|
@ -15,15 +15,15 @@ import me.arcanis.ffxivbis.service.LootSelector
|
|||||||
import scala.jdk.CollectionConverters._
|
import scala.jdk.CollectionConverters._
|
||||||
import scala.util.Random
|
import scala.util.Random
|
||||||
|
|
||||||
case class Party(partyId: String, 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 == 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 == 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 exception: Exception =>
|
||||||
@ -36,20 +36,22 @@ case class Party(partyId: String, rules: Seq[String], players: Map[PlayerId, Pla
|
|||||||
}
|
}
|
||||||
|
|
||||||
object Party {
|
object Party {
|
||||||
def apply(partyId: Option[String], config: Config): Party =
|
|
||||||
new Party(partyId.getOrElse(randomPartyId), getRules(config), Map.empty)
|
|
||||||
|
|
||||||
def apply(partyId: String, 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.mapValues(_.map(_.piece))
|
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(partyId, getRules(config), playersWithItems)
|
Party(party, getRules(config), playersWithItems)
|
||||||
}
|
}
|
||||||
|
|
||||||
def getRules(config: Config): Seq[String] =
|
def getRules(config: Config): Seq[String] =
|
||||||
|
@ -0,0 +1,19 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2020 Evgeniy Alekseev.
|
||||||
|
*
|
||||||
|
* This file is part of ffxivbis
|
||||||
|
* (see https://github.com/arcan1s/ffxivbis).
|
||||||
|
*
|
||||||
|
* License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause
|
||||||
|
*/
|
||||||
|
package me.arcanis.ffxivbis.models
|
||||||
|
|
||||||
|
case class PartyDescription(partyId: String, partyAlias: Option[String]) {
|
||||||
|
|
||||||
|
def alias: String = partyAlias.getOrElse(partyId)
|
||||||
|
}
|
||||||
|
|
||||||
|
object PartyDescription {
|
||||||
|
|
||||||
|
def empty(partyId: String): PartyDescription = PartyDescription(partyId, None)
|
||||||
|
}
|
@ -8,79 +8,88 @@
|
|||||||
*/
|
*/
|
||||||
package me.arcanis.ffxivbis.models
|
package me.arcanis.ffxivbis.models
|
||||||
|
|
||||||
sealed trait Piece {
|
sealed trait Piece extends Equals {
|
||||||
def isTome: Boolean
|
|
||||||
|
def pieceType: PieceType.PieceType
|
||||||
def job: Job.Job
|
def job: Job.Job
|
||||||
def piece: String
|
def piece: String
|
||||||
|
|
||||||
def withJob(other: Job.Job): Piece
|
def withJob(other: Job.Job): Piece
|
||||||
|
|
||||||
def isTomeToString: String = if (isTome) "yes" else "no"
|
def upgrade: Option[PieceUpgrade] = {
|
||||||
def upgrade: Option[PieceUpgrade] = this match {
|
val isTome = pieceType == PieceType.Tome
|
||||||
case _ if !isTome => None
|
Some(this).collect {
|
||||||
case _: Waist => Some(AccessoryUpgrade)
|
case _: PieceAccessory if isTome => AccessoryUpgrade
|
||||||
case _: PieceAccessory => Some(AccessoryUpgrade)
|
case _: PieceBody if isTome => BodyUpgrade
|
||||||
case _: PieceBody => Some(BodyUpgrade)
|
case _: PieceWeapon if isTome => WeaponUpgrade
|
||||||
case _: PieceWeapon => Some(WeaponUpgrade)
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// used for ring comparison
|
||||||
|
def strictEqual(obj: Any): Boolean = equals(obj)
|
||||||
}
|
}
|
||||||
|
|
||||||
trait PieceAccessory extends Piece
|
trait PieceAccessory extends Piece
|
||||||
trait PieceBody extends Piece
|
trait PieceBody extends Piece
|
||||||
trait PieceUpgrade extends Piece {
|
trait PieceUpgrade extends Piece {
|
||||||
val isTome: Boolean = true
|
val pieceType: PieceType.PieceType = PieceType.Tome
|
||||||
val job: Job.Job = Job.AnyJob
|
val job: Job.Job = Job.AnyJob
|
||||||
def withJob(other: Job.Job): Piece = this
|
def withJob(other: Job.Job): Piece = this
|
||||||
}
|
}
|
||||||
trait PieceWeapon extends Piece
|
trait PieceWeapon extends Piece
|
||||||
|
|
||||||
case class Weapon(override val isTome: Boolean, override val job: Job.Job) extends PieceWeapon {
|
case class Weapon(override val pieceType: PieceType.PieceType, override val job: Job.Job) extends PieceWeapon {
|
||||||
val piece: String = "weapon"
|
val piece: String = "weapon"
|
||||||
def withJob(other: Job.Job): Piece = copy(job = other)
|
def withJob(other: Job.Job): Piece = copy(job = other)
|
||||||
}
|
}
|
||||||
|
|
||||||
case class Head(override val isTome: Boolean, override val job: Job.Job) extends PieceBody {
|
case class Head(override val pieceType: PieceType.PieceType, override val job: Job.Job) extends PieceBody {
|
||||||
val piece: String = "head"
|
val piece: String = "head"
|
||||||
def withJob(other: Job.Job): Piece = copy(job = other)
|
def withJob(other: Job.Job): Piece = copy(job = other)
|
||||||
}
|
}
|
||||||
case class Body(override val isTome: Boolean, override val job: Job.Job) extends PieceBody {
|
case class Body(override val pieceType: PieceType.PieceType, override val job: Job.Job) extends PieceBody {
|
||||||
val piece: String = "body"
|
val piece: String = "body"
|
||||||
def withJob(other: Job.Job): Piece = copy(job = other)
|
def withJob(other: Job.Job): Piece = copy(job = other)
|
||||||
}
|
}
|
||||||
case class Hands(override val isTome: Boolean, override val job: Job.Job) extends PieceBody {
|
case class Hands(override val pieceType: PieceType.PieceType, override val job: Job.Job) extends PieceBody {
|
||||||
val piece: String = "hands"
|
val piece: String = "hands"
|
||||||
def withJob(other: Job.Job): Piece = copy(job = other)
|
def withJob(other: Job.Job): Piece = copy(job = other)
|
||||||
}
|
}
|
||||||
case class Waist(override val isTome: Boolean, override val job: Job.Job) extends PieceBody {
|
case class Legs(override val pieceType: PieceType.PieceType, override val job: Job.Job) extends PieceBody {
|
||||||
val piece: String = "waist"
|
|
||||||
def withJob(other: Job.Job): Piece = copy(job = other)
|
|
||||||
}
|
|
||||||
case class Legs(override val isTome: Boolean, override val job: Job.Job) extends PieceBody {
|
|
||||||
val piece: String = "legs"
|
val piece: String = "legs"
|
||||||
def withJob(other: Job.Job): Piece = copy(job = other)
|
def withJob(other: Job.Job): Piece = copy(job = other)
|
||||||
}
|
}
|
||||||
case class Feet(override val isTome: Boolean, override val job: Job.Job) extends PieceBody {
|
case class Feet(override val pieceType: PieceType.PieceType, override val job: Job.Job) extends PieceBody {
|
||||||
val piece: String = "feet"
|
val piece: String = "feet"
|
||||||
def withJob(other: Job.Job): Piece = copy(job = other)
|
def withJob(other: Job.Job): Piece = copy(job = other)
|
||||||
}
|
}
|
||||||
|
|
||||||
case class Ears(override val isTome: Boolean, override val job: Job.Job) extends PieceAccessory {
|
case class Ears(override val pieceType: PieceType.PieceType, override val job: Job.Job) extends PieceAccessory {
|
||||||
val piece: String = "ears"
|
val piece: String = "ears"
|
||||||
def withJob(other: Job.Job): Piece = copy(job = other)
|
def withJob(other: Job.Job): Piece = copy(job = other)
|
||||||
}
|
}
|
||||||
case class Neck(override val isTome: Boolean, override val job: Job.Job) extends PieceAccessory {
|
case class Neck(override val pieceType: PieceType.PieceType, override val job: Job.Job) extends PieceAccessory {
|
||||||
val piece: String = "neck"
|
val piece: String = "neck"
|
||||||
def withJob(other: Job.Job): Piece = copy(job = other)
|
def withJob(other: Job.Job): Piece = copy(job = other)
|
||||||
}
|
}
|
||||||
case class Wrist(override val isTome: Boolean, override val job: Job.Job) extends PieceAccessory {
|
case class Wrist(override val pieceType: PieceType.PieceType, override val job: Job.Job) extends PieceAccessory {
|
||||||
val piece: String = "wrist"
|
val piece: String = "wrist"
|
||||||
def withJob(other: Job.Job): Piece = copy(job = other)
|
def withJob(other: Job.Job): Piece = copy(job = other)
|
||||||
}
|
}
|
||||||
case class Ring(override val isTome: Boolean, override val job: Job.Job, override val piece: String = "ring")
|
case class Ring(
|
||||||
extends PieceAccessory {
|
override val pieceType: PieceType.PieceType,
|
||||||
|
override val job: Job.Job,
|
||||||
|
override val piece: String = "ring"
|
||||||
|
) extends PieceAccessory {
|
||||||
def withJob(other: Job.Job): Piece = copy(job = other)
|
def withJob(other: Job.Job): Piece = copy(job = other)
|
||||||
|
|
||||||
override def equals(obj: Any): Boolean = obj match {
|
override def equals(obj: Any): Boolean = obj match {
|
||||||
case Ring(thatIsTome, thatJob, _) => (thatIsTome == isTome) && (thatJob == job)
|
case Ring(thatPieceType, thatJob, _) => (thatPieceType == pieceType) && (thatJob == job)
|
||||||
|
case _ => false
|
||||||
|
}
|
||||||
|
|
||||||
|
override def strictEqual(obj: Any): Boolean = obj match {
|
||||||
|
case ring: Ring => equals(obj) && (ring.piece == this.piece)
|
||||||
case _ => false
|
case _ => false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -96,27 +105,38 @@ case object WeaponUpgrade extends PieceUpgrade {
|
|||||||
}
|
}
|
||||||
|
|
||||||
object Piece {
|
object Piece {
|
||||||
def apply(piece: String, isTome: Boolean, job: Job.Job = Job.AnyJob): Piece =
|
def apply(piece: String, pieceType: PieceType.PieceType, job: Job.Job = Job.AnyJob): Piece =
|
||||||
piece.toLowerCase match {
|
piece.toLowerCase match {
|
||||||
case "weapon" => Weapon(isTome, job)
|
case "weapon" => Weapon(pieceType, job)
|
||||||
case "head" => Head(isTome, job)
|
case "head" => Head(pieceType, job)
|
||||||
case "body" => Body(isTome, job)
|
case "body" => Body(pieceType, job)
|
||||||
case "hands" => Hands(isTome, job)
|
case "hands" => Hands(pieceType, job)
|
||||||
case "waist" => Waist(isTome, job)
|
case "legs" => Legs(pieceType, job)
|
||||||
case "legs" => Legs(isTome, job)
|
case "feet" => Feet(pieceType, job)
|
||||||
case "feet" => Feet(isTome, job)
|
case "ears" => Ears(pieceType, job)
|
||||||
case "ears" => Ears(isTome, job)
|
case "neck" => Neck(pieceType, job)
|
||||||
case "neck" => Neck(isTome, job)
|
case "wrist" | "wrists" => Wrist(pieceType, job)
|
||||||
case "wrist" => Wrist(isTome, job)
|
case ring @ ("ring" | "left ring" | "right ring") => Ring(pieceType, job, ring)
|
||||||
case ring @ ("ring" | "left ring" | "right ring") => Ring(isTome, job, ring)
|
|
||||||
case "accessory upgrade" => AccessoryUpgrade
|
case "accessory upgrade" => AccessoryUpgrade
|
||||||
case "body upgrade" => BodyUpgrade
|
case "body upgrade" => BodyUpgrade
|
||||||
case "weapon upgrade" => WeaponUpgrade
|
case "weapon upgrade" => WeaponUpgrade
|
||||||
case other => throw new Error(s"Unknown item type $other")
|
case other => throw new Error(s"Unknown item type $other")
|
||||||
}
|
}
|
||||||
|
|
||||||
lazy val available: Seq[String] = Seq("weapon",
|
lazy val available: Seq[String] = Seq(
|
||||||
"head", "body", "hands", "waist", "legs", "feet",
|
"weapon",
|
||||||
"ears", "neck", "wrist", "left ring", "right ring",
|
"head",
|
||||||
"accessory upgrade", "body upgrade", "weapon upgrade")
|
"body",
|
||||||
|
"hands",
|
||||||
|
"legs",
|
||||||
|
"feet",
|
||||||
|
"ears",
|
||||||
|
"neck",
|
||||||
|
"wrist",
|
||||||
|
"left ring",
|
||||||
|
"right ring",
|
||||||
|
"accessory upgrade",
|
||||||
|
"body upgrade",
|
||||||
|
"weapon upgrade"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
20
src/main/scala/me/arcanis/ffxivbis/models/PieceType.scala
Normal file
20
src/main/scala/me/arcanis/ffxivbis/models/PieceType.scala
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
package me.arcanis.ffxivbis.models
|
||||||
|
|
||||||
|
object PieceType {
|
||||||
|
|
||||||
|
sealed trait PieceType
|
||||||
|
|
||||||
|
case object Crafted extends PieceType
|
||||||
|
case object Tome extends PieceType
|
||||||
|
case object Savage extends PieceType
|
||||||
|
case object Artifact extends PieceType
|
||||||
|
|
||||||
|
lazy val available: Seq[PieceType] =
|
||||||
|
Seq(Crafted, Tome, Savage, Artifact)
|
||||||
|
|
||||||
|
def withName(pieceType: String): PieceType =
|
||||||
|
available.find(_.toString.equalsIgnoreCase(pieceType)) match {
|
||||||
|
case Some(value) => value
|
||||||
|
case _ => throw new IllegalArgumentException(s"Invalid or unknown piece type $pieceType")
|
||||||
|
}
|
||||||
|
}
|
@ -8,13 +8,16 @@
|
|||||||
*/
|
*/
|
||||||
package me.arcanis.ffxivbis.models
|
package me.arcanis.ffxivbis.models
|
||||||
|
|
||||||
case class Player(partyId: String,
|
case class Player(
|
||||||
job: Job.Job,
|
id: Long,
|
||||||
nick: String,
|
partyId: String,
|
||||||
bis: BiS,
|
job: Job.Job,
|
||||||
loot: Seq[Piece],
|
nick: String,
|
||||||
link: Option[String] = None,
|
bis: BiS,
|
||||||
priority: Int = 0) {
|
loot: Seq[Loot],
|
||||||
|
link: Option[String] = None,
|
||||||
|
priority: Int = 0
|
||||||
|
) {
|
||||||
require(job ne Job.AnyJob, "AnyJob is not allowed")
|
require(job ne Job.AnyJob, "AnyJob is not allowed")
|
||||||
|
|
||||||
val playerId: PlayerId = PlayerId(partyId, job, nick)
|
val playerId: PlayerId = PlayerId(partyId, job, nick)
|
||||||
@ -24,30 +27,36 @@ case class Player(partyId: String,
|
|||||||
}
|
}
|
||||||
def withCounters(piece: Option[Piece]): PlayerIdWithCounters =
|
def withCounters(piece: Option[Piece]): PlayerIdWithCounters =
|
||||||
PlayerIdWithCounters(
|
PlayerIdWithCounters(
|
||||||
partyId, job, nick, isRequired(piece), priority,
|
partyId,
|
||||||
bisCountTotal(piece), lootCount(piece),
|
job,
|
||||||
lootCountBiS(piece), lootCountTotal(piece))
|
nick,
|
||||||
def withLoot(piece: Piece): Player = withLoot(Seq(piece))
|
isRequired(piece),
|
||||||
def withLoot(list: Seq[Piece]): Player = list match {
|
priority,
|
||||||
case Nil => this
|
bisCountTotal(piece),
|
||||||
case _ => copy(loot = list)
|
lootCount(piece),
|
||||||
|
lootCountBiS(piece),
|
||||||
|
lootCountTotal(piece)
|
||||||
|
)
|
||||||
|
def withLoot(piece: Loot): Player = withLoot(Seq(piece))
|
||||||
|
def withLoot(list: Seq[Loot]): Player = {
|
||||||
|
require(loot.forall(_.playerId == id), "player id must be same")
|
||||||
|
copy(loot = loot ++ list)
|
||||||
}
|
}
|
||||||
|
|
||||||
def isRequired(piece: Option[Piece]): Boolean = {
|
def isRequired(piece: Option[Piece]): Boolean =
|
||||||
piece match {
|
piece match {
|
||||||
case None => false
|
case None => false
|
||||||
case Some(p) if !bis.hasPiece(p) => false
|
case Some(p) if !bis.hasPiece(p) => false
|
||||||
case Some(p: PieceUpgrade) => bis.upgrades(p) > lootCount(piece)
|
case Some(p: PieceUpgrade) => bis.upgrades(p) > lootCount(piece)
|
||||||
case Some(_) => lootCount(piece) == 0
|
case Some(_) => lootCount(piece) == 0
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
def bisCountTotal(piece: Option[Piece]): Int = bis.pieces.count(!_.isTome)
|
def bisCountTotal(piece: Option[Piece]): Int = bis.pieces.count(_.pieceType == PieceType.Savage)
|
||||||
def lootCount(piece: Option[Piece]): Int = piece match {
|
def lootCount(piece: Option[Piece]): Int = piece match {
|
||||||
case Some(p) => loot.count(_ == p)
|
case Some(p) => loot.count(item => !item.isFreeLoot && item.piece == p)
|
||||||
case None => lootCountTotal(piece)
|
case None => lootCountTotal(piece)
|
||||||
}
|
}
|
||||||
def lootCountBiS(piece: Option[Piece]): Int = loot.count(bis.hasPiece)
|
def lootCountBiS(piece: Option[Piece]): Int = loot.map(_.piece).count(bis.hasPiece)
|
||||||
def lootCountTotal(piece: Option[Piece]): Int = loot.length
|
def lootCountTotal(piece: Option[Piece]): Int = loot.count(!_.isFreeLoot)
|
||||||
def lootPriority(piece: Piece): Int = priority
|
def lootPriority(piece: Piece): Int = priority
|
||||||
}
|
}
|
||||||
|
@ -12,6 +12,7 @@ import scala.util.Try
|
|||||||
import scala.util.matching.Regex
|
import scala.util.matching.Regex
|
||||||
|
|
||||||
trait PlayerIdBase {
|
trait PlayerIdBase {
|
||||||
|
|
||||||
def job: Job.Job
|
def job: Job.Job
|
||||||
def nick: String
|
def nick: String
|
||||||
|
|
||||||
@ -21,6 +22,7 @@ trait PlayerIdBase {
|
|||||||
case class PlayerId(partyId: String, job: Job.Job, nick: String) extends PlayerIdBase
|
case class PlayerId(partyId: String, job: Job.Job, nick: String) extends PlayerIdBase
|
||||||
|
|
||||||
object PlayerId {
|
object PlayerId {
|
||||||
|
|
||||||
def apply(partyId: String, maybeNick: Option[String], maybeJob: Option[String]): Option[PlayerId] =
|
def apply(partyId: String, maybeNick: Option[String], maybeJob: Option[String]): Option[PlayerId] =
|
||||||
(maybeNick, maybeJob) match {
|
(maybeNick, maybeJob) match {
|
||||||
case (Some(nick), Some(job)) => Try(PlayerId(partyId, Job.withName(job), nick)).toOption
|
case (Some(nick), Some(job)) => Try(PlayerId(partyId, Job.withName(job), nick)).toOption
|
||||||
|
@ -8,16 +8,17 @@
|
|||||||
*/
|
*/
|
||||||
package me.arcanis.ffxivbis.models
|
package me.arcanis.ffxivbis.models
|
||||||
|
|
||||||
case class PlayerIdWithCounters(partyId: String,
|
case class PlayerIdWithCounters(
|
||||||
job: Job.Job,
|
partyId: String,
|
||||||
nick: String,
|
job: Job.Job,
|
||||||
isRequired: Boolean,
|
nick: String,
|
||||||
priority: Int,
|
isRequired: Boolean,
|
||||||
bisCountTotal: Int,
|
priority: Int,
|
||||||
lootCount: Int,
|
bisCountTotal: Int,
|
||||||
lootCountBiS: Int,
|
lootCount: Int,
|
||||||
lootCountTotal: Int)
|
lootCountBiS: Int,
|
||||||
extends PlayerIdBase {
|
lootCountTotal: Int
|
||||||
|
) extends PlayerIdBase {
|
||||||
import PlayerIdWithCounters._
|
import PlayerIdWithCounters._
|
||||||
|
|
||||||
def gt(that: PlayerIdWithCounters, orderBy: Seq[String]): Boolean =
|
def gt(that: PlayerIdWithCounters, orderBy: Seq[String]): Boolean =
|
||||||
@ -31,13 +32,15 @@ case class PlayerIdWithCounters(partyId: String,
|
|||||||
"bisCountTotal" -> bisCountTotal, // the more pieces in bis the more priority
|
"bisCountTotal" -> bisCountTotal, // the more pieces in bis the more priority
|
||||||
"lootCount" -> -lootCount, // the less loot got the more priority
|
"lootCount" -> -lootCount, // the less loot got the more priority
|
||||||
"lootCountBiS" -> -lootCountBiS, // the less bis pieces looted the more priority
|
"lootCountBiS" -> -lootCountBiS, // the less bis pieces looted the more priority
|
||||||
"lootCountTotal" -> -lootCountTotal) withDefaultValue 0 // the less pieces looted the more priority
|
"lootCountTotal" -> -lootCountTotal
|
||||||
|
).withDefaultValue(0) // the less pieces looted the more priority
|
||||||
|
|
||||||
private def withCounters(orderBy: Seq[String]): PlayerCountersComparator =
|
private def withCounters(orderBy: Seq[String]): PlayerCountersComparator =
|
||||||
PlayerCountersComparator(orderBy.map(counters): _*)
|
PlayerCountersComparator(orderBy.map(counters): _*)
|
||||||
}
|
}
|
||||||
|
|
||||||
object PlayerIdWithCounters {
|
object PlayerIdWithCounters {
|
||||||
|
|
||||||
private case class PlayerCountersComparator(values: Int*) {
|
private case class PlayerCountersComparator(values: Int*) {
|
||||||
def >(that: PlayerCountersComparator): Boolean = {
|
def >(that: PlayerCountersComparator): Boolean = {
|
||||||
@scala.annotation.tailrec
|
@scala.annotation.tailrec
|
||||||
|
@ -14,10 +14,7 @@ object Permission extends Enumeration {
|
|||||||
val get, post, admin = Value
|
val get, post, admin = Value
|
||||||
}
|
}
|
||||||
|
|
||||||
case class User(partyId: String,
|
case class User(partyId: String, username: String, password: String, permission: Permission.Value) {
|
||||||
username: String,
|
|
||||||
password: String,
|
|
||||||
permission: Permission.Value) {
|
|
||||||
|
|
||||||
def hash: String = BCrypt.hashpw(password, BCrypt.gensalt)
|
def hash: String = BCrypt.hashpw(password, BCrypt.gensalt)
|
||||||
def verify(plain: String): Boolean = BCrypt.checkpw(plain, password)
|
def verify(plain: String): Boolean = BCrypt.checkpw(plain, password)
|
||||||
|
@ -1,142 +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.service
|
|
||||||
|
|
||||||
import java.nio.file.Paths
|
|
||||||
|
|
||||||
import akka.actor.{Actor, Props}
|
|
||||||
import akka.http.scaladsl.Http
|
|
||||||
import akka.http.scaladsl.model._
|
|
||||||
import akka.pattern.pipe
|
|
||||||
import akka.stream.ActorMaterializer
|
|
||||||
import akka.stream.scaladsl.{Keep, Sink}
|
|
||||||
import akka.util.ByteString
|
|
||||||
import com.typesafe.scalalogging.StrictLogging
|
|
||||||
import me.arcanis.ffxivbis.models.{BiS, Job, Piece}
|
|
||||||
import spray.json._
|
|
||||||
|
|
||||||
import scala.concurrent.{ExecutionContext, Future}
|
|
||||||
import scala.util.Try
|
|
||||||
|
|
||||||
class Ariyala extends Actor with StrictLogging {
|
|
||||||
import Ariyala._
|
|
||||||
|
|
||||||
private val settings = context.system.settings.config
|
|
||||||
private val ariyalaUrl = settings.getString("me.arcanis.ffxivbis.ariyala.ariyala-url")
|
|
||||||
private val xivapiUrl = settings.getString("me.arcanis.ffxivbis.ariyala.xivapi-url")
|
|
||||||
private val xivapiKey = Try(settings.getString("me.arcanis.ffxivbis.ariyala.xivapi-key")).toOption
|
|
||||||
|
|
||||||
private val http = Http()(context.system)
|
|
||||||
implicit private val materializer: ActorMaterializer = ActorMaterializer()
|
|
||||||
implicit private val executionContext: ExecutionContext =
|
|
||||||
context.system.dispatchers.lookup("me.arcanis.ffxivbis.default-dispatcher")
|
|
||||||
|
|
||||||
override def receive: Receive = {
|
|
||||||
case GetBiS(link, job) =>
|
|
||||||
val client = sender()
|
|
||||||
get(link, job).map(BiS(_)).pipeTo(client)
|
|
||||||
}
|
|
||||||
|
|
||||||
override def postStop(): Unit = {
|
|
||||||
http.shutdownAllConnectionPools()
|
|
||||||
super.postStop()
|
|
||||||
}
|
|
||||||
|
|
||||||
private def get(link: String, job: Job.Job): Future[Seq[Piece]] = {
|
|
||||||
val id = Paths.get(link).normalize.getFileName.toString
|
|
||||||
val uri = Uri(ariyalaUrl)
|
|
||||||
.withPath(Uri.Path / "store.app")
|
|
||||||
.withQuery(Uri.Query(Map("identifier" -> id)))
|
|
||||||
|
|
||||||
sendRequest(uri, Ariyala.parseAriyalaJsonToPieces(job, getIsTome))
|
|
||||||
}
|
|
||||||
|
|
||||||
private def getIsTome(itemIds: Seq[Long]): Future[Map[Long, Boolean]] = {
|
|
||||||
val uri = Uri(xivapiUrl)
|
|
||||||
.withPath(Uri.Path / "item")
|
|
||||||
.withQuery(Uri.Query(Map(
|
|
||||||
"columns" -> Seq("ID", "Lot").mkString(","),
|
|
||||||
"ids" -> itemIds.mkString(","),
|
|
||||||
"private_key" -> xivapiKey.getOrElse("")
|
|
||||||
)))
|
|
||||||
|
|
||||||
sendRequest(uri, Ariyala.parseXivapiJson)
|
|
||||||
}
|
|
||||||
|
|
||||||
private def sendRequest[T](uri: Uri, parser: JsObject => Future[T]): Future[T] =
|
|
||||||
http.singleRequest(HttpRequest(uri = uri)).map {
|
|
||||||
case HttpResponse(status, _, entity, _) if status.isSuccess() =>
|
|
||||||
entity.dataBytes
|
|
||||||
.fold(ByteString.empty)(_ ++ _)
|
|
||||||
.map(_.utf8String)
|
|
||||||
.map(result => parser(result.parseJson.asJsObject))
|
|
||||||
.toMat(Sink.head)(Keep.right)
|
|
||||||
.run().flatten
|
|
||||||
case other => Future.failed(new Error(s"Invalid response from server $other"))
|
|
||||||
}.flatten
|
|
||||||
}
|
|
||||||
|
|
||||||
object Ariyala {
|
|
||||||
def props: Props = Props(new Ariyala)
|
|
||||||
|
|
||||||
case class GetBiS(link: String, job: Job.Job)
|
|
||||||
|
|
||||||
private def parseAriyalaJson(job: Job.Job)(js: JsObject)
|
|
||||||
(implicit executionContext: ExecutionContext): Future[Map[String, Long]] =
|
|
||||||
Future {
|
|
||||||
val apiJob = js.fields.get("content") match {
|
|
||||||
case Some(JsString(value)) => value
|
|
||||||
case other => throw deserializationError(s"Invalid job name $other")
|
|
||||||
}
|
|
||||||
js.fields.get("datasets") match {
|
|
||||||
case Some(datasets: JsObject) =>
|
|
||||||
val fields = datasets.fields
|
|
||||||
fields.getOrElse(apiJob, fields(job.toString)).asJsObject
|
|
||||||
.fields("normal").asJsObject
|
|
||||||
.fields("items").asJsObject
|
|
||||||
.fields.foldLeft(Map.empty[String, Long]) {
|
|
||||||
case (acc, (key, JsNumber(id))) => remapKey(key).map(k => acc + (k -> id.toLong)).getOrElse(acc)
|
|
||||||
case (acc, _) => acc
|
|
||||||
}
|
|
||||||
case other => throw deserializationError(s"Invalid json $other")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private def parseAriyalaJsonToPieces(job: Job.Job, isTome: Seq[Long] => Future[Map[Long, Boolean]])(js: JsObject)
|
|
||||||
(implicit executionContext: ExecutionContext): Future[Seq[Piece]] =
|
|
||||||
parseAriyalaJson(job)(js).flatMap { pieces =>
|
|
||||||
isTome(pieces.values.toSeq).map { tomePieces =>
|
|
||||||
pieces.view.mapValues(tomePieces).map {
|
|
||||||
case (piece, isTomePiece) => Piece(piece, isTomePiece, job)
|
|
||||||
}.toSeq
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private def parseXivapiJson(js: JsObject)
|
|
||||||
(implicit executionContext: ExecutionContext): Future[Map[Long, Boolean]] =
|
|
||||||
Future {
|
|
||||||
js.fields("Results") match {
|
|
||||||
case array: JsArray =>
|
|
||||||
array.elements.map(_.asJsObject.getFields("ID", "Lot") match {
|
|
||||||
case Seq(JsNumber(id), JsNumber(isTome)) => id.toLong -> (isTome == 0)
|
|
||||||
case other => throw deserializationError(s"Could not parse $other")
|
|
||||||
}).toMap
|
|
||||||
case other => throw deserializationError(s"Could not parse $other")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private def remapKey(key: String): Option[String] = key match {
|
|
||||||
case "mainhand" => Some("weapon")
|
|
||||||
case "chest" => Some("body")
|
|
||||||
case "ringLeft" => Some("left ring")
|
|
||||||
case "ringRight" => Some("right ring")
|
|
||||||
case "head" | "hands" | "waist" | "legs" | "feet" | "ears" | "neck" | "wrist" => Some(key)
|
|
||||||
case _ => None
|
|
||||||
}
|
|
||||||
}
|
|
@ -21,6 +21,7 @@ class LootSelector(players: Seq[Player], piece: Piece, orderBy: Seq[String]) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
object LootSelector {
|
object LootSelector {
|
||||||
|
|
||||||
def apply(players: Seq[Player], piece: Piece, orderBy: Seq[String]): LootSelectorResult =
|
def apply(players: Seq[Player], piece: Piece, orderBy: Seq[String]): LootSelectorResult =
|
||||||
new LootSelector(players, piece, orderBy).suggest
|
new LootSelector(players, piece, orderBy).suggest
|
||||||
|
|
||||||
|
@ -8,57 +8,66 @@
|
|||||||
*/
|
*/
|
||||||
package me.arcanis.ffxivbis.service
|
package me.arcanis.ffxivbis.service
|
||||||
|
|
||||||
import akka.actor.{Actor, ActorRef, Props}
|
import akka.actor.typed.scaladsl.AskPattern.Askable
|
||||||
import akka.pattern.{ask, pipe}
|
import akka.actor.typed.{ActorRef, Behavior, DispatcherSelector, Scheduler}
|
||||||
|
import akka.actor.typed.scaladsl.{AbstractBehavior, ActorContext, Behaviors}
|
||||||
import akka.util.Timeout
|
import akka.util.Timeout
|
||||||
import com.typesafe.scalalogging.StrictLogging
|
import com.typesafe.scalalogging.StrictLogging
|
||||||
|
import me.arcanis.ffxivbis.messages.{DatabaseMessage, Exists, ForgetParty, GetNewPartyId, GetParty, Message, StoreParty}
|
||||||
import me.arcanis.ffxivbis.models.Party
|
import me.arcanis.ffxivbis.models.Party
|
||||||
|
|
||||||
import scala.concurrent.duration.FiniteDuration
|
import scala.concurrent.duration.FiniteDuration
|
||||||
import scala.concurrent.{ExecutionContext, Future}
|
import scala.concurrent.{ExecutionContext, Future}
|
||||||
|
|
||||||
class PartyService(storage: ActorRef) extends Actor with StrictLogging {
|
class PartyService(context: ActorContext[Message], storage: ActorRef[DatabaseMessage])
|
||||||
import PartyService._
|
extends AbstractBehavior[Message](context)
|
||||||
|
with StrictLogging {
|
||||||
import me.arcanis.ffxivbis.utils.Implicits._
|
import me.arcanis.ffxivbis.utils.Implicits._
|
||||||
|
|
||||||
private val cacheTimeout: FiniteDuration =
|
private val cacheTimeout: FiniteDuration =
|
||||||
context.system.settings.config.getDuration("me.arcanis.ffxivbis.settings.cache-timeout")
|
context.system.settings.config.getDuration("me.arcanis.ffxivbis.settings.cache-timeout")
|
||||||
implicit private val executionContext: ExecutionContext =
|
implicit private val executionContext: ExecutionContext = {
|
||||||
context.system.dispatchers.lookup("me.arcanis.ffxivbis.default-dispatcher")
|
val selector = DispatcherSelector.fromConfig("me.arcanis.ffxivbis.default-dispatcher")
|
||||||
|
context.system.dispatchers.lookup(selector)
|
||||||
|
}
|
||||||
implicit private val timeout: Timeout =
|
implicit private val timeout: Timeout =
|
||||||
context.system.settings.config.getDuration("me.arcanis.ffxivbis.settings.request-timeout")
|
context.system.settings.config.getDuration("me.arcanis.ffxivbis.settings.request-timeout")
|
||||||
|
implicit private val scheduler: Scheduler = context.system.scheduler
|
||||||
|
|
||||||
override def receive: Receive = handle(Map.empty)
|
override def onMessage(msg: Message): Behavior[Message] = handle(Map.empty)(msg)
|
||||||
|
|
||||||
private def handle(cache: Map[String, Party]): Receive = {
|
private def handle(cache: Map[String, Party]): Message.Handler = {
|
||||||
case ForgetParty(partyId) =>
|
case ForgetParty(partyId) =>
|
||||||
context become handle(cache - partyId)
|
Behaviors.receiveMessage(handle(cache - partyId))
|
||||||
|
|
||||||
case GetNewPartyId =>
|
case GetNewPartyId(client) =>
|
||||||
val client = sender()
|
getPartyId.foreach(client ! _)
|
||||||
getPartyId.pipeTo(client)
|
Behaviors.same
|
||||||
|
|
||||||
case req @ impl.DatabasePartyHandler.GetParty(partyId) =>
|
case StoreParty(partyId, party) =>
|
||||||
val client = sender()
|
Behaviors.receiveMessage(handle(cache.updated(partyId, party)))
|
||||||
|
|
||||||
|
case GetParty(partyId, client) =>
|
||||||
val party = cache.get(partyId) match {
|
val party = cache.get(partyId) match {
|
||||||
case Some(party) => Future.successful(party)
|
case Some(party) => Future.successful(party)
|
||||||
case None =>
|
case None =>
|
||||||
(storage ? req).mapTo[Party].map { party =>
|
storage.ask(ref => GetParty(partyId, ref)).map { party =>
|
||||||
context become handle(cache + (partyId -> party))
|
context.self ! StoreParty(partyId, party)
|
||||||
context.system.scheduler.scheduleOnce(cacheTimeout, self, ForgetParty(partyId))
|
context.system.scheduler.scheduleOnce(cacheTimeout, () => context.self ! ForgetParty(partyId))
|
||||||
party
|
party
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
party.pipeTo(client)
|
party.foreach(client ! _)
|
||||||
|
Behaviors.same
|
||||||
|
|
||||||
case req: Database.DatabaseRequest =>
|
case req: DatabaseMessage =>
|
||||||
self ! ForgetParty(req.partyId)
|
storage ! req
|
||||||
storage.forward(req)
|
Behaviors.receiveMessage(handle(cache - req.partyId))
|
||||||
}
|
}
|
||||||
|
|
||||||
private def getPartyId: Future[String] = {
|
private def getPartyId: Future[String] = {
|
||||||
val partyId = Party.randomPartyId
|
val partyId = Party.randomPartyId
|
||||||
(storage ? impl.DatabaseUserHandler.Exists(partyId)).mapTo[Boolean].flatMap {
|
storage.ask(ref => Exists(partyId, ref)).flatMap {
|
||||||
case true => getPartyId
|
case true => getPartyId
|
||||||
case false => Future.successful(partyId)
|
case false => Future.successful(partyId)
|
||||||
}
|
}
|
||||||
@ -66,8 +75,7 @@ class PartyService(storage: ActorRef) extends Actor with StrictLogging {
|
|||||||
}
|
}
|
||||||
|
|
||||||
object PartyService {
|
object PartyService {
|
||||||
def props(storage: ActorRef): Props = Props(new PartyService(storage))
|
|
||||||
|
|
||||||
case class ForgetParty(partyId: String)
|
def apply(storage: ActorRef[DatabaseMessage]): Behavior[Message] =
|
||||||
case object GetNewPartyId
|
Behaviors.setup[Message](context => new PartyService(context, storage))
|
||||||
}
|
}
|
||||||
|
@ -1,47 +0,0 @@
|
|||||||
package me.arcanis.ffxivbis.service
|
|
||||||
|
|
||||||
import java.time.Instant
|
|
||||||
|
|
||||||
import akka.actor.Actor
|
|
||||||
|
|
||||||
import scala.concurrent.ExecutionContext
|
|
||||||
import scala.concurrent.duration.FiniteDuration
|
|
||||||
|
|
||||||
class RateLimiter extends Actor {
|
|
||||||
import RateLimiter._
|
|
||||||
import me.arcanis.ffxivbis.utils.Implicits._
|
|
||||||
|
|
||||||
implicit private val executionContext: ExecutionContext = context.system.dispatcher
|
|
||||||
|
|
||||||
private val maxRequestCount: Int = context.system.settings.config.getInt("me.arcanis.ffxivbis.web.limits.max-count")
|
|
||||||
private val requestInterval: FiniteDuration = context.system.settings.config.getDuration("me.arcanis.ffxivbis.web.limits.interval")
|
|
||||||
|
|
||||||
override def receive: Receive = handle(Map.empty)
|
|
||||||
|
|
||||||
private def handle(cache: Map[String, Usage]): Receive = {
|
|
||||||
case username: String =>
|
|
||||||
val client = sender()
|
|
||||||
val usage = if (cache.contains(username)) {
|
|
||||||
cache(username)
|
|
||||||
} else {
|
|
||||||
context.system.scheduler.scheduleOnce(requestInterval, self, Reset(username))
|
|
||||||
Usage()
|
|
||||||
}
|
|
||||||
context become handle(cache + (username -> usage.increment))
|
|
||||||
|
|
||||||
val response = if (usage.count > maxRequestCount) Some(usage.left) else None
|
|
||||||
client ! response
|
|
||||||
|
|
||||||
case Reset(username) =>
|
|
||||||
context become handle(cache - username)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
object RateLimiter {
|
|
||||||
private case class Usage(count: Int = 0, since: Instant = Instant.now) {
|
|
||||||
def increment: Usage = copy(count = count + 1)
|
|
||||||
def left: Long = (Instant.now.toEpochMilli - since.toEpochMilli) / 1000
|
|
||||||
}
|
|
||||||
|
|
||||||
case class Reset(username: String)
|
|
||||||
}
|
|
@ -0,0 +1,91 @@
|
|||||||
|
/*
|
||||||
|
* 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.service.bis
|
||||||
|
|
||||||
|
import java.nio.file.Paths
|
||||||
|
import akka.actor.ClassicActorSystemProvider
|
||||||
|
import akka.actor.typed.{Behavior, PostStop, Signal}
|
||||||
|
import akka.actor.typed.scaladsl.{AbstractBehavior, ActorContext, Behaviors}
|
||||||
|
import akka.http.scaladsl.model._
|
||||||
|
import com.typesafe.scalalogging.StrictLogging
|
||||||
|
import me.arcanis.ffxivbis.messages.{BiSProviderMessage, DownloadBiS}
|
||||||
|
import me.arcanis.ffxivbis.models.{BiS, Job, Piece, PieceType}
|
||||||
|
import me.arcanis.ffxivbis.service.bis.parser.Parser
|
||||||
|
import me.arcanis.ffxivbis.service.bis.parser.impl.{Ariyala, Etro}
|
||||||
|
import spray.json._
|
||||||
|
|
||||||
|
import scala.concurrent.{ExecutionContext, Future}
|
||||||
|
import scala.util.{Failure, Success}
|
||||||
|
|
||||||
|
class BisProvider(context: ActorContext[BiSProviderMessage])
|
||||||
|
extends AbstractBehavior[BiSProviderMessage](context)
|
||||||
|
with XivApi
|
||||||
|
with StrictLogging {
|
||||||
|
|
||||||
|
override def system: ClassicActorSystemProvider = context.system
|
||||||
|
|
||||||
|
override def onMessage(msg: BiSProviderMessage): Behavior[BiSProviderMessage] =
|
||||||
|
msg match {
|
||||||
|
case DownloadBiS(link, job, client) =>
|
||||||
|
get(link, job).onComplete {
|
||||||
|
case Success(items) => client ! BiS(items)
|
||||||
|
case Failure(exception) =>
|
||||||
|
logger.error("received exception while getting items", exception)
|
||||||
|
}
|
||||||
|
Behaviors.same
|
||||||
|
}
|
||||||
|
|
||||||
|
override def onSignal: PartialFunction[Signal, Behavior[BiSProviderMessage]] = { case PostStop =>
|
||||||
|
shutdown()
|
||||||
|
Behaviors.same
|
||||||
|
}
|
||||||
|
|
||||||
|
private def get(link: String, job: Job.Job): Future[Seq[Piece]] =
|
||||||
|
try {
|
||||||
|
val url = Uri(link)
|
||||||
|
val id = Paths.get(link).normalize.getFileName.toString
|
||||||
|
|
||||||
|
val parser = if (url.authority.host.address().contains("etro")) Etro else Ariyala
|
||||||
|
val uri = parser.uri(url, id)
|
||||||
|
sendRequest(uri, BisProvider.parseBisJsonToPieces(job, parser, getPieceType))
|
||||||
|
} catch {
|
||||||
|
case exception: Exception => Future.failed(exception)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
object BisProvider {
|
||||||
|
|
||||||
|
def apply(): Behavior[BiSProviderMessage] =
|
||||||
|
Behaviors.setup[BiSProviderMessage](context => new BisProvider(context))
|
||||||
|
|
||||||
|
private def parseBisJsonToPieces(
|
||||||
|
job: Job.Job,
|
||||||
|
idParser: Parser,
|
||||||
|
pieceTypes: Seq[Long] => Future[Map[Long, PieceType.PieceType]]
|
||||||
|
)(js: JsObject)(implicit executionContext: ExecutionContext): Future[Seq[Piece]] =
|
||||||
|
idParser.parse(job, js).flatMap { pieces =>
|
||||||
|
pieceTypes(pieces.values.toSeq).map { types =>
|
||||||
|
pieces.view
|
||||||
|
.mapValues(types)
|
||||||
|
.map { case (piece, pieceType) =>
|
||||||
|
Piece(piece, pieceType, job)
|
||||||
|
}
|
||||||
|
.toSeq
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def remapKey(key: String): Option[String] = key match {
|
||||||
|
case "mainhand" => Some("weapon")
|
||||||
|
case "chest" => Some("body")
|
||||||
|
case "ringLeft" | "fingerL" => Some("left ring")
|
||||||
|
case "ringRight" | "fingerR" => Some("right ring")
|
||||||
|
case "weapon" | "head" | "body" | "hands" | "legs" | "feet" | "ears" | "neck" | "wrist" | "wrists" => Some(key)
|
||||||
|
case _ => None
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,51 @@
|
|||||||
|
/*
|
||||||
|
* 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.service.bis
|
||||||
|
|
||||||
|
import akka.actor.ClassicActorSystemProvider
|
||||||
|
import akka.http.scaladsl.Http
|
||||||
|
import akka.http.scaladsl.model.headers.Location
|
||||||
|
import akka.http.scaladsl.model.{HttpRequest, HttpResponse, Uri}
|
||||||
|
import akka.stream.Materializer
|
||||||
|
import akka.stream.scaladsl.{Keep, Sink}
|
||||||
|
import akka.util.ByteString
|
||||||
|
import spray.json._
|
||||||
|
|
||||||
|
import scala.concurrent.{ExecutionContext, Future}
|
||||||
|
|
||||||
|
trait RequestExecutor {
|
||||||
|
|
||||||
|
def system: ClassicActorSystemProvider
|
||||||
|
|
||||||
|
private val http = Http()(system)
|
||||||
|
implicit val materializer: Materializer = Materializer.createMaterializer(system)
|
||||||
|
implicit val executionContext: ExecutionContext =
|
||||||
|
system.classicSystem.dispatchers.lookup("me.arcanis.ffxivbis.default-dispatcher")
|
||||||
|
|
||||||
|
def sendRequest[T](uri: Uri, parser: JsObject => Future[T]): Future[T] =
|
||||||
|
http
|
||||||
|
.singleRequest(HttpRequest(uri = uri))
|
||||||
|
.map {
|
||||||
|
case r: HttpResponse if r.status.isRedirection() =>
|
||||||
|
val location = r.header[Location].get.uri
|
||||||
|
sendRequest(uri.withPath(location.path), parser)
|
||||||
|
case HttpResponse(status, _, entity, _) if status.isSuccess() =>
|
||||||
|
entity.dataBytes
|
||||||
|
.fold(ByteString.empty)(_ ++ _)
|
||||||
|
.map(_.utf8String)
|
||||||
|
.map(result => parser(result.parseJson.asJsObject))
|
||||||
|
.toMat(Sink.head)(Keep.right)
|
||||||
|
.run()
|
||||||
|
.flatten
|
||||||
|
case other => Future.failed(new Error(s"Invalid response from server $other"))
|
||||||
|
}
|
||||||
|
.flatten
|
||||||
|
|
||||||
|
def shutdown(): Unit = http.shutdownAllConnectionPools()
|
||||||
|
}
|
142
src/main/scala/me/arcanis/ffxivbis/service/bis/XivApi.scala
Normal file
142
src/main/scala/me/arcanis/ffxivbis/service/bis/XivApi.scala
Normal file
@ -0,0 +1,142 @@
|
|||||||
|
/*
|
||||||
|
* 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.service.bis
|
||||||
|
|
||||||
|
import akka.http.scaladsl.model.Uri
|
||||||
|
import me.arcanis.ffxivbis.models.PieceType
|
||||||
|
import spray.json._
|
||||||
|
|
||||||
|
import scala.concurrent.{ExecutionContext, Future}
|
||||||
|
import scala.jdk.CollectionConverters._
|
||||||
|
import scala.util.Try
|
||||||
|
|
||||||
|
trait XivApi extends RequestExecutor {
|
||||||
|
|
||||||
|
private val config = system.classicSystem.settings.config
|
||||||
|
private val xivapiUrl = config.getString("me.arcanis.ffxivbis.bis-provider.xivapi-url")
|
||||||
|
private val xivapiKey = Try(config.getString("me.arcanis.ffxivbis.bis-provider.xivapi-key")).toOption
|
||||||
|
|
||||||
|
private val preloadedItems: Map[Long, PieceType.PieceType] =
|
||||||
|
config
|
||||||
|
.getConfigList("me.arcanis.ffxivbis.bis-provider.cached-items")
|
||||||
|
.asScala
|
||||||
|
.map { item =>
|
||||||
|
item.getLong("id") -> PieceType.withName(item.getString("source"))
|
||||||
|
}
|
||||||
|
.toMap
|
||||||
|
|
||||||
|
def getPieceType(itemIds: Seq[Long]): Future[Map[Long, PieceType.PieceType]] = {
|
||||||
|
val (local, remote) = itemIds.foldLeft((Map.empty[Long, PieceType.PieceType], Seq.empty[Long])) {
|
||||||
|
case ((l, r), id) =>
|
||||||
|
if (preloadedItems.contains(id)) (l.updated(id, preloadedItems(id)), r)
|
||||||
|
else (l, r :+ id)
|
||||||
|
}
|
||||||
|
if (remote.isEmpty) Future.successful(local)
|
||||||
|
else remotePieceType(remote).map(_ ++ local)
|
||||||
|
}
|
||||||
|
|
||||||
|
private def remotePieceType(itemIds: Seq[Long]): Future[Map[Long, PieceType.PieceType]] = {
|
||||||
|
val uriForItems = Uri(xivapiUrl)
|
||||||
|
.withPath(Uri.Path / "item")
|
||||||
|
.withQuery(
|
||||||
|
Uri.Query(
|
||||||
|
Map(
|
||||||
|
"columns" -> Seq("ID", "GameContentLinks").mkString(","),
|
||||||
|
"ids" -> itemIds.mkString(","),
|
||||||
|
"private_key" -> xivapiKey.getOrElse("")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
sendRequest(uriForItems, XivApi.parseXivapiJsonToShop).flatMap { shops =>
|
||||||
|
val shopIds = shops.values.map(_._2).toSet
|
||||||
|
val columns = shops.values.map(pair => s"ItemCost${pair._1}").toSet
|
||||||
|
val uriForShops = Uri(xivapiUrl)
|
||||||
|
.withPath(Uri.Path / "specialshop")
|
||||||
|
.withQuery(
|
||||||
|
Uri.Query(
|
||||||
|
Map(
|
||||||
|
"columns" -> (columns + "ID").mkString(","),
|
||||||
|
"ids" -> shopIds.mkString(","),
|
||||||
|
"private_key" -> xivapiKey.getOrElse("")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
sendRequest(uriForShops, XivApi.parseXivapiJsonToType(shops))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
object XivApi {
|
||||||
|
|
||||||
|
private def parseXivapiJsonToShop(
|
||||||
|
js: JsObject
|
||||||
|
)(implicit executionContext: ExecutionContext): Future[Map[Long, (String, Long)]] = {
|
||||||
|
def extractTraderId(js: JsObject) =
|
||||||
|
js.fields
|
||||||
|
.get("Recipe")
|
||||||
|
.map(_ => "crafted" -> -1L) // you can craft this item
|
||||||
|
.orElse { // lets try shop items
|
||||||
|
js.fields("SpecialShop").asJsObject.fields.collectFirst {
|
||||||
|
case (shopName, JsArray(array)) if shopName.startsWith("ItemReceive") =>
|
||||||
|
val shopId = array.head match {
|
||||||
|
case JsNumber(id) => id.toLong
|
||||||
|
case other => throw deserializationError(s"Could not parse $other")
|
||||||
|
}
|
||||||
|
shopName.replace("ItemReceive", "") -> shopId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.getOrElse(throw deserializationError(s"Could not parse $js"))
|
||||||
|
|
||||||
|
Future {
|
||||||
|
js.fields("Results") match {
|
||||||
|
case array: JsArray =>
|
||||||
|
array.elements
|
||||||
|
.map(_.asJsObject.getFields("ID", "GameContentLinks") match {
|
||||||
|
case Seq(JsNumber(id), shop: JsObject) => id.toLong -> extractTraderId(shop.asJsObject)
|
||||||
|
case other => throw deserializationError(s"Could not parse $other")
|
||||||
|
})
|
||||||
|
.toMap
|
||||||
|
case other => throw deserializationError(s"Could not parse $other")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private def parseXivapiJsonToType(
|
||||||
|
shops: Map[Long, (String, Long)]
|
||||||
|
)(js: JsObject)(implicit executionContext: ExecutionContext): Future[Map[Long, PieceType.PieceType]] =
|
||||||
|
Future {
|
||||||
|
val shopMap = js.fields("Results") match {
|
||||||
|
case array: JsArray =>
|
||||||
|
array.elements.collect { case shop: JsObject =>
|
||||||
|
shop.fields("ID") match {
|
||||||
|
case JsNumber(id) => id.toLong -> shop
|
||||||
|
case other => throw deserializationError(s"Could not parse $other")
|
||||||
|
}
|
||||||
|
}.toMap
|
||||||
|
case other => throw deserializationError(s"Could not parse $other")
|
||||||
|
}
|
||||||
|
|
||||||
|
shops.map { case (itemId, (index, shopId)) =>
|
||||||
|
val pieceType =
|
||||||
|
if (index == "crafted" && shopId == -1L) PieceType.Crafted
|
||||||
|
else
|
||||||
|
Try(shopMap(shopId).fields(s"ItemCost$index").asJsObject)
|
||||||
|
.getOrElse(throw new Exception(s"${shopMap(shopId).fields(s"ItemCost$index")}, $index"))
|
||||||
|
.getFields("IsUnique", "StackSize") match {
|
||||||
|
case Seq(JsNumber(isUnique), JsNumber(stackSize)) =>
|
||||||
|
if (isUnique == 1 || stackSize.toLong != 999) PieceType.Tome // either upgraded gear or tomes found
|
||||||
|
else PieceType.Savage
|
||||||
|
case other => throw deserializationError(s"Could not parse $other")
|
||||||
|
}
|
||||||
|
itemId -> pieceType
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,15 @@
|
|||||||
|
package me.arcanis.ffxivbis.service.bis.parser
|
||||||
|
|
||||||
|
import akka.http.scaladsl.model.Uri
|
||||||
|
import com.typesafe.scalalogging.StrictLogging
|
||||||
|
import me.arcanis.ffxivbis.models.Job
|
||||||
|
import spray.json.JsObject
|
||||||
|
|
||||||
|
import scala.concurrent.{ExecutionContext, Future}
|
||||||
|
|
||||||
|
trait Parser extends StrictLogging {
|
||||||
|
|
||||||
|
def parse(job: Job.Job, js: JsObject)(implicit executionContext: ExecutionContext): Future[Map[String, Long]]
|
||||||
|
|
||||||
|
def uri(root: Uri, id: String): Uri
|
||||||
|
}
|
@ -0,0 +1,45 @@
|
|||||||
|
package me.arcanis.ffxivbis.service.bis.parser.impl
|
||||||
|
|
||||||
|
import akka.http.scaladsl.model.Uri
|
||||||
|
import me.arcanis.ffxivbis.models.Job
|
||||||
|
import me.arcanis.ffxivbis.service.bis.BisProvider
|
||||||
|
import me.arcanis.ffxivbis.service.bis.parser.Parser
|
||||||
|
import spray.json.{deserializationError, JsNumber, JsObject, JsString}
|
||||||
|
|
||||||
|
import scala.concurrent.{ExecutionContext, Future}
|
||||||
|
|
||||||
|
object Ariyala extends Parser {
|
||||||
|
|
||||||
|
override def parse(job: Job.Job, js: JsObject)(implicit
|
||||||
|
executionContext: ExecutionContext
|
||||||
|
): Future[Map[String, Long]] =
|
||||||
|
Future {
|
||||||
|
val apiJob = js.fields.get("content") match {
|
||||||
|
case Some(JsString(value)) => value
|
||||||
|
case other => throw deserializationError(s"Invalid job name $other")
|
||||||
|
}
|
||||||
|
js.fields.get("datasets") match {
|
||||||
|
case Some(datasets: JsObject) =>
|
||||||
|
val fields = datasets.fields
|
||||||
|
fields
|
||||||
|
.getOrElse(apiJob, fields(job.toString))
|
||||||
|
.asJsObject
|
||||||
|
.fields("normal")
|
||||||
|
.asJsObject
|
||||||
|
.fields("items")
|
||||||
|
.asJsObject
|
||||||
|
.fields
|
||||||
|
.foldLeft(Map.empty[String, Long]) {
|
||||||
|
case (acc, (key, JsNumber(id))) =>
|
||||||
|
BisProvider.remapKey(key).map(k => acc + (k -> id.toLong)).getOrElse(acc)
|
||||||
|
case (acc, _) => acc
|
||||||
|
}
|
||||||
|
case other => throw deserializationError(s"Invalid json $other")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override def uri(root: Uri, id: String): Uri =
|
||||||
|
root
|
||||||
|
.withPath(Uri.Path / "store.app")
|
||||||
|
.withQuery(Uri.Query(Map("identifier" -> id)))
|
||||||
|
}
|
@ -0,0 +1,25 @@
|
|||||||
|
package me.arcanis.ffxivbis.service.bis.parser.impl
|
||||||
|
|
||||||
|
import akka.http.scaladsl.model.Uri
|
||||||
|
import me.arcanis.ffxivbis.models.Job
|
||||||
|
import me.arcanis.ffxivbis.service.bis.BisProvider
|
||||||
|
import me.arcanis.ffxivbis.service.bis.parser.Parser
|
||||||
|
import spray.json.{JsNumber, JsObject}
|
||||||
|
|
||||||
|
import scala.concurrent.{ExecutionContext, Future}
|
||||||
|
|
||||||
|
object Etro extends Parser {
|
||||||
|
|
||||||
|
override def parse(job: Job.Job, js: JsObject)(implicit
|
||||||
|
executionContext: ExecutionContext
|
||||||
|
): Future[Map[String, Long]] =
|
||||||
|
Future {
|
||||||
|
js.fields.foldLeft(Map.empty[String, Long]) {
|
||||||
|
case (acc, (key, JsNumber(id))) => BisProvider.remapKey(key).map(k => acc + (k -> id.toLong)).getOrElse(acc)
|
||||||
|
case (acc, _) => acc
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override def uri(root: Uri, id: String): Uri =
|
||||||
|
root.withPath(Uri.Path / "api" / "gearsets" / id)
|
||||||
|
}
|
@ -6,40 +6,42 @@
|
|||||||
*
|
*
|
||||||
* 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.service
|
package me.arcanis.ffxivbis.service.database
|
||||||
|
|
||||||
import akka.actor.Actor
|
import akka.actor.typed.Behavior
|
||||||
|
import akka.actor.typed.scaladsl.Behaviors
|
||||||
|
import com.typesafe.config.Config
|
||||||
import com.typesafe.scalalogging.StrictLogging
|
import com.typesafe.scalalogging.StrictLogging
|
||||||
|
import me.arcanis.ffxivbis.messages.DatabaseMessage
|
||||||
import me.arcanis.ffxivbis.models.{Party, Player, PlayerId}
|
import me.arcanis.ffxivbis.models.{Party, Player, PlayerId}
|
||||||
|
import me.arcanis.ffxivbis.service.database.impl.DatabaseImpl
|
||||||
import me.arcanis.ffxivbis.storage.DatabaseProfile
|
import me.arcanis.ffxivbis.storage.DatabaseProfile
|
||||||
|
|
||||||
import scala.concurrent.{ExecutionContext, Future}
|
import scala.concurrent.{ExecutionContext, Future}
|
||||||
|
|
||||||
trait Database extends Actor with StrictLogging {
|
trait Database extends StrictLogging {
|
||||||
|
|
||||||
implicit def executionContext: ExecutionContext
|
implicit def executionContext: ExecutionContext
|
||||||
|
def config: Config
|
||||||
def profile: DatabaseProfile
|
def profile: DatabaseProfile
|
||||||
|
|
||||||
override def postStop(): Unit = {
|
|
||||||
profile.db.close()
|
|
||||||
super.postStop()
|
|
||||||
}
|
|
||||||
|
|
||||||
def filterParty(party: Party, maybePlayerId: Option[PlayerId]): Seq[Player] =
|
def filterParty(party: Party, maybePlayerId: Option[PlayerId]): Seq[Player] =
|
||||||
(party, maybePlayerId) match {
|
maybePlayerId match {
|
||||||
case (_, Some(playerId)) => party.player(playerId).map(Seq(_)).getOrElse(Seq.empty)
|
case Some(playerId) => party.player(playerId).map(Seq(_)).getOrElse(Seq.empty)
|
||||||
case (_, _) => party.getPlayers
|
case _ => party.getPlayers
|
||||||
}
|
}
|
||||||
|
|
||||||
def getParty(partyId: String, withBiS: Boolean, withLoot: Boolean): Future[Party] =
|
def getParty(partyId: String, withBiS: Boolean, withLoot: Boolean): Future[Party] =
|
||||||
for {
|
for {
|
||||||
|
partyDescription <- profile.getPartyDescription(partyId)
|
||||||
players <- profile.getParty(partyId)
|
players <- profile.getParty(partyId)
|
||||||
bis <- if (withBiS) profile.getPiecesBiS(partyId) else Future(Seq.empty)
|
bis <- if (withBiS) profile.getPiecesBiS(partyId) else Future(Seq.empty)
|
||||||
loot <- if (withLoot) profile.getPieces(partyId) else Future(Seq.empty)
|
loot <- if (withLoot) profile.getPieces(partyId) else Future(Seq.empty)
|
||||||
} yield Party(partyId, context.system.settings.config, players, bis, loot)
|
} yield Party(partyDescription, config, players, bis, loot)
|
||||||
}
|
}
|
||||||
|
|
||||||
object Database {
|
object Database {
|
||||||
trait DatabaseRequest {
|
|
||||||
def partyId: String
|
def apply(): Behavior[DatabaseMessage] =
|
||||||
}
|
Behaviors.setup[DatabaseMessage](context => new DatabaseImpl(context))
|
||||||
}
|
}
|
@ -0,0 +1,36 @@
|
|||||||
|
/*
|
||||||
|
* 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.service.database.impl
|
||||||
|
|
||||||
|
import akka.actor.typed.scaladsl.Behaviors
|
||||||
|
import me.arcanis.ffxivbis.messages.{AddPieceToBis, DatabaseMessage, GetBiS, RemovePieceFromBiS, RemovePiecesFromBiS}
|
||||||
|
import me.arcanis.ffxivbis.service.database.Database
|
||||||
|
|
||||||
|
trait DatabaseBiSHandler { this: Database =>
|
||||||
|
|
||||||
|
def bisHandler: DatabaseMessage.Handler = {
|
||||||
|
case AddPieceToBis(playerId, piece, client) =>
|
||||||
|
profile.insertPieceBiS(playerId, piece).foreach(_ => client ! ())
|
||||||
|
Behaviors.same
|
||||||
|
|
||||||
|
case GetBiS(partyId, maybePlayerId, client) =>
|
||||||
|
getParty(partyId, withBiS = true, withLoot = false)
|
||||||
|
.map(filterParty(_, maybePlayerId))
|
||||||
|
.foreach(client ! _)
|
||||||
|
Behaviors.same
|
||||||
|
|
||||||
|
case RemovePieceFromBiS(playerId, piece, client) =>
|
||||||
|
profile.deletePieceBiS(playerId, piece).foreach(_ => client ! ())
|
||||||
|
Behaviors.same
|
||||||
|
|
||||||
|
case RemovePiecesFromBiS(playerId, client) =>
|
||||||
|
profile.deletePiecesBiS(playerId).foreach(_ => client ! ())
|
||||||
|
Behaviors.same
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,39 @@
|
|||||||
|
/*
|
||||||
|
* 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.service.database.impl
|
||||||
|
|
||||||
|
import akka.actor.typed.{Behavior, DispatcherSelector}
|
||||||
|
import akka.actor.typed.scaladsl.{AbstractBehavior, ActorContext}
|
||||||
|
import com.typesafe.config.Config
|
||||||
|
import me.arcanis.ffxivbis.messages.DatabaseMessage
|
||||||
|
import me.arcanis.ffxivbis.service.database.Database
|
||||||
|
import me.arcanis.ffxivbis.storage.DatabaseProfile
|
||||||
|
|
||||||
|
import scala.concurrent.ExecutionContext
|
||||||
|
|
||||||
|
class DatabaseImpl(context: ActorContext[DatabaseMessage])
|
||||||
|
extends AbstractBehavior[DatabaseMessage](context)
|
||||||
|
with Database
|
||||||
|
with DatabaseBiSHandler
|
||||||
|
with DatabaseLootHandler
|
||||||
|
with DatabasePartyHandler
|
||||||
|
with DatabaseUserHandler {
|
||||||
|
|
||||||
|
implicit override val executionContext: ExecutionContext = {
|
||||||
|
val selector = DispatcherSelector.fromConfig("me.arcanis.ffxivbis.default-dispatcher")
|
||||||
|
context.system.dispatchers.lookup(selector)
|
||||||
|
}
|
||||||
|
override val config: Config = context.system.settings.config
|
||||||
|
override val profile: DatabaseProfile = new DatabaseProfile(executionContext, config)
|
||||||
|
|
||||||
|
override def onMessage(msg: DatabaseMessage): Behavior[DatabaseMessage] = handle(msg)
|
||||||
|
|
||||||
|
private def handle: DatabaseMessage.Handler =
|
||||||
|
bisHandler.orElse(lootHandler).orElse(partyHandler).orElse(userHandler)
|
||||||
|
}
|
@ -0,0 +1,41 @@
|
|||||||
|
/*
|
||||||
|
* 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.service.database.impl
|
||||||
|
|
||||||
|
import java.time.Instant
|
||||||
|
import akka.actor.typed.scaladsl.Behaviors
|
||||||
|
import me.arcanis.ffxivbis.messages.{AddPieceTo, DatabaseMessage, GetLoot, RemovePieceFrom, SuggestLoot}
|
||||||
|
import me.arcanis.ffxivbis.models.Loot
|
||||||
|
import me.arcanis.ffxivbis.service.database.Database
|
||||||
|
|
||||||
|
trait DatabaseLootHandler { this: Database =>
|
||||||
|
|
||||||
|
def lootHandler: DatabaseMessage.Handler = {
|
||||||
|
case AddPieceTo(playerId, piece, isFreeLoot, client) =>
|
||||||
|
val loot = Loot(-1, piece, Instant.now, isFreeLoot)
|
||||||
|
profile.insertPiece(playerId, loot).foreach(_ => client ! ())
|
||||||
|
Behaviors.same
|
||||||
|
|
||||||
|
case GetLoot(partyId, maybePlayerId, client) =>
|
||||||
|
getParty(partyId, withBiS = false, withLoot = true)
|
||||||
|
.map(filterParty(_, maybePlayerId))
|
||||||
|
.foreach(client ! _)
|
||||||
|
Behaviors.same
|
||||||
|
|
||||||
|
case RemovePieceFrom(playerId, piece, client) =>
|
||||||
|
profile.deletePiece(playerId, piece).foreach(_ => client ! ())
|
||||||
|
Behaviors.same
|
||||||
|
|
||||||
|
case SuggestLoot(partyId, piece, client) =>
|
||||||
|
getParty(partyId, withBiS = true, withLoot = true)
|
||||||
|
.map(_.suggestLoot(piece))
|
||||||
|
.foreach(client ! _)
|
||||||
|
Behaviors.same
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,65 @@
|
|||||||
|
/*
|
||||||
|
* 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.service.database.impl
|
||||||
|
|
||||||
|
import akka.actor.typed.scaladsl.Behaviors
|
||||||
|
import me.arcanis.ffxivbis.messages.{AddPlayer, DatabaseMessage, GetParty, GetPartyDescription, GetPlayer, RemovePlayer, UpdateParty}
|
||||||
|
import me.arcanis.ffxivbis.models.{BiS, Player}
|
||||||
|
import me.arcanis.ffxivbis.service.database.Database
|
||||||
|
|
||||||
|
import scala.concurrent.Future
|
||||||
|
|
||||||
|
trait DatabasePartyHandler { this: Database =>
|
||||||
|
|
||||||
|
def partyHandler: DatabaseMessage.Handler = {
|
||||||
|
case AddPlayer(player, client) =>
|
||||||
|
profile.insertPlayer(player).foreach(_ => client ! ())
|
||||||
|
Behaviors.same
|
||||||
|
|
||||||
|
case GetParty(partyId, client) =>
|
||||||
|
getParty(partyId, withBiS = true, withLoot = true).foreach(client ! _)
|
||||||
|
Behaviors.same
|
||||||
|
|
||||||
|
case GetPartyDescription(partyId, client) =>
|
||||||
|
profile.getPartyDescription(partyId).foreach(client ! _)
|
||||||
|
Behaviors.same
|
||||||
|
|
||||||
|
case GetPlayer(playerId, client) =>
|
||||||
|
val player = profile
|
||||||
|
.getPlayerFull(playerId)
|
||||||
|
.flatMap { maybePlayerData =>
|
||||||
|
Future.traverse(maybePlayerData.toSeq) { playerData =>
|
||||||
|
for {
|
||||||
|
bis <- profile.getPiecesBiS(playerId)
|
||||||
|
loot <- profile.getPieces(playerId)
|
||||||
|
} yield Player(
|
||||||
|
playerData.id,
|
||||||
|
playerId.partyId,
|
||||||
|
playerId.job,
|
||||||
|
playerId.nick,
|
||||||
|
BiS(bis.map(_.piece)),
|
||||||
|
loot,
|
||||||
|
playerData.link,
|
||||||
|
playerData.priority
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.map(_.headOption)
|
||||||
|
player.foreach(client ! _)
|
||||||
|
Behaviors.same
|
||||||
|
|
||||||
|
case RemovePlayer(playerId, client) =>
|
||||||
|
profile.deletePlayer(playerId).foreach(_ => client ! ())
|
||||||
|
Behaviors.same
|
||||||
|
|
||||||
|
case UpdateParty(description, client) =>
|
||||||
|
profile.insertPartyDescription(description).foreach(_ => client ! ())
|
||||||
|
Behaviors.same
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,39 @@
|
|||||||
|
/*
|
||||||
|
* 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.service.database.impl
|
||||||
|
|
||||||
|
import akka.actor.typed.scaladsl.Behaviors
|
||||||
|
import me.arcanis.ffxivbis.messages.{AddUser, DatabaseMessage, DeleteUser, Exists, GetUser, GetUsers}
|
||||||
|
import me.arcanis.ffxivbis.service.database.Database
|
||||||
|
|
||||||
|
trait DatabaseUserHandler { this: Database =>
|
||||||
|
|
||||||
|
def userHandler: DatabaseMessage.Handler = {
|
||||||
|
case AddUser(user, isHashedPassword, client) =>
|
||||||
|
val toInsert = if (isHashedPassword) user else user.withHashedPassword
|
||||||
|
profile.insertUser(toInsert).foreach(_ => client ! ())
|
||||||
|
Behaviors.same
|
||||||
|
|
||||||
|
case DeleteUser(partyId, username, client) =>
|
||||||
|
profile.deleteUser(partyId, username).foreach(_ => client ! ())
|
||||||
|
Behaviors.same
|
||||||
|
|
||||||
|
case Exists(partyId, client) =>
|
||||||
|
profile.exists(partyId).foreach(client ! _)
|
||||||
|
Behaviors.same
|
||||||
|
|
||||||
|
case GetUser(partyId, username, client) =>
|
||||||
|
profile.getUser(partyId, username).foreach(client ! _)
|
||||||
|
Behaviors.same
|
||||||
|
|
||||||
|
case GetUsers(partyId, client) =>
|
||||||
|
profile.getUsers(partyId).foreach(client ! _)
|
||||||
|
Behaviors.same
|
||||||
|
}
|
||||||
|
}
|
@ -1,43 +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.service.impl
|
|
||||||
|
|
||||||
import akka.pattern.pipe
|
|
||||||
import me.arcanis.ffxivbis.models.{Piece, PlayerId}
|
|
||||||
import me.arcanis.ffxivbis.service.Database
|
|
||||||
|
|
||||||
trait DatabaseBiSHandler { this: Database =>
|
|
||||||
import DatabaseBiSHandler._
|
|
||||||
|
|
||||||
def bisHandler: Receive = {
|
|
||||||
case AddPieceToBis(playerId, piece) =>
|
|
||||||
val client = sender()
|
|
||||||
profile.insertPieceBiS(playerId, piece).pipeTo(client)
|
|
||||||
|
|
||||||
case GetBiS(partyId, maybePlayerId) =>
|
|
||||||
val client = sender()
|
|
||||||
getParty(partyId, withBiS = true, withLoot = false)
|
|
||||||
.map(filterParty(_, maybePlayerId))
|
|
||||||
.pipeTo(client)
|
|
||||||
|
|
||||||
case RemovePieceFromBiS(playerId, piece) =>
|
|
||||||
val client = sender()
|
|
||||||
profile.deletePieceBiS(playerId, piece).pipeTo(client)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
object DatabaseBiSHandler {
|
|
||||||
case class AddPieceToBis(playerId: PlayerId, piece: Piece) extends Database.DatabaseRequest {
|
|
||||||
override def partyId: String = playerId.partyId
|
|
||||||
}
|
|
||||||
case class GetBiS(partyId: String, playerId: Option[PlayerId]) extends Database.DatabaseRequest
|
|
||||||
case class RemovePieceFromBiS(playerId: PlayerId, piece: Piece) extends Database.DatabaseRequest {
|
|
||||||
override def partyId: String = playerId.partyId
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,31 +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.service.impl
|
|
||||||
|
|
||||||
import akka.actor.Props
|
|
||||||
import me.arcanis.ffxivbis.service.Database
|
|
||||||
import me.arcanis.ffxivbis.storage.DatabaseProfile
|
|
||||||
|
|
||||||
import scala.concurrent.ExecutionContext
|
|
||||||
|
|
||||||
class DatabaseImpl extends Database
|
|
||||||
with DatabaseBiSHandler with DatabaseLootHandler
|
|
||||||
with DatabasePartyHandler with DatabaseUserHandler {
|
|
||||||
|
|
||||||
implicit val executionContext: ExecutionContext =
|
|
||||||
context.system.dispatchers.lookup("me.arcanis.ffxivbis.default-dispatcher")
|
|
||||||
val profile = new DatabaseProfile(executionContext, context.system.settings.config)
|
|
||||||
|
|
||||||
override def receive: Receive =
|
|
||||||
bisHandler orElse lootHandler orElse partyHandler orElse userHandler
|
|
||||||
}
|
|
||||||
|
|
||||||
object DatabaseImpl {
|
|
||||||
def props: Props = Props(new DatabaseImpl)
|
|
||||||
}
|
|
@ -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.service.impl
|
|
||||||
|
|
||||||
import akka.pattern.pipe
|
|
||||||
import me.arcanis.ffxivbis.models.{Piece, PlayerId}
|
|
||||||
import me.arcanis.ffxivbis.service.Database
|
|
||||||
|
|
||||||
trait DatabaseLootHandler { this: Database =>
|
|
||||||
import DatabaseLootHandler._
|
|
||||||
|
|
||||||
def lootHandler: Receive = {
|
|
||||||
case AddPieceTo(playerId, piece) =>
|
|
||||||
val client = sender()
|
|
||||||
profile.insertPiece(playerId, piece).pipeTo(client)
|
|
||||||
|
|
||||||
case GetLoot(partyId, maybePlayerId) =>
|
|
||||||
val client = sender()
|
|
||||||
getParty(partyId, withBiS = false, withLoot = true)
|
|
||||||
.map(filterParty(_, maybePlayerId))
|
|
||||||
.pipeTo(client)
|
|
||||||
|
|
||||||
case RemovePieceFrom(playerId, piece) =>
|
|
||||||
val client = sender()
|
|
||||||
profile.deletePiece(playerId, piece).pipeTo(client)
|
|
||||||
|
|
||||||
case SuggestLoot(partyId, piece) =>
|
|
||||||
val client = sender()
|
|
||||||
getParty(partyId, withBiS = true, withLoot = true).map(_.suggestLoot(piece)).pipeTo(client)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
object DatabaseLootHandler {
|
|
||||||
case class AddPieceTo(playerId: PlayerId, piece: Piece) extends Database.DatabaseRequest {
|
|
||||||
override def partyId: String = playerId.partyId
|
|
||||||
}
|
|
||||||
case class GetLoot(partyId: String, playerId: Option[PlayerId]) extends Database.DatabaseRequest
|
|
||||||
case class RemovePieceFrom(playerId: PlayerId, piece: Piece) extends Database.DatabaseRequest {
|
|
||||||
override def partyId: String = playerId.partyId
|
|
||||||
}
|
|
||||||
case class SuggestLoot(partyId: String, piece: Piece) extends Database.DatabaseRequest
|
|
||||||
}
|
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user