mirror of
https://github.com/arcan1s/ffxivbis.git
synced 2025-07-07 02:45:52 +00:00
Compare commits
33 Commits
Author | SHA1 | Date | |
---|---|---|---|
bb08132237 | |||
65a4a25b3a | |||
37c444a5b9 | |||
f5a644747d | |||
ab790e87ff | |||
9faceb4f61 | |||
65b9e53b66 | |||
ad144534a9 | |||
4700768aed | |||
557038c262 | |||
6e8b64feef | |||
0a71a98482 | |||
69d35c95d9 | |||
155790465e | |||
da00a60332 | |||
0bf1edfff8 | |||
50acecd97e | |||
e03f8987b0 | |||
2ad3600da5 | |||
a84b947862 | |||
f84b9cbaba | |||
9f12647fed | |||
2a1eb9430e | |||
4cdcd80d51 | |||
b228595a1b | |||
d1001ffb8e | |||
09c7efec62 | |||
9668a0edd1 | |||
d5233361e5 | |||
eea2f1b04b | |||
49fd33fffc | |||
2d84459c4d | |||
28dabcb44e |
162
.gitignore
vendored
162
.gitignore
vendored
@ -1,96 +1,88 @@
|
|||||||
# Byte-compiled / optimized / DLL files
|
#### joe made this: http://goel.io/joe
|
||||||
__pycache__/
|
|
||||||
*.py[cod]
|
|
||||||
*$py.class
|
|
||||||
|
|
||||||
# C extensions
|
#### jetbrains ####
|
||||||
*.so
|
|
||||||
|
|
||||||
# Distribution / packaging
|
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm
|
||||||
.Python
|
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
|
||||||
env/
|
|
||||||
build/
|
|
||||||
develop-eggs/
|
|
||||||
dist/
|
|
||||||
downloads/
|
|
||||||
eggs/
|
|
||||||
.eggs/
|
|
||||||
parts/
|
|
||||||
sdist/
|
|
||||||
*.egg-info/
|
|
||||||
.installed.cfg
|
|
||||||
*.egg
|
|
||||||
|
|
||||||
# PyInstaller
|
# User-specific stuff:
|
||||||
# Usually these files are written by a python script from a template
|
.idea
|
||||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
|
||||||
*.manifest
|
|
||||||
*.spec
|
|
||||||
|
|
||||||
# Installer logs
|
## File-based project format:
|
||||||
pip-log.txt
|
*.iws
|
||||||
pip-delete-this-directory.txt
|
|
||||||
|
|
||||||
# Unit test / coverage reports
|
## Plugin-specific files:
|
||||||
htmlcov/
|
|
||||||
.tox/
|
# IntelliJ
|
||||||
.coverage
|
/out/
|
||||||
.coverage.*
|
|
||||||
|
# mpeltonen/sbt-idea plugin
|
||||||
|
.idea_modules/
|
||||||
|
|
||||||
|
# JIRA plugin
|
||||||
|
atlassian-ide-plugin.xml
|
||||||
|
|
||||||
|
# Crashlytics plugin (for Android Studio and IntelliJ)
|
||||||
|
com_crashlytics_export_strings.xml
|
||||||
|
crashlytics.properties
|
||||||
|
crashlytics-build.properties
|
||||||
|
fabric.properties
|
||||||
|
|
||||||
|
#### gradle ####
|
||||||
|
|
||||||
|
.gradle
|
||||||
|
/build/
|
||||||
|
|
||||||
|
# Ignore Gradle GUI config
|
||||||
|
gradle-app.setting
|
||||||
|
|
||||||
|
# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored)
|
||||||
|
!gradle-wrapper.jar
|
||||||
|
|
||||||
|
# Cache of project
|
||||||
|
.gradletasknamecache
|
||||||
|
|
||||||
|
# # Work around https://youtrack.jetbrains.com/issue/IDEA-116898
|
||||||
|
# gradle/wrapper/gradle-wrapper.properties
|
||||||
|
|
||||||
|
#### java ####
|
||||||
|
|
||||||
|
*.class
|
||||||
|
|
||||||
|
# Mobile Tools for Java (J2ME)
|
||||||
|
.mtj.tmp/
|
||||||
|
|
||||||
|
# Package Files #
|
||||||
|
*.jar
|
||||||
|
*.war
|
||||||
|
*.ear
|
||||||
|
|
||||||
|
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
|
||||||
|
hs_err_pid*
|
||||||
|
|
||||||
|
#### scala ####
|
||||||
|
|
||||||
|
*.class
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# sbt specific
|
||||||
.cache
|
.cache
|
||||||
nosetests.xml
|
.history
|
||||||
coverage.xml
|
.lib/
|
||||||
*,cover
|
dist/*
|
||||||
.hypothesis/
|
|
||||||
|
|
||||||
# Translations
|
|
||||||
*.mo
|
|
||||||
*.pot
|
|
||||||
|
|
||||||
# Django stuff:
|
|
||||||
*.log*
|
|
||||||
local_settings.py
|
|
||||||
|
|
||||||
# Flask stuff:
|
|
||||||
instance/
|
|
||||||
.webassets-cache
|
|
||||||
|
|
||||||
# Scrapy stuff:
|
|
||||||
.scrapy
|
|
||||||
|
|
||||||
# Sphinx documentation
|
|
||||||
docs/_build/
|
|
||||||
|
|
||||||
# PyBuilder
|
|
||||||
target/
|
target/
|
||||||
|
lib_managed/
|
||||||
|
src_managed/
|
||||||
|
project/boot/
|
||||||
|
project/plugins/project/
|
||||||
|
|
||||||
# IPython Notebook
|
# Scala-IDE specific
|
||||||
.ipynb_checkpoints
|
.scala_dependencies
|
||||||
|
.worksheet
|
||||||
|
|
||||||
# pyenv
|
# ENSIME specific
|
||||||
.python-version
|
.ensime_cache/
|
||||||
|
.ensime
|
||||||
# celery beat schedule file
|
|
||||||
celerybeat-schedule
|
|
||||||
|
|
||||||
# dotenv
|
|
||||||
.env
|
|
||||||
|
|
||||||
# virtualenv
|
|
||||||
venv/
|
|
||||||
ENV/
|
|
||||||
|
|
||||||
# Spyder project settings
|
|
||||||
.spyderproject
|
|
||||||
|
|
||||||
# Rope project settings
|
|
||||||
.ropeproject
|
|
||||||
|
|
||||||
*.deb
|
|
||||||
|
|
||||||
.idea/
|
|
||||||
|
|
||||||
.mypy_cache/
|
|
||||||
|
|
||||||
/cache
|
|
||||||
|
|
||||||
*.db
|
*.db
|
||||||
|
*.sc
|
||||||
|
9
.travis.yml
Normal file
9
.travis.yml
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
language: scala
|
||||||
|
scala:
|
||||||
|
- 2.13.1
|
||||||
|
|
||||||
|
sbt_args: -no-colors
|
||||||
|
|
||||||
|
script:
|
||||||
|
- sbt compile
|
||||||
|
- sbt test
|
71
README.md
71
README.md
@ -1,82 +1,31 @@
|
|||||||
# FFXIV BiS
|
# FFXIV BiS
|
||||||
|
|
||||||
|
[](https://travis-ci.org/arcan1s/ffxivbis) 
|
||||||
|
|
||||||
Service which allows to manage savage loot distribution easy.
|
Service which allows to manage savage loot distribution easy.
|
||||||
|
|
||||||
## Installation and usage
|
## Installation and usage
|
||||||
|
|
||||||
This service requires python >= 3.7. For other dependencies see `setup.py`.
|
In general compilation process looks like:
|
||||||
|
|
||||||
In general installation process looks like:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python setup.py build
|
sbt dist
|
||||||
python setup.py test # if you want to run tests
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Service can be run from `src` directory by using command:
|
Or alternatively you can download latest distribution zip from the releases page. Service can be run by using command:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python -m service.application.application
|
bin/ffxivbis
|
||||||
```
|
```
|
||||||
|
|
||||||
To see all available options type `--help`.
|
from the extracted archive root.
|
||||||
|
|
||||||
## Web service
|
## Web service
|
||||||
|
|
||||||
REST API documentation is available at `http://0.0.0.0:8000/api-docs`. HTML representation is available at `http://0.0.0.0:8000`.
|
REST API documentation is available at `http://0.0.0.0:8000/swagger`. HTML representation is available at `http://0.0.0.0:8000`.
|
||||||
|
|
||||||
*Note*: host and port depend on configuration settings.
|
*Note*: host and port depend on configuration settings.
|
||||||
|
|
||||||
## Configuration
|
## Public service
|
||||||
|
|
||||||
* `settings` section
|
There is also public service which is available at https://ffxivbis.arcanis.me.
|
||||||
|
|
||||||
General project settings.
|
|
||||||
|
|
||||||
* `include`: path to include configuration directory, string, optional.
|
|
||||||
* `logging`: path to logging configuration, see `logging.ini` for reference, string, optional.
|
|
||||||
* `database`: database provide name, string, required. Allowed values: `sqlite`, `postgres`.
|
|
||||||
* `priority`: methods of `Player` class which will be called to sort players for loot priority, space separated list of strings, required.
|
|
||||||
|
|
||||||
* `ariyala` section
|
|
||||||
|
|
||||||
Settings related to ariyala parser.
|
|
||||||
|
|
||||||
* `ariyala_url`: ariyala base url, string, required.
|
|
||||||
* `request_timeout`: xivapi request timeout, float, optional, default 30.
|
|
||||||
* `xivapi_key`: xivapi developer key, string, optional.
|
|
||||||
* `xivapi_url`: xivapi base url, string, required.
|
|
||||||
|
|
||||||
* `auth` section
|
|
||||||
|
|
||||||
Authentication settings.
|
|
||||||
|
|
||||||
* `enabled`: whether authentication enabled or not, boolean, required.
|
|
||||||
* `root_username`: username of administrator, string, required.
|
|
||||||
* `root_password`: md5 hashed password of administrator, string, required.
|
|
||||||
|
|
||||||
* `postgres` section
|
|
||||||
|
|
||||||
Database settings for `postgres` provider.
|
|
||||||
|
|
||||||
* `database`: database name, string, required.
|
|
||||||
* `host`: database host, string, required.
|
|
||||||
* `password`: database password, string, required.
|
|
||||||
* `port`: database port, int, required.
|
|
||||||
* `username`: database username, string, required.
|
|
||||||
* `migrations_path`: path to database migrations, string, required.
|
|
||||||
|
|
||||||
* `sqlite` section
|
|
||||||
|
|
||||||
Database settings for `sqlite` provider.
|
|
||||||
|
|
||||||
* `database_path`: path to sqlite database, string, required.
|
|
||||||
* `migrations_path`: path to database migrations, string, required.
|
|
||||||
|
|
||||||
* `web` section
|
|
||||||
|
|
||||||
Web server related settings.
|
|
||||||
|
|
||||||
* `host`: address to bind, string, required.
|
|
||||||
* `port`: port to bind, int, required.
|
|
||||||
* `templates`: path to directory with jinja templates, string, required.
|
|
4
TODO.md
4
TODO.md
@ -1,3 +1,3 @@
|
|||||||
* [ ] items improvements
|
* [x] items improvements
|
||||||
* [ ] multiple parties support
|
* [x] multiple parties support
|
||||||
* [ ] pretty UI
|
* [ ] pretty UI
|
16
build.sbt
Normal file
16
build.sbt
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
name := "ffxivbis"
|
||||||
|
|
||||||
|
scalaVersion := "2.13.1"
|
||||||
|
|
||||||
|
scalacOptions ++= Seq("-deprecation", "-feature")
|
||||||
|
|
||||||
|
enablePlugins(JavaAppPackaging)
|
||||||
|
|
||||||
|
assemblyMergeStrategy in assembly := {
|
||||||
|
case PathList("META-INF", xs @ _*) => MergeStrategy.discard
|
||||||
|
case "application.conf" => MergeStrategy.concat
|
||||||
|
case "module-info.class" => MergeStrategy.first
|
||||||
|
case x =>
|
||||||
|
val oldStrategy = (assemblyMergeStrategy in assembly).value
|
||||||
|
oldStrategy(x)
|
||||||
|
}
|
19
libraries.sbt
Normal file
19
libraries.sbt
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
libraryDependencies += "ch.qos.logback" % "logback-classic" % "1.2.3"
|
||||||
|
libraryDependencies += "com.typesafe.scala-logging" %% "scala-logging" % "3.9.2"
|
||||||
|
|
||||||
|
libraryDependencies += "com.typesafe.akka" %% "akka-http" % "10.1.10"
|
||||||
|
libraryDependencies += "com.typesafe.akka" %% "akka-http-spray-json" % "10.1.10"
|
||||||
|
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.lihaoyi" %% "scalatags" % "0.7.0"
|
||||||
|
|
||||||
|
libraryDependencies += "com.typesafe.slick" %% "slick" % "3.3.2"
|
||||||
|
libraryDependencies += "com.typesafe.slick" %% "slick-hikaricp" % "3.3.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"
|
@ -1,38 +0,0 @@
|
|||||||
'''
|
|
||||||
init tables
|
|
||||||
'''
|
|
||||||
|
|
||||||
from yoyo import step
|
|
||||||
|
|
||||||
__depends__ = {}
|
|
||||||
|
|
||||||
steps = [
|
|
||||||
step('''create table players (
|
|
||||||
player_id integer primary key,
|
|
||||||
created integer not null,
|
|
||||||
nick text not null,
|
|
||||||
job text not null,
|
|
||||||
bis_link text,
|
|
||||||
priority integer not null default 1
|
|
||||||
)'''),
|
|
||||||
step('''create unique index players_nick_job_idx on players(nick, job)'''),
|
|
||||||
|
|
||||||
step('''create table loot (
|
|
||||||
loot_id integer primary key,
|
|
||||||
player_id integer not null,
|
|
||||||
created integer not null,
|
|
||||||
piece text not null,
|
|
||||||
is_tome integer not null,
|
|
||||||
foreign key (player_id) references players(player_id) on delete cascade
|
|
||||||
)'''),
|
|
||||||
step('''create index loot_owner_idx on loot(player_id)'''),
|
|
||||||
|
|
||||||
step('''create table bis (
|
|
||||||
player_id integer not null,
|
|
||||||
created integer not null,
|
|
||||||
piece text not null,
|
|
||||||
is_tome integer not null,
|
|
||||||
foreign key (player_id) references players(player_id) on delete cascade
|
|
||||||
)'''),
|
|
||||||
step('''create unique index bis_piece_player_id_idx on bis(player_id, piece)''')
|
|
||||||
]
|
|
@ -1,17 +0,0 @@
|
|||||||
'''
|
|
||||||
users table
|
|
||||||
'''
|
|
||||||
|
|
||||||
from yoyo import step
|
|
||||||
|
|
||||||
__depends__ = {}
|
|
||||||
|
|
||||||
steps = [
|
|
||||||
step('''create table users (
|
|
||||||
user_id integer primary key,
|
|
||||||
username text not null,
|
|
||||||
password text not null,
|
|
||||||
permission text not null
|
|
||||||
)'''),
|
|
||||||
step('''create unique index users_username_idx on users(username)''')
|
|
||||||
]
|
|
@ -1,10 +0,0 @@
|
|||||||
[settings]
|
|
||||||
include = ffxivbis.ini.d
|
|
||||||
logging = ffxivbis.ini.d/logging.ini
|
|
||||||
database = sqlite
|
|
||||||
priority = is_required loot_count_bis loot_priority loot_count loot_count_total
|
|
||||||
|
|
||||||
[web]
|
|
||||||
host = 0.0.0.0
|
|
||||||
port = 8000
|
|
||||||
templates = templates
|
|
@ -1,4 +0,0 @@
|
|||||||
[ariyala]
|
|
||||||
ariyala_url = https://ffxiv.ariyala.com
|
|
||||||
request_timeout = 1
|
|
||||||
xivapi_url = https://xivapi.com
|
|
@ -1,4 +0,0 @@
|
|||||||
[auth]
|
|
||||||
enabled = yes
|
|
||||||
root_username = admin
|
|
||||||
root_password = $1$R3j4sym6$HtvrKOJ66f7w3.9Zc3U6h1
|
|
@ -1,44 +0,0 @@
|
|||||||
[loggers]
|
|
||||||
keys = root,application,database,http
|
|
||||||
|
|
||||||
[handlers]
|
|
||||||
keys = file_handler
|
|
||||||
|
|
||||||
[formatters]
|
|
||||||
keys = generic_format
|
|
||||||
|
|
||||||
[handler_console_handler]
|
|
||||||
class = StreamHandler
|
|
||||||
level = INFO
|
|
||||||
formatter = generic_format
|
|
||||||
args = (sys.stdout,)
|
|
||||||
|
|
||||||
[handler_file_handler]
|
|
||||||
class = logging.handlers.RotatingFileHandler
|
|
||||||
level = INFO
|
|
||||||
formatter = generic_format
|
|
||||||
args = ('ffxivbis.log', 'a', 20971520, 20)
|
|
||||||
|
|
||||||
[formatter_generic_format]
|
|
||||||
format = [%(levelname)s] [%(asctime)s] [%(threadName)s] [%(name)s] [%(funcName)s]: %(message)s
|
|
||||||
datefmt =
|
|
||||||
|
|
||||||
[logger_root]
|
|
||||||
level = INFO
|
|
||||||
handlers = file_handler
|
|
||||||
qualname = root
|
|
||||||
|
|
||||||
[logger_application]
|
|
||||||
level = INFO
|
|
||||||
handlers = file_handler
|
|
||||||
qualname = application
|
|
||||||
|
|
||||||
[logger_database]
|
|
||||||
level = INFO
|
|
||||||
handlers = file_handler
|
|
||||||
qualname = database
|
|
||||||
|
|
||||||
[logger_http]
|
|
||||||
level = INFO
|
|
||||||
handlers = file_handler
|
|
||||||
qualname = http
|
|
@ -1,3 +0,0 @@
|
|||||||
[sqlite]
|
|
||||||
database_path = /home/arcanis/Documents/github/ffxivbis/ffxivbis.db
|
|
||||||
migrations_path = /home/arcanis/Documents/github/ffxivbis/migrations
|
|
1
project/build.properties
Normal file
1
project/build.properties
Normal file
@ -0,0 +1 @@
|
|||||||
|
sbt.version = 1.3.3
|
3
project/plugins.sbt
Normal file
3
project/plugins.sbt
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.14.6")
|
||||||
|
addSbtPlugin("com.typesafe.sbt" % "sbt-native-packager" % "1.3.4")
|
||||||
|
addSbtPlugin("net.virtual-void" % "sbt-dependency-graph" % "0.10.0-RC1")
|
@ -1,5 +0,0 @@
|
|||||||
[aliases]
|
|
||||||
test=pytest
|
|
||||||
|
|
||||||
[tool:pytest]
|
|
||||||
addopts = --verbose --pyargs .
|
|
51
setup.py
51
setup.py
@ -1,51 +0,0 @@
|
|||||||
from distutils.util import convert_path
|
|
||||||
from setuptools import setup, find_packages
|
|
||||||
from os import path
|
|
||||||
|
|
||||||
|
|
||||||
here = path.abspath(path.dirname(__file__))
|
|
||||||
metadata = dict()
|
|
||||||
with open(convert_path('src/service/core/version.py')) as metadata_file:
|
|
||||||
exec(metadata_file.read(), metadata)
|
|
||||||
|
|
||||||
|
|
||||||
setup(
|
|
||||||
name='ffxivbis',
|
|
||||||
|
|
||||||
version=metadata['__version__'],
|
|
||||||
zip_safe=False,
|
|
||||||
|
|
||||||
description='Helper to handle loot drop',
|
|
||||||
|
|
||||||
author='Evgeniy Alekseev',
|
|
||||||
author_email='i@arcanis.me',
|
|
||||||
|
|
||||||
license='BSD',
|
|
||||||
|
|
||||||
packages=find_packages(exclude=['contrib', 'docs', 'tests']),
|
|
||||||
|
|
||||||
install_requires=[
|
|
||||||
'aiohttp',
|
|
||||||
'aiohttp_jinja2',
|
|
||||||
'aiohttp_security',
|
|
||||||
'apispec',
|
|
||||||
'Jinja2',
|
|
||||||
'passlib',
|
|
||||||
'requests',
|
|
||||||
'yoyo_migrations'
|
|
||||||
],
|
|
||||||
setup_requires=[
|
|
||||||
'pytest-runner'
|
|
||||||
],
|
|
||||||
tests_require=[
|
|
||||||
'pytest', 'pytest-aiohttp', 'pytest-asyncio'
|
|
||||||
],
|
|
||||||
|
|
||||||
include_package_data=True,
|
|
||||||
|
|
||||||
extras_require={
|
|
||||||
'Postgresql': ['aiopg'],
|
|
||||||
'SQLite': ['aiosqlite'],
|
|
||||||
'test': ['coverage', 'pytest'],
|
|
||||||
},
|
|
||||||
)
|
|
@ -0,0 +1,36 @@
|
|||||||
|
create table players (
|
||||||
|
party_id text not null,
|
||||||
|
player_id bigserial unique,
|
||||||
|
created bigint not null,
|
||||||
|
nick text not null,
|
||||||
|
job text not null,
|
||||||
|
bis_link text,
|
||||||
|
priority integer not null default 1);
|
||||||
|
create unique index players_nick_job_idx on players(party_id, nick, job);
|
||||||
|
|
||||||
|
create table loot (
|
||||||
|
loot_id bigserial unique,
|
||||||
|
player_id bigint not null,
|
||||||
|
created bigint not null,
|
||||||
|
piece text not null,
|
||||||
|
is_tome integer not null,
|
||||||
|
job text not null,
|
||||||
|
foreign key (player_id) references players(player_id) on delete cascade);
|
||||||
|
create index loot_owner_idx on loot(player_id);
|
||||||
|
|
||||||
|
create table bis (
|
||||||
|
player_id bigint not null,
|
||||||
|
created bigint not null,
|
||||||
|
piece text not null,
|
||||||
|
is_tome integer not null,
|
||||||
|
job text not null,
|
||||||
|
foreign key (player_id) references players(player_id) on delete cascade);
|
||||||
|
create unique index bis_piece_player_id_idx on bis(player_id, piece);
|
||||||
|
|
||||||
|
create table users (
|
||||||
|
party_id text not null,
|
||||||
|
user_id bigserial unique,
|
||||||
|
username text not null,
|
||||||
|
password text not null,
|
||||||
|
permission text not null);
|
||||||
|
create unique index users_username_idx on users(party_id, username);
|
@ -0,0 +1,5 @@
|
|||||||
|
update loot set piece = 'left ring' where piece = 'leftRing';
|
||||||
|
update loot set piece = 'right ring' where piece = 'rightRing';
|
||||||
|
|
||||||
|
update bis set piece = 'left ring' where piece = 'leftRing';
|
||||||
|
update bis set piece = 'right ring' where piece = 'rightRing';
|
@ -0,0 +1,36 @@
|
|||||||
|
create table players (
|
||||||
|
party_id text not null,
|
||||||
|
player_id integer primary key autoincrement,
|
||||||
|
created integer not null,
|
||||||
|
nick text not null,
|
||||||
|
job text not null,
|
||||||
|
bis_link text,
|
||||||
|
priority integer not null default 1);
|
||||||
|
create unique index players_nick_job_idx on players(party_id, nick, job);
|
||||||
|
|
||||||
|
create table loot (
|
||||||
|
loot_id integer primary key autoincrement,
|
||||||
|
player_id integer not null,
|
||||||
|
created integer not null,
|
||||||
|
piece text not null,
|
||||||
|
is_tome integer not null,
|
||||||
|
job text not null,
|
||||||
|
foreign key (player_id) references players(player_id) on delete cascade);
|
||||||
|
create index loot_owner_idx on loot(player_id);
|
||||||
|
|
||||||
|
create table bis (
|
||||||
|
player_id integer not null,
|
||||||
|
created integer not null,
|
||||||
|
piece text not null,
|
||||||
|
is_tome integer not null,
|
||||||
|
job text not null,
|
||||||
|
foreign key (player_id) references players(player_id) on delete cascade);
|
||||||
|
create unique index bis_piece_player_id_idx on bis(player_id, piece);
|
||||||
|
|
||||||
|
create table users (
|
||||||
|
party_id text not null,
|
||||||
|
user_id integer primary key autoincrement,
|
||||||
|
username text not null,
|
||||||
|
password text not null,
|
||||||
|
permission text not null);
|
||||||
|
create unique index users_username_idx on users(party_id, username);
|
@ -0,0 +1,5 @@
|
|||||||
|
update loot set piece = 'left ring' where piece = 'leftRing';
|
||||||
|
update loot set piece = 'right ring' where piece = 'rightRing';
|
||||||
|
|
||||||
|
update bis set piece = 'left ring' where piece = 'leftRing';
|
||||||
|
update bis set piece = 'right ring' where piece = 'rightRing';
|
27
src/main/resources/logback-application.xml
Normal file
27
src/main/resources/logback-application.xml
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
<included>
|
||||||
|
|
||||||
|
<appender name="application-base" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||||
|
<encoder>
|
||||||
|
<pattern>[%-5level %d{yyyy-MM-dd HH:mm:ss.SSS}] [%thread] [%logger{50}]: %msg%n</pattern>
|
||||||
|
</encoder>
|
||||||
|
|
||||||
|
<file>application.log</file>
|
||||||
|
|
||||||
|
<rollingPolicy class="ch.qos.logback.core.rolling.FixedWindowRollingPolicy">
|
||||||
|
<minIndex>1</minIndex>
|
||||||
|
<maxIndex>20</maxIndex>
|
||||||
|
<fileNamePattern>application.log.%i.gz</fileNamePattern>
|
||||||
|
</rollingPolicy>
|
||||||
|
|
||||||
|
<triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
|
||||||
|
<maxFileSize>100MB</maxFileSize>
|
||||||
|
</triggeringPolicy>
|
||||||
|
</appender>
|
||||||
|
|
||||||
|
<appender name="application" class="ch.qos.logback.classic.AsyncAppender">
|
||||||
|
<appender-ref ref="application-base"/>
|
||||||
|
<queueSize>50000</queueSize>
|
||||||
|
<neverBlock>true</neverBlock>
|
||||||
|
</appender>
|
||||||
|
|
||||||
|
</included>
|
27
src/main/resources/logback-http.xml
Normal file
27
src/main/resources/logback-http.xml
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
<included>
|
||||||
|
|
||||||
|
<appender name="http-base" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||||
|
<encoder>
|
||||||
|
<pattern>%msg%n</pattern>
|
||||||
|
</encoder>
|
||||||
|
|
||||||
|
<file>http.log</file>
|
||||||
|
|
||||||
|
<rollingPolicy class="ch.qos.logback.core.rolling.FixedWindowRollingPolicy">
|
||||||
|
<minIndex>1</minIndex>
|
||||||
|
<maxIndex>20</maxIndex>
|
||||||
|
<fileNamePattern>http.log.%i.gz</fileNamePattern>
|
||||||
|
</rollingPolicy>
|
||||||
|
|
||||||
|
<triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
|
||||||
|
<maxFileSize>100MB</maxFileSize>
|
||||||
|
</triggeringPolicy>
|
||||||
|
</appender>
|
||||||
|
|
||||||
|
<appender name="http" class="ch.qos.logback.classic.AsyncAppender">
|
||||||
|
<appender-ref ref="http-base"/>
|
||||||
|
<queueSize>50000</queueSize>
|
||||||
|
<neverBlock>true</neverBlock>
|
||||||
|
</appender>
|
||||||
|
|
||||||
|
</included>
|
16
src/main/resources/logback.xml
Normal file
16
src/main/resources/logback.xml
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
<configuration>
|
||||||
|
|
||||||
|
<include resource="logback-application.xml" />
|
||||||
|
<include resource="logback-http.xml" />
|
||||||
|
|
||||||
|
<root level="debug">
|
||||||
|
<appender-ref ref="application" />
|
||||||
|
</root>
|
||||||
|
|
||||||
|
<logger name="me.arcanis.ffxivbis" level="DEBUG" />
|
||||||
|
<logger name="http" level="DEBUG">
|
||||||
|
<appender-ref ref="http" />
|
||||||
|
</logger>
|
||||||
|
<logger name="slick" level="INFO" />
|
||||||
|
|
||||||
|
</configuration>
|
75
src/main/resources/reference.conf
Normal file
75
src/main/resources/reference.conf
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
me.arcanis.ffxivbis {
|
||||||
|
ariyala {
|
||||||
|
# ariyala base url, string, required
|
||||||
|
ariyala-url = "https://ffxiv.ariyala.com"
|
||||||
|
# xivapi base url, string, required
|
||||||
|
xivapi-url = "https://xivapi.com"
|
||||||
|
# xivapi developer key, string, optional
|
||||||
|
#xivapi-key = "abcdef"
|
||||||
|
}
|
||||||
|
|
||||||
|
database {
|
||||||
|
# database section. Section must be declared inside
|
||||||
|
# for more detailed section descriptions refer to slick documentation
|
||||||
|
mode = "sqlite"
|
||||||
|
|
||||||
|
sqlite {
|
||||||
|
profile = "slick.jdbc.SQLiteProfile$"
|
||||||
|
db {
|
||||||
|
url = "jdbc:sqlite:ffxivbis.db"
|
||||||
|
user = "user"
|
||||||
|
password = "password"
|
||||||
|
}
|
||||||
|
numThreads = 10
|
||||||
|
}
|
||||||
|
|
||||||
|
postgresql {
|
||||||
|
profile = "slick.jdbc.PostgresProfile$"
|
||||||
|
db {
|
||||||
|
url = "jdbc:postgresql://localhost/ffxivbis"
|
||||||
|
user = "ffxivbis"
|
||||||
|
password = "ffxivbis"
|
||||||
|
|
||||||
|
connectionPool = disabled
|
||||||
|
keepAliveConnection = yes
|
||||||
|
}
|
||||||
|
numThreads = 10
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
settings {
|
||||||
|
# counters of Player class which will be called to sort players for loot priority
|
||||||
|
# list of strings, required
|
||||||
|
priority = [
|
||||||
|
"isRequired", "lootCountBiS", "priority", "lootCount", "lootCountTotal"
|
||||||
|
]
|
||||||
|
# general request timeout, duratin, required
|
||||||
|
request-timeout = 10s
|
||||||
|
# party in-memory storage lifetime
|
||||||
|
cache-timeout = 1m
|
||||||
|
}
|
||||||
|
|
||||||
|
web {
|
||||||
|
# address to bind, string, required
|
||||||
|
host = "127.0.0.1"
|
||||||
|
# port to bind, int, required
|
||||||
|
port = 8000
|
||||||
|
# hostname to use in docs, if not set host:port will be used
|
||||||
|
#hostname = "127.0.0.1:8000"
|
||||||
|
|
||||||
|
# rate limits
|
||||||
|
limits {
|
||||||
|
intetval = 1m
|
||||||
|
max-count = 60
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
default-dispatcher {
|
||||||
|
type = Dispatcher
|
||||||
|
executor = "thread-pool-executor"
|
||||||
|
thread-pool-executor {
|
||||||
|
fixed-pool-size = 16
|
||||||
|
}
|
||||||
|
throughput = 1
|
||||||
|
}
|
||||||
|
}
|
277
src/main/resources/static/styles.css
Normal file
277
src/main/resources/static/styles.css
Normal file
@ -0,0 +1,277 @@
|
|||||||
|
/* in-text images */
|
||||||
|
figure.img {
|
||||||
|
float: right;
|
||||||
|
border: 0px solid #333;
|
||||||
|
padding: 0px;
|
||||||
|
margin: 5px 0px 5px 10px;
|
||||||
|
}
|
||||||
|
figure.img img {
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
figure.img figcaption {
|
||||||
|
margin: 0px;
|
||||||
|
font-size: 90%;
|
||||||
|
font-style: italic;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 .octicon-link, h2 .octicon-link, h3 .octicon-link, h4 .octicon-link, h5 .octicon-link, h6 .octicon-link {
|
||||||
|
display: none;
|
||||||
|
color: #222222;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1:hover .anchor, h2:hover .anchor, h3:hover .anchor, h4:hover .anchor, h5:hover .anchor, h6:hover .anchor{
|
||||||
|
padding-left: 8px;
|
||||||
|
margin-left: -24px;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1:hover .anchor .octicon-link, h2:hover .anchor .octicon-link, h3:hover .anchor .octicon-link, h4:hover .anchor .octicon-link, h5:hover .anchor .octicon-link, h6:hover .anchor .octicon-link {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
padding: 50px;
|
||||||
|
font: 14px/1.5 "Liberation Sans", Helvetica, Arial, sans-serif;
|
||||||
|
color: #555555;
|
||||||
|
background: #eaeaea
|
||||||
|
}
|
||||||
|
|
||||||
|
h1, h2, h3, h4, h5, h6 {
|
||||||
|
color: #222222;
|
||||||
|
margin: 0 0 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
p, ul, ol, table, pre, dl {
|
||||||
|
margin: 0 0 20px;
|
||||||
|
text-align: justify;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1, h2, h3 {
|
||||||
|
line-height: 1.1;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
color: #393939;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3, h4, h5, h6 {
|
||||||
|
color: #494949;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: #3399cc;
|
||||||
|
font-weight: 350;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
a small {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #777777;
|
||||||
|
margin-top: -0.6em;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wrapper {
|
||||||
|
width: 80%;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
blockquote {
|
||||||
|
border-left: 1px solid #ffffff;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0 0 0 20px;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
code, pre {
|
||||||
|
font-family: "Liberation Mono", Monaco, Bitstream Vera Sans Mono, Lucida Console, Terminal;
|
||||||
|
color: #222222;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre {
|
||||||
|
padding: 8px 15px;
|
||||||
|
border-radius: 5px;
|
||||||
|
border: 1px solid #e5e5e5;
|
||||||
|
overflow-x: auto;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
input, select{
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
|
||||||
|
th, td {
|
||||||
|
padding: 5px 10px;
|
||||||
|
border-bottom: 1px solid #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
td {
|
||||||
|
text-align: justify;
|
||||||
|
}
|
||||||
|
|
||||||
|
dt {
|
||||||
|
color: #444444;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
th {
|
||||||
|
text-align: left;
|
||||||
|
color: #444444;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
header {
|
||||||
|
width: 20%;
|
||||||
|
float: left;
|
||||||
|
position: fixed;
|
||||||
|
}
|
||||||
|
|
||||||
|
header ul {
|
||||||
|
list-style: none;
|
||||||
|
height: 40px;
|
||||||
|
padding: 0;
|
||||||
|
background: #eeeeee;
|
||||||
|
border-radius: 5px;
|
||||||
|
border: 1px solid #d2d2d2;
|
||||||
|
box-shadow: inset #fff 0 1px 0, inset rgba(0,0,0,0.03) 0 -1px 0;
|
||||||
|
width: 15%;
|
||||||
|
}
|
||||||
|
|
||||||
|
header li {
|
||||||
|
width: 8%;
|
||||||
|
float: left;
|
||||||
|
border-right: 1px solid #d2d2d2;
|
||||||
|
height: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
header ul a {
|
||||||
|
line-height: 1;
|
||||||
|
font-size: 11px;
|
||||||
|
color: #999999;
|
||||||
|
display: block;
|
||||||
|
text-align: center;
|
||||||
|
padding-top: 6px;
|
||||||
|
height: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
strong {
|
||||||
|
color: #222222;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
header ul li + li {
|
||||||
|
width: 8%;
|
||||||
|
border-left: 1px solid #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
header ul li + li + li {
|
||||||
|
width: 8%;
|
||||||
|
border-right: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
header ul a strong {
|
||||||
|
font-size: 14px;
|
||||||
|
display: block;
|
||||||
|
color: #222222;
|
||||||
|
}
|
||||||
|
|
||||||
|
section {
|
||||||
|
width: 70%;
|
||||||
|
float: right;
|
||||||
|
padding-bottom: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
small {
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
hr {
|
||||||
|
border: 0;
|
||||||
|
background: #ffffff;
|
||||||
|
height: 1px;
|
||||||
|
margin: 0 0 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
footer {
|
||||||
|
width: 20%;
|
||||||
|
float: left;
|
||||||
|
position: fixed;
|
||||||
|
bottom: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media print, screen and (max-width: 960px) {
|
||||||
|
div.wrapper {
|
||||||
|
width: auto;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
header, section, footer {
|
||||||
|
float: none;
|
||||||
|
position: static;
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
header {
|
||||||
|
padding-right: 320px;
|
||||||
|
}
|
||||||
|
section {
|
||||||
|
border: 1px solid #e5e5e5;
|
||||||
|
border-width: 1px 0;
|
||||||
|
padding: 20px 0;
|
||||||
|
margin: 0 0 20px;
|
||||||
|
}
|
||||||
|
header a small {
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
header ul {
|
||||||
|
position: absolute;
|
||||||
|
right: 50px;
|
||||||
|
top: 52px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media print, screen and (max-width: 720px) {
|
||||||
|
body {
|
||||||
|
word-wrap: break-word;
|
||||||
|
}
|
||||||
|
header {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
header ul, header p.view {
|
||||||
|
position: static;
|
||||||
|
}
|
||||||
|
pre, code {
|
||||||
|
word-wrap: normal;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media print, screen and (max-width: 480px) {
|
||||||
|
body {
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
header ul {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media print {
|
||||||
|
body {
|
||||||
|
padding: 0.4in;
|
||||||
|
font-size: 12pt;
|
||||||
|
color: #444444;
|
||||||
|
}
|
||||||
|
}
|
@ -17,7 +17,7 @@ function exportTableToCsv(filename) {
|
|||||||
var csv = [];
|
var csv = [];
|
||||||
for (var i = 0; i < rows.length; i++) {
|
for (var i = 0; i < rows.length; i++) {
|
||||||
if (rows[i].style.display === "none")
|
if (rows[i].style.display === "none")
|
||||||
continue
|
continue;
|
||||||
var cols = rows[i].querySelectorAll("td, th");
|
var cols = rows[i].querySelectorAll("td, th");
|
||||||
|
|
||||||
var row = [];
|
var row = [];
|
||||||
@ -28,4 +28,4 @@ function exportTableToCsv(filename) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
downloadCsv(csv.join("\n"), filename);
|
downloadCsv(csv.join("\n"), filename);
|
||||||
}
|
}
|
@ -18,4 +18,4 @@ function searchTable() {
|
|||||||
}
|
}
|
||||||
tr[i].style.display = display;
|
tr[i].style.display = display;
|
||||||
}
|
}
|
||||||
}
|
}
|
24
src/main/resources/swagger-info/description.md
Normal file
24
src/main/resources/swagger-info/description.md
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
REST json API description to interact with FFXIVBiS service.
|
||||||
|
|
||||||
|
# Basic workflow
|
||||||
|
|
||||||
|
* Create party using `PUT /api/v1/party` endpoint. It consumes username and password of administrator (which can't be restored). As the result it returns unique id of created party.
|
||||||
|
* Create new users which have access to this party. Note that user belongs to specific party id and in scope of the specified party it must be unique.
|
||||||
|
* Add players with their best in slot sets (probably by using ariyala links).
|
||||||
|
* Add loot items if any.
|
||||||
|
* By using `PUT /api/v1/party/{partyId}/loot` API find players which are better for the specified loot.
|
||||||
|
* Add new loot item to the selected player.
|
||||||
|
|
||||||
|
# Limitations
|
||||||
|
|
||||||
|
# Authentication
|
||||||
|
|
||||||
|
For the most party utils service requires user to be authenticated. User permission can be one of `get`, `post` or `admin`.
|
||||||
|
|
||||||
|
* `admin` permission means that the user is allowed to do anything, especially this permission is required to be able to add or modify users.
|
||||||
|
* `post` permission is required to deal with the most POST API endpoints, but to be precise only endpoints which modifies party content require this permission.
|
||||||
|
* `get` permission is required to have access to party.
|
||||||
|
|
||||||
|
`admin` permission includes any other permissions, `post` allows to perform get requests.
|
||||||
|
|
||||||
|
<security-definitions />
|
@ -1,6 +1,6 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<title>ReDoc</title>
|
<title>ReDoc</title>
|
||||||
<!-- needed for adaptive design -->
|
<!-- needed for adaptive design -->
|
||||||
<meta charset="utf-8"/>
|
<meta charset="utf-8"/>
|
||||||
@ -11,14 +11,14 @@
|
|||||||
ReDoc doesn't change outer page styles
|
ReDoc doesn't change outer page styles
|
||||||
-->
|
-->
|
||||||
<style>
|
<style>
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<redoc spec-url='/api-docs/swagger.json'></redoc>
|
<redoc spec-url='/api-docs/swagger.json'></redoc>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/redoc@next/bundles/redoc.standalone.js"> </script>
|
<script src="https://cdn.jsdelivr.net/npm/redoc@next/bundles/redoc.standalone.js"> </script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
52
src/main/scala/me/arcanis/ffxivbis/Application.scala
Normal file
52
src/main/scala/me/arcanis/ffxivbis/Application.scala
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
/*
|
||||||
|
* 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
|
||||||
|
|
||||||
|
import akka.actor.{Actor, Props}
|
||||||
|
import akka.http.scaladsl.Http
|
||||||
|
import akka.stream.ActorMaterializer
|
||||||
|
import com.typesafe.scalalogging.StrictLogging
|
||||||
|
import me.arcanis.ffxivbis.http.RootEndpoint
|
||||||
|
import me.arcanis.ffxivbis.service.impl.DatabaseImpl
|
||||||
|
import me.arcanis.ffxivbis.service.{Ariyala, PartyService}
|
||||||
|
import me.arcanis.ffxivbis.storage.Migration
|
||||||
|
|
||||||
|
import scala.concurrent.duration.Duration
|
||||||
|
import scala.concurrent.{Await, ExecutionContext}
|
||||||
|
import scala.util.{Failure, Success}
|
||||||
|
|
||||||
|
class Application extends Actor with StrictLogging {
|
||||||
|
implicit private val executionContext: ExecutionContext = context.system.dispatcher
|
||||||
|
implicit private val materializer: ActorMaterializer = ActorMaterializer()
|
||||||
|
|
||||||
|
private val config = context.system.settings.config
|
||||||
|
private val host = config.getString("me.arcanis.ffxivbis.web.host")
|
||||||
|
private val port = config.getInt("me.arcanis.ffxivbis.web.port")
|
||||||
|
|
||||||
|
override def receive: Receive = Actor.emptyBehavior
|
||||||
|
|
||||||
|
Migration(config).onComplete {
|
||||||
|
case Success(_) =>
|
||||||
|
val ariyala = context.system.actorOf(Ariyala.props, "ariyala")
|
||||||
|
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")
|
||||||
|
val bind = Http()(context.system).bindAndHandle(http.route, host, port)
|
||||||
|
Await.result(context.system.whenTerminated, Duration.Inf)
|
||||||
|
bind.foreach(_.unbind())
|
||||||
|
|
||||||
|
case Failure(exception) => throw exception
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
object Application {
|
||||||
|
def props: Props = Props(new Application)
|
||||||
|
}
|
20
src/main/scala/me/arcanis/ffxivbis/ffxivbis.scala
Normal file
20
src/main/scala/me/arcanis/ffxivbis/ffxivbis.scala
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
/*
|
||||||
|
* 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
|
||||||
|
|
||||||
|
import akka.actor.ActorSystem
|
||||||
|
import com.typesafe.config.ConfigFactory
|
||||||
|
|
||||||
|
object ffxivbis {
|
||||||
|
def main(args: Array[String]): Unit = {
|
||||||
|
val config = ConfigFactory.load()
|
||||||
|
val actorSystem = ActorSystem("ffxivbis", config)
|
||||||
|
actorSystem.actorOf(Application.props, "ffxivbis")
|
||||||
|
}
|
||||||
|
}
|
24
src/main/scala/me/arcanis/ffxivbis/http/AriyalaHelper.scala
Normal file
24
src/main/scala/me/arcanis/ffxivbis/http/AriyalaHelper.scala
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
/*
|
||||||
|
* 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]
|
||||||
|
}
|
61
src/main/scala/me/arcanis/ffxivbis/http/Authorization.scala
Normal file
61
src/main/scala/me/arcanis/ffxivbis/http/Authorization.scala
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
/*
|
||||||
|
* 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.http.scaladsl.model.headers._
|
||||||
|
import akka.http.scaladsl.server.AuthenticationFailedRejection._
|
||||||
|
import akka.http.scaladsl.server.Directives._
|
||||||
|
import akka.http.scaladsl.server._
|
||||||
|
import akka.pattern.ask
|
||||||
|
import akka.util.Timeout
|
||||||
|
import me.arcanis.ffxivbis.models.{Permission, User}
|
||||||
|
import me.arcanis.ffxivbis.service.impl.DatabaseUserHandler
|
||||||
|
|
||||||
|
import scala.concurrent.{ExecutionContext, Future}
|
||||||
|
|
||||||
|
// idea comes from https://synkre.com/bcrypt-for-akka-http-password-encryption/
|
||||||
|
trait Authorization {
|
||||||
|
|
||||||
|
def storage: ActorRef
|
||||||
|
|
||||||
|
def authenticateBasicBCrypt[T](realm: String,
|
||||||
|
authenticate: (String, String) => Future[Option[T]]): Directive1[T] = {
|
||||||
|
def challenge = HttpChallenges.basic(realm)
|
||||||
|
|
||||||
|
extractCredentials.flatMap {
|
||||||
|
case Some(BasicHttpCredentials(username, password)) =>
|
||||||
|
onSuccess(authenticate(username, password)).flatMap {
|
||||||
|
case Some(client) => provide(client)
|
||||||
|
case None => reject(AuthenticationFailedRejection(CredentialsRejected, challenge))
|
||||||
|
}
|
||||||
|
case _ => reject(AuthenticationFailedRejection(CredentialsMissing, challenge))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def authenticator(scope: Permission.Value)(partyId: String)
|
||||||
|
(username: String, password: String)
|
||||||
|
(implicit executionContext: ExecutionContext, timeout: Timeout): Future[Option[String]] =
|
||||||
|
(storage ? DatabaseUserHandler.GetUser(partyId, username)).mapTo[Option[User]].map {
|
||||||
|
case Some(user) if user.verify(password) && user.verityScope(scope) => Some(username)
|
||||||
|
case _ => None
|
||||||
|
}
|
||||||
|
|
||||||
|
def authAdmin(partyId: String)(username: String, password: String)
|
||||||
|
(implicit executionContext: ExecutionContext, timeout: Timeout): Future[Option[String]] =
|
||||||
|
authenticator(Permission.admin)(partyId)(username, password)
|
||||||
|
|
||||||
|
def authGet(partyId: String)(username: String, password: String)
|
||||||
|
(implicit executionContext: ExecutionContext, timeout: Timeout): Future[Option[String]] =
|
||||||
|
authenticator(Permission.get)(partyId)(username, password)
|
||||||
|
|
||||||
|
def authPost(partyId: String)(username: String, password: String)
|
||||||
|
(implicit executionContext: ExecutionContext, timeout: Timeout): Future[Option[String]] =
|
||||||
|
authenticator(Permission.post)(partyId)(username, password)
|
||||||
|
}
|
47
src/main/scala/me/arcanis/ffxivbis/http/BiSHelper.scala
Normal file
47
src/main/scala/me/arcanis/ffxivbis/http/BiSHelper.scala
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
/*
|
||||||
|
* 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.http.api.v1.json.ApiAction
|
||||||
|
import me.arcanis.ffxivbis.models.{Piece, Player, PlayerId}
|
||||||
|
import me.arcanis.ffxivbis.service.impl.DatabaseBiSHandler
|
||||||
|
|
||||||
|
import scala.concurrent.{ExecutionContext, Future}
|
||||||
|
|
||||||
|
class BiSHelper(storage: ActorRef, ariyala: ActorRef) extends AriyalaHelper(ariyala) {
|
||||||
|
|
||||||
|
def addPieceBiS(playerId: PlayerId, piece: Piece)
|
||||||
|
(implicit executionContext: ExecutionContext, timeout: Timeout): Future[Int] =
|
||||||
|
(storage ? DatabaseBiSHandler.AddPieceToBis(playerId, piece.withJob(playerId.job))).mapTo[Int]
|
||||||
|
|
||||||
|
def bis(partyId: String, playerId: Option[PlayerId])
|
||||||
|
(implicit executionContext: ExecutionContext, timeout: Timeout): Future[Seq[Player]] =
|
||||||
|
(storage ? DatabaseBiSHandler.GetBiS(partyId, playerId)).mapTo[Seq[Player]]
|
||||||
|
|
||||||
|
def doModifyBiS(action: ApiAction.Value, playerId: PlayerId, piece: Piece)
|
||||||
|
(implicit executionContext: ExecutionContext, timeout: Timeout): Future[Int] =
|
||||||
|
action match {
|
||||||
|
case ApiAction.add => addPieceBiS(playerId, piece)
|
||||||
|
case ApiAction.remove => removePieceBiS(playerId, piece)
|
||||||
|
}
|
||||||
|
|
||||||
|
def putBiS(playerId: PlayerId, link: String)
|
||||||
|
(implicit executionContext: ExecutionContext, timeout: Timeout): Future[Unit] =
|
||||||
|
downloadBiS(link, playerId.job).flatMap { bis =>
|
||||||
|
Future.traverse(bis.pieces)(addPieceBiS(playerId, _))
|
||||||
|
}.map(_ => ())
|
||||||
|
|
||||||
|
def removePieceBiS(playerId: PlayerId, piece: Piece)
|
||||||
|
(implicit executionContext: ExecutionContext, timeout: Timeout): Future[Int] =
|
||||||
|
(storage ? DatabaseBiSHandler.RemovePieceFromBiS(playerId, piece)).mapTo[Int]
|
||||||
|
|
||||||
|
}
|
45
src/main/scala/me/arcanis/ffxivbis/http/LootHelper.scala
Normal file
45
src/main/scala/me/arcanis/ffxivbis/http/LootHelper.scala
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
/*
|
||||||
|
* 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.http.api.v1.json.ApiAction
|
||||||
|
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}
|
||||||
|
|
||||||
|
class LootHelper(storage: ActorRef) {
|
||||||
|
|
||||||
|
def addPieceLoot(playerId: PlayerId, piece: Piece)
|
||||||
|
(implicit executionContext: ExecutionContext, timeout: Timeout): Future[Int] =
|
||||||
|
(storage ? DatabaseLootHandler.AddPieceTo(playerId, piece)).mapTo[Int]
|
||||||
|
|
||||||
|
def doModifyLoot(action: ApiAction.Value, playerId: PlayerId, piece: Piece)
|
||||||
|
(implicit executionContext: ExecutionContext, timeout: Timeout): Future[Int] =
|
||||||
|
action match {
|
||||||
|
case ApiAction.add => addPieceLoot(playerId, piece)
|
||||||
|
case ApiAction.remove => removePieceLoot(playerId, piece)
|
||||||
|
}
|
||||||
|
|
||||||
|
def loot(partyId: String, playerId: Option[PlayerId])
|
||||||
|
(implicit executionContext: ExecutionContext, timeout: Timeout): Future[Seq[Player]] =
|
||||||
|
(storage ? DatabaseLootHandler.GetLoot(partyId, playerId)).mapTo[Seq[Player]]
|
||||||
|
|
||||||
|
def removePieceLoot(playerId: PlayerId, piece: Piece)
|
||||||
|
(implicit executionContext: ExecutionContext, timeout: Timeout): Future[Int] =
|
||||||
|
(storage ? DatabaseLootHandler.RemovePieceFrom(playerId, piece)).mapTo[Int]
|
||||||
|
|
||||||
|
def suggestPiece(partyId: String, piece: Piece)
|
||||||
|
(implicit executionContext: ExecutionContext, timeout: Timeout): Future[Seq[PlayerIdWithCounters]] =
|
||||||
|
(storage ? DatabaseLootHandler.SuggestLoot(partyId, piece)).mapTo[LootSelectorResult].map(_.result)
|
||||||
|
}
|
53
src/main/scala/me/arcanis/ffxivbis/http/PlayerHelper.scala
Normal file
53
src/main/scala/me/arcanis/ffxivbis/http/PlayerHelper.scala
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
/*
|
||||||
|
* 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.http.api.v1.json.ApiAction
|
||||||
|
import me.arcanis.ffxivbis.models.{Party, Player, PlayerId}
|
||||||
|
import me.arcanis.ffxivbis.service.impl.{DatabaseBiSHandler, DatabasePartyHandler}
|
||||||
|
|
||||||
|
import scala.concurrent.{ExecutionContext, Future}
|
||||||
|
|
||||||
|
class PlayerHelper(storage: ActorRef, ariyala: ActorRef) extends AriyalaHelper(ariyala) {
|
||||||
|
|
||||||
|
def addPlayer(player: Player)
|
||||||
|
(implicit executionContext: ExecutionContext, timeout: Timeout): Future[Int] =
|
||||||
|
(storage ? DatabasePartyHandler.AddPlayer(player)).mapTo[Int].map { res =>
|
||||||
|
player.link match {
|
||||||
|
case Some(link) =>
|
||||||
|
downloadBiS(link, player.job).map { bis =>
|
||||||
|
bis.pieces.map(storage ? DatabaseBiSHandler.AddPieceToBis(player.playerId, _))
|
||||||
|
}.map(_ => res)
|
||||||
|
case None => Future.successful(res)
|
||||||
|
}
|
||||||
|
}.flatten
|
||||||
|
|
||||||
|
def doModifyPlayer(action: ApiAction.Value, player: Player)
|
||||||
|
(implicit executionContext: ExecutionContext, timeout: Timeout): Future[Int] =
|
||||||
|
action match {
|
||||||
|
case ApiAction.add => addPlayer(player)
|
||||||
|
case ApiAction.remove => removePlayer(player.playerId)
|
||||||
|
}
|
||||||
|
|
||||||
|
def getPlayers(partyId: String, maybePlayerId: Option[PlayerId])
|
||||||
|
(implicit executionContext: ExecutionContext, timeout: Timeout): Future[Seq[Player]] =
|
||||||
|
maybePlayerId match {
|
||||||
|
case Some(playerId) =>
|
||||||
|
(storage ? DatabasePartyHandler.GetPlayer(playerId)).mapTo[Option[Player]].map(_.toSeq)
|
||||||
|
case None =>
|
||||||
|
(storage ? DatabasePartyHandler.GetParty(partyId)).mapTo[Party].map(_.players.values.toSeq)
|
||||||
|
}
|
||||||
|
|
||||||
|
def removePlayer(playerId: PlayerId)
|
||||||
|
(implicit executionContext: ExecutionContext, timeout: Timeout): Future[Int] =
|
||||||
|
(storage ? DatabasePartyHandler.RemovePlayer(playerId)).mapTo[Int]
|
||||||
|
}
|
71
src/main/scala/me/arcanis/ffxivbis/http/RootEndpoint.scala
Normal file
71
src/main/scala/me/arcanis/ffxivbis/http/RootEndpoint.scala
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
/*
|
||||||
|
* 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 java.time.Instant
|
||||||
|
|
||||||
|
import akka.actor.{ActorRef, ActorSystem}
|
||||||
|
import akka.http.scaladsl.server.Directives._
|
||||||
|
import akka.http.scaladsl.server._
|
||||||
|
import akka.util.Timeout
|
||||||
|
import com.typesafe.scalalogging.{Logger, StrictLogging}
|
||||||
|
import me.arcanis.ffxivbis.http.api.v1.RootApiV1Endpoint
|
||||||
|
import me.arcanis.ffxivbis.http.view.RootView
|
||||||
|
|
||||||
|
class RootEndpoint(system: ActorSystem, storage: ActorRef, ariyala: ActorRef)
|
||||||
|
extends StrictLogging {
|
||||||
|
import me.arcanis.ffxivbis.utils.Implicits._
|
||||||
|
|
||||||
|
private val config = system.settings.config
|
||||||
|
|
||||||
|
implicit val timeout: Timeout =
|
||||||
|
config.getDuration("me.arcanis.ffxivbis.settings.request-timeout")
|
||||||
|
|
||||||
|
private val rootApiV1Endpoint: RootApiV1Endpoint = new RootApiV1Endpoint(storage, ariyala, config)
|
||||||
|
private val rootView: RootView = new RootView(storage, ariyala)
|
||||||
|
private val swagger: Swagger = new Swagger(config)
|
||||||
|
private val httpLogger = Logger("http")
|
||||||
|
|
||||||
|
private val withHttpLog: Directive0 =
|
||||||
|
extractRequestContext.flatMap { context =>
|
||||||
|
val start = Instant.now.toEpochMilli
|
||||||
|
mapResponse { response =>
|
||||||
|
val time = (Instant.now.toEpochMilli - start) / 1000.0
|
||||||
|
httpLogger.debug(s"""- - [${Instant.now}] "${context.request.method.name()} ${context.request.uri.path}" ${response.status.intValue()} ${response.entity.getContentLengthOption.getAsLong} $time""")
|
||||||
|
response
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def route: Route =
|
||||||
|
withHttpLog {
|
||||||
|
apiRoute ~ htmlRoute ~ swagger.routes ~ swaggerUIRoute
|
||||||
|
}
|
||||||
|
|
||||||
|
private def apiRoute: Route =
|
||||||
|
ignoreTrailingSlash {
|
||||||
|
pathPrefix("api") {
|
||||||
|
pathPrefix(Segment) {
|
||||||
|
case "v1" => rootApiV1Endpoint.route
|
||||||
|
case _ => reject
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private def htmlRoute: Route =
|
||||||
|
ignoreTrailingSlash {
|
||||||
|
pathPrefix("static") {
|
||||||
|
getFromResourceDirectory("static")
|
||||||
|
} ~ rootView.route
|
||||||
|
}
|
||||||
|
|
||||||
|
private def swaggerUIRoute: Route =
|
||||||
|
path("swagger") {
|
||||||
|
getFromResource("swagger/index.html")
|
||||||
|
} ~ getFromResourceDirectory("swagger")
|
||||||
|
}
|
45
src/main/scala/me/arcanis/ffxivbis/http/Swagger.scala
Normal file
45
src/main/scala/me/arcanis/ffxivbis/http/Swagger.scala
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
/*
|
||||||
|
* 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 com.github.swagger.akka.SwaggerHttpService
|
||||||
|
import com.github.swagger.akka.model.{Info, License}
|
||||||
|
import com.typesafe.config.Config
|
||||||
|
import io.swagger.v3.oas.models.security.SecurityScheme
|
||||||
|
|
||||||
|
import scala.io.Source
|
||||||
|
|
||||||
|
class Swagger(config: Config) extends SwaggerHttpService {
|
||||||
|
override val apiClasses: Set[Class[_]] = Set(
|
||||||
|
classOf[api.v1.BiSEndpoint], classOf[api.v1.LootEndpoint],
|
||||||
|
classOf[api.v1.PlayerEndpoint], classOf[api.v1.TypesEndpoint],
|
||||||
|
classOf[api.v1.UserEndpoint]
|
||||||
|
)
|
||||||
|
|
||||||
|
override val info: Info = Info(
|
||||||
|
description = Source.fromResource("swagger-info/description.md").mkString,
|
||||||
|
version = getClass.getPackage.getImplementationVersion,
|
||||||
|
title = "FFXIV static loot tracker",
|
||||||
|
license = Some(License("BSD", "https://raw.githubusercontent.com/arcan1s/ffxivbis/master/LICENSE"))
|
||||||
|
)
|
||||||
|
|
||||||
|
override val host: String =
|
||||||
|
if (config.hasPath("me.arcanis.ffxivbis.web.hostname")) config.getString("me.arcanis.ffxivbis.web.hostname")
|
||||||
|
else s"${config.getString("me.arcanis.ffxivbis.web.host")}:${config.getString("me.arcanis.ffxivbis.web.port")}"
|
||||||
|
|
||||||
|
private val basicAuth = new SecurityScheme()
|
||||||
|
.description("basic http auth")
|
||||||
|
.`type`(SecurityScheme.Type.HTTP)
|
||||||
|
.in(SecurityScheme.In.HEADER)
|
||||||
|
.scheme("bearer")
|
||||||
|
override val securitySchemes: Map[String, SecurityScheme] = Map("basic auth" -> basicAuth)
|
||||||
|
|
||||||
|
override val unwantedDefinitions: Seq[String] =
|
||||||
|
Seq("Function1", "Function1RequestContextFutureRouteResult")
|
||||||
|
}
|
40
src/main/scala/me/arcanis/ffxivbis/http/UserHelper.scala
Normal file
40
src/main/scala/me/arcanis/ffxivbis/http/UserHelper.scala
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
/*
|
||||||
|
* 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.User
|
||||||
|
import me.arcanis.ffxivbis.service.PartyService
|
||||||
|
import me.arcanis.ffxivbis.service.impl.DatabaseUserHandler
|
||||||
|
|
||||||
|
import scala.concurrent.{ExecutionContext, Future}
|
||||||
|
|
||||||
|
class UserHelper(storage: ActorRef) {
|
||||||
|
|
||||||
|
def addUser(user: User, isHashedPassword: Boolean)
|
||||||
|
(implicit executionContext: ExecutionContext, timeout: Timeout): Future[Int] =
|
||||||
|
(storage ? DatabaseUserHandler.AddUser(user, isHashedPassword)).mapTo[Int]
|
||||||
|
|
||||||
|
def newPartyId(implicit executionContext: ExecutionContext, timeout: Timeout): Future[String] =
|
||||||
|
(storage ? PartyService.GetNewPartyId).mapTo[String]
|
||||||
|
|
||||||
|
def user(partyId: String, username: String)
|
||||||
|
(implicit executionContext: ExecutionContext, timeout: Timeout): Future[Option[User]] =
|
||||||
|
(storage ? DatabaseUserHandler.GetUser(partyId, username)).mapTo[Option[User]]
|
||||||
|
|
||||||
|
def users(partyId: String)
|
||||||
|
(implicit executionContext: ExecutionContext, timeout: Timeout): Future[Seq[User]] =
|
||||||
|
(storage ? DatabaseUserHandler.GetUsers(partyId)).mapTo[Seq[User]]
|
||||||
|
|
||||||
|
def removeUser(partyId: String, username: String)
|
||||||
|
(implicit executionContext: ExecutionContext, timeout: Timeout): Future[Int] =
|
||||||
|
(storage ? DatabaseUserHandler.DeleteUser(partyId, username)).mapTo[Int]
|
||||||
|
}
|
157
src/main/scala/me/arcanis/ffxivbis/http/api/v1/BiSEndpoint.scala
Normal file
157
src/main/scala/me/arcanis/ffxivbis/http/api/v1/BiSEndpoint.scala
Normal file
@ -0,0 +1,157 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2019 Evgeniy Alekseev.
|
||||||
|
*
|
||||||
|
* This file is part of ffxivbis
|
||||||
|
* (see https://github.com/arcan1s/ffxivbis).
|
||||||
|
*
|
||||||
|
* License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause
|
||||||
|
*/
|
||||||
|
package me.arcanis.ffxivbis.http.api.v1
|
||||||
|
|
||||||
|
import akka.actor.ActorRef
|
||||||
|
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.{ArraySchema, 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 javax.ws.rs._
|
||||||
|
import me.arcanis.ffxivbis.http.api.v1.json._
|
||||||
|
import me.arcanis.ffxivbis.http.{Authorization, BiSHelper}
|
||||||
|
import me.arcanis.ffxivbis.models.PlayerId
|
||||||
|
|
||||||
|
import scala.util.{Failure, Success}
|
||||||
|
|
||||||
|
@Path("api/v1")
|
||||||
|
class BiSEndpoint(override val storage: ActorRef, ariyala: ActorRef)(implicit timeout: Timeout)
|
||||||
|
extends BiSHelper(storage, ariyala) with Authorization with JsonSupport {
|
||||||
|
|
||||||
|
def route: Route = createBiS ~ getBiS ~ modifyBiS
|
||||||
|
|
||||||
|
@PUT
|
||||||
|
@Path("party/{partyId}/bis")
|
||||||
|
@Consumes(value = Array("application/json"))
|
||||||
|
@Operation(summary = "create best in slot", description = "Create the best in slot set",
|
||||||
|
parameters = Array(
|
||||||
|
new Parameter(name = "partyId", in = ParameterIn.PATH, description = "unique party ID", example = "abcdefgh"),
|
||||||
|
),
|
||||||
|
requestBody = new RequestBody(description = "player best in slot description", required = true,
|
||||||
|
content = Array(new Content(schema = new Schema(implementation = classOf[PlayerBiSLinkResponse])))),
|
||||||
|
responses = Array(
|
||||||
|
new ApiResponse(responseCode = "201", description = "Best in slot set has been created"),
|
||||||
|
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("best in slot"),
|
||||||
|
)
|
||||||
|
def createBiS: Route =
|
||||||
|
path("party" / Segment / "bis") { partyId =>
|
||||||
|
extractExecutionContext { implicit executionContext =>
|
||||||
|
authenticateBasicBCrypt(s"party $partyId", authPost(partyId)) { _ =>
|
||||||
|
put {
|
||||||
|
entity(as[PlayerBiSLinkResponse]) { bisLink =>
|
||||||
|
val playerId = bisLink.playerId.withPartyId(partyId)
|
||||||
|
onComplete(putBiS(playerId, bisLink.link)) {
|
||||||
|
case Success(_) => complete(StatusCodes.Created, HttpEntity.Empty)
|
||||||
|
case Failure(exception) => throw exception
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("party/{partyId}/bis")
|
||||||
|
@Produces(value = Array("application/json"))
|
||||||
|
@Operation(summary = "get best in slot", description = "Return the best in slot items",
|
||||||
|
parameters = Array(
|
||||||
|
new Parameter(name = "partyId", in = ParameterIn.PATH, description = "unique party ID", example = "abcdefgh"),
|
||||||
|
new Parameter(name = "nick", in = ParameterIn.QUERY, description = "player nick name to filter", example = "Siuan Sanche"),
|
||||||
|
new Parameter(name = "job", in = ParameterIn.QUERY, description = "player job to filter", example = "DNC"),
|
||||||
|
),
|
||||||
|
responses = Array(
|
||||||
|
new ApiResponse(responseCode = "200", description = "Best in slot",
|
||||||
|
content = Array(new Content(
|
||||||
|
array = new ArraySchema(schema = new Schema(implementation = classOf[PlayerResponse]))
|
||||||
|
))),
|
||||||
|
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("best in slot"),
|
||||||
|
)
|
||||||
|
def getBiS: Route =
|
||||||
|
path("party" / Segment / "bis") { partyId =>
|
||||||
|
extractExecutionContext { implicit executionContext =>
|
||||||
|
authenticateBasicBCrypt(s"party $partyId", authGet(partyId)) { _ =>
|
||||||
|
get {
|
||||||
|
parameters("nick".as[String].?, "job".as[String].?) { (maybeNick, maybeJob) =>
|
||||||
|
val playerId = PlayerId(partyId, maybeNick, maybeJob)
|
||||||
|
onComplete(bis(partyId, playerId)) {
|
||||||
|
case Success(response) => complete(response.map(PlayerResponse.fromPlayer))
|
||||||
|
case Failure(exception) => throw exception
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Path("party/{partyId}/bis")
|
||||||
|
@Consumes(value = Array("application/json"))
|
||||||
|
@Operation(summary = "modify best in slot", description = "Add or remove an item from the best in slot",
|
||||||
|
parameters = Array(
|
||||||
|
new Parameter(name = "partyId", in = ParameterIn.PATH, description = "unique party ID", example = "abcdefgh"),
|
||||||
|
),
|
||||||
|
requestBody = new RequestBody(description = "action and piece description", required = true,
|
||||||
|
content = Array(new Content(schema = new Schema(implementation = classOf[PieceActionResponse])))),
|
||||||
|
responses = Array(
|
||||||
|
new ApiResponse(responseCode = "202", description = "Best in slot set 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("best in slot"),
|
||||||
|
)
|
||||||
|
def modifyBiS: Route =
|
||||||
|
path("party" / Segment / "bis") { partyId =>
|
||||||
|
extractExecutionContext { implicit executionContext =>
|
||||||
|
authenticateBasicBCrypt(s"party $partyId", authPost(partyId)) { _ =>
|
||||||
|
post {
|
||||||
|
entity(as[PieceActionResponse]) { action =>
|
||||||
|
val playerId = action.playerId.withPartyId(partyId)
|
||||||
|
onComplete(doModifyBiS(action.action, playerId, action.piece.toPiece)) {
|
||||||
|
case Success(_) => complete(StatusCodes.Accepted, HttpEntity.Empty)
|
||||||
|
case Failure(exception) => throw exception
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,37 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2019 Evgeniy Alekseev.
|
||||||
|
*
|
||||||
|
* This file is part of ffxivbis
|
||||||
|
* (see https://github.com/arcan1s/ffxivbis).
|
||||||
|
*
|
||||||
|
* License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause
|
||||||
|
*/
|
||||||
|
package me.arcanis.ffxivbis.http.api.v1
|
||||||
|
|
||||||
|
import akka.http.scaladsl.model._
|
||||||
|
import akka.http.scaladsl.server.Directives._
|
||||||
|
import akka.http.scaladsl.server._
|
||||||
|
import com.typesafe.scalalogging.StrictLogging
|
||||||
|
import me.arcanis.ffxivbis.http.api.v1.json._
|
||||||
|
import spray.json._
|
||||||
|
|
||||||
|
trait HttpHandler extends StrictLogging { this: JsonSupport =>
|
||||||
|
|
||||||
|
implicit def exceptionHandler: ExceptionHandler = ExceptionHandler {
|
||||||
|
case ex: IllegalArgumentException =>
|
||||||
|
complete(StatusCodes.BadRequest, ErrorResponse(ex.getMessage))
|
||||||
|
|
||||||
|
case other: Exception =>
|
||||||
|
logger.error("exception during request completion", other)
|
||||||
|
complete(StatusCodes.InternalServerError, ErrorResponse("unknown server error"))
|
||||||
|
}
|
||||||
|
|
||||||
|
implicit def rejectionHandler: RejectionHandler =
|
||||||
|
RejectionHandler.default
|
||||||
|
.mapRejectionResponse {
|
||||||
|
case response @ HttpResponse(_, _, entity: HttpEntity.Strict, _) =>
|
||||||
|
val message = ErrorResponse(entity.data.utf8String).toJson
|
||||||
|
response.copy(entity = HttpEntity(ContentTypes.`application/json`, message.compactPrint))
|
||||||
|
case other => other
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,158 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2019 Evgeniy Alekseev.
|
||||||
|
*
|
||||||
|
* This file is part of ffxivbis
|
||||||
|
* (see https://github.com/arcan1s/ffxivbis).
|
||||||
|
*
|
||||||
|
* License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause
|
||||||
|
*/
|
||||||
|
package me.arcanis.ffxivbis.http.api.v1
|
||||||
|
|
||||||
|
import akka.actor.ActorRef
|
||||||
|
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.{ArraySchema, 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 javax.ws.rs._
|
||||||
|
import me.arcanis.ffxivbis.http.api.v1.json._
|
||||||
|
import me.arcanis.ffxivbis.http.{Authorization, LootHelper}
|
||||||
|
import me.arcanis.ffxivbis.models.PlayerId
|
||||||
|
|
||||||
|
import scala.util.{Failure, Success}
|
||||||
|
|
||||||
|
@Path("api/v1")
|
||||||
|
class LootEndpoint(override val storage: ActorRef)(implicit timeout: Timeout)
|
||||||
|
extends LootHelper(storage) with Authorization with JsonSupport with HttpHandler {
|
||||||
|
|
||||||
|
def route: Route = getLoot ~ modifyLoot
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("party/{partyId}/loot")
|
||||||
|
@Produces(value = Array("application/json"))
|
||||||
|
@Operation(summary = "get loot list", description = "Return the looted items",
|
||||||
|
parameters = Array(
|
||||||
|
new Parameter(name = "partyId", in = ParameterIn.PATH, description = "unique party ID", example = "abcdefgh"),
|
||||||
|
new Parameter(name = "nick", in = ParameterIn.QUERY, description = "player nick name to filter", example = "Siuan Sanche"),
|
||||||
|
new Parameter(name = "job", in = ParameterIn.QUERY, description = "player job to filter", example = "DNC"),
|
||||||
|
),
|
||||||
|
responses = Array(
|
||||||
|
new ApiResponse(responseCode = "200", description = "Loot list",
|
||||||
|
content = Array(new Content(
|
||||||
|
array = new ArraySchema(schema = new Schema(implementation = classOf[PlayerResponse]))
|
||||||
|
))),
|
||||||
|
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("loot"),
|
||||||
|
)
|
||||||
|
def getLoot: Route =
|
||||||
|
path("party" / Segment / "loot") { partyId =>
|
||||||
|
extractExecutionContext { implicit executionContext =>
|
||||||
|
authenticateBasicBCrypt(s"party $partyId", authGet(partyId)) { _ =>
|
||||||
|
get {
|
||||||
|
parameters("nick".as[String].?, "job".as[String].?) { (maybeNick, maybeJob) =>
|
||||||
|
val playerId = PlayerId(partyId, maybeNick, maybeJob)
|
||||||
|
onComplete(loot(partyId, playerId)) {
|
||||||
|
case Success(response) => complete(response.map(PlayerResponse.fromPlayer))
|
||||||
|
case Failure(exception) => throw exception
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Consumes(value = Array("application/json"))
|
||||||
|
@Path("party/{partyId}/loot")
|
||||||
|
@Operation(summary = "modify loot list", description = "Add or remove an item from the loot list",
|
||||||
|
parameters = Array(
|
||||||
|
new Parameter(name = "partyId", in = ParameterIn.PATH, description = "unique party ID", example = "abcdefgh"),
|
||||||
|
),
|
||||||
|
requestBody = new RequestBody(description = "action and piece description", required = true,
|
||||||
|
content = Array(new Content(schema = new Schema(implementation = classOf[PieceActionResponse])))),
|
||||||
|
responses = Array(
|
||||||
|
new ApiResponse(responseCode = "202", description = "Loot list 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("loot"),
|
||||||
|
)
|
||||||
|
def modifyLoot: Route =
|
||||||
|
path("party" / Segment / "loot") { partyId =>
|
||||||
|
extractExecutionContext { implicit executionContext =>
|
||||||
|
authenticateBasicBCrypt(s"party $partyId", authPost(partyId)) { _ =>
|
||||||
|
post {
|
||||||
|
entity(as[PieceActionResponse]) { action =>
|
||||||
|
val playerId = action.playerId.withPartyId(partyId)
|
||||||
|
onComplete(doModifyLoot(action.action, playerId, action.piece.toPiece)) {
|
||||||
|
case Success(_) => complete(StatusCodes.Accepted, HttpEntity.Empty)
|
||||||
|
case Failure(exception) => throw exception
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@PUT
|
||||||
|
@Path("party/{partyId}/loot")
|
||||||
|
@Consumes(value = Array("application/json"))
|
||||||
|
@Produces(value = Array("application/json"))
|
||||||
|
@Operation(summary = "suggest loot", description = "Suggest loot piece to party",
|
||||||
|
parameters = Array(
|
||||||
|
new Parameter(name = "partyId", in = ParameterIn.PATH, description = "unique party ID", example = "abcdefgh"),
|
||||||
|
),
|
||||||
|
requestBody = new RequestBody(description = "piece description", required = true,
|
||||||
|
content = Array(new Content(schema = new Schema(implementation = classOf[PieceResponse])))),
|
||||||
|
responses = Array(
|
||||||
|
new ApiResponse(responseCode = "200", description = "Players with counters ordered by priority to get this item",
|
||||||
|
content = Array(new Content(
|
||||||
|
array = new ArraySchema(schema = new Schema(implementation = classOf[PlayerIdWithCountersResponse])),
|
||||||
|
))),
|
||||||
|
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("get"))),
|
||||||
|
tags = Array("loot"),
|
||||||
|
)
|
||||||
|
def suggestLoot: Route =
|
||||||
|
path("party" / Segment / "loot") { partyId =>
|
||||||
|
extractExecutionContext { implicit executionContext =>
|
||||||
|
authenticateBasicBCrypt(s"party $partyId", authGet(partyId)) { _ =>
|
||||||
|
put {
|
||||||
|
entity(as[PieceResponse]) { piece =>
|
||||||
|
onComplete(suggestPiece(partyId, piece.toPiece)) {
|
||||||
|
case Success(response) => complete(response.map(PlayerIdWithCountersResponse.fromPlayerId))
|
||||||
|
case Failure(exception) => throw exception
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,113 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2019 Evgeniy Alekseev.
|
||||||
|
*
|
||||||
|
* This file is part of ffxivbis
|
||||||
|
* (see https://github.com/arcan1s/ffxivbis).
|
||||||
|
*
|
||||||
|
* License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause
|
||||||
|
*/
|
||||||
|
package me.arcanis.ffxivbis.http.api.v1
|
||||||
|
|
||||||
|
import akka.actor.ActorRef
|
||||||
|
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.{ArraySchema, 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 javax.ws.rs._
|
||||||
|
import me.arcanis.ffxivbis.http.api.v1.json._
|
||||||
|
import me.arcanis.ffxivbis.http.{Authorization, PlayerHelper}
|
||||||
|
import me.arcanis.ffxivbis.models.PlayerId
|
||||||
|
|
||||||
|
import scala.util.{Failure, Success}
|
||||||
|
|
||||||
|
@Path("api/v1")
|
||||||
|
class PlayerEndpoint(override val storage: ActorRef, ariyala: ActorRef)(implicit timeout: Timeout)
|
||||||
|
extends PlayerHelper(storage, ariyala) with Authorization with JsonSupport with HttpHandler {
|
||||||
|
|
||||||
|
def route: Route = getParty ~ modifyParty
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("party/{partyId}")
|
||||||
|
@Produces(value = Array("application/json"))
|
||||||
|
@Operation(summary = "get party", description = "Return the players who belong to the party",
|
||||||
|
parameters = Array(
|
||||||
|
new Parameter(name = "partyId", in = ParameterIn.PATH, description = "unique party ID", example = "abcdefgh"),
|
||||||
|
new Parameter(name = "nick", in = ParameterIn.QUERY, description = "player nick name to filter", example = "Siuan Sanche"),
|
||||||
|
new Parameter(name = "job", in = ParameterIn.QUERY, description = "player job to filter", example = "DNC"),
|
||||||
|
),
|
||||||
|
responses = Array(
|
||||||
|
new ApiResponse(responseCode = "200", description = "Players list",
|
||||||
|
content = Array(new Content(
|
||||||
|
array = new ArraySchema(schema = new Schema(implementation = classOf[PlayerResponse])),
|
||||||
|
))),
|
||||||
|
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 getParty: Route =
|
||||||
|
path("party" / Segment) { partyId =>
|
||||||
|
extractExecutionContext { implicit executionContext =>
|
||||||
|
authenticateBasicBCrypt(s"party $partyId", authGet(partyId)) { _ =>
|
||||||
|
get {
|
||||||
|
parameters("nick".as[String].?, "job".as[String].?) { (maybeNick, maybeJob) =>
|
||||||
|
val playerId = PlayerId(partyId, maybeNick, maybeJob)
|
||||||
|
onComplete(getPlayers(partyId, playerId)) {
|
||||||
|
case Success(response) => complete(response.map(PlayerResponse.fromPlayer))
|
||||||
|
case Failure(exception) => throw exception
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Path("party/{partyId}")
|
||||||
|
@Consumes(value = Array("application/json"))
|
||||||
|
@Operation(summary = "modify party", description = "Add or remove a player from party list",
|
||||||
|
parameters = Array(
|
||||||
|
new Parameter(name = "partyId", in = ParameterIn.PATH, description = "unique party ID", example = "abcdefgh"),
|
||||||
|
),
|
||||||
|
requestBody = new RequestBody(description = "player description", required = true,
|
||||||
|
content = Array(new Content(schema = new Schema(implementation = classOf[PlayerActionResponse])))),
|
||||||
|
responses = Array(
|
||||||
|
new ApiResponse(responseCode = "202", description = "Party 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 modifyParty: Route =
|
||||||
|
path("party" / Segment) { partyId =>
|
||||||
|
extractExecutionContext { implicit executionContext =>
|
||||||
|
authenticateBasicBCrypt(s"party $partyId", authPost(partyId)) { _ =>
|
||||||
|
entity(as[PlayerActionResponse]) { action =>
|
||||||
|
val player = action.playerId.toPlayer.copy(partyId = partyId)
|
||||||
|
onComplete(doModifyPlayer(action.action, player)) {
|
||||||
|
case Success(_) => complete(StatusCodes.Accepted, HttpEntity.Empty)
|
||||||
|
case Failure(exception) => throw exception
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,35 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2019 Evgeniy Alekseev.
|
||||||
|
*
|
||||||
|
* This file is part of ffxivbis
|
||||||
|
* (see https://github.com/arcan1s/ffxivbis).
|
||||||
|
*
|
||||||
|
* License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause
|
||||||
|
*/
|
||||||
|
package me.arcanis.ffxivbis.http.api.v1
|
||||||
|
|
||||||
|
import akka.actor.ActorRef
|
||||||
|
import akka.http.scaladsl.server.Directives._
|
||||||
|
import akka.http.scaladsl.server.Route
|
||||||
|
import akka.util.Timeout
|
||||||
|
import com.typesafe.config.Config
|
||||||
|
import me.arcanis.ffxivbis.http.api.v1.json.JsonSupport
|
||||||
|
|
||||||
|
class RootApiV1Endpoint(storage: ActorRef, ariyala: ActorRef, config: Config)
|
||||||
|
(implicit timeout: Timeout)
|
||||||
|
extends JsonSupport with HttpHandler {
|
||||||
|
|
||||||
|
private val biSEndpoint = new BiSEndpoint(storage, ariyala)
|
||||||
|
private val lootEndpoint = new LootEndpoint(storage)
|
||||||
|
private val playerEndpoint = new PlayerEndpoint(storage, ariyala)
|
||||||
|
private val typesEndpoint = new TypesEndpoint(config)
|
||||||
|
private val userEndpoint = new UserEndpoint(storage)
|
||||||
|
|
||||||
|
def route: Route =
|
||||||
|
handleExceptions(exceptionHandler) {
|
||||||
|
handleRejections(rejectionHandler) {
|
||||||
|
biSEndpoint.route ~ lootEndpoint.route ~ playerEndpoint.route ~
|
||||||
|
typesEndpoint.route ~ userEndpoint.route
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,109 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2019 Evgeniy Alekseev.
|
||||||
|
*
|
||||||
|
* This file is part of ffxivbis
|
||||||
|
* (see https://github.com/arcan1s/ffxivbis).
|
||||||
|
*
|
||||||
|
* License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause
|
||||||
|
*/
|
||||||
|
package me.arcanis.ffxivbis.http.api.v1
|
||||||
|
|
||||||
|
import akka.http.scaladsl.server.Directives._
|
||||||
|
import akka.http.scaladsl.server._
|
||||||
|
import com.typesafe.config.Config
|
||||||
|
import io.swagger.v3.oas.annotations.media.{ArraySchema, Content, Schema}
|
||||||
|
import io.swagger.v3.oas.annotations.responses.ApiResponse
|
||||||
|
import io.swagger.v3.oas.annotations.Operation
|
||||||
|
import javax.ws.rs._
|
||||||
|
import me.arcanis.ffxivbis.http.api.v1.json._
|
||||||
|
import me.arcanis.ffxivbis.models.{Job, Party, Permission, Piece}
|
||||||
|
|
||||||
|
@Path("api/v1")
|
||||||
|
class TypesEndpoint(config: Config) extends JsonSupport {
|
||||||
|
|
||||||
|
def route: Route = getJobs ~ getPermissions ~ getPieces ~ getPriority
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("types/jobs")
|
||||||
|
@Produces(value = Array("application/json"))
|
||||||
|
@Operation(summary = "jobs list", description = "Returns the available jobs",
|
||||||
|
responses = Array(
|
||||||
|
new ApiResponse(responseCode = "200", description = "List of available jobs",
|
||||||
|
content = Array(new Content(
|
||||||
|
array = new ArraySchema(schema = new Schema(implementation = classOf[String]))
|
||||||
|
))),
|
||||||
|
new ApiResponse(responseCode = "500", description = "Internal server error",
|
||||||
|
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))),
|
||||||
|
),
|
||||||
|
tags = Array("types"),
|
||||||
|
)
|
||||||
|
def getJobs: Route =
|
||||||
|
path("types" / "jobs") {
|
||||||
|
get {
|
||||||
|
complete(Job.availableWithAnyJob.map(_.toString))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("types/permissions")
|
||||||
|
@Produces(value = Array("application/json"))
|
||||||
|
@Operation(summary = "permissions list", description = "Returns the available permissions",
|
||||||
|
responses = Array(
|
||||||
|
new ApiResponse(responseCode = "200", description = "List of available permissions",
|
||||||
|
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 getPermissions: Route =
|
||||||
|
path("types" / "permissions") {
|
||||||
|
get {
|
||||||
|
complete(Permission.values.toSeq.sorted.map(_.toString))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("types/pieces")
|
||||||
|
@Produces(value = Array("application/json"))
|
||||||
|
@Operation(summary = "pieces list", description = "Returns the available pieces",
|
||||||
|
responses = Array(
|
||||||
|
new ApiResponse(responseCode = "200", description = "List of available pieces",
|
||||||
|
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 getPieces: Route =
|
||||||
|
path("types" / "pieces") {
|
||||||
|
get {
|
||||||
|
complete(Piece.available)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("types/priority")
|
||||||
|
@Produces(value = Array("application/json"))
|
||||||
|
@Operation(summary = "priority list", description = "Returns the current priority list",
|
||||||
|
responses = Array(
|
||||||
|
new ApiResponse(responseCode = "200", description = "Priority order",
|
||||||
|
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 getPriority: Route =
|
||||||
|
path("types" / "priority") {
|
||||||
|
get {
|
||||||
|
complete(Party.getRules(config))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,179 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2019 Evgeniy Alekseev.
|
||||||
|
*
|
||||||
|
* This file is part of ffxivbis
|
||||||
|
* (see https://github.com/arcan1s/ffxivbis).
|
||||||
|
*
|
||||||
|
* License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause
|
||||||
|
*/
|
||||||
|
package me.arcanis.ffxivbis.http.api.v1
|
||||||
|
|
||||||
|
import akka.actor.ActorRef
|
||||||
|
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.{ArraySchema, 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 javax.ws.rs._
|
||||||
|
import me.arcanis.ffxivbis.http.api.v1.json._
|
||||||
|
import me.arcanis.ffxivbis.http.{Authorization, UserHelper}
|
||||||
|
import me.arcanis.ffxivbis.models.Permission
|
||||||
|
|
||||||
|
import scala.util.{Failure, Success}
|
||||||
|
|
||||||
|
@Path("api/v1")
|
||||||
|
class UserEndpoint(override val storage: ActorRef)(implicit timeout: Timeout)
|
||||||
|
extends UserHelper(storage) with Authorization with JsonSupport {
|
||||||
|
|
||||||
|
def route: Route = createParty ~ createUser ~ deleteUser ~ getUsers
|
||||||
|
|
||||||
|
@PUT
|
||||||
|
@Path("party")
|
||||||
|
@Consumes(value = Array("application/json"))
|
||||||
|
@Operation(summary = "create new party", 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(
|
||||||
|
new ApiResponse(responseCode = "200", description = "Party has been created"),
|
||||||
|
new ApiResponse(responseCode = "400", description = "Invalid parameters were supplied",
|
||||||
|
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"),
|
||||||
|
)
|
||||||
|
def createParty: Route =
|
||||||
|
path("party") {
|
||||||
|
extractExecutionContext { implicit executionContext =>
|
||||||
|
put {
|
||||||
|
entity(as[UserResponse]) { user =>
|
||||||
|
onComplete(newPartyId) {
|
||||||
|
case Success(partyId) =>
|
||||||
|
val admin = user.toUser.copy(partyId = partyId, permission = Permission.admin)
|
||||||
|
onComplete(addUser(admin, isHashedPassword = false)) {
|
||||||
|
case Success(_) => complete(PartyIdResponse(partyId))
|
||||||
|
case Failure(exception) => throw exception
|
||||||
|
}
|
||||||
|
case Failure(exception) => throw exception
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Path("party/{partyId}/users")
|
||||||
|
@Consumes(value = Array("application/json"))
|
||||||
|
@Operation(summary = "create new user", description = "Add an user to the specified party",
|
||||||
|
parameters = Array(
|
||||||
|
new Parameter(name = "partyId", in = ParameterIn.PATH, description = "unique party ID", example = "abcdefgh"),
|
||||||
|
),
|
||||||
|
requestBody = new RequestBody(description = "user description", required = true,
|
||||||
|
content = Array(new Content(schema = new Schema(implementation = classOf[UserResponse])))),
|
||||||
|
responses = Array(
|
||||||
|
new ApiResponse(responseCode = "201", description = "User has been created"),
|
||||||
|
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("admin"))),
|
||||||
|
tags = Array("users"),
|
||||||
|
)
|
||||||
|
def createUser: Route =
|
||||||
|
path("party" / Segment / "users") { partyId =>
|
||||||
|
extractExecutionContext { implicit executionContext =>
|
||||||
|
authenticateBasicBCrypt(s"party $partyId", authAdmin(partyId)) { _ =>
|
||||||
|
post {
|
||||||
|
entity(as[UserResponse]) { user =>
|
||||||
|
val withPartyId = user.toUser.copy(partyId = partyId)
|
||||||
|
onComplete(addUser(withPartyId, isHashedPassword = false)) {
|
||||||
|
case Success(_) => complete(StatusCodes.Accepted, HttpEntity.Empty)
|
||||||
|
case Failure(exception) => throw exception
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@DELETE
|
||||||
|
@Path("party/{partyId}/users/{username}")
|
||||||
|
@Operation(summary = "remove user", description = "Remove an user from the specified party",
|
||||||
|
parameters = Array(
|
||||||
|
new Parameter(name = "partyId", in = ParameterIn.PATH, description = "unique party ID", example = "abcdefgh"),
|
||||||
|
new Parameter(name = "username", in = ParameterIn.PATH, description = "username to remove", example = "siuan"),
|
||||||
|
),
|
||||||
|
responses = Array(
|
||||||
|
new ApiResponse(responseCode = "202", description = "User has been removed"),
|
||||||
|
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"))),
|
||||||
|
tags = Array("users"),
|
||||||
|
)
|
||||||
|
def deleteUser: Route =
|
||||||
|
path("party" / Segment / "users" / Segment) { (partyId, username) =>
|
||||||
|
extractExecutionContext { implicit executionContext =>
|
||||||
|
authenticateBasicBCrypt(s"party $partyId", authAdmin(partyId)) { _ =>
|
||||||
|
delete {
|
||||||
|
onComplete(removeUser(partyId, username)) {
|
||||||
|
case Success(_) => complete(StatusCodes.Accepted, HttpEntity.Empty)
|
||||||
|
case Failure(exception) => throw exception
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("party/{partyId}/users")
|
||||||
|
@Produces(value = Array("application/json"))
|
||||||
|
@Operation(summary = "get users", description = "Return the list of users belong to party",
|
||||||
|
parameters = Array(
|
||||||
|
new Parameter(name = "partyId", in = ParameterIn.PATH, description = "unique party ID", example = "abcdefgh"),
|
||||||
|
),
|
||||||
|
responses = Array(
|
||||||
|
new ApiResponse(responseCode = "200", description = "Users list",
|
||||||
|
content = Array(new Content(
|
||||||
|
array = new ArraySchema(schema = new Schema(implementation = classOf[UserResponse])),
|
||||||
|
))),
|
||||||
|
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"))),
|
||||||
|
tags = Array("users"),
|
||||||
|
)
|
||||||
|
def getUsers: Route =
|
||||||
|
path("party" / Segment / "users") { partyId =>
|
||||||
|
extractExecutionContext { implicit executionContext =>
|
||||||
|
authenticateBasicBCrypt(s"party $partyId", authAdmin(partyId)) { _ =>
|
||||||
|
get {
|
||||||
|
onComplete(users(partyId)) {
|
||||||
|
case Success(response) => complete(response.map(UserResponse.fromUser))
|
||||||
|
case Failure(exception) => throw exception
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,13 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2019 Evgeniy Alekseev.
|
||||||
|
*
|
||||||
|
* This file is part of ffxivbis
|
||||||
|
* (see https://github.com/arcan1s/ffxivbis).
|
||||||
|
*
|
||||||
|
* License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause
|
||||||
|
*/
|
||||||
|
package me.arcanis.ffxivbis.http.api.v1.json
|
||||||
|
|
||||||
|
object ApiAction extends Enumeration {
|
||||||
|
val add, remove = Value
|
||||||
|
}
|
@ -0,0 +1,14 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2019 Evgeniy Alekseev.
|
||||||
|
*
|
||||||
|
* This file is part of ffxivbis
|
||||||
|
* (see https://github.com/arcan1s/ffxivbis).
|
||||||
|
*
|
||||||
|
* License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause
|
||||||
|
*/
|
||||||
|
package me.arcanis.ffxivbis.http.api.v1.json
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema
|
||||||
|
|
||||||
|
case class ErrorResponse(
|
||||||
|
@Schema(description = "error message", required = true) message: String)
|
@ -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.http.api.v1.json
|
||||||
|
|
||||||
|
import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport
|
||||||
|
import me.arcanis.ffxivbis.models.Permission
|
||||||
|
import spray.json._
|
||||||
|
|
||||||
|
trait JsonSupport extends SprayJsonSupport with DefaultJsonProtocol {
|
||||||
|
|
||||||
|
private def enumFormat[E <: Enumeration](enum: E): RootJsonFormat[E#Value] =
|
||||||
|
new RootJsonFormat[E#Value] {
|
||||||
|
override def write(obj: E#Value): JsValue = obj.toString.toJson
|
||||||
|
override def read(json: JsValue): E#Value = json match {
|
||||||
|
case JsNumber(value) => enum(value.toInt)
|
||||||
|
case JsString(name) => enum.withName(name)
|
||||||
|
case other => deserializationError(s"String or number expected, got $other")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
implicit val actionFormat: RootJsonFormat[ApiAction.Value] = enumFormat(ApiAction)
|
||||||
|
implicit val permissionFormat: RootJsonFormat[Permission.Value] = enumFormat(Permission)
|
||||||
|
|
||||||
|
implicit val errorFormat: RootJsonFormat[ErrorResponse] = jsonFormat1(ErrorResponse.apply)
|
||||||
|
implicit val partyIdFormat: RootJsonFormat[PartyIdResponse] = jsonFormat1(PartyIdResponse.apply)
|
||||||
|
implicit val pieceFormat: RootJsonFormat[PieceResponse] = jsonFormat3(PieceResponse.apply)
|
||||||
|
implicit val playerFormat: RootJsonFormat[PlayerResponse] = jsonFormat7(PlayerResponse.apply)
|
||||||
|
implicit val playerActionFormat: RootJsonFormat[PlayerActionResponse] = jsonFormat2(PlayerActionResponse.apply)
|
||||||
|
implicit val playerIdFormat: RootJsonFormat[PlayerIdResponse] = jsonFormat3(PlayerIdResponse.apply)
|
||||||
|
implicit val pieceActionFormat: RootJsonFormat[PieceActionResponse] = jsonFormat3(PieceActionResponse.apply)
|
||||||
|
implicit val playerBiSLinkFormat: RootJsonFormat[PlayerBiSLinkResponse] = jsonFormat2(PlayerBiSLinkResponse.apply)
|
||||||
|
implicit val playerIdWithCountersFormat: RootJsonFormat[PlayerIdWithCountersResponse] =
|
||||||
|
jsonFormat9(PlayerIdWithCountersResponse.apply)
|
||||||
|
implicit val userFormat: RootJsonFormat[UserResponse] = jsonFormat4(UserResponse.apply)
|
||||||
|
}
|
@ -0,0 +1,14 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2019 Evgeniy Alekseev.
|
||||||
|
*
|
||||||
|
* This file is part of ffxivbis
|
||||||
|
* (see https://github.com/arcan1s/ffxivbis).
|
||||||
|
*
|
||||||
|
* License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause
|
||||||
|
*/
|
||||||
|
package me.arcanis.ffxivbis.http.api.v1.json
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema
|
||||||
|
|
||||||
|
case class PartyIdResponse(
|
||||||
|
@Schema(description = "party id", required = true) partyId: String)
|
@ -0,0 +1,16 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2019 Evgeniy Alekseev.
|
||||||
|
*
|
||||||
|
* This file is part of ffxivbis
|
||||||
|
* (see https://github.com/arcan1s/ffxivbis).
|
||||||
|
*
|
||||||
|
* License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause
|
||||||
|
*/
|
||||||
|
package me.arcanis.ffxivbis.http.api.v1.json
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema
|
||||||
|
|
||||||
|
case class PieceActionResponse(
|
||||||
|
@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 = "player description", required = true) playerId: PlayerIdResponse)
|
@ -0,0 +1,24 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2019 Evgeniy Alekseev.
|
||||||
|
*
|
||||||
|
* This file is part of ffxivbis
|
||||||
|
* (see https://github.com/arcan1s/ffxivbis).
|
||||||
|
*
|
||||||
|
* License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause
|
||||||
|
*/
|
||||||
|
package me.arcanis.ffxivbis.http.api.v1.json
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema
|
||||||
|
import me.arcanis.ffxivbis.models.{Job, Piece}
|
||||||
|
|
||||||
|
case class PieceResponse(
|
||||||
|
@Schema(description = "is piece tome gear", required = true) isTome: Boolean,
|
||||||
|
@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) {
|
||||||
|
def toPiece: Piece = Piece(piece, isTome, Job.withName(job))
|
||||||
|
}
|
||||||
|
|
||||||
|
object PieceResponse {
|
||||||
|
def fromPiece(piece: Piece): PieceResponse =
|
||||||
|
PieceResponse(piece.isTome, piece.job.toString, piece.piece)
|
||||||
|
}
|
@ -0,0 +1,15 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2019 Evgeniy Alekseev.
|
||||||
|
*
|
||||||
|
* This file is part of ffxivbis
|
||||||
|
* (see https://github.com/arcan1s/ffxivbis).
|
||||||
|
*
|
||||||
|
* License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause
|
||||||
|
*/
|
||||||
|
package me.arcanis.ffxivbis.http.api.v1.json
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema
|
||||||
|
|
||||||
|
case class PlayerActionResponse(
|
||||||
|
@Schema(description = "action to perform", required = true, `type` = "string", allowableValues = Array("add", "remove"), example = "add") action: ApiAction.Value,
|
||||||
|
@Schema(description = "player description", required = true) playerId: PlayerResponse)
|
@ -0,0 +1,15 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2019 Evgeniy Alekseev.
|
||||||
|
*
|
||||||
|
* This file is part of ffxivbis
|
||||||
|
* (see https://github.com/arcan1s/ffxivbis).
|
||||||
|
*
|
||||||
|
* License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause
|
||||||
|
*/
|
||||||
|
package me.arcanis.ffxivbis.http.api.v1.json
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema
|
||||||
|
|
||||||
|
case class PlayerBiSLinkResponse(
|
||||||
|
@Schema(description = "link to player best in slot", required = true, example = "https://ffxiv.ariyala.com/19V5R") link: String,
|
||||||
|
@Schema(description = "player description", required = true) playerId: PlayerIdResponse)
|
@ -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.api.v1.json
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema
|
||||||
|
import me.arcanis.ffxivbis.models.{Job, PlayerId}
|
||||||
|
|
||||||
|
case class PlayerIdResponse(
|
||||||
|
@Schema(description = "unique party ID. Required in responses", example = "abcdefgh") partyId: Option[String],
|
||||||
|
@Schema(description = "job name", required = true, example = "DNC") job: String,
|
||||||
|
@Schema(description = "player nick name", required = true, example = "Siuan Sanche") nick: String) {
|
||||||
|
def withPartyId(partyId: String): PlayerId =
|
||||||
|
PlayerId(partyId, Job.withName(job), nick)
|
||||||
|
}
|
||||||
|
|
||||||
|
object PlayerIdResponse {
|
||||||
|
def fromPlayerId(playerId: PlayerId): PlayerIdResponse =
|
||||||
|
PlayerIdResponse(Some(playerId.partyId), playerId.job.toString, playerId.nick)
|
||||||
|
}
|
@ -0,0 +1,37 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2019 Evgeniy Alekseev.
|
||||||
|
*
|
||||||
|
* This file is part of ffxivbis
|
||||||
|
* (see https://github.com/arcan1s/ffxivbis).
|
||||||
|
*
|
||||||
|
* License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause
|
||||||
|
*/
|
||||||
|
package me.arcanis.ffxivbis.http.api.v1.json
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema
|
||||||
|
import me.arcanis.ffxivbis.models.PlayerIdWithCounters
|
||||||
|
|
||||||
|
case class PlayerIdWithCountersResponse(
|
||||||
|
@Schema(description = "unique party ID", required = true, example = "abcdefgh") partyId: String,
|
||||||
|
@Schema(description = "job name", required = true, example = "DNC") job: String,
|
||||||
|
@Schema(description = "player nick name", required = true, example = "Siuan Sanche") nick: String,
|
||||||
|
@Schema(description = "is piece required by player or not", required = true) isRequired: Boolean,
|
||||||
|
@Schema(description = "player loot priority", required = true) priority: Int,
|
||||||
|
@Schema(description = "count of savage pieces in best in slot", required = true) bisCountTotal: Int,
|
||||||
|
@Schema(description = "count of 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 = "total count of looted pieces", required = true) lootCountTotal: Int)
|
||||||
|
|
||||||
|
object PlayerIdWithCountersResponse {
|
||||||
|
def fromPlayerId(playerIdWithCounters: PlayerIdWithCounters): PlayerIdWithCountersResponse =
|
||||||
|
PlayerIdWithCountersResponse(
|
||||||
|
playerIdWithCounters.partyId,
|
||||||
|
playerIdWithCounters.job.toString,
|
||||||
|
playerIdWithCounters.nick,
|
||||||
|
playerIdWithCounters.isRequired,
|
||||||
|
playerIdWithCounters.priority,
|
||||||
|
playerIdWithCounters.bisCountTotal,
|
||||||
|
playerIdWithCounters.lootCount,
|
||||||
|
playerIdWithCounters.lootCountBiS,
|
||||||
|
playerIdWithCounters.lootCountTotal)
|
||||||
|
}
|
@ -0,0 +1,33 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2019 Evgeniy Alekseev.
|
||||||
|
*
|
||||||
|
* This file is part of ffxivbis
|
||||||
|
* (see https://github.com/arcan1s/ffxivbis).
|
||||||
|
*
|
||||||
|
* License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause
|
||||||
|
*/
|
||||||
|
package me.arcanis.ffxivbis.http.api.v1.json
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema
|
||||||
|
import me.arcanis.ffxivbis.models.{BiS, Job, Player}
|
||||||
|
|
||||||
|
case class PlayerResponse(
|
||||||
|
@Schema(description = "unique party ID", required = true, example = "abcdefgh") partyId: String,
|
||||||
|
@Schema(description = "job name", required = true, example = "DNC") job: String,
|
||||||
|
@Schema(description = "player nick name", required = true, example = "Siuan Sanche") nick: String,
|
||||||
|
@Schema(description = "pieces in best in slot") bis: Option[Seq[PieceResponse]],
|
||||||
|
@Schema(description = "looted pieces") loot: Option[Seq[PieceResponse]],
|
||||||
|
@Schema(description = "link to best in slot", example = "https://ffxiv.ariyala.com/19V5R") link: Option[String],
|
||||||
|
@Schema(description = "player loot priority", `type` = "number") priority: Option[Int]) {
|
||||||
|
def toPlayer: Player =
|
||||||
|
Player(partyId, Job.withName(job), nick,
|
||||||
|
BiS(bis.getOrElse(Seq.empty).map(_.toPiece)), loot.getOrElse(Seq.empty).map(_.toPiece),
|
||||||
|
link, priority.getOrElse(0))
|
||||||
|
}
|
||||||
|
|
||||||
|
object PlayerResponse {
|
||||||
|
def fromPlayer(player: Player): PlayerResponse =
|
||||||
|
PlayerResponse(player.partyId, player.job.toString, player.nick,
|
||||||
|
Some(player.bis.pieces.map(PieceResponse.fromPiece)), Some(player.loot.map(PieceResponse.fromPiece)),
|
||||||
|
player.link, Some(player.priority))
|
||||||
|
}
|
@ -0,0 +1,26 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2019 Evgeniy Alekseev.
|
||||||
|
*
|
||||||
|
* This file is part of ffxivbis
|
||||||
|
* (see https://github.com/arcan1s/ffxivbis).
|
||||||
|
*
|
||||||
|
* License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause
|
||||||
|
*/
|
||||||
|
package me.arcanis.ffxivbis.http.api.v1.json
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema
|
||||||
|
import me.arcanis.ffxivbis.models.{Permission, User}
|
||||||
|
|
||||||
|
case class UserResponse(
|
||||||
|
@Schema(description = "unique party ID", required = true, example = "abcdefgh") partyId: String,
|
||||||
|
@Schema(description = "username to login to party", required = true, example = "siuan") username: String,
|
||||||
|
@Schema(description = "password to login to party", required = true, example = "pa55w0rd") password: String,
|
||||||
|
@Schema(description = "user permission", defaultValue = "get", allowableValues = Array("get", "post", "admin")) permission: Option[Permission.Value] = None) {
|
||||||
|
def toUser: User =
|
||||||
|
User(partyId, username, password, permission.getOrElse(Permission.get))
|
||||||
|
}
|
||||||
|
|
||||||
|
object UserResponse {
|
||||||
|
def fromUser(user: User): UserResponse =
|
||||||
|
UserResponse(user.partyId, user.username, "", Some(user.permission))
|
||||||
|
}
|
@ -0,0 +1,64 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2019 Evgeniy Alekseev.
|
||||||
|
*
|
||||||
|
* This file is part of ffxivbis
|
||||||
|
* (see https://github.com/arcan1s/ffxivbis).
|
||||||
|
*
|
||||||
|
* License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause
|
||||||
|
*/
|
||||||
|
package me.arcanis.ffxivbis.http.view
|
||||||
|
|
||||||
|
import akka.actor.ActorRef
|
||||||
|
import akka.http.scaladsl.model.StatusCodes
|
||||||
|
import akka.http.scaladsl.server.Directives._
|
||||||
|
import akka.http.scaladsl.server._
|
||||||
|
import akka.util.Timeout
|
||||||
|
import me.arcanis.ffxivbis.http.Authorization
|
||||||
|
|
||||||
|
class BasePartyView(override val storage: ActorRef)(implicit timeout: Timeout)
|
||||||
|
extends Authorization {
|
||||||
|
|
||||||
|
def route: Route = getIndex
|
||||||
|
|
||||||
|
def getIndex: Route =
|
||||||
|
path("party" / Segment) { partyId =>
|
||||||
|
extractExecutionContext { implicit executionContext =>
|
||||||
|
authenticateBasicBCrypt(s"party $partyId", authGet(partyId)) { _ =>
|
||||||
|
get {
|
||||||
|
complete {
|
||||||
|
(StatusCodes.OK, RootView.toHtml(BasePartyView.template(partyId)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
object BasePartyView {
|
||||||
|
import scalatags.Text
|
||||||
|
import scalatags.Text.all._
|
||||||
|
import scalatags.Text.tags2.{title => titleTag}
|
||||||
|
|
||||||
|
def root(partyId: String): Text.TypedTag[String] =
|
||||||
|
a(href:=s"/party/$partyId", title:="root")("root")
|
||||||
|
|
||||||
|
def template(partyId: String): String =
|
||||||
|
"<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01//EN\" \"http://www.w3.org/TR/html4/strict.dtd\">" +
|
||||||
|
html(lang:="en",
|
||||||
|
head(
|
||||||
|
titleTag(s"Party $partyId"),
|
||||||
|
link(rel:="stylesheet", `type`:="text/css", href:="/static/styles.css")
|
||||||
|
),
|
||||||
|
|
||||||
|
body(
|
||||||
|
h2(s"Party $partyId"),
|
||||||
|
br,
|
||||||
|
h2(a(href:=s"/party/$partyId/players", title:="party")("party")),
|
||||||
|
h2(a(href:=s"/party/$partyId/bis", title:="bis management")("best in slot")),
|
||||||
|
h2(a(href:=s"/party/$partyId/loot", title:="loot management")("loot")),
|
||||||
|
h2(a(href:=s"/party/$partyId/suggest", title:="suggest loot")("suggest")),
|
||||||
|
hr,
|
||||||
|
h2(a(href:=s"/party/$partyId/users", title:="user management")("users"))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
152
src/main/scala/me/arcanis/ffxivbis/http/view/BiSView.scala
Normal file
152
src/main/scala/me/arcanis/ffxivbis/http/view/BiSView.scala
Normal file
@ -0,0 +1,152 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2019 Evgeniy Alekseev.
|
||||||
|
*
|
||||||
|
* This file is part of ffxivbis
|
||||||
|
* (see https://github.com/arcan1s/ffxivbis).
|
||||||
|
*
|
||||||
|
* License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause
|
||||||
|
*/
|
||||||
|
package me.arcanis.ffxivbis.http.view
|
||||||
|
|
||||||
|
import akka.actor.ActorRef
|
||||||
|
import akka.http.scaladsl.model.StatusCodes
|
||||||
|
import akka.http.scaladsl.server.Directives._
|
||||||
|
import akka.http.scaladsl.server._
|
||||||
|
import akka.util.Timeout
|
||||||
|
import me.arcanis.ffxivbis.http.{Authorization, BiSHelper}
|
||||||
|
import me.arcanis.ffxivbis.models.{Piece, Player, PlayerId}
|
||||||
|
|
||||||
|
import scala.concurrent.{ExecutionContext, Future}
|
||||||
|
import scala.util.Try
|
||||||
|
|
||||||
|
class BiSView(override val storage: ActorRef, ariyala: ActorRef)(implicit timeout: Timeout)
|
||||||
|
extends BiSHelper(storage, ariyala) with Authorization {
|
||||||
|
|
||||||
|
def route: Route = getBiS ~ modifyBiS
|
||||||
|
|
||||||
|
def getBiS: Route =
|
||||||
|
path("party" / Segment / "bis") { partyId: String =>
|
||||||
|
extractExecutionContext { implicit executionContext =>
|
||||||
|
authenticateBasicBCrypt(s"party $partyId", authGet(partyId)) { _ =>
|
||||||
|
get {
|
||||||
|
complete {
|
||||||
|
bis(partyId, None).map { players =>
|
||||||
|
BiSView.template(partyId, players, None)
|
||||||
|
}.map { text =>
|
||||||
|
(StatusCodes.OK, RootView.toHtml(text))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def modifyBiS: Route =
|
||||||
|
path("party" / Segment / "bis") { partyId: String =>
|
||||||
|
extractExecutionContext { implicit executionContext =>
|
||||||
|
authenticateBasicBCrypt(s"party $partyId", authPost(partyId)) { _ =>
|
||||||
|
post {
|
||||||
|
formFields("player".as[String], "piece".as[String].?, "is_tome".as[String].?, "link".as[String].?, "action".as[String]) {
|
||||||
|
(player, maybePiece, maybeIsTome, maybeLink, action) =>
|
||||||
|
onComplete(modifyBiSCall(partyId, player, maybePiece, maybeIsTome, maybeLink, action)) {
|
||||||
|
case _ => redirect(s"/party/$partyId/bis", StatusCodes.Found)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private def modifyBiSCall(partyId: String, player: String,
|
||||||
|
maybePiece: Option[String], maybeIsTome: Option[String],
|
||||||
|
maybeLink: Option[String], action: String)
|
||||||
|
(implicit executionContext: ExecutionContext, timeout: Timeout): Future[Unit] = {
|
||||||
|
import me.arcanis.ffxivbis.utils.Implicits._
|
||||||
|
|
||||||
|
def getPiece(playerId: PlayerId, piece: String) =
|
||||||
|
Try(Piece(piece, maybeIsTome, playerId.job)).toOption
|
||||||
|
|
||||||
|
PlayerId(partyId, player) match {
|
||||||
|
case Some(playerId) => (maybePiece, action, maybeLink) match {
|
||||||
|
case (Some(piece), "add", _) => getPiece(playerId, piece) match {
|
||||||
|
case Some(item) => addPieceBiS(playerId, item).map(_ => ())
|
||||||
|
case _ => Future.failed(new Error(s"Could not construct piece from `$piece`"))
|
||||||
|
}
|
||||||
|
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`"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
object BiSView {
|
||||||
|
import scalatags.Text.all._
|
||||||
|
import scalatags.Text.tags2.{title => titleTag}
|
||||||
|
|
||||||
|
def template(partyId: String, party: Seq[Player], error: Option[String]): String =
|
||||||
|
"<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01//EN\" \"http://www.w3.org/TR/html4/strict.dtd\">" +
|
||||||
|
html(lang:="en",
|
||||||
|
head(
|
||||||
|
titleTag("Best in slot"),
|
||||||
|
link(rel:="stylesheet", `type`:="text/css", href:="/static/styles.css")
|
||||||
|
),
|
||||||
|
|
||||||
|
body(
|
||||||
|
h2("Best in slot"),
|
||||||
|
|
||||||
|
ErrorView.template(error),
|
||||||
|
SearchLineView.template,
|
||||||
|
|
||||||
|
form(action:=s"/party/$partyId/bis", method:="post")(
|
||||||
|
select(name:="player", id:="player", title:="player")
|
||||||
|
(for (player <- party) yield option(player.playerId.toString)),
|
||||||
|
select(name:="piece", id:="piece", title:="piece")
|
||||||
|
(for (piece <- Piece.available) yield option(piece)),
|
||||||
|
input(name:="is_tome", id:="is_tome", title:="is tome", `type`:="checkbox"),
|
||||||
|
label(`for`:="is_tome")("is tome gear"),
|
||||||
|
input(name:="action", id:="action", `type`:="hidden", value:="add"),
|
||||||
|
input(name:="add", id:="add", `type`:="submit", value:="add")
|
||||||
|
),
|
||||||
|
|
||||||
|
form(action:="/bis", method:="post")(
|
||||||
|
select(name:="player", id:="player", title:="player")
|
||||||
|
(for (player <- party) yield option(player.playerId.toString)),
|
||||||
|
input(name:="link", id:="link", placeholder:="player bis link", title:="link", `type`:="text"),
|
||||||
|
input(name:="action", id:="action", `type`:="hidden", value:="create"),
|
||||||
|
input(name:="add", id:="add", `type`:="submit", value:="add")
|
||||||
|
),
|
||||||
|
|
||||||
|
table(id:="result")(
|
||||||
|
tr(
|
||||||
|
th("player"),
|
||||||
|
th("piece"),
|
||||||
|
th("is tome"),
|
||||||
|
th("")
|
||||||
|
),
|
||||||
|
for (player <- party; piece <- player.bis.pieces) yield tr(
|
||||||
|
td(`class`:="include_search")(player.playerId.toString),
|
||||||
|
td(`class`:="include_search")(piece.piece),
|
||||||
|
td(piece.isTomeToString),
|
||||||
|
td(
|
||||||
|
form(action:=s"/party/$partyId/bis", method:="post")(
|
||||||
|
input(name:="player", id:="player", `type`:="hidden", value:=player.playerId.toString),
|
||||||
|
input(name:="piece", id:="piece", `type`:="hidden", value:=piece.piece),
|
||||||
|
input(name:="is_tome", id:="is_tome", `type`:="hidden", value:=piece.isTomeToString),
|
||||||
|
input(name:="action", id:="action", `type`:="hidden", value:="remove"),
|
||||||
|
input(name:="remove", id:="remove", `type`:="submit", value:="x")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
|
||||||
|
ExportToCSVView.template,
|
||||||
|
BasePartyView.root(partyId),
|
||||||
|
script(src:="/static/table_search.js", `type`:="text/javascript")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
19
src/main/scala/me/arcanis/ffxivbis/http/view/ErrorView.scala
Normal file
19
src/main/scala/me/arcanis/ffxivbis/http/view/ErrorView.scala
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2019 Evgeniy Alekseev.
|
||||||
|
*
|
||||||
|
* This file is part of ffxivbis
|
||||||
|
* (see https://github.com/arcan1s/ffxivbis).
|
||||||
|
*
|
||||||
|
* License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause
|
||||||
|
*/
|
||||||
|
package me.arcanis.ffxivbis.http.view
|
||||||
|
|
||||||
|
import scalatags.Text
|
||||||
|
import scalatags.Text.all._
|
||||||
|
|
||||||
|
object ErrorView {
|
||||||
|
def template(error: Option[String]): Text.TypedTag[String] = error match {
|
||||||
|
case Some(text) => p(id:="error", s"Error occurs: $text")
|
||||||
|
case None => p("")
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,20 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2019 Evgeniy Alekseev.
|
||||||
|
*
|
||||||
|
* This file is part of ffxivbis
|
||||||
|
* (see https://github.com/arcan1s/ffxivbis).
|
||||||
|
*
|
||||||
|
* License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause
|
||||||
|
*/
|
||||||
|
package me.arcanis.ffxivbis.http.view
|
||||||
|
|
||||||
|
import scalatags.Text
|
||||||
|
import scalatags.Text.all._
|
||||||
|
|
||||||
|
object ExportToCSVView {
|
||||||
|
def template: Text.TypedTag[String] =
|
||||||
|
div(
|
||||||
|
button(onclick:="exportTableToCsv('result.csv')")("Export to CSV"),
|
||||||
|
script(src:="/static/table_export.js", `type`:="text/javascript")
|
||||||
|
)
|
||||||
|
}
|
85
src/main/scala/me/arcanis/ffxivbis/http/view/IndexView.scala
Normal file
85
src/main/scala/me/arcanis/ffxivbis/http/view/IndexView.scala
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2019 Evgeniy Alekseev.
|
||||||
|
*
|
||||||
|
* This file is part of ffxivbis
|
||||||
|
* (see https://github.com/arcan1s/ffxivbis).
|
||||||
|
*
|
||||||
|
* License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause
|
||||||
|
*/
|
||||||
|
package me.arcanis.ffxivbis.http.view
|
||||||
|
|
||||||
|
import akka.actor.ActorRef
|
||||||
|
import akka.http.scaladsl.model.StatusCodes
|
||||||
|
import akka.http.scaladsl.server.Directives._
|
||||||
|
import akka.http.scaladsl.server._
|
||||||
|
import akka.util.Timeout
|
||||||
|
import me.arcanis.ffxivbis.http.UserHelper
|
||||||
|
import me.arcanis.ffxivbis.models.{Permission, User}
|
||||||
|
|
||||||
|
import scala.util.{Failure, Success}
|
||||||
|
|
||||||
|
class IndexView(storage: ActorRef)(implicit timeout: Timeout)
|
||||||
|
extends UserHelper(storage) {
|
||||||
|
|
||||||
|
def route: Route = createParty ~ getIndex
|
||||||
|
|
||||||
|
def createParty: Route =
|
||||||
|
path("party") {
|
||||||
|
extractExecutionContext { implicit executionContext =>
|
||||||
|
post {
|
||||||
|
formFields("username".as[String], "password".as[String]) { (username, password) =>
|
||||||
|
onComplete(newPartyId) {
|
||||||
|
case Success(partyId) =>
|
||||||
|
val user = User(partyId, username, password, Permission.admin)
|
||||||
|
onComplete(addUser(user, isHashedPassword = false)) {
|
||||||
|
case _ => redirect(s"/party/$partyId", StatusCodes.Found)
|
||||||
|
}
|
||||||
|
case Failure(exception) => throw exception
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def getIndex: Route =
|
||||||
|
pathEndOrSingleSlash {
|
||||||
|
get {
|
||||||
|
parameters("partyId".as[String].?) {
|
||||||
|
case Some(partyId) => redirect(s"/party/$partyId", StatusCodes.Found)
|
||||||
|
case _ => complete(StatusCodes.OK, RootView.toHtml(IndexView.template))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
object IndexView {
|
||||||
|
import scalatags.Text.all._
|
||||||
|
import scalatags.Text.tags2.{title => titleTag}
|
||||||
|
|
||||||
|
def template: String =
|
||||||
|
"<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01//EN\" \"http://www.w3.org/TR/html4/strict.dtd\">" +
|
||||||
|
html(
|
||||||
|
head(
|
||||||
|
titleTag("FFXIV loot helper"),
|
||||||
|
link(rel:="stylesheet", `type`:="text/css", href:="/static/styles.css")
|
||||||
|
),
|
||||||
|
|
||||||
|
body(
|
||||||
|
form(action:=s"party", method:="post")(
|
||||||
|
label("create a new party"),
|
||||||
|
input(name:="username", id:="username", placeholder:="username", title:="username", `type`:="text"),
|
||||||
|
input(name:="password", id:="password", placeholder:="password", title:="password", `type`:="password"),
|
||||||
|
input(name:="add", id:="add", `type`:="submit", value:="add")
|
||||||
|
),
|
||||||
|
|
||||||
|
br,
|
||||||
|
|
||||||
|
form(action:="/", method:="get")(
|
||||||
|
label("already have party?"),
|
||||||
|
input(name:="partyId", id:="partyId", placeholder:="party id", title:="party id", `type`:="text"),
|
||||||
|
input(name:="go", id:="go", `type`:="submit", value:="go")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,132 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2019 Evgeniy Alekseev.
|
||||||
|
*
|
||||||
|
* This file is part of ffxivbis
|
||||||
|
* (see https://github.com/arcan1s/ffxivbis).
|
||||||
|
*
|
||||||
|
* License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause
|
||||||
|
*/
|
||||||
|
package me.arcanis.ffxivbis.http.view
|
||||||
|
|
||||||
|
import akka.actor.ActorRef
|
||||||
|
import akka.http.scaladsl.model.StatusCodes
|
||||||
|
import akka.http.scaladsl.server.Directives._
|
||||||
|
import akka.http.scaladsl.server.Route
|
||||||
|
import akka.util.Timeout
|
||||||
|
import me.arcanis.ffxivbis.http.{Authorization, LootHelper}
|
||||||
|
import me.arcanis.ffxivbis.models.{Job, Piece, PlayerIdWithCounters}
|
||||||
|
|
||||||
|
import scala.concurrent.{ExecutionContext, Future}
|
||||||
|
import scala.util.{Failure, Success, Try}
|
||||||
|
|
||||||
|
class LootSuggestView(override val storage: ActorRef)(implicit timeout: Timeout)
|
||||||
|
extends LootHelper(storage) with Authorization {
|
||||||
|
|
||||||
|
def route: Route = getIndex ~ suggestLoot
|
||||||
|
|
||||||
|
def getIndex: Route =
|
||||||
|
path("party" / Segment / "suggest") { partyId: String =>
|
||||||
|
extractExecutionContext { implicit executionContext =>
|
||||||
|
authenticateBasicBCrypt(s"party $partyId", authGet(partyId)) { _ =>
|
||||||
|
get {
|
||||||
|
complete {
|
||||||
|
val text = LootSuggestView.template(partyId, Seq.empty, None, None)
|
||||||
|
(StatusCodes.OK, RootView.toHtml(text))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def suggestLoot: Route =
|
||||||
|
path("party" / Segment / "suggest") { partyId: String =>
|
||||||
|
extractExecutionContext { implicit executionContext =>
|
||||||
|
authenticateBasicBCrypt(s"party $partyId", authGet(partyId)) { _ =>
|
||||||
|
post {
|
||||||
|
formFields("piece".as[String], "job".as[String], "is_tome".as[String].?) { (piece, job, maybeTome) =>
|
||||||
|
import me.arcanis.ffxivbis.utils.Implicits._
|
||||||
|
val maybePiece = Try(Piece(piece, maybeTome, Job.withName(job))).toOption
|
||||||
|
|
||||||
|
onComplete(suggestLootCall(partyId, maybePiece)) {
|
||||||
|
case Success(players) =>
|
||||||
|
val text = LootSuggestView.template(partyId, players, maybePiece, None)
|
||||||
|
complete(StatusCodes.OK, RootView.toHtml(text))
|
||||||
|
case Failure(exception) =>
|
||||||
|
val text = LootSuggestView.template(partyId, Seq.empty, maybePiece, Some(exception.getMessage))
|
||||||
|
complete(StatusCodes.OK, RootView.toHtml(text))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private def suggestLootCall(partyId: String, maybePiece: Option[Piece])
|
||||||
|
(implicit executionContext: ExecutionContext, timeout: Timeout): Future[Seq[PlayerIdWithCounters]] =
|
||||||
|
maybePiece match {
|
||||||
|
case Some(piece) => suggestPiece(partyId, piece)
|
||||||
|
case _ => Future.failed(new Error(s"Could not construct piece from `$maybePiece`"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
object LootSuggestView {
|
||||||
|
import scalatags.Text.all._
|
||||||
|
import scalatags.Text.tags2.{title => titleTag}
|
||||||
|
|
||||||
|
def template(partyId: String, party: Seq[PlayerIdWithCounters], piece: Option[Piece], error: Option[String]): String =
|
||||||
|
"<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01//EN\" \"http://www.w3.org/TR/html4/strict.dtd\">" +
|
||||||
|
html(lang:="en",
|
||||||
|
head(
|
||||||
|
titleTag("Suggest loot"),
|
||||||
|
link(rel:="stylesheet", `type`:="text/css", href:="/static/styles.css")
|
||||||
|
),
|
||||||
|
|
||||||
|
body(
|
||||||
|
h2("Suggest loot"),
|
||||||
|
|
||||||
|
ErrorView.template(error),
|
||||||
|
SearchLineView.template,
|
||||||
|
|
||||||
|
form(action:=s"/party/$partyId/suggest", method:="post")(
|
||||||
|
select(name:="piece", id:="piece", title:="piece")
|
||||||
|
(for (piece <- Piece.available) yield option(piece)),
|
||||||
|
select(name:="job", id:="job", title:="job")
|
||||||
|
(for (job <- Job.availableWithAnyJob) yield option(job.toString)),
|
||||||
|
input(name:="is_tome", id:="is_tome", title:="is tome", `type`:="checkbox"),
|
||||||
|
label(`for`:="is_tome")("is tome gear"),
|
||||||
|
input(name:="suggest", id:="suggest", `type`:="submit", value:="suggest")
|
||||||
|
),
|
||||||
|
|
||||||
|
table(id:="result")(
|
||||||
|
tr(
|
||||||
|
th("player"),
|
||||||
|
th("is required"),
|
||||||
|
th("these pieces looted"),
|
||||||
|
th("total bis pieces looted"),
|
||||||
|
th("total pieces looted"),
|
||||||
|
th("")
|
||||||
|
),
|
||||||
|
for (player <- party) yield tr(
|
||||||
|
td(`class`:="include_search")(player.playerId.toString),
|
||||||
|
td(player.isRequiredToString),
|
||||||
|
td(player.lootCount),
|
||||||
|
td(player.lootCountBiS),
|
||||||
|
td(player.lootCountTotal),
|
||||||
|
td(
|
||||||
|
form(action:=s"/party/$partyId/loot", method:="post")(
|
||||||
|
input(name:="player", id:="player", `type`:="hidden", value:=player.playerId.toString),
|
||||||
|
input(name:="piece", id:="piece", `type`:="hidden", value:=piece.map(_.piece).getOrElse("")),
|
||||||
|
input(name:="is_tome", id:="is_tome", `type`:="hidden", value:=piece.map(_.isTomeToString).getOrElse("")),
|
||||||
|
input(name:="action", id:="action", `type`:="hidden", value:="add"),
|
||||||
|
input(name:="add", id:="add", `type`:="submit", value:="add")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
|
||||||
|
ExportToCSVView.template,
|
||||||
|
BasePartyView.root(partyId),
|
||||||
|
script(src:="/static/table_search.js", `type`:="text/javascript")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
138
src/main/scala/me/arcanis/ffxivbis/http/view/LootView.scala
Normal file
138
src/main/scala/me/arcanis/ffxivbis/http/view/LootView.scala
Normal file
@ -0,0 +1,138 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2019 Evgeniy Alekseev.
|
||||||
|
*
|
||||||
|
* This file is part of ffxivbis
|
||||||
|
* (see https://github.com/arcan1s/ffxivbis).
|
||||||
|
*
|
||||||
|
* License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause
|
||||||
|
*/
|
||||||
|
package me.arcanis.ffxivbis.http.view
|
||||||
|
|
||||||
|
import akka.actor.ActorRef
|
||||||
|
import akka.http.scaladsl.model.StatusCodes
|
||||||
|
import akka.http.scaladsl.server.Directives._
|
||||||
|
import akka.http.scaladsl.server.Route
|
||||||
|
import akka.util.Timeout
|
||||||
|
import me.arcanis.ffxivbis.http.{Authorization, LootHelper}
|
||||||
|
import me.arcanis.ffxivbis.models.{Piece, Player, PlayerId}
|
||||||
|
|
||||||
|
import scala.concurrent.{ExecutionContext, Future}
|
||||||
|
import scala.util.Try
|
||||||
|
|
||||||
|
class LootView (override val storage: ActorRef)(implicit timeout: Timeout)
|
||||||
|
extends LootHelper(storage) with Authorization {
|
||||||
|
|
||||||
|
def route: Route = getLoot ~ modifyLoot
|
||||||
|
|
||||||
|
def getLoot: Route =
|
||||||
|
path("party" / Segment / "loot") { partyId: String =>
|
||||||
|
extractExecutionContext { implicit executionContext =>
|
||||||
|
authenticateBasicBCrypt(s"party $partyId", authGet(partyId)) { _ =>
|
||||||
|
get {
|
||||||
|
complete {
|
||||||
|
loot(partyId, None).map { players =>
|
||||||
|
LootView.template(partyId, players, None)
|
||||||
|
}.map { text =>
|
||||||
|
(StatusCodes.OK, RootView.toHtml(text))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def modifyLoot: Route =
|
||||||
|
path("party" / Segment / "loot") { partyId: String =>
|
||||||
|
extractExecutionContext { implicit executionContext =>
|
||||||
|
authenticateBasicBCrypt(s"party $partyId", authPost(partyId)) { _ =>
|
||||||
|
post {
|
||||||
|
formFields("player".as[String], "piece".as[String], "is_tome".as[String].?, "action".as[String]) {
|
||||||
|
(player, maybePiece, maybeIsTome, action) =>
|
||||||
|
onComplete(modifyLootCall(partyId, player, maybePiece, maybeIsTome, action)) {
|
||||||
|
case _ => redirect(s"/party/$partyId/loot", StatusCodes.Found)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private def modifyLootCall(partyId: String, player: String,
|
||||||
|
maybePiece: String, maybeIsTome: Option[String],
|
||||||
|
action: String)
|
||||||
|
(implicit executionContext: ExecutionContext, timeout: Timeout): Future[Unit] = {
|
||||||
|
import me.arcanis.ffxivbis.utils.Implicits._
|
||||||
|
|
||||||
|
def getPiece(playerId: PlayerId) =
|
||||||
|
Try(Piece(maybePiece, maybeIsTome, playerId.job)).toOption
|
||||||
|
|
||||||
|
PlayerId(partyId, player) match {
|
||||||
|
case Some(playerId) => (getPiece(playerId), action) match {
|
||||||
|
case (Some(piece), "add") => addPieceLoot(playerId, piece).map(_ => ())
|
||||||
|
case (Some(piece), "remove") => removePieceLoot(playerId, piece).map(_ => ())
|
||||||
|
case _ => Future.failed(new Error(s"Could not construct piece from `$maybePiece`"))
|
||||||
|
}
|
||||||
|
case _ => Future.failed(new Error(s"Could not construct player id from `$player`"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
object LootView {
|
||||||
|
import scalatags.Text.all._
|
||||||
|
import scalatags.Text.tags2.{title => titleTag}
|
||||||
|
|
||||||
|
def template(partyId: String, party: Seq[Player], error: Option[String]): String =
|
||||||
|
"<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01//EN\" \"http://www.w3.org/TR/html4/strict.dtd\">" +
|
||||||
|
html(lang:="en",
|
||||||
|
head(
|
||||||
|
titleTag("Loot"),
|
||||||
|
link(rel:="stylesheet", `type`:="text/css", href:="/static/styles.css")
|
||||||
|
),
|
||||||
|
|
||||||
|
body(
|
||||||
|
h2("Loot"),
|
||||||
|
|
||||||
|
ErrorView.template(error),
|
||||||
|
SearchLineView.template,
|
||||||
|
|
||||||
|
form(action:=s"/party/$partyId/loot", method:="post")(
|
||||||
|
select(name:="player", id:="player", title:="player")
|
||||||
|
(for (player <- party) yield option(player.playerId.toString)),
|
||||||
|
select(name:="piece", id:="piece", title:="piece")
|
||||||
|
(for (piece <- Piece.available) yield option(piece)),
|
||||||
|
input(name:="is_tome", id:="is_tome", title:="is tome", `type`:="checkbox"),
|
||||||
|
label(`for`:="is_tome")("is tome gear"),
|
||||||
|
input(name:="action", id:="action", `type`:="hidden", value:="add"),
|
||||||
|
input(name:="add", id:="add", `type`:="submit", value:="add")
|
||||||
|
),
|
||||||
|
|
||||||
|
table(id:="result")(
|
||||||
|
tr(
|
||||||
|
th("player"),
|
||||||
|
th("piece"),
|
||||||
|
th("is tome"),
|
||||||
|
th("")
|
||||||
|
),
|
||||||
|
for (player <- party; piece <- player.loot) yield tr(
|
||||||
|
td(`class`:="include_search")(player.playerId.toString),
|
||||||
|
td(`class`:="include_search")(piece.piece),
|
||||||
|
td(piece.isTomeToString),
|
||||||
|
td(
|
||||||
|
form(action:=s"/party/$partyId/loot", method:="post")(
|
||||||
|
input(name:="player", id:="player", `type`:="hidden", value:=player.playerId.toString),
|
||||||
|
input(name:="piece", id:="piece", `type`:="hidden", value:=piece.piece),
|
||||||
|
input(name:="is_tome", id:="is_tome", `type`:="hidden", value:=piece.isTomeToString),
|
||||||
|
input(name:="action", id:="action", `type`:="hidden", value:="remove"),
|
||||||
|
input(name:="remove", id:="remove", `type`:="submit", value:="x")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
|
||||||
|
ExportToCSVView.template,
|
||||||
|
BasePartyView.root(partyId),
|
||||||
|
script(src:="/static/table_search.js", `type`:="text/javascript")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
134
src/main/scala/me/arcanis/ffxivbis/http/view/PlayerView.scala
Normal file
134
src/main/scala/me/arcanis/ffxivbis/http/view/PlayerView.scala
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2019 Evgeniy Alekseev.
|
||||||
|
*
|
||||||
|
* This file is part of ffxivbis
|
||||||
|
* (see https://github.com/arcan1s/ffxivbis).
|
||||||
|
*
|
||||||
|
* License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause
|
||||||
|
*/
|
||||||
|
package me.arcanis.ffxivbis.http.view
|
||||||
|
|
||||||
|
import akka.actor.ActorRef
|
||||||
|
import akka.http.scaladsl.model.StatusCodes
|
||||||
|
import akka.http.scaladsl.server.Directives._
|
||||||
|
import akka.http.scaladsl.server.Route
|
||||||
|
import akka.util.Timeout
|
||||||
|
import me.arcanis.ffxivbis.http.{Authorization, PlayerHelper}
|
||||||
|
import me.arcanis.ffxivbis.models._
|
||||||
|
|
||||||
|
import scala.concurrent.{ExecutionContext, Future}
|
||||||
|
|
||||||
|
class PlayerView(override val storage: ActorRef, ariyala: ActorRef)(implicit timeout: Timeout)
|
||||||
|
extends PlayerHelper(storage, ariyala) with Authorization {
|
||||||
|
|
||||||
|
def route: Route = getParty ~ modifyParty
|
||||||
|
|
||||||
|
def getParty: Route =
|
||||||
|
path("party" / Segment / "players") { partyId: String =>
|
||||||
|
extractExecutionContext { implicit executionContext =>
|
||||||
|
authenticateBasicBCrypt(s"party $partyId", authGet(partyId)) { _ =>
|
||||||
|
get {
|
||||||
|
complete {
|
||||||
|
getPlayers(partyId, None).map { players =>
|
||||||
|
PlayerView.template(partyId, players.map(_.withCounters(None)), None)
|
||||||
|
}.map { text =>
|
||||||
|
(StatusCodes.OK, RootView.toHtml(text))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def modifyParty: Route =
|
||||||
|
path("party" / Segment / "players") { partyId: String =>
|
||||||
|
extractExecutionContext { implicit executionContext =>
|
||||||
|
authenticateBasicBCrypt(s"party $partyId", authPost(partyId)) { _ =>
|
||||||
|
post {
|
||||||
|
formFields("nick".as[String], "job".as[String], "priority".as[Int].?, "link".as[String].?, "action".as[String]) {
|
||||||
|
(nick, job, maybePriority, maybeLink, action) =>
|
||||||
|
onComplete(modifyPartyCall(partyId, nick, job, maybePriority, maybeLink, action)) {
|
||||||
|
case _ => redirect(s"/party/$partyId/players", StatusCodes.Found)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private def modifyPartyCall(partyId: String, nick: String, job: String,
|
||||||
|
maybePriority: Option[Int], maybeLink: Option[String],
|
||||||
|
action: String)
|
||||||
|
(implicit executionContext: ExecutionContext, timeout: Timeout): Future[Unit] = {
|
||||||
|
def maybePlayerId = PlayerId(partyId, Some(nick), Some(job))
|
||||||
|
def player(playerId: PlayerId) =
|
||||||
|
Player(partyId, playerId.job, playerId.nick, BiS(), Seq.empty, maybeLink, maybePriority.getOrElse(0))
|
||||||
|
|
||||||
|
(action, maybePlayerId) match {
|
||||||
|
case ("add", Some(playerId)) => addPlayer(player(playerId)).map(_ => ())
|
||||||
|
case ("remove", Some(playerId)) => removePlayer(playerId).map(_ => ())
|
||||||
|
case _ => Future.failed(new Error(s"Could not perform $action with $nick ($job)"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
object PlayerView {
|
||||||
|
import scalatags.Text.all._
|
||||||
|
import scalatags.Text.tags2.{title => titleTag}
|
||||||
|
|
||||||
|
def template(partyId: String, party: Seq[PlayerIdWithCounters], error: Option[String]): String =
|
||||||
|
"<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01//EN\" \"http://www.w3.org/TR/html4/strict.dtd\">" +
|
||||||
|
html(lang:="en",
|
||||||
|
head(
|
||||||
|
titleTag("Party"),
|
||||||
|
link(rel:="stylesheet", `type`:="text/css", href:="/static/styles.css")
|
||||||
|
),
|
||||||
|
|
||||||
|
body(
|
||||||
|
h2("Party"),
|
||||||
|
|
||||||
|
ErrorView.template(error),
|
||||||
|
SearchLineView.template,
|
||||||
|
|
||||||
|
form(action:=s"/party/$partyId/players", method:="post")(
|
||||||
|
input(name:="nick", id:="nick", placeholder:="nick", title:="nick", `type`:="nick"),
|
||||||
|
select(name:="job", id:="job", title:="job")
|
||||||
|
(for (job <- Job.available) yield option(job.toString)),
|
||||||
|
input(name:="link", id:="link", placeholder:="player bis link", title:="link", `type`:="text"),
|
||||||
|
input(name:="prioiry", id:="priority", placeholder:="priority", title:="priority", `type`:="number", value:="0"),
|
||||||
|
input(name:="action", id:="action", `type`:="hidden", value:="add"),
|
||||||
|
input(name:="add", id:="add", `type`:="submit", value:="add")
|
||||||
|
),
|
||||||
|
|
||||||
|
table(id:="result")(
|
||||||
|
tr(
|
||||||
|
th("nick"),
|
||||||
|
th("job"),
|
||||||
|
th("total bis pieces looted"),
|
||||||
|
th("total pieces looted"),
|
||||||
|
th("priority"),
|
||||||
|
th("")
|
||||||
|
),
|
||||||
|
for (player <- party) yield tr(
|
||||||
|
td(`class`:="include_search")(player.nick),
|
||||||
|
td(`class`:="include_search")(player.job.toString),
|
||||||
|
td(player.lootCountBiS),
|
||||||
|
td(player.lootCountTotal),
|
||||||
|
td(player.priority),
|
||||||
|
td(
|
||||||
|
form(action:=s"/party/$partyId/players", method:="post")(
|
||||||
|
input(name:="nick", id:="nick", `type`:="hidden", value:=player.nick),
|
||||||
|
input(name:="job", id:="job", `type`:="hidden", value:=player.job.toString),
|
||||||
|
input(name:="action", id:="action", `type`:="hidden", value:="remove"),
|
||||||
|
input(name:="remove", id:="remove", `type`:="submit", value:="x")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
|
||||||
|
ExportToCSVView.template,
|
||||||
|
BasePartyView.root(partyId),
|
||||||
|
script(src:="/static/table_search.js", `type`:="text/javascript")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
36
src/main/scala/me/arcanis/ffxivbis/http/view/RootView.scala
Normal file
36
src/main/scala/me/arcanis/ffxivbis/http/view/RootView.scala
Normal file
@ -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.http.view
|
||||||
|
|
||||||
|
import akka.actor.ActorRef
|
||||||
|
import akka.http.scaladsl.model.{ContentTypes, HttpEntity}
|
||||||
|
import akka.http.scaladsl.server.Directives._
|
||||||
|
import akka.http.scaladsl.server.Route
|
||||||
|
import akka.util.Timeout
|
||||||
|
|
||||||
|
class RootView(storage: ActorRef, ariyala: ActorRef)(implicit timeout: Timeout) {
|
||||||
|
|
||||||
|
private val basePartyView = new BasePartyView(storage)
|
||||||
|
private val indexView = new IndexView(storage)
|
||||||
|
|
||||||
|
private val biSView = new BiSView(storage, ariyala)
|
||||||
|
private val lootView = new LootView(storage)
|
||||||
|
private val lootSuggestView = new LootSuggestView(storage)
|
||||||
|
private val playerView = new PlayerView(storage, ariyala)
|
||||||
|
private val userView = new UserView(storage)
|
||||||
|
|
||||||
|
def route: Route =
|
||||||
|
basePartyView.route ~ indexView.route ~
|
||||||
|
biSView.route ~ lootView.route ~ lootSuggestView.route ~ playerView.route ~ userView.route
|
||||||
|
}
|
||||||
|
|
||||||
|
object RootView {
|
||||||
|
def toHtml(template: String): HttpEntity.Strict =
|
||||||
|
HttpEntity(ContentTypes.`text/html(UTF-8)`, template)
|
||||||
|
}
|
@ -0,0 +1,22 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2019 Evgeniy Alekseev.
|
||||||
|
*
|
||||||
|
* This file is part of ffxivbis
|
||||||
|
* (see https://github.com/arcan1s/ffxivbis).
|
||||||
|
*
|
||||||
|
* License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause
|
||||||
|
*/
|
||||||
|
package me.arcanis.ffxivbis.http.view
|
||||||
|
|
||||||
|
import scalatags.Text
|
||||||
|
import scalatags.Text.all._
|
||||||
|
|
||||||
|
object SearchLineView {
|
||||||
|
def template: Text.TypedTag[String] =
|
||||||
|
div(
|
||||||
|
input(
|
||||||
|
`type`:="text", id:="search", onkeyup:="searchTable()",
|
||||||
|
placeholder:="search for data", title:="search"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
128
src/main/scala/me/arcanis/ffxivbis/http/view/UserView.scala
Normal file
128
src/main/scala/me/arcanis/ffxivbis/http/view/UserView.scala
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2019 Evgeniy Alekseev.
|
||||||
|
*
|
||||||
|
* This file is part of ffxivbis
|
||||||
|
* (see https://github.com/arcan1s/ffxivbis).
|
||||||
|
*
|
||||||
|
* License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause
|
||||||
|
*/
|
||||||
|
package me.arcanis.ffxivbis.http.view
|
||||||
|
|
||||||
|
import akka.actor.ActorRef
|
||||||
|
import akka.http.scaladsl.model.StatusCodes
|
||||||
|
import akka.http.scaladsl.server.Directives._
|
||||||
|
import akka.http.scaladsl.server.Route
|
||||||
|
import akka.util.Timeout
|
||||||
|
import me.arcanis.ffxivbis.http.{Authorization, UserHelper}
|
||||||
|
import me.arcanis.ffxivbis.models.{Permission, User}
|
||||||
|
|
||||||
|
import scala.concurrent.{ExecutionContext, Future}
|
||||||
|
import scala.util.Try
|
||||||
|
|
||||||
|
class UserView(override val storage: ActorRef)(implicit timeout: Timeout)
|
||||||
|
extends UserHelper(storage) with Authorization {
|
||||||
|
|
||||||
|
def route: Route = getUsers ~ modifyUsers
|
||||||
|
|
||||||
|
def getUsers: Route =
|
||||||
|
path("party" / Segment / "users") { partyId: String =>
|
||||||
|
extractExecutionContext { implicit executionContext =>
|
||||||
|
authenticateBasicBCrypt(s"party $partyId", authAdmin(partyId)) { _ =>
|
||||||
|
get {
|
||||||
|
complete {
|
||||||
|
users(partyId).map { users =>
|
||||||
|
UserView.template(partyId, users, None)
|
||||||
|
}.map { text =>
|
||||||
|
(StatusCodes.OK, RootView.toHtml(text))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def modifyUsers: Route =
|
||||||
|
path("party" / Segment / "users") { partyId: String =>
|
||||||
|
extractExecutionContext { implicit executionContext =>
|
||||||
|
authenticateBasicBCrypt(s"party $partyId", authAdmin(partyId)) { _ =>
|
||||||
|
post {
|
||||||
|
formFields("username".as[String], "password".as[String].?, "permission".as[String].?, "action".as[String]) {
|
||||||
|
(username, maybePassword, maybePermission, action) =>
|
||||||
|
onComplete(modifyUsersCall(partyId, username, maybePassword, maybePermission, action)) {
|
||||||
|
case _ => redirect(s"/party/$partyId/users", StatusCodes.Found)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private def modifyUsersCall(partyId: String, username: String,
|
||||||
|
maybePassword: Option[String], maybePermission: Option[String],
|
||||||
|
action: String)
|
||||||
|
(implicit executionContext: ExecutionContext, timeout: Timeout): Future[Unit] = {
|
||||||
|
def permission: Option[Permission.Value] =
|
||||||
|
maybePermission.flatMap(p => Try(Permission.withName(p)).toOption)
|
||||||
|
|
||||||
|
action match {
|
||||||
|
case "add" => (maybePassword, permission) match {
|
||||||
|
case (Some(password), Some(permission)) => addUser(User(partyId, username, password, permission), isHashedPassword = false).map(_ => ())
|
||||||
|
case _ => Future.failed(new Error(s"Could not construct permission/password from `$maybePermission`/`$maybePassword`"))
|
||||||
|
}
|
||||||
|
case "remove" => removeUser(partyId, username).map(_ => ())
|
||||||
|
case _ => Future.failed(new Error(s"Could not perform $action"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
object UserView {
|
||||||
|
import scalatags.Text.all._
|
||||||
|
import scalatags.Text.tags2.{title => titleTag}
|
||||||
|
|
||||||
|
def template(partyId: String, users: Seq[User], error: Option[String]) =
|
||||||
|
"<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01//EN\" \"http://www.w3.org/TR/html4/strict.dtd\">" +
|
||||||
|
html(lang:="en",
|
||||||
|
head(
|
||||||
|
titleTag("Users"),
|
||||||
|
link(rel:="stylesheet", `type`:="text/css", href:="/static/styles.css")
|
||||||
|
),
|
||||||
|
|
||||||
|
body(
|
||||||
|
h2("Users"),
|
||||||
|
|
||||||
|
ErrorView.template(error),
|
||||||
|
SearchLineView.template,
|
||||||
|
|
||||||
|
form(action:=s"/party/$partyId/users", method:="post")(
|
||||||
|
input(name:="username", id:="username", placeholder:="username", title:="username", `type`:="text"),
|
||||||
|
input(name:="password", id:="password", placeholder:="password", title:="password", `type`:="password"),
|
||||||
|
select(name:="permission", id:="permission", title:="permission")(option("get"), option("post")),
|
||||||
|
input(name:="action", id:="action", `type`:="hidden", value:="add"),
|
||||||
|
input(name:="add", id:="add", `type`:="submit", value:="add")
|
||||||
|
),
|
||||||
|
|
||||||
|
table(id:="result")(
|
||||||
|
tr(
|
||||||
|
th("username"),
|
||||||
|
th("permission"),
|
||||||
|
th("")
|
||||||
|
),
|
||||||
|
for (user <- users) yield tr(
|
||||||
|
td(`class`:="include_search")(user.username),
|
||||||
|
td(user.permission.toString),
|
||||||
|
td(
|
||||||
|
form(action:=s"/party/$partyId/users", method:="post")(
|
||||||
|
input(name:="username", id:="username", `type`:="hidden", value:=user.username.toString),
|
||||||
|
input(name:="action", id:="action", `type`:="hidden", value:="remove"),
|
||||||
|
input(name:="remove", id:="remove", `type`:="submit", value:="x")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
|
||||||
|
ExportToCSVView.template,
|
||||||
|
BasePartyView.root(partyId),
|
||||||
|
script(src:="/static/table_search.js", `type`:="text/javascript")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
80
src/main/scala/me/arcanis/ffxivbis/models/BiS.scala
Normal file
80
src/main/scala/me/arcanis/ffxivbis/models/BiS.scala
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
/*
|
||||||
|
* 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.models
|
||||||
|
|
||||||
|
case class BiS(weapon: Option[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 {
|
||||||
|
case upgrade: PieceUpgrade => upgrades.contains(upgrade)
|
||||||
|
case _ => pieces.contains(piece)
|
||||||
|
}
|
||||||
|
|
||||||
|
def upgrades: Map[PieceUpgrade, Int] =
|
||||||
|
pieces.groupBy(_.upgrade).foldLeft(Map.empty[PieceUpgrade, Int]) {
|
||||||
|
case (acc, (Some(k), v)) => acc + (k -> v.length)
|
||||||
|
case (acc, _) => acc
|
||||||
|
} withDefaultValue 0
|
||||||
|
|
||||||
|
def withPiece(piece: Piece): BiS = copyWithPiece(piece.piece, Some(piece))
|
||||||
|
def withoutPiece(piece: Piece): BiS = copyWithPiece(piece.piece, None)
|
||||||
|
|
||||||
|
private def copyWithPiece(name: String, piece: Option[Piece]): BiS = {
|
||||||
|
val params = Map(
|
||||||
|
"weapon" -> weapon,
|
||||||
|
"head" -> head,
|
||||||
|
"body" -> body,
|
||||||
|
"hands" -> hands,
|
||||||
|
"waist" -> waist,
|
||||||
|
"legs" -> legs,
|
||||||
|
"feet" -> feet,
|
||||||
|
"ears" -> ears,
|
||||||
|
"neck" -> neck,
|
||||||
|
"wrist" -> wrist,
|
||||||
|
"left ring" -> leftRing,
|
||||||
|
"right ring" -> rightRing
|
||||||
|
) + (name -> piece)
|
||||||
|
BiS(params)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 apply(pieces: Seq[Piece]): BiS =
|
||||||
|
BiS(pieces.map(piece => piece.piece -> Some(piece)).toMap)
|
||||||
|
}
|
109
src/main/scala/me/arcanis/ffxivbis/models/Job.scala
Normal file
109
src/main/scala/me/arcanis/ffxivbis/models/Job.scala
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
/*
|
||||||
|
* 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.models
|
||||||
|
|
||||||
|
object Job {
|
||||||
|
sealed trait RightSide
|
||||||
|
object AccessoriesDex extends RightSide
|
||||||
|
object AccessoriesInt extends RightSide
|
||||||
|
object AccessoriesMnd extends RightSide
|
||||||
|
object AccessoriesStr extends RightSide
|
||||||
|
object AccessoriesVit extends RightSide
|
||||||
|
|
||||||
|
sealed trait LeftSide
|
||||||
|
object BodyCasters extends LeftSide
|
||||||
|
object BodyDrgs extends LeftSide
|
||||||
|
object BodyHealers extends LeftSide
|
||||||
|
object BodyMnks extends LeftSide
|
||||||
|
object BodyNins extends LeftSide
|
||||||
|
object BodyTanks extends LeftSide
|
||||||
|
object BodyRanges extends LeftSide
|
||||||
|
|
||||||
|
sealed trait Job {
|
||||||
|
def leftSide: LeftSide
|
||||||
|
def rightSide: RightSide
|
||||||
|
|
||||||
|
// conversion to string to avoid recursion
|
||||||
|
override def equals(obj: Any): Boolean = {
|
||||||
|
def canEqual(obj: Any): Boolean = obj.isInstanceOf[Job]
|
||||||
|
def equality(objRepr: String): Boolean = objRepr match {
|
||||||
|
case _ if objRepr == AnyJob.toString => true
|
||||||
|
case _ if this.toString == AnyJob.toString => true
|
||||||
|
case _ => this.toString == objRepr
|
||||||
|
}
|
||||||
|
|
||||||
|
canEqual(obj) && equality(obj.toString)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case object AnyJob extends Job {
|
||||||
|
val leftSide: LeftSide = null
|
||||||
|
val rightSide: RightSide = null
|
||||||
|
}
|
||||||
|
|
||||||
|
trait Casters extends Job {
|
||||||
|
val leftSide: LeftSide = BodyCasters
|
||||||
|
val rightSide: RightSide = AccessoriesInt
|
||||||
|
}
|
||||||
|
trait Healers extends Job {
|
||||||
|
val leftSide: LeftSide = BodyHealers
|
||||||
|
val rightSide: RightSide = AccessoriesMnd
|
||||||
|
}
|
||||||
|
trait Mnks extends Job {
|
||||||
|
val leftSide: LeftSide = BodyMnks
|
||||||
|
val rightSide: RightSide = AccessoriesStr
|
||||||
|
}
|
||||||
|
trait Tanks extends Job {
|
||||||
|
val leftSide: LeftSide = BodyTanks
|
||||||
|
val rightSide: RightSide = AccessoriesVit
|
||||||
|
}
|
||||||
|
trait Ranges extends Job {
|
||||||
|
val leftSide: LeftSide = BodyRanges
|
||||||
|
val rightSide: RightSide = AccessoriesDex
|
||||||
|
}
|
||||||
|
|
||||||
|
case object PLD extends Tanks
|
||||||
|
case object WAR extends Tanks
|
||||||
|
case object DRK extends Tanks
|
||||||
|
case object GNB extends Tanks
|
||||||
|
|
||||||
|
case object WHM extends Healers
|
||||||
|
case object SCH extends Healers
|
||||||
|
case object AST extends Healers
|
||||||
|
|
||||||
|
case object MNK extends Mnks
|
||||||
|
case object DRG extends Job {
|
||||||
|
val leftSide: LeftSide = BodyDrgs
|
||||||
|
val rightSide: RightSide = AccessoriesStr
|
||||||
|
}
|
||||||
|
case object NIN extends Job {
|
||||||
|
val leftSide: LeftSide = BodyNins
|
||||||
|
val rightSide: RightSide = AccessoriesDex
|
||||||
|
}
|
||||||
|
case object SAM extends Mnks
|
||||||
|
|
||||||
|
case object BRD extends Ranges
|
||||||
|
case object MCH extends Ranges
|
||||||
|
case object DNC extends Ranges
|
||||||
|
|
||||||
|
case object BLM extends Casters
|
||||||
|
case object SMN extends Casters
|
||||||
|
case object RDM extends Casters
|
||||||
|
|
||||||
|
lazy val available: Seq[Job] =
|
||||||
|
Seq(PLD, WAR, DRK, GNB, WHM, SCH, AST, MNK, DRG, NIN, SAM, BRD, MCH, DNC, BLM, SMN, RDM)
|
||||||
|
lazy val availableWithAnyJob: Seq[Job] = available.prepended(AnyJob)
|
||||||
|
|
||||||
|
def withName(job: String): Job.Job =
|
||||||
|
availableWithAnyJob.find(_.toString.equalsIgnoreCase(job.toUpperCase)) match {
|
||||||
|
case Some(value) => value
|
||||||
|
case None if job.isEmpty => AnyJob
|
||||||
|
case _ => throw new IllegalArgumentException("Invalid or unknown job")
|
||||||
|
}
|
||||||
|
}
|
11
src/main/scala/me/arcanis/ffxivbis/models/Loot.scala
Normal file
11
src/main/scala/me/arcanis/ffxivbis/models/Loot.scala
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
/*
|
||||||
|
* 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.models
|
||||||
|
|
||||||
|
case class Loot(playerId: Long, piece: Piece)
|
59
src/main/scala/me/arcanis/ffxivbis/models/Party.scala
Normal file
59
src/main/scala/me/arcanis/ffxivbis/models/Party.scala
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
/*
|
||||||
|
* 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.models
|
||||||
|
|
||||||
|
import com.typesafe.config.Config
|
||||||
|
import com.typesafe.scalalogging.StrictLogging
|
||||||
|
import me.arcanis.ffxivbis.service.LootSelector
|
||||||
|
|
||||||
|
import scala.jdk.CollectionConverters._
|
||||||
|
import scala.util.Random
|
||||||
|
|
||||||
|
case class Party(partyId: String, rules: Seq[String], players: Map[PlayerId, Player])
|
||||||
|
extends StrictLogging {
|
||||||
|
require(players.keys.forall(_.partyId == partyId), "party id must be same")
|
||||||
|
|
||||||
|
def getPlayers: Seq[Player] = players.values.toSeq
|
||||||
|
def player(playerId: PlayerId): Option[Player] = players.get(playerId)
|
||||||
|
def withPlayer(player: Player): Party =
|
||||||
|
try {
|
||||||
|
require(player.partyId == partyId, "player must belong to this party")
|
||||||
|
copy(players = players + (player.playerId -> player))
|
||||||
|
} catch {
|
||||||
|
case exception: Exception =>
|
||||||
|
logger.error("cannot add player", exception)
|
||||||
|
this
|
||||||
|
}
|
||||||
|
|
||||||
|
def suggestLoot(piece: Piece): LootSelector.LootSelectorResult =
|
||||||
|
LootSelector(getPlayers, piece, rules)
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
players: Map[Long, Player], bis: Seq[Loot], loot: Seq[Loot]): Party = {
|
||||||
|
val bisByPlayer = bis.groupBy(_.playerId).view.mapValues(piece => BiS(piece.map(_.piece)))
|
||||||
|
val lootByPlayer = loot.groupBy(_.playerId).view.mapValues(_.map(_.piece))
|
||||||
|
val playersWithItems = players.foldLeft(Map.empty[PlayerId, Player]) {
|
||||||
|
case (acc, (playerId, player)) =>
|
||||||
|
acc + (player.playerId -> player
|
||||||
|
.withBiS(bisByPlayer.get(playerId))
|
||||||
|
.withLoot(lootByPlayer.getOrElse(playerId, Seq.empty)))
|
||||||
|
}
|
||||||
|
Party(partyId, getRules(config), playersWithItems)
|
||||||
|
}
|
||||||
|
|
||||||
|
def getRules(config: Config): Seq[String] =
|
||||||
|
config.getStringList("me.arcanis.ffxivbis.settings.priority").asScala.toSeq
|
||||||
|
|
||||||
|
def randomPartyId: String = Random.alphanumeric.take(20).mkString
|
||||||
|
}
|
122
src/main/scala/me/arcanis/ffxivbis/models/Piece.scala
Normal file
122
src/main/scala/me/arcanis/ffxivbis/models/Piece.scala
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
/*
|
||||||
|
* 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.models
|
||||||
|
|
||||||
|
sealed trait Piece {
|
||||||
|
def isTome: Boolean
|
||||||
|
def job: Job.Job
|
||||||
|
def piece: String
|
||||||
|
|
||||||
|
def withJob(other: Job.Job): Piece
|
||||||
|
|
||||||
|
def isTomeToString: String = if (isTome) "yes" else "no"
|
||||||
|
def upgrade: Option[PieceUpgrade] = this match {
|
||||||
|
case _ if !isTome => None
|
||||||
|
case _: Waist => Some(AccessoryUpgrade)
|
||||||
|
case _: PieceAccessory => Some(AccessoryUpgrade)
|
||||||
|
case _: PieceBody => Some(BodyUpgrade)
|
||||||
|
case _: PieceWeapon => Some(WeaponUpgrade)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
trait PieceAccessory extends Piece
|
||||||
|
trait PieceBody extends Piece
|
||||||
|
trait PieceUpgrade extends Piece {
|
||||||
|
val isTome: Boolean = true
|
||||||
|
val job: Job.Job = Job.AnyJob
|
||||||
|
def withJob(other: Job.Job): Piece = this
|
||||||
|
}
|
||||||
|
trait PieceWeapon extends Piece
|
||||||
|
|
||||||
|
case class Weapon(override val isTome: Boolean, override val job: Job.Job) extends PieceWeapon {
|
||||||
|
val piece: String = "weapon"
|
||||||
|
def withJob(other: Job.Job): Piece = copy(job = other)
|
||||||
|
}
|
||||||
|
|
||||||
|
case class Head(override val isTome: Boolean, override val job: Job.Job) extends PieceBody {
|
||||||
|
val piece: String = "head"
|
||||||
|
def withJob(other: Job.Job): Piece = copy(job = other)
|
||||||
|
}
|
||||||
|
case class Body(override val isTome: Boolean, override val job: Job.Job) extends PieceBody {
|
||||||
|
val piece: String = "body"
|
||||||
|
def withJob(other: Job.Job): Piece = copy(job = other)
|
||||||
|
}
|
||||||
|
case class Hands(override val isTome: Boolean, override val job: Job.Job) extends PieceBody {
|
||||||
|
val piece: String = "hands"
|
||||||
|
def withJob(other: Job.Job): Piece = copy(job = other)
|
||||||
|
}
|
||||||
|
case class Waist(override val isTome: Boolean, 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"
|
||||||
|
def withJob(other: Job.Job): Piece = copy(job = other)
|
||||||
|
}
|
||||||
|
case class Feet(override val isTome: Boolean, override val job: Job.Job) extends PieceBody {
|
||||||
|
val piece: String = "feet"
|
||||||
|
def withJob(other: Job.Job): Piece = copy(job = other)
|
||||||
|
}
|
||||||
|
|
||||||
|
case class Ears(override val isTome: Boolean, override val job: Job.Job) extends PieceAccessory {
|
||||||
|
val piece: String = "ears"
|
||||||
|
def withJob(other: Job.Job): Piece = copy(job = other)
|
||||||
|
}
|
||||||
|
case class Neck(override val isTome: Boolean, override val job: Job.Job) extends PieceAccessory {
|
||||||
|
val piece: String = "neck"
|
||||||
|
def withJob(other: Job.Job): Piece = copy(job = other)
|
||||||
|
}
|
||||||
|
case class Wrist(override val isTome: Boolean, override val job: Job.Job) extends PieceAccessory {
|
||||||
|
val piece: String = "wrist"
|
||||||
|
def withJob(other: Job.Job): Piece = copy(job = other)
|
||||||
|
}
|
||||||
|
case class Ring(override val isTome: Boolean, override val job: Job.Job, override val piece: String = "ring")
|
||||||
|
extends PieceAccessory {
|
||||||
|
def withJob(other: Job.Job): Piece = copy(job = other)
|
||||||
|
override def equals(obj: Any): Boolean = obj match {
|
||||||
|
case Ring(thatIsTome, thatJob, _) => (thatIsTome == isTome) && (thatJob == job)
|
||||||
|
case _ => false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case object AccessoryUpgrade extends PieceUpgrade {
|
||||||
|
val piece: String = "accessory upgrade"
|
||||||
|
}
|
||||||
|
case object BodyUpgrade extends PieceUpgrade {
|
||||||
|
val piece: String = "body upgrade"
|
||||||
|
}
|
||||||
|
case object WeaponUpgrade extends PieceUpgrade {
|
||||||
|
val piece: String = "weapon upgrade"
|
||||||
|
}
|
||||||
|
|
||||||
|
object Piece {
|
||||||
|
def apply(piece: String, isTome: Boolean, job: Job.Job = Job.AnyJob): Piece =
|
||||||
|
piece.toLowerCase match {
|
||||||
|
case "weapon" => Weapon(isTome, job)
|
||||||
|
case "head" => Head(isTome, job)
|
||||||
|
case "body" => Body(isTome, job)
|
||||||
|
case "hands" => Hands(isTome, job)
|
||||||
|
case "waist" => Waist(isTome, job)
|
||||||
|
case "legs" => Legs(isTome, job)
|
||||||
|
case "feet" => Feet(isTome, job)
|
||||||
|
case "ears" => Ears(isTome, job)
|
||||||
|
case "neck" => Neck(isTome, job)
|
||||||
|
case "wrist" => Wrist(isTome, job)
|
||||||
|
case ring @ ("ring" | "left ring" | "right ring") => Ring(isTome, job, ring)
|
||||||
|
case "accessory upgrade" => AccessoryUpgrade
|
||||||
|
case "body upgrade" => BodyUpgrade
|
||||||
|
case "weapon upgrade" => WeaponUpgrade
|
||||||
|
case other => throw new Error(s"Unknown item type $other")
|
||||||
|
}
|
||||||
|
|
||||||
|
lazy val available: Seq[String] = Seq("weapon",
|
||||||
|
"head", "body", "hands", "waist", "legs", "feet",
|
||||||
|
"ears", "neck", "wrist", "left ring", "right ring",
|
||||||
|
"accessory upgrade", "body upgrade", "weapon upgrade")
|
||||||
|
}
|
53
src/main/scala/me/arcanis/ffxivbis/models/Player.scala
Normal file
53
src/main/scala/me/arcanis/ffxivbis/models/Player.scala
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
/*
|
||||||
|
* 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.models
|
||||||
|
|
||||||
|
case class Player(partyId: String,
|
||||||
|
job: Job.Job,
|
||||||
|
nick: String,
|
||||||
|
bis: BiS,
|
||||||
|
loot: Seq[Piece],
|
||||||
|
link: Option[String] = None,
|
||||||
|
priority: Int = 0) {
|
||||||
|
require(job ne Job.AnyJob, "AnyJob is not allowed")
|
||||||
|
|
||||||
|
val playerId: PlayerId = PlayerId(partyId, job, nick)
|
||||||
|
def withBiS(set: Option[BiS]): Player = set match {
|
||||||
|
case Some(value) => copy(bis = value)
|
||||||
|
case None => this
|
||||||
|
}
|
||||||
|
def withCounters(piece: Option[Piece]): PlayerIdWithCounters =
|
||||||
|
PlayerIdWithCounters(
|
||||||
|
partyId, job, nick, isRequired(piece), priority,
|
||||||
|
bisCountTotal(piece), lootCount(piece),
|
||||||
|
lootCountBiS(piece), lootCountTotal(piece))
|
||||||
|
def withLoot(piece: Piece): Player = withLoot(Seq(piece))
|
||||||
|
def withLoot(list: Seq[Piece]): Player = list match {
|
||||||
|
case Nil => this
|
||||||
|
case _ => copy(loot = list)
|
||||||
|
}
|
||||||
|
|
||||||
|
def isRequired(piece: Option[Piece]): Boolean = {
|
||||||
|
piece match {
|
||||||
|
case None => false
|
||||||
|
case Some(p) if !bis.hasPiece(p) => false
|
||||||
|
case Some(p: PieceUpgrade) => bis.upgrades(p) > lootCount(piece)
|
||||||
|
case Some(_) => lootCount(piece) == 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def bisCountTotal(piece: Option[Piece]): Int = bis.pieces.count(!_.isTome)
|
||||||
|
def lootCount(piece: Option[Piece]): Int = piece match {
|
||||||
|
case Some(p) => loot.count(_ == p)
|
||||||
|
case None => lootCountTotal(piece)
|
||||||
|
}
|
||||||
|
def lootCountBiS(piece: Option[Piece]): Int = loot.count(bis.hasPiece)
|
||||||
|
def lootCountTotal(piece: Option[Piece]): Int = loot.length
|
||||||
|
def lootPriority(piece: Piece): Int = priority
|
||||||
|
}
|
35
src/main/scala/me/arcanis/ffxivbis/models/PlayerId.scala
Normal file
35
src/main/scala/me/arcanis/ffxivbis/models/PlayerId.scala
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
/*
|
||||||
|
* 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.models
|
||||||
|
|
||||||
|
import scala.util.Try
|
||||||
|
import scala.util.matching.Regex
|
||||||
|
|
||||||
|
trait PlayerIdBase {
|
||||||
|
def job: Job.Job
|
||||||
|
def nick: String
|
||||||
|
|
||||||
|
override def toString: String = s"$nick ($job)"
|
||||||
|
}
|
||||||
|
|
||||||
|
case class PlayerId(partyId: String, job: Job.Job, nick: String) extends PlayerIdBase
|
||||||
|
|
||||||
|
object PlayerId {
|
||||||
|
def apply(partyId: String, maybeNick: Option[String], maybeJob: Option[String]): Option[PlayerId] =
|
||||||
|
(maybeNick, maybeJob) match {
|
||||||
|
case (Some(nick), Some(job)) => Try(PlayerId(partyId, Job.withName(job), nick)).toOption
|
||||||
|
case _ => None
|
||||||
|
}
|
||||||
|
|
||||||
|
private val prettyPlayerIdRegex: Regex = "^(.*) \\(([A-Z]{3})\\)$".r
|
||||||
|
def apply(partyId: String, player: String): Option[PlayerId] = player match {
|
||||||
|
case s"${prettyPlayerIdRegex(nick, job)}" => Try(PlayerId(partyId, Job.withName(job), nick)).toOption
|
||||||
|
case _ => None
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,53 @@
|
|||||||
|
/*
|
||||||
|
* 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.models
|
||||||
|
|
||||||
|
case class PlayerIdWithCounters(partyId: String,
|
||||||
|
job: Job.Job,
|
||||||
|
nick: String,
|
||||||
|
isRequired: Boolean,
|
||||||
|
priority: Int,
|
||||||
|
bisCountTotal: Int,
|
||||||
|
lootCount: Int,
|
||||||
|
lootCountBiS: Int,
|
||||||
|
lootCountTotal: Int)
|
||||||
|
extends PlayerIdBase {
|
||||||
|
import PlayerIdWithCounters._
|
||||||
|
|
||||||
|
def gt(that: PlayerIdWithCounters, orderBy: Seq[String]): Boolean =
|
||||||
|
withCounters(orderBy) > that.withCounters(orderBy)
|
||||||
|
def isRequiredToString: String = if (isRequired) "yes" else "no"
|
||||||
|
def playerId: PlayerId = PlayerId(partyId, job, nick)
|
||||||
|
|
||||||
|
private val counters: Map[String, Int] = Map(
|
||||||
|
"isRequired" -> (if (isRequired) 1 else 0), // true has more priority
|
||||||
|
"priority" -> -priority, // the less value the more priority
|
||||||
|
"bisCountTotal" -> bisCountTotal, // the more pieces in bis the more priority
|
||||||
|
"lootCount" -> -lootCount, // the less loot got the more priority
|
||||||
|
"lootCountBiS" -> -lootCountBiS, // the less bis pieces looted the more priority
|
||||||
|
"lootCountTotal" -> -lootCountTotal) withDefaultValue 0 // the less pieces looted the more priority
|
||||||
|
|
||||||
|
private def withCounters(orderBy: Seq[String]): PlayerCountersComparator =
|
||||||
|
PlayerCountersComparator(orderBy.map(counters): _*)
|
||||||
|
}
|
||||||
|
|
||||||
|
object PlayerIdWithCounters {
|
||||||
|
private case class PlayerCountersComparator(values: Int*) {
|
||||||
|
def >(that: PlayerCountersComparator): Boolean = {
|
||||||
|
@scala.annotation.tailrec
|
||||||
|
def compareLists(left: List[Int], right: List[Int]): Boolean =
|
||||||
|
(left, right) match {
|
||||||
|
case (hl :: tl, hr :: tr) => if (hl == hr) compareLists(tl, tr) else hl > hr
|
||||||
|
case (_ :: _, Nil) => true
|
||||||
|
case (_, _) => false
|
||||||
|
}
|
||||||
|
compareLists(values.toList, that.values.toList)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
26
src/main/scala/me/arcanis/ffxivbis/models/User.scala
Normal file
26
src/main/scala/me/arcanis/ffxivbis/models/User.scala
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
/*
|
||||||
|
* 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.models
|
||||||
|
|
||||||
|
import org.mindrot.jbcrypt.BCrypt
|
||||||
|
|
||||||
|
object Permission extends Enumeration {
|
||||||
|
val get, post, admin = Value
|
||||||
|
}
|
||||||
|
|
||||||
|
case class User(partyId: String,
|
||||||
|
username: String,
|
||||||
|
password: String,
|
||||||
|
permission: Permission.Value) {
|
||||||
|
|
||||||
|
def hash: String = BCrypt.hashpw(password, BCrypt.gensalt)
|
||||||
|
def verify(plain: String): Boolean = BCrypt.checkpw(plain, password)
|
||||||
|
def verityScope(scope: Permission.Value): Boolean = permission >= scope
|
||||||
|
def withHashedPassword: User = copy(password = hash)
|
||||||
|
}
|
142
src/main/scala/me/arcanis/ffxivbis/service/Ariyala.scala
Normal file
142
src/main/scala/me/arcanis/ffxivbis/service/Ariyala.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
|
||||||
|
|
||||||
|
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", "IsEquippable").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", "IsEquippable") 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
|
||||||
|
}
|
||||||
|
}
|
45
src/main/scala/me/arcanis/ffxivbis/service/Database.scala
Normal file
45
src/main/scala/me/arcanis/ffxivbis/service/Database.scala
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
/*
|
||||||
|
* 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 akka.actor.Actor
|
||||||
|
import com.typesafe.scalalogging.StrictLogging
|
||||||
|
import me.arcanis.ffxivbis.models.{Party, Player, PlayerId}
|
||||||
|
import me.arcanis.ffxivbis.storage.DatabaseProfile
|
||||||
|
|
||||||
|
import scala.concurrent.{ExecutionContext, Future}
|
||||||
|
|
||||||
|
trait Database extends Actor with StrictLogging {
|
||||||
|
implicit def executionContext: ExecutionContext
|
||||||
|
def profile: DatabaseProfile
|
||||||
|
|
||||||
|
override def postStop(): Unit = {
|
||||||
|
profile.db.close()
|
||||||
|
super.postStop()
|
||||||
|
}
|
||||||
|
|
||||||
|
def filterParty(party: Party, maybePlayerId: Option[PlayerId]): Seq[Player] =
|
||||||
|
(party, maybePlayerId) match {
|
||||||
|
case (_, Some(playerId)) => party.player(playerId).map(Seq(_)).getOrElse(Seq.empty)
|
||||||
|
case (_, _) => party.getPlayers
|
||||||
|
}
|
||||||
|
|
||||||
|
def getParty(partyId: String, withBiS: Boolean, withLoot: Boolean): Future[Party] =
|
||||||
|
for {
|
||||||
|
players <- profile.getParty(partyId)
|
||||||
|
bis <- if (withBiS) profile.getPiecesBiS(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)
|
||||||
|
}
|
||||||
|
|
||||||
|
object Database {
|
||||||
|
trait DatabaseRequest {
|
||||||
|
def partyId: String
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,28 @@
|
|||||||
|
/*
|
||||||
|
* 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 me.arcanis.ffxivbis.models.{Piece, Player, PlayerIdWithCounters}
|
||||||
|
|
||||||
|
class LootSelector(players: Seq[Player], piece: Piece, orderBy: Seq[String]) {
|
||||||
|
|
||||||
|
val counters: Seq[PlayerIdWithCounters] = players.map(_.withCounters(Some(piece)))
|
||||||
|
|
||||||
|
def suggest: LootSelector.LootSelectorResult =
|
||||||
|
LootSelector.LootSelectorResult {
|
||||||
|
counters.sortWith { case (left, right) => left.gt(right, orderBy) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
object LootSelector {
|
||||||
|
def apply(players: Seq[Player], piece: Piece, orderBy: Seq[String]): LootSelectorResult =
|
||||||
|
new LootSelector(players, piece, orderBy).suggest
|
||||||
|
|
||||||
|
case class LootSelectorResult(result: Seq[PlayerIdWithCounters])
|
||||||
|
}
|
@ -0,0 +1,73 @@
|
|||||||
|
/*
|
||||||
|
* 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 akka.actor.{Actor, ActorRef, Props}
|
||||||
|
import akka.pattern.{ask, pipe}
|
||||||
|
import akka.util.Timeout
|
||||||
|
import com.typesafe.scalalogging.StrictLogging
|
||||||
|
import me.arcanis.ffxivbis.models.Party
|
||||||
|
|
||||||
|
import scala.concurrent.duration.FiniteDuration
|
||||||
|
import scala.concurrent.{ExecutionContext, Future}
|
||||||
|
|
||||||
|
class PartyService(storage: ActorRef) extends Actor with StrictLogging {
|
||||||
|
import PartyService._
|
||||||
|
import me.arcanis.ffxivbis.utils.Implicits._
|
||||||
|
|
||||||
|
private val cacheTimeout: FiniteDuration =
|
||||||
|
context.system.settings.config.getDuration("me.arcanis.ffxivbis.settings.cache-timeout")
|
||||||
|
implicit private val executionContext: ExecutionContext =
|
||||||
|
context.system.dispatchers.lookup("me.arcanis.ffxivbis.default-dispatcher")
|
||||||
|
implicit private val timeout: Timeout =
|
||||||
|
context.system.settings.config.getDuration("me.arcanis.ffxivbis.settings.request-timeout")
|
||||||
|
|
||||||
|
override def receive: Receive = handle(Map.empty)
|
||||||
|
|
||||||
|
private def handle(cache: Map[String, Party]): Receive = {
|
||||||
|
case ForgetParty(partyId) =>
|
||||||
|
context become handle(cache - partyId)
|
||||||
|
|
||||||
|
case GetNewPartyId =>
|
||||||
|
val client = sender()
|
||||||
|
getPartyId.pipeTo(client)
|
||||||
|
|
||||||
|
case req @ impl.DatabasePartyHandler.GetParty(partyId) =>
|
||||||
|
val client = sender()
|
||||||
|
val party = cache.get(partyId) match {
|
||||||
|
case Some(party) => Future.successful(party)
|
||||||
|
case None =>
|
||||||
|
(storage ? req).mapTo[Party].map { party =>
|
||||||
|
context become handle(cache + (partyId -> party))
|
||||||
|
context.system.scheduler.scheduleOnce(cacheTimeout, self, ForgetParty(partyId))
|
||||||
|
party
|
||||||
|
}
|
||||||
|
}
|
||||||
|
party.pipeTo(client)
|
||||||
|
|
||||||
|
case req: Database.DatabaseRequest =>
|
||||||
|
self ! ForgetParty(req.partyId)
|
||||||
|
storage.forward(req)
|
||||||
|
}
|
||||||
|
|
||||||
|
private def getPartyId: Future[String] = {
|
||||||
|
val partyId = Party.randomPartyId
|
||||||
|
(storage ? impl.DatabaseUserHandler.Exists(partyId)).mapTo[Boolean].flatMap {
|
||||||
|
case true => getPartyId
|
||||||
|
case false => Future.successful(partyId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
object PartyService {
|
||||||
|
def props(storage: ActorRef): Props = Props(new PartyService(storage))
|
||||||
|
|
||||||
|
case class ForgetParty(partyId: String)
|
||||||
|
case object GetNewPartyId
|
||||||
|
}
|
47
src/main/scala/me/arcanis/ffxivbis/service/RateLimiter.scala
Normal file
47
src/main/scala/me/arcanis/ffxivbis/service/RateLimiter.scala
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
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,43 @@
|
|||||||
|
/*
|
||||||
|
* 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
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,31 @@
|
|||||||
|
/*
|
||||||
|
* 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)
|
||||||
|
}
|
@ -0,0 +1,48 @@
|
|||||||
|
/*
|
||||||
|
* 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
|
||||||
|
}
|
@ -0,0 +1,60 @@
|
|||||||
|
/*
|
||||||
|
* 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.{BiS, Player, PlayerId}
|
||||||
|
import me.arcanis.ffxivbis.service.Database
|
||||||
|
|
||||||
|
import scala.concurrent.Future
|
||||||
|
|
||||||
|
trait DatabasePartyHandler { this: Database =>
|
||||||
|
import DatabasePartyHandler._
|
||||||
|
|
||||||
|
def partyHandler: Receive = {
|
||||||
|
case AddPlayer(player) =>
|
||||||
|
val client = sender()
|
||||||
|
profile.insertPlayer(player).pipeTo(client)
|
||||||
|
|
||||||
|
case GetParty(partyId) =>
|
||||||
|
val client = sender()
|
||||||
|
getParty(partyId, withBiS = true, withLoot = true).pipeTo(client)
|
||||||
|
|
||||||
|
case GetPlayer(playerId) =>
|
||||||
|
val client = sender()
|
||||||
|
val player = profile.getPlayerFull(playerId).flatMap { maybePlayerData =>
|
||||||
|
Future.traverse(maybePlayerData.toSeq) { playerData =>
|
||||||
|
for {
|
||||||
|
bis <- profile.getPiecesBiS(playerId)
|
||||||
|
loot <- profile.getPieces(playerId)
|
||||||
|
} yield Player(playerId.partyId, playerId.job, playerId.nick,
|
||||||
|
BiS(bis.map(_.piece)), loot.map(_.piece),
|
||||||
|
playerData.link, playerData.priority)
|
||||||
|
}
|
||||||
|
}.map(_.headOption)
|
||||||
|
player.pipeTo(client)
|
||||||
|
|
||||||
|
case RemovePlayer(playerId) =>
|
||||||
|
val client = sender()
|
||||||
|
profile.deletePlayer(playerId).pipeTo(client)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
object DatabasePartyHandler {
|
||||||
|
case class AddPlayer(player: Player) extends Database.DatabaseRequest {
|
||||||
|
override def partyId: String = player.partyId
|
||||||
|
}
|
||||||
|
case class GetParty(partyId: String) extends Database.DatabaseRequest
|
||||||
|
case class GetPlayer(playerId: PlayerId) extends Database.DatabaseRequest {
|
||||||
|
override def partyId: String = playerId.partyId
|
||||||
|
}
|
||||||
|
case class RemovePlayer(playerId: PlayerId) extends Database.DatabaseRequest {
|
||||||
|
override def partyId: String = playerId.partyId
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,50 @@
|
|||||||
|
/*
|
||||||
|
* 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.User
|
||||||
|
import me.arcanis.ffxivbis.service.Database
|
||||||
|
|
||||||
|
trait DatabaseUserHandler { this: Database =>
|
||||||
|
import DatabaseUserHandler._
|
||||||
|
|
||||||
|
def userHandler: Receive = {
|
||||||
|
case AddUser(user, isHashedPassword) =>
|
||||||
|
val client = sender()
|
||||||
|
val toInsert = if (isHashedPassword) user else user.withHashedPassword
|
||||||
|
profile.insertUser(toInsert).pipeTo(client)
|
||||||
|
|
||||||
|
case DeleteUser(partyId, username) =>
|
||||||
|
val client = sender()
|
||||||
|
profile.deleteUser(partyId, username).pipeTo(client)
|
||||||
|
|
||||||
|
case Exists(partyId) =>
|
||||||
|
val client = sender()
|
||||||
|
profile.exists(partyId).pipeTo(client)
|
||||||
|
|
||||||
|
case GetUser(partyId, username) =>
|
||||||
|
val client = sender()
|
||||||
|
profile.getUser(partyId, username).pipeTo(client)
|
||||||
|
|
||||||
|
case GetUsers(partyId) =>
|
||||||
|
val client = sender()
|
||||||
|
profile.getUsers(partyId).pipeTo(client)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
object DatabaseUserHandler {
|
||||||
|
case class AddUser(user: User, isHashedPassword: Boolean) extends Database.DatabaseRequest {
|
||||||
|
override def partyId: String = user.partyId
|
||||||
|
}
|
||||||
|
case class DeleteUser(partyId: String, username: String) extends Database.DatabaseRequest
|
||||||
|
case class Exists(partyId: String) extends Database.DatabaseRequest
|
||||||
|
case class GetUser(partyId: String, username: String) extends Database.DatabaseRequest
|
||||||
|
case class GetUsers(partyId: String) extends Database.DatabaseRequest
|
||||||
|
}
|
54
src/main/scala/me/arcanis/ffxivbis/storage/BiSProfile.scala
Normal file
54
src/main/scala/me/arcanis/ffxivbis/storage/BiSProfile.scala
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
/*
|
||||||
|
* 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.storage
|
||||||
|
|
||||||
|
import me.arcanis.ffxivbis.models.{Job, Loot, Piece}
|
||||||
|
import slick.lifted.ForeignKeyQuery
|
||||||
|
|
||||||
|
import scala.concurrent.Future
|
||||||
|
|
||||||
|
trait BiSProfile { this: DatabaseProfile =>
|
||||||
|
import dbConfig.profile.api._
|
||||||
|
|
||||||
|
case class BiSRep(playerId: Long, created: Long, piece: String, isTome: Int, job: String) {
|
||||||
|
def toLoot: Loot = Loot(playerId, Piece(piece, isTome == 1, Job.withName(job)))
|
||||||
|
}
|
||||||
|
object BiSRep {
|
||||||
|
def fromPiece(playerId: Long, piece: Piece) =
|
||||||
|
BiSRep(playerId, DatabaseProfile.now, piece.piece, if (piece.isTome) 1 else 0,
|
||||||
|
piece.job.toString)
|
||||||
|
}
|
||||||
|
|
||||||
|
class BiSPieces(tag: Tag) extends Table[BiSRep](tag, "bis") {
|
||||||
|
def playerId: Rep[Long] = column[Long]("player_id", O.PrimaryKey)
|
||||||
|
def created: Rep[Long] = column[Long]("created")
|
||||||
|
def piece: Rep[String] = column[String]("piece", O.PrimaryKey)
|
||||||
|
def isTome: Rep[Int] = column[Int]("is_tome")
|
||||||
|
def job: Rep[String] = column[String]("job")
|
||||||
|
|
||||||
|
def * =
|
||||||
|
(playerId, created, piece, isTome, job) <> ((BiSRep.apply _).tupled, BiSRep.unapply)
|
||||||
|
|
||||||
|
def fkPlayerId: ForeignKeyQuery[Players, PlayerRep] =
|
||||||
|
foreignKey("player_id", playerId, playersTable)(_.playerId, onDelete = ForeignKeyAction.Cascade)
|
||||||
|
}
|
||||||
|
|
||||||
|
def deletePieceBiSById(piece: Piece)(playerId: Long): Future[Int] =
|
||||||
|
db.run(pieceBiS(BiSRep.fromPiece(playerId, piece)).delete)
|
||||||
|
def getPiecesBiSById(playerId: Long): Future[Seq[Loot]] = getPiecesBiSById(Seq(playerId))
|
||||||
|
def getPiecesBiSById(playerIds: Seq[Long]): Future[Seq[Loot]] =
|
||||||
|
db.run(piecesBiS(playerIds).result).map(_.map(_.toLoot))
|
||||||
|
def insertPieceBiSById(piece: Piece)(playerId: Long): Future[Int] =
|
||||||
|
db.run(bisTable.insertOrUpdate(BiSRep.fromPiece(playerId, piece)))
|
||||||
|
|
||||||
|
private def pieceBiS(piece: BiSRep) =
|
||||||
|
piecesBiS(Seq(piece.playerId)).filter(_.piece === piece.piece)
|
||||||
|
private def piecesBiS(playerIds: Seq[Long]) =
|
||||||
|
bisTable.filter(_.playerId.inSet(playerIds.toSet))
|
||||||
|
}
|
@ -0,0 +1,70 @@
|
|||||||
|
/*
|
||||||
|
* 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.storage
|
||||||
|
|
||||||
|
import java.time.Instant
|
||||||
|
|
||||||
|
import com.typesafe.config.Config
|
||||||
|
import me.arcanis.ffxivbis.models.{Loot, Piece, PlayerId}
|
||||||
|
import slick.basic.DatabaseConfig
|
||||||
|
import slick.jdbc.JdbcProfile
|
||||||
|
|
||||||
|
import scala.concurrent.{ExecutionContext, Future}
|
||||||
|
|
||||||
|
class DatabaseProfile(context: ExecutionContext, config: Config)
|
||||||
|
extends BiSProfile with LootProfile with PlayersProfile with UsersProfile {
|
||||||
|
|
||||||
|
implicit val executionContext: ExecutionContext = context
|
||||||
|
|
||||||
|
val dbConfig: DatabaseConfig[JdbcProfile] =
|
||||||
|
DatabaseConfig.forConfig[JdbcProfile]("", DatabaseProfile.getSection(config))
|
||||||
|
import dbConfig.profile.api._
|
||||||
|
val db = dbConfig.db
|
||||||
|
|
||||||
|
val bisTable: TableQuery[BiSPieces] = TableQuery[BiSPieces]
|
||||||
|
val lootTable: TableQuery[LootPieces] = TableQuery[LootPieces]
|
||||||
|
val playersTable: TableQuery[Players] = TableQuery[Players]
|
||||||
|
val usersTable: TableQuery[Users] = TableQuery[Users]
|
||||||
|
|
||||||
|
// generic bis api
|
||||||
|
def deletePieceBiS(playerId: PlayerId, piece: Piece): Future[Int] =
|
||||||
|
byPlayerId(playerId, deletePieceBiSById(piece))
|
||||||
|
def getPiecesBiS(playerId: PlayerId): Future[Seq[Loot]] =
|
||||||
|
byPlayerId(playerId, getPiecesBiSById)
|
||||||
|
def getPiecesBiS(partyId: String): Future[Seq[Loot]] =
|
||||||
|
byPartyId(partyId, getPiecesBiSById)
|
||||||
|
def insertPieceBiS(playerId: PlayerId, piece: Piece): Future[Int] =
|
||||||
|
byPlayerId(playerId, insertPieceBiSById(piece))
|
||||||
|
|
||||||
|
// generic loot api
|
||||||
|
def deletePiece(playerId: PlayerId, piece: Piece): Future[Int] =
|
||||||
|
byPlayerId(playerId, deletePieceById(piece))
|
||||||
|
def getPieces(playerId: PlayerId): Future[Seq[Loot]] =
|
||||||
|
byPlayerId(playerId, getPiecesById)
|
||||||
|
def getPieces(partyId: String): Future[Seq[Loot]] =
|
||||||
|
byPartyId(partyId, getPiecesById)
|
||||||
|
def insertPiece(playerId: PlayerId, piece: Piece): Future[Int] =
|
||||||
|
byPlayerId(playerId, insertPieceById(piece))
|
||||||
|
|
||||||
|
private def byPartyId[T](partyId: String, callback: Seq[Long] => Future[T]): Future[T] =
|
||||||
|
getPlayers(partyId).map(callback).flatten
|
||||||
|
private def byPlayerId[T](playerId: PlayerId, callback: Long => Future[T]): Future[T] =
|
||||||
|
getPlayer(playerId).map {
|
||||||
|
case Some(id) => callback(id)
|
||||||
|
case None => Future.failed(new Error(s"Could not find player $playerId"))
|
||||||
|
}.flatten
|
||||||
|
}
|
||||||
|
|
||||||
|
object DatabaseProfile {
|
||||||
|
def now: Long = Instant.now.toEpochMilli
|
||||||
|
def getSection(config: Config): Config = {
|
||||||
|
val section = config.getString("me.arcanis.ffxivbis.database.mode")
|
||||||
|
config.getConfig("me.arcanis.ffxivbis.database").getConfig(section)
|
||||||
|
}
|
||||||
|
}
|
61
src/main/scala/me/arcanis/ffxivbis/storage/LootProfile.scala
Normal file
61
src/main/scala/me/arcanis/ffxivbis/storage/LootProfile.scala
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
/*
|
||||||
|
* 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.storage
|
||||||
|
|
||||||
|
import me.arcanis.ffxivbis.models.{Job, Loot, Piece}
|
||||||
|
import slick.lifted.{ForeignKeyQuery, Index}
|
||||||
|
|
||||||
|
import scala.concurrent.Future
|
||||||
|
|
||||||
|
trait LootProfile { this: DatabaseProfile =>
|
||||||
|
import dbConfig.profile.api._
|
||||||
|
|
||||||
|
case class LootRep(lootId: Option[Long], playerId: Long, created: Long, piece: String,
|
||||||
|
isTome: Int, job: String) {
|
||||||
|
def toLoot: Loot = Loot(playerId, Piece(piece, isTome == 1, Job.withName(job)))
|
||||||
|
}
|
||||||
|
object LootRep {
|
||||||
|
def fromPiece(playerId: Long, piece: Piece) =
|
||||||
|
LootRep(None, playerId, DatabaseProfile.now, piece.piece, if (piece.isTome) 1 else 0,
|
||||||
|
piece.job.toString)
|
||||||
|
}
|
||||||
|
|
||||||
|
class LootPieces(tag: Tag) extends Table[LootRep](tag, "loot") {
|
||||||
|
def lootId: Rep[Long] = column[Long]("loot_id", O.AutoInc, O.PrimaryKey)
|
||||||
|
def playerId: Rep[Long] = column[Long]("player_id")
|
||||||
|
def created: Rep[Long] = column[Long]("created")
|
||||||
|
def piece: Rep[String] = column[String]("piece")
|
||||||
|
def isTome: Rep[Int] = column[Int]("is_tome")
|
||||||
|
def job: Rep[String] = column[String]("job")
|
||||||
|
|
||||||
|
def * =
|
||||||
|
(lootId.?, playerId, created, piece, isTome, job) <> ((LootRep.apply _).tupled, LootRep.unapply)
|
||||||
|
|
||||||
|
def fkPlayerId: ForeignKeyQuery[Players, PlayerRep] =
|
||||||
|
foreignKey("player_id", playerId, playersTable)(_.playerId, onDelete = ForeignKeyAction.Cascade)
|
||||||
|
def lootOwnerIdx: Index =
|
||||||
|
index("loot_owner_idx", (playerId), unique = false)
|
||||||
|
}
|
||||||
|
|
||||||
|
def deletePieceById(piece: Piece)(playerId: Long): Future[Int] =
|
||||||
|
db.run(pieceLoot(LootRep.fromPiece(playerId, piece)).map(_.lootId).max.result).flatMap {
|
||||||
|
case Some(id) => db.run(lootTable.filter(_.lootId === id).delete)
|
||||||
|
case _ => throw new IllegalArgumentException(s"Could not find piece $piece belong to $playerId")
|
||||||
|
}
|
||||||
|
def getPiecesById(playerId: Long): Future[Seq[Loot]] = getPiecesById(Seq(playerId))
|
||||||
|
def getPiecesById(playerIds: Seq[Long]): Future[Seq[Loot]] =
|
||||||
|
db.run(piecesLoot(playerIds).result).map(_.map(_.toLoot))
|
||||||
|
def insertPieceById(piece: Piece)(playerId: Long): Future[Int] =
|
||||||
|
db.run(lootTable.insertOrUpdate(LootRep.fromPiece(playerId, piece)))
|
||||||
|
|
||||||
|
private def pieceLoot(piece: LootRep) =
|
||||||
|
piecesLoot(Seq(piece.playerId)).filter(_.piece === piece.piece)
|
||||||
|
private def piecesLoot(playerIds: Seq[Long]) =
|
||||||
|
lootTable.filter(_.playerId.inSet(playerIds.toSet))
|
||||||
|
}
|
41
src/main/scala/me/arcanis/ffxivbis/storage/Migration.scala
Normal file
41
src/main/scala/me/arcanis/ffxivbis/storage/Migration.scala
Normal file
@ -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.storage
|
||||||
|
|
||||||
|
import com.typesafe.config.Config
|
||||||
|
import org.flywaydb.core.Flyway
|
||||||
|
import org.flywaydb.core.api.configuration.ClassicConfiguration
|
||||||
|
|
||||||
|
import scala.concurrent.Future
|
||||||
|
|
||||||
|
class Migration(config: Config) {
|
||||||
|
def performMigration(): Future[Int] = {
|
||||||
|
val section = DatabaseProfile.getSection(config)
|
||||||
|
|
||||||
|
val url = section.getString("db.url")
|
||||||
|
val username = section.getString("db.user")
|
||||||
|
val password = section.getString("db.password")
|
||||||
|
|
||||||
|
val provider = url match {
|
||||||
|
case s"jdbc:$p:$_" => p
|
||||||
|
case other => throw new NotImplementedError(s"unknown could not parse jdbc url from $other")
|
||||||
|
}
|
||||||
|
|
||||||
|
val flywayConfiguration = new ClassicConfiguration
|
||||||
|
flywayConfiguration.setLocationsAsStrings(s"db/migration/$provider")
|
||||||
|
flywayConfiguration.setDataSource(url, username, password)
|
||||||
|
val flyway = new Flyway(flywayConfiguration)
|
||||||
|
|
||||||
|
Future.successful(flyway.migrate())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
object Migration {
|
||||||
|
def apply(config: Config): Future[Int] = new Migration(config).performMigration()
|
||||||
|
}
|
@ -0,0 +1,68 @@
|
|||||||
|
/*
|
||||||
|
* 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.storage
|
||||||
|
|
||||||
|
import me.arcanis.ffxivbis.models.{BiS, Job, Player, PlayerId}
|
||||||
|
|
||||||
|
import scala.concurrent.Future
|
||||||
|
|
||||||
|
trait PlayersProfile { this: DatabaseProfile =>
|
||||||
|
import dbConfig.profile.api._
|
||||||
|
|
||||||
|
case class PlayerRep(partyId: String, playerId: Option[Long], created: Long, nick: String,
|
||||||
|
job: String, link: Option[String], priority: Int) {
|
||||||
|
def toPlayer: Player =
|
||||||
|
Player(partyId, Job.withName(job), nick, BiS(Seq.empty), List.empty, link, priority)
|
||||||
|
}
|
||||||
|
object PlayerRep {
|
||||||
|
def fromPlayer(player: Player, id: Option[Long]): PlayerRep =
|
||||||
|
PlayerRep(player.partyId, id, DatabaseProfile.now, player.nick,
|
||||||
|
player.job.toString, player.link, player.priority)
|
||||||
|
}
|
||||||
|
|
||||||
|
class Players(tag: Tag) extends Table[PlayerRep](tag, "players") {
|
||||||
|
def partyId: Rep[String] = column[String]("party_id")
|
||||||
|
def playerId: Rep[Long] = column[Long]("player_id", O.AutoInc, O.PrimaryKey)
|
||||||
|
def created: Rep[Long] = column[Long]("created")
|
||||||
|
def nick: Rep[String] = column[String]("nick")
|
||||||
|
def job: Rep[String] = column[String]("job")
|
||||||
|
def bisLink: Rep[Option[String]] = column[Option[String]]("bis_link")
|
||||||
|
def priority: Rep[Int] = column[Int]("priority", O.Default(1))
|
||||||
|
|
||||||
|
def * =
|
||||||
|
(partyId, playerId.?, created, nick, job, bisLink, priority) <> ((PlayerRep.apply _).tupled, PlayerRep.unapply)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def deletePlayer(playerId: PlayerId): Future[Int] = db.run(player(playerId).delete)
|
||||||
|
def getParty(partyId: String): Future[Map[Long, Player]] =
|
||||||
|
db.run(players(partyId).result).map(_.foldLeft(Map.empty[Long, Player]) {
|
||||||
|
case (acc, p @ PlayerRep(_, Some(id), _, _, _, _, _)) => acc + (id -> p.toPlayer)
|
||||||
|
case (acc, _) => acc
|
||||||
|
})
|
||||||
|
def getPlayer(playerId: PlayerId): Future[Option[Long]] =
|
||||||
|
db.run(player(playerId).map(_.playerId).result.headOption)
|
||||||
|
def getPlayerFull(playerId: PlayerId): Future[Option[Player]] =
|
||||||
|
db.run(player(playerId).result.headOption.map(_.map(_.toPlayer)))
|
||||||
|
def getPlayers(partyId: String): Future[Seq[Long]] =
|
||||||
|
db.run(players(partyId).map(_.playerId).result)
|
||||||
|
def insertPlayer(playerObj: Player): Future[Int] =
|
||||||
|
getPlayer(playerObj.playerId).map {
|
||||||
|
case Some(id) => db.run(playersTable.update(PlayerRep.fromPlayer(playerObj, Some(id))))
|
||||||
|
case _ => db.run(playersTable.insertOrUpdate(PlayerRep.fromPlayer(playerObj, None)))
|
||||||
|
}.flatten
|
||||||
|
|
||||||
|
private def player(playerId: PlayerId) =
|
||||||
|
playersTable
|
||||||
|
.filter(_.partyId === playerId.partyId)
|
||||||
|
.filter(_.job === playerId.job.toString)
|
||||||
|
.filter(_.nick === playerId.nick)
|
||||||
|
private def players(partyId: String) =
|
||||||
|
playersTable.filter(_.partyId === partyId)
|
||||||
|
}
|
@ -0,0 +1,61 @@
|
|||||||
|
/*
|
||||||
|
* 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.storage
|
||||||
|
|
||||||
|
import me.arcanis.ffxivbis.models.{Permission, User}
|
||||||
|
import slick.lifted.{Index, PrimaryKey}
|
||||||
|
|
||||||
|
import scala.concurrent.Future
|
||||||
|
|
||||||
|
trait UsersProfile { this: DatabaseProfile =>
|
||||||
|
import dbConfig.profile.api._
|
||||||
|
|
||||||
|
case class UserRep(partyId: String, userId: Option[Long], username: String, password: String,
|
||||||
|
permission: String) {
|
||||||
|
def toUser: User = User(partyId, username, password, Permission.withName(permission))
|
||||||
|
}
|
||||||
|
object UserRep {
|
||||||
|
def fromUser(user: User, id: Option[Long]): UserRep =
|
||||||
|
UserRep(user.partyId, id, user.username, user.password, user.permission.toString)
|
||||||
|
}
|
||||||
|
|
||||||
|
class Users(tag: Tag) extends Table[UserRep](tag, "users") {
|
||||||
|
def partyId: Rep[String] = column[String]("party_id")
|
||||||
|
def userId: Rep[Long] = column[Long]("user_id", O.AutoInc, O.PrimaryKey)
|
||||||
|
def username: Rep[String] = column[String]("username")
|
||||||
|
def password: Rep[String] = column[String]("password")
|
||||||
|
def permission: Rep[String] = column[String]("permission")
|
||||||
|
|
||||||
|
def * =
|
||||||
|
(partyId, userId.?, username, password, permission) <> ((UserRep.apply _).tupled, UserRep.unapply)
|
||||||
|
|
||||||
|
def pk: PrimaryKey = primaryKey("users_username_idx", (partyId, username))
|
||||||
|
def usersUsernameIdx: Index =
|
||||||
|
index("users_username_idx", (partyId, username), unique = true)
|
||||||
|
}
|
||||||
|
|
||||||
|
def deleteUser(partyId: String, username: String): Future[Int] =
|
||||||
|
db.run(user(partyId, Some(username)).delete)
|
||||||
|
def exists(partyId: String): Future[Boolean] =
|
||||||
|
db.run(user(partyId, None).exists.result)
|
||||||
|
def getUser(partyId: String, username: String): Future[Option[User]] =
|
||||||
|
db.run(user(partyId, Some(username)).result.headOption).map(_.map(_.toUser))
|
||||||
|
def getUsers(partyId: String): Future[Seq[User]] =
|
||||||
|
db.run(user(partyId, None).result).map(_.map(_.toUser))
|
||||||
|
def insertUser(userObj: User): Future[Int] =
|
||||||
|
db.run(user(userObj.partyId, Some(userObj.username)).map(_.userId).result.headOption).map {
|
||||||
|
case Some(id) => db.run(usersTable.insertOrUpdate(UserRep.fromUser(userObj, Some(id))))
|
||||||
|
case _ => db.run(usersTable.insertOrUpdate(UserRep.fromUser(userObj, None)))
|
||||||
|
}.flatten
|
||||||
|
|
||||||
|
private def user(partyId: String, username: Option[String]) =
|
||||||
|
usersTable
|
||||||
|
.filter(_.partyId === partyId)
|
||||||
|
.filterIf(username.isDefined)(_.username === username.orNull)
|
||||||
|
}
|
30
src/main/scala/me/arcanis/ffxivbis/utils/Implicits.scala
Normal file
30
src/main/scala/me/arcanis/ffxivbis/utils/Implicits.scala
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
/*
|
||||||
|
* 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.utils
|
||||||
|
|
||||||
|
import java.time.Duration
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
|
import akka.util.Timeout
|
||||||
|
|
||||||
|
import scala.concurrent.duration.FiniteDuration
|
||||||
|
import scala.language.implicitConversions
|
||||||
|
|
||||||
|
object Implicits {
|
||||||
|
implicit def getBooleanFromOptionString(maybeYes: Option[String]): Boolean = maybeYes.map(_.toLowerCase) match {
|
||||||
|
case Some("yes" | "on") => true
|
||||||
|
case _ => false
|
||||||
|
}
|
||||||
|
|
||||||
|
implicit def getFiniteDuration(duration: Duration): FiniteDuration =
|
||||||
|
FiniteDuration(duration.toNanos, TimeUnit.NANOSECONDS)
|
||||||
|
|
||||||
|
implicit def getTimeout(duration: Duration): Timeout =
|
||||||
|
FiniteDuration(duration.toNanos, TimeUnit.NANOSECONDS)
|
||||||
|
}
|
@ -1,53 +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
|
|
||||||
#
|
|
||||||
from aiohttp.web import middleware, Request, Response
|
|
||||||
from aiohttp_security import AbstractAuthorizationPolicy, check_permission
|
|
||||||
from typing import Callable, Optional
|
|
||||||
|
|
||||||
from service.core.database import Database
|
|
||||||
|
|
||||||
|
|
||||||
class AuthorizationPolicy(AbstractAuthorizationPolicy):
|
|
||||||
|
|
||||||
def __init__(self, database: Database) -> None:
|
|
||||||
self.database = database
|
|
||||||
|
|
||||||
async def authorized_userid(self, identity: str) -> Optional[str]:
|
|
||||||
user = await self.database.get_user(identity)
|
|
||||||
return identity if user is not None else None
|
|
||||||
|
|
||||||
async def permits(self, identity: str, permission: str, context: str = None) -> bool:
|
|
||||||
user = await self.database.get_user(identity)
|
|
||||||
if user is None:
|
|
||||||
return False
|
|
||||||
if user.username != identity:
|
|
||||||
return False
|
|
||||||
if user.permission == 'admin':
|
|
||||||
return True
|
|
||||||
return permission == 'get' or user.permission == permission
|
|
||||||
|
|
||||||
|
|
||||||
def authorize_factory() -> Callable:
|
|
||||||
allowed_paths = {'/', '/favicon.ico', '/api/v1/login', '/api/v1/logout'}
|
|
||||||
allowed_paths_groups = {'/api-docs', '/static'}
|
|
||||||
|
|
||||||
@middleware
|
|
||||||
async def authorize(request: Request, handler: Callable) -> Response:
|
|
||||||
if request.path.startswith('/admin'):
|
|
||||||
permission = 'admin'
|
|
||||||
else:
|
|
||||||
permission = 'get' if request.method in ('GET', 'HEAD') else 'post'
|
|
||||||
if request.path not in allowed_paths \
|
|
||||||
and not any(request.path.startswith(path) for path in allowed_paths_groups):
|
|
||||||
await check_permission(request, permission)
|
|
||||||
|
|
||||||
return await handler(request)
|
|
||||||
|
|
||||||
return authorize
|
|
||||||
|
|
@ -1,34 +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
|
|
||||||
#
|
|
||||||
from enum import Enum
|
|
||||||
from json import JSONEncoder
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
|
|
||||||
class HttpEncoder(JSONEncoder):
|
|
||||||
def default(self, obj: Any) -> Any:
|
|
||||||
if isinstance(obj, dict):
|
|
||||||
data = {}
|
|
||||||
for key, value in obj.items():
|
|
||||||
data[key] = self.default(value)
|
|
||||||
return data
|
|
||||||
elif isinstance(obj, Enum):
|
|
||||||
return obj.name
|
|
||||||
elif hasattr(obj, '_ast'):
|
|
||||||
return self.default(obj._ast())
|
|
||||||
elif hasattr(obj, '__iter__') and not isinstance(obj, str):
|
|
||||||
return [self.default(value) for value in obj]
|
|
||||||
elif hasattr(obj, '__dict__'):
|
|
||||||
data = {
|
|
||||||
key: self.default(value)
|
|
||||||
for key, value in obj.__dict__.items()
|
|
||||||
if not callable(value) and not key.startswith('_')}
|
|
||||||
return data
|
|
||||||
else:
|
|
||||||
return obj
|
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user