mirror of
https://github.com/arcan1s/ffxivbis.git
synced 2025-04-25 17:57:17 +00:00
initial migration to scala
This commit is contained in:
parent
2d84459c4d
commit
e9d5d8c363
161
.gitignore
vendored
161
.gitignore
vendored
@ -1,96 +1,87 @@
|
|||||||
# 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
|
||||||
|
27
build.sbt
Normal file
27
build.sbt
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
name := "ffxivbis-scala"
|
||||||
|
|
||||||
|
version := "0.9.0"
|
||||||
|
|
||||||
|
scalaVersion := "2.13.1"
|
||||||
|
|
||||||
|
scalacOptions ++= Seq("-deprecation", "-feature")
|
||||||
|
|
||||||
|
libraryDependencies += "ch.qos.logback" % "logback-classic" % "1.2.3"
|
||||||
|
libraryDependencies += "com.typesafe.scala-logging" %% "scala-logging" % "3.9.2"
|
||||||
|
|
||||||
|
libraryDependencies += "io.spray" %% "spray-json" % "1.3.5"
|
||||||
|
|
||||||
|
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 += "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,3 +0,0 @@
|
|||||||
[ariyala]
|
|
||||||
ariyala_url = https://ffxiv.ariyala.com
|
|
||||||
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/assembly.sbt
Normal file
1
project/assembly.sbt
Normal file
@ -0,0 +1 @@
|
|||||||
|
addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.14.6")
|
1
project/build.properties
Normal file
1
project/build.properties
Normal file
@ -0,0 +1 @@
|
|||||||
|
sbt.version = 1.3.2
|
@ -1,5 +0,0 @@
|
|||||||
[aliases]
|
|
||||||
test=pytest
|
|
||||||
|
|
||||||
[tool:pytest]
|
|
||||||
addopts = --verbose --pyargs .
|
|
52
setup.py
52
setup.py
@ -1,52 +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/ffxivbis/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',
|
|
||||||
|
|
||||||
package_dir={'': 'src'},
|
|
||||||
packages=find_packages(where='src', exclude=['contrib', 'docs', 'test']),
|
|
||||||
|
|
||||||
install_requires=[
|
|
||||||
'aiohttp==3.6.0',
|
|
||||||
'aiohttp_jinja2',
|
|
||||||
'aiohttp_security',
|
|
||||||
'apispec',
|
|
||||||
'iniherit',
|
|
||||||
'Jinja2',
|
|
||||||
'passlib',
|
|
||||||
'yoyo_migrations'
|
|
||||||
],
|
|
||||||
setup_requires=[
|
|
||||||
'pytest-runner'
|
|
||||||
],
|
|
||||||
tests_require=[
|
|
||||||
'pytest', 'pytest-aiohttp', 'pytest-asyncio'
|
|
||||||
],
|
|
||||||
|
|
||||||
include_package_data=True,
|
|
||||||
|
|
||||||
extras_require={
|
|
||||||
'Postgresql': ['asyncpg'],
|
|
||||||
'SQLite': ['aiosqlite'],
|
|
||||||
'test': ['coverage', 'pytest'],
|
|
||||||
},
|
|
||||||
)
|
|
@ -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 ffxivbis.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
|
|
@ -1,66 +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 Application
|
|
||||||
|
|
||||||
from .views.api.bis import BiSView
|
|
||||||
from .views.api.login import LoginView
|
|
||||||
from .views.api.logout import LogoutView
|
|
||||||
from .views.api.loot import LootView
|
|
||||||
from .views.api.player import PlayerView
|
|
||||||
from .views.html.api import ApiDocVIew, ApiHtmlView
|
|
||||||
from .views.html.bis import BiSHtmlView
|
|
||||||
from .views.html.index import IndexHtmlView
|
|
||||||
from .views.html.loot import LootHtmlView
|
|
||||||
from .views.html.loot_suggest import LootSuggestHtmlView
|
|
||||||
from .views.html.player import PlayerHtmlView
|
|
||||||
from .views.html.static import StaticHtmlView
|
|
||||||
from .views.html.users import UsersHtmlView
|
|
||||||
|
|
||||||
|
|
||||||
def setup_routes(app: Application) -> None:
|
|
||||||
# api routes
|
|
||||||
app.router.add_delete('/admin/api/v1/login/{username}', LoginView)
|
|
||||||
app.router.add_post('/api/v1/login', LoginView)
|
|
||||||
app.router.add_post('/api/v1/logout', LogoutView)
|
|
||||||
app.router.add_put('/admin/api/v1/login', LoginView)
|
|
||||||
|
|
||||||
app.router.add_get('/api/v1/party', PlayerView)
|
|
||||||
app.router.add_post('/api/v1/party', PlayerView)
|
|
||||||
|
|
||||||
app.router.add_get('/api/v1/party/bis', BiSView)
|
|
||||||
app.router.add_post('/api/v1/party/bis', BiSView)
|
|
||||||
app.router.add_put('/api/v1/party/bis', BiSView)
|
|
||||||
|
|
||||||
app.router.add_get('/api/v1/party/loot', LootView)
|
|
||||||
app.router.add_post('/api/v1/party/loot', LootView)
|
|
||||||
app.router.add_put('/api/v1/party/loot', LootView)
|
|
||||||
|
|
||||||
# html routes
|
|
||||||
app.router.add_get('/', IndexHtmlView)
|
|
||||||
app.router.add_get('/static/{resource_id}', StaticHtmlView)
|
|
||||||
|
|
||||||
app.router.add_get('/api-docs', ApiHtmlView)
|
|
||||||
app.router.add_get('/api-docs/swagger.json', ApiDocVIew)
|
|
||||||
|
|
||||||
app.router.add_get('/party', PlayerHtmlView)
|
|
||||||
app.router.add_post('/party', PlayerHtmlView)
|
|
||||||
|
|
||||||
app.router.add_get('/bis', BiSHtmlView)
|
|
||||||
app.router.add_post('/bis', BiSHtmlView)
|
|
||||||
|
|
||||||
app.router.add_get('/loot', LootHtmlView)
|
|
||||||
app.router.add_post('/loot', LootHtmlView)
|
|
||||||
|
|
||||||
app.router.add_get('/suggest', LootSuggestHtmlView)
|
|
||||||
app.router.add_post('/suggest', LootSuggestHtmlView)
|
|
||||||
|
|
||||||
app.router.add_get('/admin/users', UsersHtmlView)
|
|
||||||
app.router.add_post('/admin/users', UsersHtmlView)
|
|
||||||
|
|
||||||
|
|
@ -1,78 +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 Application
|
|
||||||
from apispec import APISpec
|
|
||||||
|
|
||||||
from ffxivbis.core.version import __version__
|
|
||||||
from ffxivbis.models.action import Action
|
|
||||||
from ffxivbis.models.bis import BiS, BiSLink
|
|
||||||
from ffxivbis.models.error import Error
|
|
||||||
from ffxivbis.models.job import Job
|
|
||||||
from ffxivbis.models.loot import Loot
|
|
||||||
from ffxivbis.models.piece import Piece
|
|
||||||
from ffxivbis.models.player import Player, PlayerId, PlayerIdWithCounters
|
|
||||||
from ffxivbis.models.player_edit import PlayerEdit
|
|
||||||
from ffxivbis.models.upgrade import Upgrade
|
|
||||||
from ffxivbis.models.user import User
|
|
||||||
|
|
||||||
|
|
||||||
def get_spec(app: Application) -> APISpec:
|
|
||||||
spec = APISpec(
|
|
||||||
title='FFXIV loot helper',
|
|
||||||
version=__version__,
|
|
||||||
openapi_version='3.0.2',
|
|
||||||
info=dict(description='Loot manager for FFXIV statics'),
|
|
||||||
)
|
|
||||||
|
|
||||||
# routes
|
|
||||||
for route in app.router.routes():
|
|
||||||
path = route.get_info().get('path') or route.get_info().get('formatter')
|
|
||||||
method = route.method.lower()
|
|
||||||
|
|
||||||
spec_method = f'endpoint_{method}_spec'
|
|
||||||
if not hasattr(route.handler, spec_method):
|
|
||||||
continue
|
|
||||||
operations = getattr(route.handler, spec_method)()
|
|
||||||
if not operations:
|
|
||||||
continue
|
|
||||||
|
|
||||||
spec.path(path, operations={method: operations})
|
|
||||||
|
|
||||||
# components
|
|
||||||
spec.components.schema(Action.model_name(), Action.model_spec())
|
|
||||||
spec.components.schema(BiS.model_name(), BiS.model_spec())
|
|
||||||
spec.components.schema(BiSLink.model_name(), BiSLink.model_spec())
|
|
||||||
spec.components.schema(Error.model_name(), Error.model_spec())
|
|
||||||
spec.components.schema(Job.model_name(), Job.model_spec())
|
|
||||||
spec.components.schema(Loot.model_name(), Loot.model_spec())
|
|
||||||
spec.components.schema(Piece.model_name(), Piece.model_spec())
|
|
||||||
spec.components.schema(Player.model_name(), Player.model_spec())
|
|
||||||
spec.components.schema(PlayerEdit.model_name(), PlayerEdit.model_spec())
|
|
||||||
spec.components.schema(PlayerId.model_name(), PlayerId.model_spec())
|
|
||||||
spec.components.schema(PlayerIdWithCounters.model_name(), PlayerIdWithCounters.model_spec())
|
|
||||||
spec.components.schema(Upgrade.model_name(), Upgrade.model_spec())
|
|
||||||
spec.components.schema(User.model_name(), User.model_spec())
|
|
||||||
|
|
||||||
# default responses
|
|
||||||
spec.components.response('BadRequest', dict(
|
|
||||||
description='Bad parameters applied or bad request was formed',
|
|
||||||
content={'application/json': {'schema': {'$ref': Error.model_ref('Error')}}}
|
|
||||||
))
|
|
||||||
spec.components.response('Forbidden', dict(
|
|
||||||
description='User permissions do not allow this action'
|
|
||||||
))
|
|
||||||
spec.components.response('ServerError', dict(
|
|
||||||
description='Server was unable to process request',
|
|
||||||
content={'application/json': {'schema': {'$ref': Error.model_ref('Error')}}}
|
|
||||||
))
|
|
||||||
spec.components.response('Unauthorized', dict(
|
|
||||||
description='User was not authorized'
|
|
||||||
))
|
|
||||||
|
|
||||||
return spec
|
|
@ -1,42 +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
|
|
||||||
#
|
|
||||||
import json
|
|
||||||
|
|
||||||
from aiohttp.web import HTTPException, Response
|
|
||||||
from typing import Any, Mapping, List
|
|
||||||
|
|
||||||
from .json import HttpEncoder
|
|
||||||
|
|
||||||
|
|
||||||
def make_json(response: Any) -> str:
|
|
||||||
return json.dumps(response, cls=HttpEncoder, sort_keys=True)
|
|
||||||
|
|
||||||
|
|
||||||
def wrap_exception(exception: Exception, args: Mapping[str, Any], code: int = 500) -> Response:
|
|
||||||
if isinstance(exception, HTTPException):
|
|
||||||
raise exception # reraise return
|
|
||||||
return wrap_json({
|
|
||||||
'message': repr(exception),
|
|
||||||
'arguments': dict(args)
|
|
||||||
}, code)
|
|
||||||
|
|
||||||
|
|
||||||
def wrap_invalid_param(params: List[str], args: Mapping[str, Any], code: int = 400) -> Response:
|
|
||||||
return wrap_json({
|
|
||||||
'message': f'invalid or missing parameters: `{params}`',
|
|
||||||
'arguments': dict(args)
|
|
||||||
}, code)
|
|
||||||
|
|
||||||
|
|
||||||
def wrap_json(response: Any, code: int = 200) -> Response:
|
|
||||||
return Response(
|
|
||||||
text=make_json(response),
|
|
||||||
status=code,
|
|
||||||
content_type='application/json'
|
|
||||||
)
|
|
@ -1,159 +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 Response
|
|
||||||
from typing import Any, Dict, List, Optional, Type
|
|
||||||
|
|
||||||
from ffxivbis.models.job import Job
|
|
||||||
from ffxivbis.models.piece import Piece
|
|
||||||
from ffxivbis.models.player import PlayerId
|
|
||||||
|
|
||||||
from ffxivbis.api.utils import wrap_exception, wrap_invalid_param, wrap_json
|
|
||||||
from ffxivbis.api.views.common.bis_base import BiSBaseView
|
|
||||||
|
|
||||||
from .openapi import OpenApi
|
|
||||||
|
|
||||||
|
|
||||||
class BiSView(BiSBaseView, OpenApi):
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def endpoint_get_description(cls: Type[OpenApi]) -> Optional[str]:
|
|
||||||
return 'Get party players BiS items'
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def endpoint_get_parameters(cls: Type[OpenApi]) -> List[Dict[str, Any]]:
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
'name': 'nick',
|
|
||||||
'in': 'query',
|
|
||||||
'description': 'player nick name to filter',
|
|
||||||
'required': False,
|
|
||||||
'type': 'string'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def endpoint_get_responses(cls: Type[OpenApi]) -> Dict[str, Any]:
|
|
||||||
return {
|
|
||||||
'200': {'content': {'application/json': { 'schema': {
|
|
||||||
'type': 'array',
|
|
||||||
'items': {
|
|
||||||
'allOf': [{'$ref': cls.model_ref('Piece')}]
|
|
||||||
}}}}}
|
|
||||||
}
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def endpoint_get_summary(cls: Type[OpenApi]) -> Optional[str]:
|
|
||||||
return 'get party BiS items'
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def endpoint_get_tags(cls: Type[OpenApi]) -> List[str]:
|
|
||||||
return ['BiS']
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def endpoint_post_description(cls: Type[OpenApi]) -> Optional[str]:
|
|
||||||
return 'Add new item to player BiS or remove existing'
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def endpoint_post_request_body(cls: Type[OpenApi], content_type: str) -> List[str]:
|
|
||||||
return ['Piece', 'PlayerEdit']
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def endpoint_post_responses(cls: Type[OpenApi]) -> Dict[str, Any]:
|
|
||||||
return {
|
|
||||||
'200': {'content': {'application/json': {'schema': {'$ref': cls.model_ref('Loot')}}}}
|
|
||||||
}
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def endpoint_post_summary(cls: Type[OpenApi]) -> Optional[str]:
|
|
||||||
return 'edit BiS'
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def endpoint_post_tags(cls: Type[OpenApi]) -> List[str]:
|
|
||||||
return ['BiS']
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def endpoint_put_consumes(cls: Type[OpenApi]) -> List[str]:
|
|
||||||
return ['application/json']
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def endpoint_put_description(cls: Type[OpenApi]) -> Optional[str]:
|
|
||||||
return 'Generate new BiS set'
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def endpoint_put_request_body(cls: Type[OpenApi], content_type: str) -> List[str]:
|
|
||||||
return ['BiSLink']
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def endpoint_put_responses(cls: Type[OpenApi]) -> Dict[str, Any]:
|
|
||||||
return {
|
|
||||||
'200': {'content': {'application/json': {'schema': {'$ref': cls.model_ref('BiS')}}}}
|
|
||||||
}
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def endpoint_put_summary(cls: Type[OpenApi]) -> Optional[str]:
|
|
||||||
return 'update BiS'
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def endpoint_put_tags(cls: Type[OpenApi]) -> List[str]:
|
|
||||||
return ['BiS']
|
|
||||||
|
|
||||||
async def get(self) -> Response:
|
|
||||||
try:
|
|
||||||
loot = self.bis_get(self.request.query.getone('nick', None))
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
self.request.app.logger.exception('could not get bis')
|
|
||||||
return wrap_exception(e, self.request.query)
|
|
||||||
|
|
||||||
return wrap_json(loot)
|
|
||||||
|
|
||||||
async def post(self) -> Response:
|
|
||||||
try:
|
|
||||||
data = await self.request.json()
|
|
||||||
except Exception:
|
|
||||||
data = dict(await self.request.post())
|
|
||||||
|
|
||||||
required = ['action', 'is_tome', 'job', 'name', 'nick']
|
|
||||||
if any(param not in data for param in required):
|
|
||||||
return wrap_invalid_param(required, data)
|
|
||||||
|
|
||||||
action = data.get('action')
|
|
||||||
if action not in ('add', 'remove'):
|
|
||||||
return wrap_invalid_param(['action'], data)
|
|
||||||
|
|
||||||
try:
|
|
||||||
player_id = PlayerId(Job[data['job']], data['nick'])
|
|
||||||
piece: Piece = Piece.get(data) # type: ignore
|
|
||||||
await self.bis_post(action, player_id, piece)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
self.request.app.logger.exception('could not add bis')
|
|
||||||
return wrap_exception(e, data)
|
|
||||||
|
|
||||||
return wrap_json({'piece': piece, 'player_id': player_id})
|
|
||||||
|
|
||||||
async def put(self) -> Response:
|
|
||||||
try:
|
|
||||||
data = await self.request.json()
|
|
||||||
except Exception:
|
|
||||||
data = dict(await self.request.post())
|
|
||||||
|
|
||||||
required = ['job', 'link', 'nick']
|
|
||||||
if any(param not in data for param in required):
|
|
||||||
return wrap_invalid_param(required, data)
|
|
||||||
|
|
||||||
try:
|
|
||||||
player_id = PlayerId(Job[data['job']], data['nick'])
|
|
||||||
bis = await self.bis_put(player_id, data['link'])
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
self.request.app.logger.exception('could not parse bis')
|
|
||||||
return wrap_exception(e, data)
|
|
||||||
|
|
||||||
return wrap_json(bis)
|
|
@ -1,139 +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 Response
|
|
||||||
from typing import Any, Dict, List, Optional, Type
|
|
||||||
|
|
||||||
from ffxivbis.api.utils import wrap_exception, wrap_invalid_param, wrap_json
|
|
||||||
from ffxivbis.api.views.common.login_base import LoginBaseView
|
|
||||||
|
|
||||||
from .openapi import OpenApi
|
|
||||||
|
|
||||||
|
|
||||||
class LoginView(LoginBaseView, OpenApi):
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def endpoint_delete_description(cls: Type[OpenApi]) -> Optional[str]:
|
|
||||||
return 'Delete registered user'
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def endpoint_delete_parameters(cls: Type[OpenApi]) -> List[Dict[str, Any]]:
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
'name': 'username',
|
|
||||||
'in': 'path',
|
|
||||||
'description': 'username to remove',
|
|
||||||
'required': True,
|
|
||||||
'type': 'string'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def endpoint_delete_responses(cls: Type[OpenApi]) -> Dict[str, Any]:
|
|
||||||
return {
|
|
||||||
'200': {'content': {'application/json': {'type': 'object'}}}
|
|
||||||
}
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def endpoint_delete_summary(cls: Type[OpenApi]) -> Optional[str]:
|
|
||||||
return 'delete user'
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def endpoint_delete_tags(cls: Type[OpenApi]) -> List[str]:
|
|
||||||
return ['users']
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def endpoint_post_description(cls: Type[OpenApi]) -> Optional[str]:
|
|
||||||
return 'Login as user'
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def endpoint_post_request_body(cls: Type[OpenApi], content_type: str) -> List[str]:
|
|
||||||
return ['User']
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def endpoint_post_responses(cls: Type[OpenApi]) -> Dict[str, Any]:
|
|
||||||
return {
|
|
||||||
'200': {'content': {'application/json': {'type': 'object'}}}
|
|
||||||
}
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def endpoint_post_summary(cls: Type[OpenApi]) -> Optional[str]:
|
|
||||||
return 'login'
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def endpoint_post_tags(cls: Type[OpenApi]) -> List[str]:
|
|
||||||
return ['users']
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def endpoint_put_description(cls: Type[OpenApi]) -> Optional[str]:
|
|
||||||
return 'Create new user'
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def endpoint_put_request_body(cls: Type[OpenApi], content_type: str) -> List[str]:
|
|
||||||
return ['User']
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def endpoint_put_responses(cls: Type[OpenApi]) -> Dict[str, Any]:
|
|
||||||
return {
|
|
||||||
'200': {'content': {'application/json': {'type': 'object'}}}
|
|
||||||
}
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def endpoint_put_summary(cls: Type[OpenApi]) -> Optional[str]:
|
|
||||||
return 'create user'
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def endpoint_put_tags(cls: Type[OpenApi]) -> List[str]:
|
|
||||||
return ['users']
|
|
||||||
|
|
||||||
async def delete(self) -> Response:
|
|
||||||
username = self.request.match_info['username']
|
|
||||||
|
|
||||||
try:
|
|
||||||
await self.remove_user(username)
|
|
||||||
except Exception as e:
|
|
||||||
self.request.app.logger.exception('cannot remove user')
|
|
||||||
return wrap_exception(e, {'username': username})
|
|
||||||
|
|
||||||
return wrap_json({})
|
|
||||||
|
|
||||||
async def post(self) -> Response:
|
|
||||||
try:
|
|
||||||
data = await self.request.json()
|
|
||||||
except Exception:
|
|
||||||
data = dict(await self.request.post())
|
|
||||||
|
|
||||||
required = ['username', 'password']
|
|
||||||
if any(param not in data for param in required):
|
|
||||||
return wrap_invalid_param(required, data)
|
|
||||||
|
|
||||||
try:
|
|
||||||
await self.login(data['username'], data['password'])
|
|
||||||
except Exception as e:
|
|
||||||
self.request.app.logger.exception('cannot login user')
|
|
||||||
return wrap_exception(e, data)
|
|
||||||
|
|
||||||
return wrap_json({})
|
|
||||||
|
|
||||||
async def put(self) -> Response:
|
|
||||||
try:
|
|
||||||
data = await self.request.json()
|
|
||||||
except Exception:
|
|
||||||
data = dict(await self.request.post())
|
|
||||||
|
|
||||||
required = ['username', 'password']
|
|
||||||
if any(param not in data for param in required):
|
|
||||||
return wrap_invalid_param(required, data)
|
|
||||||
|
|
||||||
try:
|
|
||||||
await self.create_user(data['username'], data['password'], data.get('permission', 'get'))
|
|
||||||
except Exception as e:
|
|
||||||
self.request.app.logger.exception('cannot create user')
|
|
||||||
return wrap_exception(e, data)
|
|
||||||
|
|
||||||
return wrap_json({})
|
|
@ -1,46 +0,0 @@
|
|||||||
# 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 Response
|
|
||||||
from typing import Any, Dict, List, Optional, Type
|
|
||||||
|
|
||||||
from ffxivbis.api.utils import wrap_exception, wrap_json
|
|
||||||
from ffxivbis.api.views.common.login_base import LoginBaseView
|
|
||||||
|
|
||||||
from .openapi import OpenApi
|
|
||||||
|
|
||||||
|
|
||||||
class LogoutView(LoginBaseView, OpenApi):
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def endpoint_post_description(cls: Type[OpenApi]) -> Optional[str]:
|
|
||||||
return 'Logout'
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def endpoint_post_request_body(cls: Type[OpenApi], content_type: str) -> List[str]:
|
|
||||||
return []
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def endpoint_post_responses(cls: Type[OpenApi]) -> Dict[str, Any]:
|
|
||||||
return {
|
|
||||||
'200': {'content': {'application/json': {'type': 'object'}}}
|
|
||||||
}
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def endpoint_post_summary(cls: Type[OpenApi]) -> Optional[str]:
|
|
||||||
return 'logout'
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def endpoint_post_tags(cls: Type[OpenApi]) -> List[str]:
|
|
||||||
return ['users']
|
|
||||||
|
|
||||||
async def post(self) -> Response:
|
|
||||||
try:
|
|
||||||
await self.logout()
|
|
||||||
except Exception as e:
|
|
||||||
self.request.app.logger.exception('cannot logout user')
|
|
||||||
return wrap_exception(e, {})
|
|
||||||
|
|
||||||
return wrap_json({})
|
|
@ -1,159 +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 Response
|
|
||||||
from typing import Any, Dict, List, Optional, Type
|
|
||||||
|
|
||||||
from ffxivbis.models.job import Job
|
|
||||||
from ffxivbis.models.piece import Piece
|
|
||||||
from ffxivbis.models.player import PlayerId
|
|
||||||
|
|
||||||
from ffxivbis.api.utils import wrap_exception, wrap_invalid_param, wrap_json
|
|
||||||
from ffxivbis.api.views.common.loot_base import LootBaseView
|
|
||||||
|
|
||||||
from .openapi import OpenApi
|
|
||||||
|
|
||||||
|
|
||||||
class LootView(LootBaseView, OpenApi):
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def endpoint_get_description(cls: Type[OpenApi]) -> Optional[str]:
|
|
||||||
return 'Get party players loot'
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def endpoint_get_parameters(cls: Type[OpenApi]) -> List[Dict[str, Any]]:
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
'name': 'nick',
|
|
||||||
'in': 'query',
|
|
||||||
'description': 'player nick name to filter',
|
|
||||||
'required': False,
|
|
||||||
'type': 'string'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def endpoint_get_responses(cls: Type[OpenApi]) -> Dict[str, Any]:
|
|
||||||
return {
|
|
||||||
'200': {'content': {'application/json': {'schema': {
|
|
||||||
'type': 'array',
|
|
||||||
'items': {
|
|
||||||
'allOf': [{'$ref': cls.model_ref('Piece')}]
|
|
||||||
}}}}}
|
|
||||||
}
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def endpoint_get_summary(cls: Type[OpenApi]) -> Optional[str]:
|
|
||||||
return 'get party loot'
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def endpoint_get_tags(cls: Type[OpenApi]) -> List[str]:
|
|
||||||
return ['loot']
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def endpoint_post_description(cls: Type[OpenApi]) -> Optional[str]:
|
|
||||||
return 'Add new loot item or remove existing'
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def endpoint_post_request_body(cls: Type[OpenApi], content_type: str) -> List[str]:
|
|
||||||
return ['Piece', 'PlayerEdit']
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def endpoint_post_responses(cls: Type[OpenApi]) -> Dict[str, Any]:
|
|
||||||
return {
|
|
||||||
'200': {'content': {'application/json': {'schema': {'$ref': cls.model_ref('Loot')}}}}
|
|
||||||
}
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def endpoint_post_summary(cls: Type[OpenApi]) -> Optional[str]:
|
|
||||||
return 'edit loot'
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def endpoint_post_tags(cls: Type[OpenApi]) -> List[str]:
|
|
||||||
return ['loot']
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def endpoint_put_description(cls: Type[OpenApi]) -> Optional[str]:
|
|
||||||
return 'Suggest loot to party member'
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def endpoint_put_request_body(cls: Type[OpenApi], content_type: str) -> List[str]:
|
|
||||||
return ['Piece']
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def endpoint_put_responses(cls: Type[OpenApi]) -> Dict[str, Any]:
|
|
||||||
return {
|
|
||||||
'200': {'content': {'application/json': {'schema': {
|
|
||||||
'type': 'array',
|
|
||||||
'items': {
|
|
||||||
'allOf': [{'$ref': cls.model_ref('PlayerIdWithCounters')}]
|
|
||||||
}}}}}
|
|
||||||
}
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def endpoint_put_summary(cls: Type[OpenApi]) -> Optional[str]:
|
|
||||||
return 'suggest loot'
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def endpoint_put_tags(cls: Type[OpenApi]) -> List[str]:
|
|
||||||
return ['loot']
|
|
||||||
|
|
||||||
async def get(self) -> Response:
|
|
||||||
try:
|
|
||||||
loot = self.loot_get(self.request.query.getone('nick', None))
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
self.request.app.logger.exception('could not get loot')
|
|
||||||
return wrap_exception(e, self.request.query)
|
|
||||||
|
|
||||||
return wrap_json(loot)
|
|
||||||
|
|
||||||
async def post(self) -> Response:
|
|
||||||
try:
|
|
||||||
data = await self.request.json()
|
|
||||||
except Exception:
|
|
||||||
data = dict(await self.request.post())
|
|
||||||
|
|
||||||
required = ['action', 'is_tome', 'job', 'name', 'nick']
|
|
||||||
if any(param not in data for param in required):
|
|
||||||
return wrap_invalid_param(required, data)
|
|
||||||
|
|
||||||
action = data.get('action')
|
|
||||||
if action not in ('add', 'remove'):
|
|
||||||
return wrap_invalid_param(['action'], data)
|
|
||||||
|
|
||||||
try:
|
|
||||||
player_id = PlayerId(Job[data['job']], data['nick'])
|
|
||||||
piece: Piece = Piece.get(data) # type: ignore
|
|
||||||
await self.loot_post(action, player_id, piece)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
self.request.app.logger.exception('could not add loot')
|
|
||||||
return wrap_exception(e, data)
|
|
||||||
|
|
||||||
return wrap_json({'piece': piece, 'player_id': player_id})
|
|
||||||
|
|
||||||
async def put(self) -> Response:
|
|
||||||
try:
|
|
||||||
data = await self.request.json()
|
|
||||||
except Exception:
|
|
||||||
data = dict(await self.request.post())
|
|
||||||
|
|
||||||
required = ['is_tome', 'name']
|
|
||||||
if any(param not in data for param in required):
|
|
||||||
return wrap_invalid_param(required, data)
|
|
||||||
|
|
||||||
try:
|
|
||||||
piece: Piece = Piece.get(data) # type: ignore
|
|
||||||
players = self.loot_put(piece)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
self.request.app.logger.exception('could not suggest loot')
|
|
||||||
return wrap_exception(e, data)
|
|
||||||
|
|
||||||
return wrap_json(players)
|
|
@ -1,195 +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 __future__ import annotations
|
|
||||||
|
|
||||||
from typing import Any, Dict, List, Optional, Type
|
|
||||||
|
|
||||||
from ffxivbis.models.serializable import Serializable
|
|
||||||
|
|
||||||
|
|
||||||
class OpenApi(Serializable):
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def endpoint_delete_description(cls: Type[OpenApi]) -> Optional[str]:
|
|
||||||
return None
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def endpoint_delete_parameters(cls: Type[OpenApi]) -> List[Dict[str, Any]]:
|
|
||||||
return []
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def endpoint_delete_responses(cls: Type[OpenApi]) -> Dict[str, Any]:
|
|
||||||
return {}
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def endpoint_delete_summary(cls: Type[OpenApi]) -> Optional[str]:
|
|
||||||
return None
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def endpoint_delete_tags(cls: Type[OpenApi]) -> List[str]:
|
|
||||||
return []
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def endpoint_delete_spec(cls: Type[OpenApi]) -> Dict[str, Any]:
|
|
||||||
description = cls.endpoint_delete_description()
|
|
||||||
if description is None:
|
|
||||||
return {}
|
|
||||||
return {
|
|
||||||
'description': description,
|
|
||||||
'parameters': cls.endpoint_delete_parameters(),
|
|
||||||
'responses': cls.endpoint_with_default_responses(cls.endpoint_delete_responses()),
|
|
||||||
'summary': cls.endpoint_delete_summary(),
|
|
||||||
'tags': cls.endpoint_delete_tags()
|
|
||||||
}
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def endpoint_get_description(cls: Type[OpenApi]) -> Optional[str]:
|
|
||||||
return None
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def endpoint_get_parameters(cls: Type[OpenApi]) -> List[Dict[str, Any]]:
|
|
||||||
return []
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def endpoint_get_responses(cls: Type[OpenApi]) -> Dict[str, Any]:
|
|
||||||
return {}
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def endpoint_get_summary(cls: Type[OpenApi]) -> Optional[str]:
|
|
||||||
return None
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def endpoint_get_tags(cls: Type[OpenApi]) -> List[str]:
|
|
||||||
return []
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def endpoint_get_spec(cls: Type[OpenApi]) -> Dict[str, Any]:
|
|
||||||
description = cls.endpoint_get_description()
|
|
||||||
if description is None:
|
|
||||||
return {}
|
|
||||||
return {
|
|
||||||
'description': description,
|
|
||||||
'parameters': cls.endpoint_get_parameters(),
|
|
||||||
'responses': cls.endpoint_with_default_responses(cls.endpoint_get_responses()),
|
|
||||||
'summary': cls.endpoint_get_summary(),
|
|
||||||
'tags': cls.endpoint_get_tags()
|
|
||||||
}
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def endpoint_post_consumes(cls: Type[OpenApi]) -> List[str]:
|
|
||||||
return ['application/json']
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def endpoint_post_description(cls: Type[OpenApi]) -> Optional[str]:
|
|
||||||
return None
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def endpoint_post_request_body(cls: Type[OpenApi], content_type: str) -> List[str]:
|
|
||||||
return []
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def endpoint_post_responses(cls: Type[OpenApi]) -> Dict[str, Any]:
|
|
||||||
return {}
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def endpoint_post_summary(cls: Type[OpenApi]) -> Optional[str]:
|
|
||||||
return None
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def endpoint_post_tags(cls: Type[OpenApi]) -> List[str]:
|
|
||||||
return []
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def endpoint_post_spec(cls: Type[OpenApi]) -> Dict[str, Any]:
|
|
||||||
description = cls.endpoint_post_description()
|
|
||||||
if description is None:
|
|
||||||
return {}
|
|
||||||
return {
|
|
||||||
'consumes': cls.endpoint_post_consumes(),
|
|
||||||
'description': description,
|
|
||||||
'requestBody': {
|
|
||||||
'content': {
|
|
||||||
content_type: {
|
|
||||||
'schema': {'allOf': [
|
|
||||||
{'$ref': cls.model_ref(ref)}
|
|
||||||
for ref in cls.endpoint_post_request_body(content_type)
|
|
||||||
]}
|
|
||||||
}
|
|
||||||
for content_type in cls.endpoint_post_consumes()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
'responses': cls.endpoint_with_default_responses(cls.endpoint_post_responses()),
|
|
||||||
'summary': cls.endpoint_post_summary(),
|
|
||||||
'tags': cls.endpoint_post_tags()
|
|
||||||
}
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def endpoint_put_consumes(cls: Type[OpenApi]) -> List[str]:
|
|
||||||
return ['application/json']
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def endpoint_put_description(cls: Type[OpenApi]) -> Optional[str]:
|
|
||||||
return None
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def endpoint_put_request_body(cls: Type[OpenApi], content_type: str) -> List[str]:
|
|
||||||
return []
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def endpoint_put_responses(cls: Type[OpenApi]) -> Dict[str, Any]:
|
|
||||||
return {}
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def endpoint_put_summary(cls: Type[OpenApi]) -> Optional[str]:
|
|
||||||
return None
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def endpoint_put_tags(cls: Type[OpenApi]) -> List[str]:
|
|
||||||
return []
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def endpoint_put_spec(cls: Type[OpenApi]) -> Dict[str, Any]:
|
|
||||||
description = cls.endpoint_put_description()
|
|
||||||
if description is None:
|
|
||||||
return {}
|
|
||||||
return {
|
|
||||||
'consumes': cls.endpoint_put_consumes(),
|
|
||||||
'description': description,
|
|
||||||
'requestBody': {
|
|
||||||
'content': {
|
|
||||||
content_type: {
|
|
||||||
'schema': {'allOf': [
|
|
||||||
{'$ref': cls.model_ref(ref)}
|
|
||||||
for ref in cls.endpoint_put_request_body(content_type)
|
|
||||||
]}
|
|
||||||
}
|
|
||||||
for content_type in cls.endpoint_put_consumes()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
'responses': cls.endpoint_with_default_responses(cls.endpoint_put_responses()),
|
|
||||||
'summary': cls.endpoint_put_summary(),
|
|
||||||
'tags': cls.endpoint_put_tags()
|
|
||||||
}
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def endpoint_spec(cls: Type[OpenApi], operations: List[str]) -> Dict[str, Any]:
|
|
||||||
return {
|
|
||||||
operation.lower(): getattr(cls, f'endpoint_{operation.lower()}_spec')
|
|
||||||
for operation in operations
|
|
||||||
}
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def endpoint_with_default_responses(cls: Type[OpenApi], responses: Dict[str, Any]) -> Dict[str, Any]:
|
|
||||||
responses.update({
|
|
||||||
'400': {'$ref': cls.model_ref('BadRequest', 'responses')},
|
|
||||||
'401': {'$ref': cls.model_ref('Unauthorized', 'responses')},
|
|
||||||
'403': {'$ref': cls.model_ref('Forbidden', 'responses')},
|
|
||||||
'500': {'$ref': cls.model_ref('ServerError', 'responses')}
|
|
||||||
})
|
|
||||||
return responses
|
|
@ -1,107 +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 Response
|
|
||||||
from typing import Any, Dict, List, Optional, Type
|
|
||||||
|
|
||||||
from ffxivbis.models.job import Job
|
|
||||||
|
|
||||||
from ffxivbis.api.utils import wrap_exception, wrap_invalid_param, wrap_json
|
|
||||||
from ffxivbis.api.views.common.player_base import PlayerBaseView
|
|
||||||
|
|
||||||
from .openapi import OpenApi
|
|
||||||
|
|
||||||
|
|
||||||
class PlayerView(PlayerBaseView, OpenApi):
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def endpoint_get_description(cls: Type[OpenApi]) -> Optional[str]:
|
|
||||||
return 'Get party players with optional nick filter'
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def endpoint_get_parameters(cls: Type[OpenApi]) -> List[Dict[str, Any]]:
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
'name': 'nick',
|
|
||||||
'in': 'query',
|
|
||||||
'description': 'player nick name to filter',
|
|
||||||
'required': False,
|
|
||||||
'type': 'string'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def endpoint_get_responses(cls: Type[OpenApi]) -> Dict[str, Any]:
|
|
||||||
return {
|
|
||||||
'200': {'content': {'application/json': {'schema': {'$ref': cls.model_ref('Player')}}}}
|
|
||||||
}
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def endpoint_get_summary(cls: Type[OpenApi]) -> Optional[str]:
|
|
||||||
return 'get party players'
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def endpoint_get_tags(cls: Type[OpenApi]) -> List[str]:
|
|
||||||
return ['party']
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def endpoint_post_description(cls: Type[OpenApi]) -> Optional[str]:
|
|
||||||
return 'Create new party player or remove existing'
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def endpoint_post_request_body(cls: Type[OpenApi], content_type: str) -> List[str]:
|
|
||||||
return ['PlayerEdit']
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def endpoint_post_responses(cls: Type[OpenApi]) -> Dict[str, Any]:
|
|
||||||
return {
|
|
||||||
'200': {'content': {'application/json': {'schema': {'$ref': cls.model_ref('PlayerId')}}}}
|
|
||||||
}
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def endpoint_post_summary(cls: Type[OpenApi]) -> Optional[str]:
|
|
||||||
return 'add or remove player'
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def endpoint_post_tags(cls: Type[OpenApi]) -> List[str]:
|
|
||||||
return ['party']
|
|
||||||
|
|
||||||
async def get(self) -> Response:
|
|
||||||
try:
|
|
||||||
party = self.player_get(self.request.query.getone('nick', None))
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
self.request.app.logger.exception('could not get party')
|
|
||||||
return wrap_exception(e, self.request.query)
|
|
||||||
|
|
||||||
return wrap_json(party)
|
|
||||||
|
|
||||||
async def post(self) -> Response:
|
|
||||||
try:
|
|
||||||
data = await self.request.json()
|
|
||||||
except Exception:
|
|
||||||
data = dict(await self.request.post())
|
|
||||||
|
|
||||||
required = ['action', 'job', 'nick']
|
|
||||||
if any(param not in data for param in required):
|
|
||||||
return wrap_invalid_param(required, data)
|
|
||||||
priority = data.get('priority', 0)
|
|
||||||
link = data.get('link', None)
|
|
||||||
|
|
||||||
action = data.get('action')
|
|
||||||
if action not in ('add', 'remove'):
|
|
||||||
return wrap_invalid_param(['action'], data)
|
|
||||||
|
|
||||||
try:
|
|
||||||
player_id = await self.player_post(action, Job[data['job']], data['nick'], link, priority)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
self.request.app.logger.exception('could not add loot')
|
|
||||||
return wrap_exception(e, data)
|
|
||||||
|
|
||||||
return wrap_json(player_id)
|
|
@ -1,49 +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 View
|
|
||||||
from typing import List, Optional
|
|
||||||
|
|
||||||
from ffxivbis.core.ariyala_parser import AriyalaParser
|
|
||||||
from ffxivbis.models.bis import BiS
|
|
||||||
from ffxivbis.models.piece import Piece
|
|
||||||
from ffxivbis.models.player import PlayerId
|
|
||||||
|
|
||||||
|
|
||||||
class BiSBaseView(View):
|
|
||||||
|
|
||||||
async def bis_add(self, player_id: PlayerId, piece: Piece) -> Piece:
|
|
||||||
await self.request.app['party'].set_item_bis(player_id, piece)
|
|
||||||
return piece
|
|
||||||
|
|
||||||
def bis_get(self, nick: Optional[str]) -> List[Piece]:
|
|
||||||
party = [
|
|
||||||
player
|
|
||||||
for player in self.request.app['party'].party
|
|
||||||
if nick is None or player.nick == nick
|
|
||||||
]
|
|
||||||
return list(sum([player.bis.pieces for player in party], []))
|
|
||||||
|
|
||||||
async def bis_post(self, action: str, player_id: PlayerId, piece: Piece) -> Optional[Piece]:
|
|
||||||
if action == 'add':
|
|
||||||
return await self.bis_add(player_id, piece)
|
|
||||||
elif action == 'remove':
|
|
||||||
return await self.bis_remove(player_id, piece)
|
|
||||||
return None
|
|
||||||
|
|
||||||
async def bis_put(self, player_id: PlayerId, link: str) -> BiS:
|
|
||||||
parser = AriyalaParser(self.request.app['config'])
|
|
||||||
items = await parser.get(link, player_id.job.name)
|
|
||||||
for piece in items:
|
|
||||||
await self.request.app['party'].set_item_bis(player_id, piece)
|
|
||||||
await self.request.app['party'].set_bis_link(player_id, link)
|
|
||||||
return self.request.app['party'].players[player_id].bis
|
|
||||||
|
|
||||||
async def bis_remove(self, player_id: PlayerId, piece: Piece) -> Piece:
|
|
||||||
await self.request.app['party'].remove_item_bis(player_id, piece)
|
|
||||||
return piece
|
|
@ -1,43 +0,0 @@
|
|||||||
#
|
|
||||||
# Copyright (c) 2019 Evgeniy Alekseev.
|
|
||||||
#
|
|
||||||
# This file is part of ffxivbis
|
|
||||||
# (see https://github.com/arcan1s/ffxivbis).
|
|
||||||
#
|
|
||||||
# License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause
|
|
||||||
#
|
|
||||||
from aiohttp.web import HTTPFound, HTTPUnauthorized, View
|
|
||||||
from aiohttp_security import check_authorized, forget, remember
|
|
||||||
from passlib.hash import md5_crypt
|
|
||||||
|
|
||||||
from ffxivbis.models.user import User
|
|
||||||
|
|
||||||
|
|
||||||
class LoginBaseView(View):
|
|
||||||
|
|
||||||
async def check_credentials(self, username: str, password: str) -> bool:
|
|
||||||
user = await self.request.app['database'].get_user(username)
|
|
||||||
if user is None:
|
|
||||||
return False
|
|
||||||
return md5_crypt.verify(password, user.password)
|
|
||||||
|
|
||||||
async def create_user(self, username: str, password: str, permission: str) -> None:
|
|
||||||
await self.request.app['database'].insert_user(User(username, password, permission), False)
|
|
||||||
|
|
||||||
async def login(self, username: str, password: str) -> None:
|
|
||||||
if await self.check_credentials(username, password):
|
|
||||||
response = HTTPFound('/')
|
|
||||||
await remember(self.request, response, username)
|
|
||||||
raise response
|
|
||||||
|
|
||||||
raise HTTPUnauthorized()
|
|
||||||
|
|
||||||
async def logout(self) -> None:
|
|
||||||
await check_authorized(self.request)
|
|
||||||
response = HTTPFound('/')
|
|
||||||
await forget(self.request, response)
|
|
||||||
|
|
||||||
raise response
|
|
||||||
|
|
||||||
async def remove_user(self, username: str) -> None:
|
|
||||||
await self.request.app['database'].delete_user(username)
|
|
@ -1,43 +0,0 @@
|
|||||||
#
|
|
||||||
# Copyright (c) 2019 Evgeniy Alekseev.
|
|
||||||
#
|
|
||||||
# This file is part of ffxivbis
|
|
||||||
# (see https://github.com/arcan1s/ffxivbis).
|
|
||||||
#
|
|
||||||
# License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause
|
|
||||||
#
|
|
||||||
from aiohttp.web import View
|
|
||||||
from typing import List, Optional, Union
|
|
||||||
|
|
||||||
from ffxivbis.models.piece import Piece
|
|
||||||
from ffxivbis.models.player import PlayerId, PlayerIdWithCounters
|
|
||||||
from ffxivbis.models.upgrade import Upgrade
|
|
||||||
|
|
||||||
|
|
||||||
class LootBaseView(View):
|
|
||||||
|
|
||||||
async def loot_add(self, player_id: PlayerId, piece: Piece) -> Piece:
|
|
||||||
await self.request.app['party'].set_item(player_id, piece)
|
|
||||||
return piece
|
|
||||||
|
|
||||||
def loot_get(self, nick: Optional[str]) -> List[Piece]:
|
|
||||||
party = [
|
|
||||||
player
|
|
||||||
for player in self.request.app['party'].party
|
|
||||||
if nick is None or player.nick == nick
|
|
||||||
]
|
|
||||||
return list(sum([player.loot for player in party], []))
|
|
||||||
|
|
||||||
async def loot_post(self, action: str, player_id: PlayerId, piece: Piece) -> Optional[Piece]:
|
|
||||||
if action == 'add':
|
|
||||||
return await self.loot_add(player_id, piece)
|
|
||||||
elif action == 'remove':
|
|
||||||
return await self.loot_remove(player_id, piece)
|
|
||||||
return None
|
|
||||||
|
|
||||||
def loot_put(self, piece: Union[Piece, Upgrade]) -> List[PlayerIdWithCounters]:
|
|
||||||
return self.request.app['loot'].suggest(piece)
|
|
||||||
|
|
||||||
async def loot_remove(self, player_id: PlayerId, piece: Piece) -> Piece:
|
|
||||||
await self.request.app['party'].remove_item(player_id, piece)
|
|
||||||
return piece
|
|
@ -1,50 +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 View
|
|
||||||
from typing import List, Optional
|
|
||||||
|
|
||||||
from ffxivbis.core.ariyala_parser import AriyalaParser
|
|
||||||
from ffxivbis.models.bis import BiS
|
|
||||||
from ffxivbis.models.job import Job
|
|
||||||
from ffxivbis.models.player import Player, PlayerId
|
|
||||||
|
|
||||||
|
|
||||||
class PlayerBaseView(View):
|
|
||||||
|
|
||||||
async def player_add(self, job: Job, nick: str, link: Optional[str], priority: int) -> PlayerId:
|
|
||||||
player = Player(job, nick, BiS(), [], link, int(priority))
|
|
||||||
player_id = player.player_id
|
|
||||||
await self.request.app['party'].set_player(player)
|
|
||||||
|
|
||||||
if link:
|
|
||||||
parser = AriyalaParser(self.request.app['config'])
|
|
||||||
items = await parser.get(link, job.name)
|
|
||||||
for piece in items:
|
|
||||||
await self.request.app['party'].set_item_bis(player_id, piece)
|
|
||||||
|
|
||||||
return player_id
|
|
||||||
|
|
||||||
def player_get(self, nick: Optional[str]) -> List[Player]:
|
|
||||||
return [
|
|
||||||
player
|
|
||||||
for player in self.request.app['party'].party
|
|
||||||
if nick is None or player.nick == nick
|
|
||||||
]
|
|
||||||
|
|
||||||
async def player_post(self, action: str, job: Job, nick: str, link: Optional[str], priority: int) -> Optional[PlayerId]:
|
|
||||||
if action == 'add':
|
|
||||||
return await self.player_add(job, nick, link, priority)
|
|
||||||
elif action == 'remove':
|
|
||||||
return await self.player_remove(job, nick)
|
|
||||||
return None
|
|
||||||
|
|
||||||
async def player_remove(self, job: Job, nick: str) -> PlayerId:
|
|
||||||
player_id = PlayerId(job, nick)
|
|
||||||
await self.request.app['party'].remove_player(player_id)
|
|
||||||
return player_id
|
|
@ -1,29 +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
|
|
||||||
#
|
|
||||||
import json
|
|
||||||
|
|
||||||
from aiohttp.web import Response, View
|
|
||||||
from aiohttp_jinja2 import template
|
|
||||||
from typing import Any, Dict
|
|
||||||
|
|
||||||
|
|
||||||
class ApiDocVIew(View):
|
|
||||||
|
|
||||||
async def get(self) -> Response:
|
|
||||||
return Response(
|
|
||||||
text=json.dumps(self.request.app['spec'].to_dict()),
|
|
||||||
status=200,
|
|
||||||
content_type='application/json'
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class ApiHtmlView(View):
|
|
||||||
|
|
||||||
@template('api.jinja2')
|
|
||||||
async def get(self) -> Dict[str, Any]:
|
|
||||||
return {}
|
|
@ -1,82 +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 HTTPFound, Response
|
|
||||||
from aiohttp_jinja2 import template
|
|
||||||
from typing import Any, Dict, List
|
|
||||||
|
|
||||||
from ffxivbis.models.piece import Piece
|
|
||||||
from ffxivbis.models.player import Player, PlayerId
|
|
||||||
|
|
||||||
from ffxivbis.api.utils import wrap_exception, wrap_invalid_param
|
|
||||||
from ffxivbis.api.views.common.bis_base import BiSBaseView
|
|
||||||
from ffxivbis.api.views.common.player_base import PlayerBaseView
|
|
||||||
|
|
||||||
|
|
||||||
class BiSHtmlView(BiSBaseView, PlayerBaseView):
|
|
||||||
|
|
||||||
@template('bis.jinja2')
|
|
||||||
async def get(self) -> Dict[str, Any]:
|
|
||||||
error = None
|
|
||||||
items: List[Dict[str, str]] = []
|
|
||||||
players: List[Player] = []
|
|
||||||
|
|
||||||
try:
|
|
||||||
players = self.player_get(None)
|
|
||||||
items = [
|
|
||||||
{
|
|
||||||
'player': player.player_id.pretty_name,
|
|
||||||
'piece': piece.name,
|
|
||||||
'is_tome': 'yes' if piece.is_tome else 'no'
|
|
||||||
}
|
|
||||||
for player in players
|
|
||||||
for piece in player.bis.pieces
|
|
||||||
]
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
self.request.app.logger.exception('could not get bis')
|
|
||||||
error = repr(e)
|
|
||||||
|
|
||||||
return {
|
|
||||||
'items': items,
|
|
||||||
'pieces': Piece.available(),
|
|
||||||
'players': [player.player_id.pretty_name for player in players],
|
|
||||||
'request_error': error
|
|
||||||
}
|
|
||||||
|
|
||||||
async def post(self) -> Response:
|
|
||||||
data = await self.request.post()
|
|
||||||
|
|
||||||
required = ['method', 'player']
|
|
||||||
if any(param not in data for param in required):
|
|
||||||
return wrap_invalid_param(required, data)
|
|
||||||
|
|
||||||
try:
|
|
||||||
method = data.getone('method')
|
|
||||||
player_id = PlayerId.from_pretty_name(data.getone('player')) # type: ignore
|
|
||||||
|
|
||||||
if method == 'post':
|
|
||||||
required = ['action', 'piece']
|
|
||||||
if any(param not in data for param in required):
|
|
||||||
return wrap_invalid_param(required, data)
|
|
||||||
is_tome = (data.getone('is_tome', None) == 'on')
|
|
||||||
await self.bis_post(data.getone('action'), player_id, # type: ignore
|
|
||||||
Piece.get({'piece': data.getone('piece'), 'is_tome': is_tome})) # type: ignore
|
|
||||||
|
|
||||||
elif method == 'put':
|
|
||||||
required = ['bis']
|
|
||||||
if any(param not in data for param in required):
|
|
||||||
return wrap_invalid_param(required, data)
|
|
||||||
|
|
||||||
await self.bis_put(player_id, data.getone('bis')) # type: ignore
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
self.request.app.logger.exception('could not manage bis')
|
|
||||||
return wrap_exception(e, data)
|
|
||||||
|
|
||||||
return HTTPFound(self.request.url)
|
|
@ -1,23 +0,0 @@
|
|||||||
#
|
|
||||||
# Copyright (c) 2019 Evgeniy Alekseev.
|
|
||||||
#
|
|
||||||
# This file is part of ffxivbis
|
|
||||||
# (see https://github.com/arcan1s/ffxivbis).
|
|
||||||
#
|
|
||||||
# License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause
|
|
||||||
#
|
|
||||||
from aiohttp.web import View
|
|
||||||
from aiohttp_jinja2 import template
|
|
||||||
from aiohttp_security import authorized_userid
|
|
||||||
from typing import Any, Dict
|
|
||||||
|
|
||||||
|
|
||||||
class IndexHtmlView(View):
|
|
||||||
|
|
||||||
@template('index.jinja2')
|
|
||||||
async def get(self) -> Dict[str, Any]:
|
|
||||||
username = await authorized_userid(self.request)
|
|
||||||
|
|
||||||
return {
|
|
||||||
'logged': username
|
|
||||||
}
|
|
@ -1,70 +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 HTTPFound, Response
|
|
||||||
from aiohttp_jinja2 import template
|
|
||||||
from typing import Any, Dict, List
|
|
||||||
|
|
||||||
from ffxivbis.models.piece import Piece
|
|
||||||
from ffxivbis.models.player import Player, PlayerId
|
|
||||||
from ffxivbis.models.upgrade import Upgrade
|
|
||||||
|
|
||||||
from ffxivbis.api.utils import wrap_exception, wrap_invalid_param
|
|
||||||
from ffxivbis.api.views.common.loot_base import LootBaseView
|
|
||||||
from ffxivbis.api.views.common.player_base import PlayerBaseView
|
|
||||||
|
|
||||||
|
|
||||||
class LootHtmlView(LootBaseView, PlayerBaseView):
|
|
||||||
|
|
||||||
@template('loot.jinja2')
|
|
||||||
async def get(self) -> Dict[str, Any]:
|
|
||||||
error = None
|
|
||||||
items: List[Dict[str, str]] = []
|
|
||||||
players: List[Player] = []
|
|
||||||
|
|
||||||
try:
|
|
||||||
players = self.player_get(None)
|
|
||||||
items = [
|
|
||||||
{
|
|
||||||
'player': player.player_id.pretty_name,
|
|
||||||
'piece': piece.name,
|
|
||||||
'is_tome': 'yes' if getattr(piece, 'is_tome', True) else 'no'
|
|
||||||
}
|
|
||||||
for player in players
|
|
||||||
for piece in player.loot
|
|
||||||
]
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
self.request.app.logger.exception('could not get loot')
|
|
||||||
error = repr(e)
|
|
||||||
|
|
||||||
return {
|
|
||||||
'items': items,
|
|
||||||
'pieces': Piece.available() + [upgrade.name for upgrade in Upgrade],
|
|
||||||
'players': [player.player_id.pretty_name for player in players],
|
|
||||||
'request_error': error
|
|
||||||
}
|
|
||||||
|
|
||||||
async def post(self) -> Response:
|
|
||||||
data = await self.request.post()
|
|
||||||
|
|
||||||
required = ['action', 'piece', 'player']
|
|
||||||
if any(param not in data for param in required):
|
|
||||||
return wrap_invalid_param(required, data)
|
|
||||||
|
|
||||||
try:
|
|
||||||
player_id = PlayerId.from_pretty_name(data.getone('player')) # type: ignore
|
|
||||||
is_tome = (data.getone('is_tome', None) == 'on')
|
|
||||||
await self.loot_post(data.getone('action'), player_id, # type: ignore
|
|
||||||
Piece.get({'piece': data.getone('piece'), 'is_tome': is_tome})) # type: ignore
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
self.request.app.logger.exception('could not manage loot')
|
|
||||||
return wrap_exception(e, data)
|
|
||||||
|
|
||||||
return HTTPFound(self.request.url)
|
|
@ -1,64 +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 Response
|
|
||||||
from aiohttp_jinja2 import template
|
|
||||||
from typing import Any, Dict, List, Union
|
|
||||||
|
|
||||||
from ffxivbis.models.piece import Piece
|
|
||||||
from ffxivbis.models.player import PlayerIdWithCounters
|
|
||||||
from ffxivbis.models.upgrade import Upgrade
|
|
||||||
|
|
||||||
from ffxivbis.api.utils import wrap_invalid_param
|
|
||||||
from ffxivbis.api.views.common.loot_base import LootBaseView
|
|
||||||
from ffxivbis.api.views.common.player_base import PlayerBaseView
|
|
||||||
|
|
||||||
|
|
||||||
class LootSuggestHtmlView(LootBaseView, PlayerBaseView):
|
|
||||||
|
|
||||||
@template('loot_suggest.jinja2')
|
|
||||||
async def get(self) -> Dict[str, Any]:
|
|
||||||
return {
|
|
||||||
'pieces': Piece.available() + [upgrade.name for upgrade in Upgrade]
|
|
||||||
}
|
|
||||||
|
|
||||||
@template('loot_suggest.jinja2')
|
|
||||||
async def post(self) -> Union[Dict[str, Any], Response]:
|
|
||||||
data = await self.request.post()
|
|
||||||
error = None
|
|
||||||
item_values: Dict[str, Any] = {}
|
|
||||||
players: List[PlayerIdWithCounters] = []
|
|
||||||
|
|
||||||
required = ['piece']
|
|
||||||
if any(param not in data for param in required):
|
|
||||||
return wrap_invalid_param(required, data)
|
|
||||||
|
|
||||||
try:
|
|
||||||
piece = Piece.get({'piece': data.getone('piece'), 'is_tome': data.getone('is_tome', False)})
|
|
||||||
players = self.loot_put(piece)
|
|
||||||
item_values = {'piece': piece.name, 'is_tome': getattr(piece, 'is_tome', True)}
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
self.request.app.logger.exception('could not manage loot')
|
|
||||||
error = repr(e)
|
|
||||||
|
|
||||||
return {
|
|
||||||
'item': item_values,
|
|
||||||
'pieces': Piece.available() + [upgrade.name for upgrade in Upgrade],
|
|
||||||
'request_error': error,
|
|
||||||
'suggest': [
|
|
||||||
{
|
|
||||||
'player': player.pretty_name,
|
|
||||||
'is_required': 'yes' if player.is_required else 'no',
|
|
||||||
'loot_count': player.loot_count,
|
|
||||||
'loot_count_bis': player.loot_count_bis,
|
|
||||||
'loot_count_total': player.loot_count_total
|
|
||||||
}
|
|
||||||
for player in players
|
|
||||||
]
|
|
||||||
}
|
|
@ -1,67 +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 HTTPFound, Response
|
|
||||||
from aiohttp_jinja2 import template
|
|
||||||
from typing import Any, Dict, List
|
|
||||||
|
|
||||||
from ffxivbis.models.job import Job
|
|
||||||
from ffxivbis.models.player import PlayerIdWithCounters
|
|
||||||
|
|
||||||
from ffxivbis.api.utils import wrap_exception, wrap_invalid_param
|
|
||||||
from ffxivbis.api.views.common.player_base import PlayerBaseView
|
|
||||||
|
|
||||||
|
|
||||||
class PlayerHtmlView(PlayerBaseView):
|
|
||||||
|
|
||||||
@template('party.jinja2')
|
|
||||||
async def get(self) -> Dict[str, Any]:
|
|
||||||
counters: List[PlayerIdWithCounters] = []
|
|
||||||
error = None
|
|
||||||
|
|
||||||
try:
|
|
||||||
party = self.player_get(None)
|
|
||||||
counters = [player.player_id_with_counters(None) for player in party]
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
self.request.app.logger.exception('could not get party')
|
|
||||||
error = repr(e)
|
|
||||||
|
|
||||||
return {
|
|
||||||
'jobs': [job.name for job in Job],
|
|
||||||
'players': [
|
|
||||||
{
|
|
||||||
'job': player.job.name,
|
|
||||||
'nick': player.nick,
|
|
||||||
'loot_count_bis': player.loot_count_bis,
|
|
||||||
'loot_count_total': player.loot_count_total,
|
|
||||||
'priority': player.priority
|
|
||||||
}
|
|
||||||
for player in counters
|
|
||||||
],
|
|
||||||
'request_error': error
|
|
||||||
}
|
|
||||||
|
|
||||||
async def post(self) -> Response:
|
|
||||||
data = await self.request.post()
|
|
||||||
|
|
||||||
required = ['action', 'job', 'nick']
|
|
||||||
if any(param not in data for param in required):
|
|
||||||
return wrap_invalid_param(required, data)
|
|
||||||
|
|
||||||
try:
|
|
||||||
action = data.getone('action')
|
|
||||||
priority = data.getone('priority', 0)
|
|
||||||
link = data.getone('bis', None)
|
|
||||||
await self.player_post(action, Job[data['job'].upper()], data['nick'], link, priority) # type: ignore
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
self.request.app.logger.exception('could not manage players')
|
|
||||||
return wrap_exception(e, data)
|
|
||||||
|
|
||||||
return HTTPFound(self.request.url)
|
|
@ -1,31 +0,0 @@
|
|||||||
# Copyright (c) 2019 Evgeniy Alekseev.
|
|
||||||
#
|
|
||||||
# This file is part of ffxivbis
|
|
||||||
# (see https://github.com/arcan1s/ffxivbis).
|
|
||||||
#
|
|
||||||
# License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause
|
|
||||||
#
|
|
||||||
import os
|
|
||||||
|
|
||||||
from aiohttp.web import HTTPNotFound, Response, View
|
|
||||||
|
|
||||||
|
|
||||||
class StaticHtmlView(View):
|
|
||||||
|
|
||||||
def __get_content_type(self, filename: str) -> str:
|
|
||||||
_, ext = os.path.splitext(filename)
|
|
||||||
if ext == '.css':
|
|
||||||
return 'text/css'
|
|
||||||
elif ext == '.js':
|
|
||||||
return 'text/javascript'
|
|
||||||
return 'text/plain'
|
|
||||||
|
|
||||||
async def get(self) -> Response:
|
|
||||||
resource_name = self.request.match_info['resource_id']
|
|
||||||
resource_path = os.path.join(self.request.app['templates_root'], 'static', resource_name)
|
|
||||||
if not os.path.exists(resource_path) or os.path.isdir(resource_path):
|
|
||||||
return HTTPNotFound()
|
|
||||||
content_type = self.__get_content_type(resource_name)
|
|
||||||
|
|
||||||
with open(resource_path) as resource_file:
|
|
||||||
return Response(text=resource_file.read(), content_type=content_type)
|
|
@ -1,62 +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 HTTPFound, Response
|
|
||||||
from aiohttp_jinja2 import template
|
|
||||||
from typing import Any, Dict, List
|
|
||||||
|
|
||||||
from ffxivbis.models.user import User
|
|
||||||
|
|
||||||
from ffxivbis.api.utils import wrap_exception, wrap_invalid_param
|
|
||||||
from ffxivbis.api.views.common.login_base import LoginBaseView
|
|
||||||
|
|
||||||
|
|
||||||
class UsersHtmlView(LoginBaseView):
|
|
||||||
|
|
||||||
@template('users.jinja2')
|
|
||||||
async def get(self) -> Dict[str, Any]:
|
|
||||||
error = None
|
|
||||||
users: List[User] = []
|
|
||||||
|
|
||||||
try:
|
|
||||||
users = await self.request.app['database'].get_users()
|
|
||||||
except Exception as e:
|
|
||||||
self.request.app.logger.exception('could not get users')
|
|
||||||
error = repr(e)
|
|
||||||
|
|
||||||
return {
|
|
||||||
'request_error': error,
|
|
||||||
'users': users
|
|
||||||
}
|
|
||||||
|
|
||||||
async def post(self) -> Response:
|
|
||||||
data = await self.request.post()
|
|
||||||
|
|
||||||
required = ['action', 'username']
|
|
||||||
if any(param not in data for param in required):
|
|
||||||
return wrap_invalid_param(required, data)
|
|
||||||
|
|
||||||
try:
|
|
||||||
action = data.getone('action')
|
|
||||||
username = str(data.getone('username'))
|
|
||||||
|
|
||||||
if action == 'add':
|
|
||||||
required = ['password', 'permission']
|
|
||||||
if any(param not in data for param in required):
|
|
||||||
return wrap_invalid_param(required, data)
|
|
||||||
await self.create_user(username, data.getone('password'), data.getone('permission')) # type: ignore
|
|
||||||
elif action == 'remove':
|
|
||||||
await self.remove_user(username)
|
|
||||||
else:
|
|
||||||
return wrap_invalid_param(['action'], data)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
self.request.app.logger.exception('could not manage users')
|
|
||||||
return wrap_exception(e, data)
|
|
||||||
|
|
||||||
return HTTPFound(self.request.url)
|
|
@ -1,71 +0,0 @@
|
|||||||
#
|
|
||||||
# Copyright (c) 2019 Evgeniy Alekseev.
|
|
||||||
#
|
|
||||||
# This file is part of ffxivbis
|
|
||||||
# (see https://github.com/arcan1s/ffxivbis).
|
|
||||||
#
|
|
||||||
# License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause
|
|
||||||
#
|
|
||||||
import aiohttp_jinja2
|
|
||||||
import jinja2
|
|
||||||
import logging
|
|
||||||
|
|
||||||
from aiohttp import web
|
|
||||||
from aiohttp_security import setup as setup_security
|
|
||||||
from aiohttp_security import CookiesIdentityPolicy
|
|
||||||
|
|
||||||
from ffxivbis.core.config import Configuration
|
|
||||||
from ffxivbis.core.database import Database
|
|
||||||
from ffxivbis.core.loot_selector import LootSelector
|
|
||||||
from ffxivbis.core.party import Party
|
|
||||||
|
|
||||||
from .auth import AuthorizationPolicy, authorize_factory
|
|
||||||
from .routes import setup_routes
|
|
||||||
from .spec import get_spec
|
|
||||||
|
|
||||||
|
|
||||||
async def on_shutdown(app: web.Application) -> None:
|
|
||||||
app.logger.warning('server terminated')
|
|
||||||
|
|
||||||
|
|
||||||
def run_server(app: web.Application) -> None:
|
|
||||||
app.logger.info('start server')
|
|
||||||
web.run_app(app,
|
|
||||||
host=app['config'].get('web', 'host'),
|
|
||||||
port=app['config'].getint('web', 'port'),
|
|
||||||
handle_signals=False)
|
|
||||||
|
|
||||||
def setup_service(config: Configuration, database: Database, loot: LootSelector, party: Party) -> web.Application:
|
|
||||||
app = web.Application(logger=logging.getLogger('http'))
|
|
||||||
app.on_shutdown.append(on_shutdown)
|
|
||||||
|
|
||||||
app.middlewares.append(web.normalize_path_middleware(append_slash=False, remove_slash=True))
|
|
||||||
|
|
||||||
# auth related
|
|
||||||
auth_required = config.getboolean('auth', 'enabled')
|
|
||||||
if auth_required:
|
|
||||||
setup_security(app, CookiesIdentityPolicy(), AuthorizationPolicy(database))
|
|
||||||
app.middlewares.append(authorize_factory())
|
|
||||||
|
|
||||||
# routes
|
|
||||||
app.logger.info('setup routes')
|
|
||||||
setup_routes(app)
|
|
||||||
if config.has_option('web', 'templates'):
|
|
||||||
templates_root = app['templates_root'] = config.get('web', 'templates')
|
|
||||||
app['static_root_url'] = '/static'
|
|
||||||
aiohttp_jinja2.setup(app, loader=jinja2.FileSystemLoader(templates_root))
|
|
||||||
app['spec'] = get_spec(app)
|
|
||||||
|
|
||||||
app.logger.info('setup configuration')
|
|
||||||
app['config'] = config
|
|
||||||
|
|
||||||
app.logger.info('setup database')
|
|
||||||
app['database'] = database
|
|
||||||
|
|
||||||
app.logger.info('setup loot selector')
|
|
||||||
app['loot'] = loot
|
|
||||||
|
|
||||||
app.logger.info('setup party worker')
|
|
||||||
app['party'] = party
|
|
||||||
|
|
||||||
return app
|
|
@ -1,31 +0,0 @@
|
|||||||
#
|
|
||||||
# Copyright (c) 2019 Evgeniy Alekseev.
|
|
||||||
#
|
|
||||||
# This file is part of ffxivbis
|
|
||||||
# (see https://github.com/arcan1s/ffxivbis).
|
|
||||||
#
|
|
||||||
# License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause
|
|
||||||
#
|
|
||||||
from ffxivbis.core.config import Configuration
|
|
||||||
|
|
||||||
from .core import Application
|
|
||||||
|
|
||||||
|
|
||||||
def get_config(config_path: str) -> Configuration:
|
|
||||||
config = Configuration()
|
|
||||||
config.load(config_path, {})
|
|
||||||
config.load_logging()
|
|
||||||
|
|
||||||
return config
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
import argparse
|
|
||||||
|
|
||||||
parser = argparse.ArgumentParser(description='Simple loot recorder for FFXIV')
|
|
||||||
parser.add_argument('-c', '--config', help='configuration path', default='ffxivbis.ini')
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
config = get_config(args.config)
|
|
||||||
app = Application(config)
|
|
||||||
app.run()
|
|
@ -1,41 +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
|
|
||||||
#
|
|
||||||
import asyncio
|
|
||||||
import logging
|
|
||||||
|
|
||||||
from ffxivbis.api.web import run_server, setup_service
|
|
||||||
from ffxivbis.core.config import Configuration
|
|
||||||
from ffxivbis.core.database import Database
|
|
||||||
from ffxivbis.core.loot_selector import LootSelector
|
|
||||||
from ffxivbis.core.party import Party
|
|
||||||
from ffxivbis.models.user import User
|
|
||||||
|
|
||||||
|
|
||||||
class Application:
|
|
||||||
|
|
||||||
def __init__(self, config: Configuration) -> None:
|
|
||||||
self.config = config
|
|
||||||
self.logger = logging.getLogger('application')
|
|
||||||
|
|
||||||
def run(self) -> None:
|
|
||||||
loop = asyncio.get_event_loop()
|
|
||||||
|
|
||||||
database = loop.run_until_complete(Database.get(self.config))
|
|
||||||
database.migration()
|
|
||||||
|
|
||||||
party = loop.run_until_complete(Party.get(database))
|
|
||||||
|
|
||||||
admin = User(self.config.get('auth', 'root_username'), self.config.get('auth', 'root_password'), 'admin')
|
|
||||||
loop.run_until_complete(database.insert_user(admin, True))
|
|
||||||
|
|
||||||
priority = self.config.get('settings', 'priority').split()
|
|
||||||
loot_selector = LootSelector(party, priority)
|
|
||||||
|
|
||||||
web = setup_service(self.config, database, loot_selector, party)
|
|
||||||
run_server(web)
|
|
@ -1,83 +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
|
|
||||||
#
|
|
||||||
import os
|
|
||||||
import socket
|
|
||||||
|
|
||||||
from aiohttp import ClientSession
|
|
||||||
from typing import Dict, List, Optional
|
|
||||||
|
|
||||||
from ffxivbis.models.piece import Piece
|
|
||||||
|
|
||||||
from .config import Configuration
|
|
||||||
|
|
||||||
|
|
||||||
class AriyalaParser:
|
|
||||||
|
|
||||||
def __init__(self, config: Configuration) -> None:
|
|
||||||
self.ariyala_url = config.get('ariyala', 'ariyala_url')
|
|
||||||
self.xivapi_key = config.get('ariyala', 'xivapi_key', fallback=None)
|
|
||||||
self.xivapi_url = config.get('ariyala', 'xivapi_url')
|
|
||||||
|
|
||||||
def __remap_key(self, key: str) -> Optional[str]:
|
|
||||||
if key == 'mainhand':
|
|
||||||
return 'weapon'
|
|
||||||
elif key == 'chest':
|
|
||||||
return 'body'
|
|
||||||
elif key == 'ringLeft':
|
|
||||||
return 'left_ring'
|
|
||||||
elif key == 'ringRight':
|
|
||||||
return 'right_ring'
|
|
||||||
elif key in ('head', 'hands', 'waist', 'legs', 'feet', 'ears', 'neck', 'wrist'):
|
|
||||||
return key
|
|
||||||
return None
|
|
||||||
|
|
||||||
async def get(self, url: str, job: str) -> List[Piece]:
|
|
||||||
items = await self.get_ids(url, job)
|
|
||||||
return [
|
|
||||||
Piece.get({'piece': slot, 'is_tome': await self.get_is_tome(item_id)}) # type: ignore
|
|
||||||
for slot, item_id in items.items()
|
|
||||||
]
|
|
||||||
|
|
||||||
async def get_ids(self, url: str, job: str) -> Dict[str, int]:
|
|
||||||
norm_path = os.path.normpath(url)
|
|
||||||
set_id = os.path.basename(norm_path)
|
|
||||||
async with ClientSession() as session:
|
|
||||||
async with session.get(f'{self.ariyala_url}/store.app', params={'identifier': set_id}) as response:
|
|
||||||
response.raise_for_status()
|
|
||||||
data = await response.json(content_type='text/html')
|
|
||||||
|
|
||||||
# it has job in response but for some reasons job name differs sometimes from one in dictionary,
|
|
||||||
# e.g. http://ffxiv.ariyala.com/store.app?identifier=1AJB8
|
|
||||||
api_job = data['content']
|
|
||||||
try:
|
|
||||||
bis = data['datasets'][api_job]['normal']['items']
|
|
||||||
except KeyError:
|
|
||||||
bis = data['datasets'][job]['normal']['items']
|
|
||||||
|
|
||||||
result: Dict[str, int] = {}
|
|
||||||
for original_key, value in bis.items():
|
|
||||||
key = self.__remap_key(original_key)
|
|
||||||
if key is None:
|
|
||||||
continue
|
|
||||||
result[key] = value
|
|
||||||
return result
|
|
||||||
|
|
||||||
async def get_is_tome(self, item_id: int) -> bool:
|
|
||||||
params = {'columns': 'IsEquippable'}
|
|
||||||
if self.xivapi_key is not None:
|
|
||||||
params['private_key'] = self.xivapi_key
|
|
||||||
|
|
||||||
async with ClientSession() as session:
|
|
||||||
# for some reasons ipv6 does not work for me
|
|
||||||
session.connector._family = socket.AF_INET # type: ignore
|
|
||||||
async with session.get(f'{self.xivapi_url}/item/{item_id}', params=params) as response:
|
|
||||||
response.raise_for_status()
|
|
||||||
data = await response.json()
|
|
||||||
|
|
||||||
return data['IsEquippable'] == 0 # don't ask
|
|
@ -1,65 +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
|
|
||||||
#
|
|
||||||
import configparser
|
|
||||||
import os
|
|
||||||
|
|
||||||
from logging.config import fileConfig
|
|
||||||
from typing import Any, Dict, Mapping, Optional
|
|
||||||
|
|
||||||
from .exceptions import MissingConfiguration
|
|
||||||
|
|
||||||
|
|
||||||
class Configuration(configparser.RawConfigParser):
|
|
||||||
|
|
||||||
def __init__(self) -> None:
|
|
||||||
configparser.RawConfigParser.__init__(self, allow_no_value=True)
|
|
||||||
self.path: Optional[str] = None
|
|
||||||
self.root_path: Optional[str] = None
|
|
||||||
|
|
||||||
@property
|
|
||||||
def include(self) -> str:
|
|
||||||
return self.__with_root_path(self.get('settings', 'include'))
|
|
||||||
|
|
||||||
def __load_section(self, conf: str) -> None:
|
|
||||||
self.read(os.path.join(self.include, conf))
|
|
||||||
|
|
||||||
def __with_root_path(self, path: str) -> str:
|
|
||||||
if self.root_path is None:
|
|
||||||
return path
|
|
||||||
return os.path.join(self.root_path, path)
|
|
||||||
|
|
||||||
def get_section(self, section: str) -> Dict[str, str]:
|
|
||||||
if not self.has_section(section):
|
|
||||||
raise MissingConfiguration(section)
|
|
||||||
return dict(self[section])
|
|
||||||
|
|
||||||
def load(self, path: str, values: Mapping[str, Mapping[str, Any]]) -> None:
|
|
||||||
self.path = path
|
|
||||||
self.root_path = os.path.dirname(self.path)
|
|
||||||
|
|
||||||
self.read(self.path)
|
|
||||||
self.load_includes()
|
|
||||||
|
|
||||||
# don't use direct ConfigParser.update here, it overrides whole section
|
|
||||||
for section, options in values.items():
|
|
||||||
if section not in self:
|
|
||||||
self.add_section(section)
|
|
||||||
for key, value in options.items():
|
|
||||||
self.set(section, key, value)
|
|
||||||
|
|
||||||
def load_includes(self) -> None:
|
|
||||||
try:
|
|
||||||
include_dir = self.include
|
|
||||||
for conf in filter(lambda p: p.endswith('.ini'), sorted(os.listdir(include_dir))):
|
|
||||||
self.__load_section(conf)
|
|
||||||
except (FileNotFoundError, configparser.NoOptionError, configparser.NoSectionError):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def load_logging(self) -> None:
|
|
||||||
fileConfig(self.__with_root_path(self.get('settings', 'logging')))
|
|
@ -1,110 +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 __future__ import annotations
|
|
||||||
|
|
||||||
import datetime
|
|
||||||
import logging
|
|
||||||
|
|
||||||
from yoyo import get_backend, read_migrations
|
|
||||||
from typing import List, Mapping, Optional, Type, Union
|
|
||||||
|
|
||||||
from ffxivbis.models.loot import Loot
|
|
||||||
from ffxivbis.models.piece import Piece
|
|
||||||
from ffxivbis.models.player import Player, PlayerId
|
|
||||||
from ffxivbis.models.upgrade import Upgrade
|
|
||||||
from ffxivbis.models.user import User
|
|
||||||
|
|
||||||
from .config import Configuration
|
|
||||||
from .exceptions import InvalidDatabase
|
|
||||||
|
|
||||||
|
|
||||||
class Database:
|
|
||||||
|
|
||||||
def __init__(self, migrations_path: str) -> None:
|
|
||||||
self.migrations_path = migrations_path
|
|
||||||
self.logger = logging.getLogger('database')
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def now() -> int:
|
|
||||||
return int(datetime.datetime.now().timestamp())
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
async def get(cls: Type[Database], config: Configuration) -> Database:
|
|
||||||
database_type = config.get('settings', 'database')
|
|
||||||
database_settings = config.get_section(database_type)
|
|
||||||
|
|
||||||
if database_type == 'sqlite':
|
|
||||||
from .sqlite import SQLiteDatabase
|
|
||||||
obj: Type[Database] = SQLiteDatabase
|
|
||||||
elif database_type == 'postgres':
|
|
||||||
from .postgres import PostgresDatabase
|
|
||||||
obj = PostgresDatabase
|
|
||||||
else:
|
|
||||||
raise InvalidDatabase(database_type)
|
|
||||||
|
|
||||||
database = obj(**database_settings)
|
|
||||||
await database.init()
|
|
||||||
return database
|
|
||||||
|
|
||||||
@property
|
|
||||||
def connection(self) -> str:
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
async def init(self) -> None:
|
|
||||||
pass
|
|
||||||
|
|
||||||
async def delete_piece(self, player_id: PlayerId, piece: Union[Piece, Upgrade]) -> None:
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
async def delete_piece_bis(self, player_id: PlayerId, piece: Piece) -> None:
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
async def delete_player(self, player_id: PlayerId) -> None:
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
async def delete_user(self, username: str) -> None:
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
async def get_party(self) -> List[Player]:
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
async def get_player(self, player_id: PlayerId) -> Optional[int]:
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
async def get_user(self, username: str) -> Optional[User]:
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
async def get_users(self) -> List[User]:
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
async def insert_piece(self, player_id: PlayerId, piece: Union[Piece, Upgrade]) -> None:
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
async def insert_piece_bis(self, player_id: PlayerId, piece: Piece) -> None:
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
async def insert_player(self, player: Player) -> None:
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
async def insert_user(self, user: User, hashed_password: bool) -> None:
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
def migration(self) -> None:
|
|
||||||
self.logger.info('perform migrations')
|
|
||||||
backend = get_backend(self.connection)
|
|
||||||
migrations = read_migrations(self.migrations_path)
|
|
||||||
with backend.lock():
|
|
||||||
backend.apply_migrations(backend.to_apply(migrations))
|
|
||||||
|
|
||||||
def set_loot(self, party: Mapping[int, Player], bis: List[Loot], loot: List[Loot]) -> List[Player]:
|
|
||||||
for piece in bis:
|
|
||||||
party[piece.player_id].bis.set_item(piece.piece)
|
|
||||||
for piece in loot:
|
|
||||||
party[piece.player_id].loot.append(piece.piece)
|
|
||||||
return list(party.values())
|
|
@ -1,27 +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 typing import Any, Mapping
|
|
||||||
|
|
||||||
|
|
||||||
class InvalidDatabase(Exception):
|
|
||||||
|
|
||||||
def __init__(self, database_type: str) -> None:
|
|
||||||
Exception.__init__(self, f'Unsupported database {database_type}')
|
|
||||||
|
|
||||||
|
|
||||||
class InvalidDataRow(Exception):
|
|
||||||
|
|
||||||
def __init__(self, data: Mapping[str, Any]) -> None:
|
|
||||||
Exception.__init__(self, f'Invalid data row `{data}`')
|
|
||||||
|
|
||||||
|
|
||||||
class MissingConfiguration(Exception):
|
|
||||||
|
|
||||||
def __init__(self, section: str) -> None:
|
|
||||||
Exception.__init__(self, f'Missing configuration section {section}')
|
|
@ -1,32 +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 typing import Iterable, List, Tuple, Union
|
|
||||||
|
|
||||||
from ffxivbis.models.player import Player, PlayerIdWithCounters
|
|
||||||
from ffxivbis.models.piece import Piece
|
|
||||||
from ffxivbis.models.upgrade import Upgrade
|
|
||||||
|
|
||||||
from .party import Party
|
|
||||||
|
|
||||||
|
|
||||||
class LootSelector:
|
|
||||||
|
|
||||||
def __init__(self, party: Party, order_by: List[str] = None) -> None:
|
|
||||||
self.party = party
|
|
||||||
self.order_by = order_by or ['is_required', 'loot_count_bis', 'loot_count_total', 'loot_count', 'loot_priority']
|
|
||||||
|
|
||||||
def __order_by(self, player: Player, piece: Union[Piece, Upgrade]) -> Tuple:
|
|
||||||
return tuple(map(lambda method: getattr(player, method)(piece), self.order_by))
|
|
||||||
|
|
||||||
def __sorted_by(self, piece: Union[Piece, Upgrade]) -> Iterable[Player]:
|
|
||||||
# pycharm is lying, don't trust it
|
|
||||||
return sorted(self.party.players.values(), key=lambda player: self.__order_by(player, piece), reverse=True)
|
|
||||||
|
|
||||||
def suggest(self, piece: Union[Piece, Upgrade]) -> List[PlayerIdWithCounters]:
|
|
||||||
return [player.player_id_with_counters(piece) for player in self.__sorted_by(piece)]
|
|
@ -1,81 +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 __future__ import annotations
|
|
||||||
|
|
||||||
from threading import Lock
|
|
||||||
from typing import Dict, List, Optional, Type, Union
|
|
||||||
|
|
||||||
from ffxivbis.models.piece import Piece
|
|
||||||
from ffxivbis.models.player import Player, PlayerId
|
|
||||||
from ffxivbis.models.upgrade import Upgrade
|
|
||||||
|
|
||||||
from .database import Database
|
|
||||||
|
|
||||||
|
|
||||||
class Party:
|
|
||||||
|
|
||||||
def __init__(self, database: Database) -> None:
|
|
||||||
self.lock = Lock()
|
|
||||||
self.players: Dict[PlayerId, Player] = {}
|
|
||||||
self.database = database
|
|
||||||
|
|
||||||
@property
|
|
||||||
def party(self) -> List[Player]:
|
|
||||||
with self.lock:
|
|
||||||
return list(self.players.values())
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
async def get(cls: Type[Party], database: Database) -> Party:
|
|
||||||
obj = Party(database)
|
|
||||||
players = await database.get_party()
|
|
||||||
for player in players:
|
|
||||||
obj.players[player.player_id] = player
|
|
||||||
return obj
|
|
||||||
|
|
||||||
async def set_bis_link(self, player_id: PlayerId, link: str) -> None:
|
|
||||||
with self.lock:
|
|
||||||
player = self.players[player_id]
|
|
||||||
player.link = link
|
|
||||||
await self.database.insert_player(player)
|
|
||||||
|
|
||||||
async def remove_player(self, player_id: PlayerId) -> Optional[Player]:
|
|
||||||
await self.database.delete_player(player_id)
|
|
||||||
with self.lock:
|
|
||||||
player = self.players.pop(player_id, None)
|
|
||||||
return player
|
|
||||||
|
|
||||||
async def set_player(self, player: Player) -> PlayerId:
|
|
||||||
player_id = player.player_id
|
|
||||||
await self.database.insert_player(player)
|
|
||||||
with self.lock:
|
|
||||||
self.players[player_id] = player
|
|
||||||
return player_id
|
|
||||||
|
|
||||||
async def set_item(self, player_id: PlayerId, piece: Union[Piece, Upgrade]) -> None:
|
|
||||||
await self.database.insert_piece(player_id, piece)
|
|
||||||
with self.lock:
|
|
||||||
self.players[player_id].loot.append(piece)
|
|
||||||
|
|
||||||
async def remove_item(self, player_id: PlayerId, piece: Union[Piece, Upgrade]) -> None:
|
|
||||||
await self.database.delete_piece(player_id, piece)
|
|
||||||
with self.lock:
|
|
||||||
try:
|
|
||||||
self.players[player_id].loot.remove(piece)
|
|
||||||
except ValueError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
async def set_item_bis(self, player_id: PlayerId, piece: Piece) -> None:
|
|
||||||
await self.database.insert_piece_bis(player_id, piece)
|
|
||||||
with self.lock:
|
|
||||||
self.players[player_id].bis.set_item(piece)
|
|
||||||
|
|
||||||
async def remove_item_bis(self, player_id: PlayerId, piece: Piece) -> None:
|
|
||||||
await self.database.delete_piece_bis(player_id, piece)
|
|
||||||
with self.lock:
|
|
||||||
self.players[player_id].bis.remove_item(piece)
|
|
@ -1,164 +0,0 @@
|
|||||||
#
|
|
||||||
# Copyright (c) 2019 Evgeniy Alekseev.
|
|
||||||
#
|
|
||||||
# This file is part of ffxivbis
|
|
||||||
# (see https://github.com/arcan1s/ffxivbis).
|
|
||||||
#
|
|
||||||
# License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause
|
|
||||||
#
|
|
||||||
import asyncpg
|
|
||||||
|
|
||||||
from passlib.hash import md5_crypt
|
|
||||||
from psycopg2.extras import DictCursor
|
|
||||||
from typing import List, Optional, Union
|
|
||||||
|
|
||||||
from ffxivbis.models.bis import BiS
|
|
||||||
from ffxivbis.models.job import Job
|
|
||||||
from ffxivbis.models.loot import Loot
|
|
||||||
from ffxivbis.models.piece import Piece
|
|
||||||
from ffxivbis.models.player import Player, PlayerId
|
|
||||||
from ffxivbis.models.upgrade import Upgrade
|
|
||||||
from ffxivbis.models.user import User
|
|
||||||
|
|
||||||
from .database import Database
|
|
||||||
|
|
||||||
|
|
||||||
class PostgresDatabase(Database):
|
|
||||||
|
|
||||||
def __init__(self, host: str, port: int, username: str, password: str, database: str, migrations_path: str) -> None:
|
|
||||||
Database.__init__(self, migrations_path)
|
|
||||||
self.host = host
|
|
||||||
self.port = int(port)
|
|
||||||
self.username = username
|
|
||||||
self.password = password
|
|
||||||
self.database = database
|
|
||||||
self.pool: asyncpg.pool.Pool = None # type: ignore
|
|
||||||
|
|
||||||
@property
|
|
||||||
def connection(self) -> str:
|
|
||||||
return f'postgresql://{self.username}:{self.password}@{self.host}:{self.port}/{self.database}'
|
|
||||||
|
|
||||||
async def init(self) -> None:
|
|
||||||
self.pool = await asyncpg.create_pool(host=self.host, port=self.port, username=self.username,
|
|
||||||
password=self.password, database=self.database)
|
|
||||||
|
|
||||||
async def delete_piece(self, player_id: PlayerId, piece: Union[Piece, Upgrade]) -> None:
|
|
||||||
player = await self.get_player(player_id)
|
|
||||||
if player is None:
|
|
||||||
return
|
|
||||||
|
|
||||||
async with self.pool.acquire() as conn:
|
|
||||||
await conn.execute(
|
|
||||||
'''delete from loot
|
|
||||||
where loot_id in (
|
|
||||||
select loot_id from loot
|
|
||||||
where player_id = $1 and piece = $2 and is_tome = $3 order by created desc limit 1
|
|
||||||
)''',
|
|
||||||
player, piece.name, getattr(piece, 'is_tome', True)
|
|
||||||
)
|
|
||||||
|
|
||||||
async def delete_piece_bis(self, player_id: PlayerId, piece: Piece) -> None:
|
|
||||||
player = await self.get_player(player_id)
|
|
||||||
if player is None:
|
|
||||||
return
|
|
||||||
|
|
||||||
async with self.pool.acquire() as conn:
|
|
||||||
await conn.execute(
|
|
||||||
'''delete from bis where player_id = $1 and piece = $2''',
|
|
||||||
player, piece.name)
|
|
||||||
|
|
||||||
async def delete_player(self, player_id: PlayerId) -> None:
|
|
||||||
async with self.pool.acquire() as conn:
|
|
||||||
await conn.execute('''delete from players where nick = $1 and job = $2''',
|
|
||||||
player_id.nick, player_id.job.name)
|
|
||||||
|
|
||||||
async def delete_user(self, username: str) -> None:
|
|
||||||
async with self.pool.acquire() as conn:
|
|
||||||
await conn.execute('''delete from users where username = $1''', username)
|
|
||||||
|
|
||||||
async def get_party(self) -> List[Player]:
|
|
||||||
async with self.pool.acquire() as conn:
|
|
||||||
rows = await conn.fetch('''select * from bis''')
|
|
||||||
bis_pieces = [Loot(row['player_id'], Piece.get(row)) for row in rows]
|
|
||||||
|
|
||||||
rows = await conn.fetch('''select * from loot''')
|
|
||||||
loot_pieces = [Loot(row['player_id'], Piece.get(row)) for row in rows]
|
|
||||||
|
|
||||||
rows = await conn.fetch('''select * from players''')
|
|
||||||
party = {
|
|
||||||
row['player_id']: Player(Job[row['job']], row['nick'], BiS(), [], row['bis_link'], row['priority'])
|
|
||||||
for row in rows
|
|
||||||
}
|
|
||||||
|
|
||||||
return self.set_loot(party, bis_pieces, loot_pieces)
|
|
||||||
|
|
||||||
async def get_player(self, player_id: PlayerId) -> Optional[int]:
|
|
||||||
async with self.pool.acquire() as conn:
|
|
||||||
player = await conn.fetchrow('''select player_id from players where nick = $1 and job = $2''',
|
|
||||||
player_id.nick, player_id.job.name)
|
|
||||||
return player['player_id'] if player is not None else None
|
|
||||||
|
|
||||||
async def get_user(self, username: str) -> Optional[User]:
|
|
||||||
async with self.pool.acquire() as conn:
|
|
||||||
user = await conn.fetchrow('''select * from users where username = $1''', username)
|
|
||||||
return User(user['username'], user['password'], user['permission']) if user is not None else None
|
|
||||||
|
|
||||||
async def get_users(self) -> List[User]:
|
|
||||||
async with self.pool.acquire() as conn:
|
|
||||||
users = await conn.fetch('''select * from users''')
|
|
||||||
return [User(user['username'], user['password'], user['permission']) for user in users]
|
|
||||||
|
|
||||||
async def insert_piece(self, player_id: PlayerId, piece: Union[Piece, Upgrade]) -> None:
|
|
||||||
player = await self.get_player(player_id)
|
|
||||||
if player is None:
|
|
||||||
return
|
|
||||||
|
|
||||||
async with self.pool.acquire() as conn:
|
|
||||||
await conn.execute(
|
|
||||||
'''insert into loot
|
|
||||||
(created, piece, is_tome, player_id)
|
|
||||||
values
|
|
||||||
($1, $2, $3, $4)''',
|
|
||||||
Database.now(), piece.name, getattr(piece, 'is_tome', True), player
|
|
||||||
)
|
|
||||||
|
|
||||||
async def insert_piece_bis(self, player_id: PlayerId, piece: Piece) -> None:
|
|
||||||
player = await self.get_player(player_id)
|
|
||||||
if player is None:
|
|
||||||
return
|
|
||||||
|
|
||||||
async with self.pool.acquire() as conn:
|
|
||||||
await conn.execute(
|
|
||||||
'''insert into bis
|
|
||||||
(created, piece, is_tome, player_id)
|
|
||||||
values
|
|
||||||
($1, $2, $3, $4)
|
|
||||||
on conflict on constraint bis_piece_player_id_idx do update set
|
|
||||||
created = $1, is_tome = $3''',
|
|
||||||
Database.now(), piece.name, piece.is_tome, player
|
|
||||||
)
|
|
||||||
|
|
||||||
async def insert_player(self, player: Player) -> None:
|
|
||||||
async with self.pool.acquire() as conn:
|
|
||||||
await conn.execute(
|
|
||||||
'''insert into players
|
|
||||||
(created, nick, job, bis_link, priority)
|
|
||||||
values
|
|
||||||
($1, $2, $3, $4, $5)
|
|
||||||
on conflict on constraint players_nick_job_idx do update set
|
|
||||||
created = $1, bis_link = $4, priority = $5''',
|
|
||||||
Database.now(), player.nick, player.job.name, player.link, player.priority
|
|
||||||
)
|
|
||||||
|
|
||||||
async def insert_user(self, user: User, hashed_password: bool) -> None:
|
|
||||||
password = user.password if hashed_password else md5_crypt.hash(user.password)
|
|
||||||
async with self.pool.acquire() as conn:
|
|
||||||
await conn.execute(
|
|
||||||
'''insert into users
|
|
||||||
(username, password, permission)
|
|
||||||
values
|
|
||||||
($1, $2, $3)
|
|
||||||
on conflict on constraint users_username_idx do update set
|
|
||||||
password = $2, permission = $3''',
|
|
||||||
user.username, password, user.permission
|
|
||||||
)
|
|
@ -1,152 +0,0 @@
|
|||||||
#
|
|
||||||
# Copyright (c) 2019 Evgeniy Alekseev.
|
|
||||||
#
|
|
||||||
# This file is part of ffxivbis
|
|
||||||
# (see https://github.com/arcan1s/ffxivbis).
|
|
||||||
#
|
|
||||||
# License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause
|
|
||||||
#
|
|
||||||
from passlib.hash import md5_crypt
|
|
||||||
from typing import List, Optional, Union
|
|
||||||
|
|
||||||
from ffxivbis.models.bis import BiS
|
|
||||||
from ffxivbis.models.job import Job
|
|
||||||
from ffxivbis.models.loot import Loot
|
|
||||||
from ffxivbis.models.piece import Piece
|
|
||||||
from ffxivbis.models.player import Player, PlayerId
|
|
||||||
from ffxivbis.models.upgrade import Upgrade
|
|
||||||
from ffxivbis.models.user import User
|
|
||||||
|
|
||||||
from .database import Database
|
|
||||||
from .sqlite_helper import SQLiteHelper
|
|
||||||
|
|
||||||
|
|
||||||
class SQLiteDatabase(Database):
|
|
||||||
|
|
||||||
def __init__(self, database_path: str, migrations_path: str) -> None:
|
|
||||||
Database.__init__(self, migrations_path)
|
|
||||||
self.database_path = database_path
|
|
||||||
|
|
||||||
@property
|
|
||||||
def connection(self) -> str:
|
|
||||||
return f'sqlite:///{self.database_path}'
|
|
||||||
|
|
||||||
async def delete_piece(self, player_id: PlayerId, piece: Union[Piece, Upgrade]) -> None:
|
|
||||||
player = await self.get_player(player_id)
|
|
||||||
if player is None:
|
|
||||||
return
|
|
||||||
|
|
||||||
async with SQLiteHelper(self.database_path) as cursor:
|
|
||||||
await cursor.execute(
|
|
||||||
'''delete from loot
|
|
||||||
where loot_id in (
|
|
||||||
select loot_id from loot
|
|
||||||
where player_id = ? and piece = ? and is_tome = ? order by created desc limit 1
|
|
||||||
)''',
|
|
||||||
(player, piece.name, getattr(piece, 'is_tome', True)))
|
|
||||||
|
|
||||||
async def delete_piece_bis(self, player_id: PlayerId, piece: Piece) -> None:
|
|
||||||
player = await self.get_player(player_id)
|
|
||||||
if player is None:
|
|
||||||
return
|
|
||||||
|
|
||||||
async with SQLiteHelper(self.database_path) as cursor:
|
|
||||||
await cursor.execute(
|
|
||||||
'''delete from bis where player_id = ? and piece = ?''',
|
|
||||||
(player, piece.name))
|
|
||||||
|
|
||||||
async def delete_player(self, player_id: PlayerId) -> None:
|
|
||||||
async with SQLiteHelper(self.database_path) as cursor:
|
|
||||||
await cursor.execute('''delete from players where nick = ? and job = ?''',
|
|
||||||
(player_id.nick, player_id.job.name))
|
|
||||||
|
|
||||||
async def delete_user(self, username: str) -> None:
|
|
||||||
async with SQLiteHelper(self.database_path) as cursor:
|
|
||||||
await cursor.execute('''delete from users where username = ?''', (username,))
|
|
||||||
|
|
||||||
async def get_party(self) -> List[Player]:
|
|
||||||
async with SQLiteHelper(self.database_path) as cursor:
|
|
||||||
await cursor.execute('''select * from bis''')
|
|
||||||
rows = await cursor.fetchall()
|
|
||||||
bis_pieces = [Loot(row['player_id'], Piece.get(row)) for row in rows]
|
|
||||||
|
|
||||||
await cursor.execute('''select * from loot''')
|
|
||||||
rows = await cursor.fetchall()
|
|
||||||
loot_pieces = [Loot(row['player_id'], Piece.get(row)) for row in rows]
|
|
||||||
|
|
||||||
await cursor.execute('''select * from players''')
|
|
||||||
rows = await cursor.fetchall()
|
|
||||||
party = {
|
|
||||||
row['player_id']: Player(Job[row['job']], row['nick'], BiS(), [], row['bis_link'], row['priority'])
|
|
||||||
for row in rows
|
|
||||||
}
|
|
||||||
|
|
||||||
return self.set_loot(party, bis_pieces, loot_pieces)
|
|
||||||
|
|
||||||
async def get_player(self, player_id: PlayerId) -> Optional[int]:
|
|
||||||
async with SQLiteHelper(self.database_path) as cursor:
|
|
||||||
await cursor.execute('''select player_id from players where nick = ? and job = ?''',
|
|
||||||
(player_id.nick, player_id.job.name))
|
|
||||||
player = await cursor.fetchone()
|
|
||||||
return player['player_id'] if player is not None else None
|
|
||||||
|
|
||||||
async def get_user(self, username: str) -> Optional[User]:
|
|
||||||
async with SQLiteHelper(self.database_path) as cursor:
|
|
||||||
await cursor.execute('''select * from users where username = ?''', (username,))
|
|
||||||
user = await cursor.fetchone()
|
|
||||||
return User(user['username'], user['password'], user['permission']) if user is not None else None
|
|
||||||
|
|
||||||
async def get_users(self) -> List[User]:
|
|
||||||
async with SQLiteHelper(self.database_path) as cursor:
|
|
||||||
await cursor.execute('''select * from users''')
|
|
||||||
users = await cursor.fetchall()
|
|
||||||
return [User(user['username'], user['password'], user['permission']) for user in users]
|
|
||||||
|
|
||||||
async def insert_piece(self, player_id: PlayerId, piece: Union[Piece, Upgrade]) -> None:
|
|
||||||
player = await self.get_player(player_id)
|
|
||||||
if player is None:
|
|
||||||
return
|
|
||||||
|
|
||||||
async with SQLiteHelper(self.database_path) as cursor:
|
|
||||||
await cursor.execute(
|
|
||||||
'''insert into loot
|
|
||||||
(created, piece, is_tome, player_id)
|
|
||||||
values
|
|
||||||
(?, ?, ?, ?)''',
|
|
||||||
(Database.now(), piece.name, getattr(piece, 'is_tome', True), player)
|
|
||||||
)
|
|
||||||
|
|
||||||
async def insert_piece_bis(self, player_id: PlayerId, piece: Piece) -> None:
|
|
||||||
player = await self.get_player(player_id)
|
|
||||||
if player is None:
|
|
||||||
return
|
|
||||||
|
|
||||||
async with SQLiteHelper(self.database_path) as cursor:
|
|
||||||
await cursor.execute(
|
|
||||||
'''replace into bis
|
|
||||||
(created, piece, is_tome, player_id)
|
|
||||||
values
|
|
||||||
(?, ?, ?, ?)''',
|
|
||||||
(Database.now(), piece.name, piece.is_tome, player)
|
|
||||||
)
|
|
||||||
|
|
||||||
async def insert_player(self, player: Player) -> None:
|
|
||||||
async with SQLiteHelper(self.database_path) as cursor:
|
|
||||||
await cursor.execute(
|
|
||||||
'''replace into players
|
|
||||||
(created, nick, job, bis_link, priority)
|
|
||||||
values
|
|
||||||
(?, ?, ?, ?, ?)''',
|
|
||||||
(Database.now(), player.nick, player.job.name, player.link, player.priority)
|
|
||||||
)
|
|
||||||
|
|
||||||
async def insert_user(self, user: User, hashed_password: bool) -> None:
|
|
||||||
password = user.password if hashed_password else md5_crypt.hash(user.password)
|
|
||||||
async with SQLiteHelper(self.database_path) as cursor:
|
|
||||||
await cursor.execute(
|
|
||||||
'''replace into users
|
|
||||||
(username, password, permission)
|
|
||||||
values
|
|
||||||
(?, ?, ?)''',
|
|
||||||
(user.username, password, user.permission)
|
|
||||||
)
|
|
@ -1,36 +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
|
|
||||||
#
|
|
||||||
# because sqlite3 does not support context management
|
|
||||||
import aiosqlite
|
|
||||||
|
|
||||||
from types import TracebackType
|
|
||||||
from typing import Any, Dict, Optional, Type
|
|
||||||
|
|
||||||
|
|
||||||
def dict_factory(cursor: aiosqlite.Cursor, row: aiosqlite.Row) -> Dict[str, Any]:
|
|
||||||
return {
|
|
||||||
key: value
|
|
||||||
for key, value in zip([column[0] for column in cursor.description], row)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class SQLiteHelper():
|
|
||||||
def __init__(self, database_path: str) -> None:
|
|
||||||
self.database_path = database_path
|
|
||||||
|
|
||||||
async def __aenter__(self) -> aiosqlite.Cursor:
|
|
||||||
self.conn = await aiosqlite.connect(self.database_path)
|
|
||||||
self.conn.row_factory = dict_factory
|
|
||||||
await self.conn.execute('''pragma foreign_keys = on''')
|
|
||||||
return await self.conn.cursor()
|
|
||||||
|
|
||||||
async def __aexit__(self, exc_type: Optional[Type[BaseException]], exc_value: Optional[BaseException],
|
|
||||||
traceback: Optional[TracebackType]) -> None:
|
|
||||||
await self.conn.commit()
|
|
||||||
await self.conn.close()
|
|
@ -1,9 +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
|
|
||||||
#
|
|
||||||
__version__ = '0.1.1'
|
|
@ -1,16 +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 auto
|
|
||||||
|
|
||||||
from .serializable import SerializableEnum
|
|
||||||
|
|
||||||
|
|
||||||
class Action(SerializableEnum):
|
|
||||||
add = auto()
|
|
||||||
remove = auto()
|
|
@ -1,140 +0,0 @@
|
|||||||
#
|
|
||||||
# Copyright (c) 2019 Evgeniy Alekseev.
|
|
||||||
#
|
|
||||||
# This file is part of ffxivbis
|
|
||||||
# (see https://github.com/arcan1s/ffxivbis).
|
|
||||||
#
|
|
||||||
# License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause
|
|
||||||
#
|
|
||||||
import itertools
|
|
||||||
|
|
||||||
from dataclasses import dataclass
|
|
||||||
from typing import Any, Dict, List, Optional, Type, Union
|
|
||||||
|
|
||||||
from .job import Job
|
|
||||||
from .piece import Piece
|
|
||||||
from .serializable import Serializable
|
|
||||||
from .upgrade import Upgrade
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class BiSLink(Serializable):
|
|
||||||
nick: str
|
|
||||||
job: Job
|
|
||||||
link: str
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def model_properties(cls: Type[Serializable]) -> Dict[str, Any]:
|
|
||||||
return {
|
|
||||||
'job': {
|
|
||||||
'description': 'player job name',
|
|
||||||
'$ref': cls.model_ref('Job')
|
|
||||||
},
|
|
||||||
'link': {
|
|
||||||
'description': 'link to BiS set',
|
|
||||||
'example': 'https://ffxiv.ariyala.com/19V5R',
|
|
||||||
'type': 'string'
|
|
||||||
},
|
|
||||||
'nick': {
|
|
||||||
'description': 'player nick name',
|
|
||||||
'example': 'Siuan Sanche',
|
|
||||||
'type': 'string'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def model_required(cls: Type[Serializable]) -> List[str]:
|
|
||||||
return ['job', 'link', 'nick']
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class BiS(Serializable):
|
|
||||||
weapon: Optional[Piece] = None
|
|
||||||
head: Optional[Piece] = None
|
|
||||||
body: Optional[Piece] = None
|
|
||||||
hands: Optional[Piece] = None
|
|
||||||
waist: Optional[Piece] = None
|
|
||||||
legs: Optional[Piece] = None
|
|
||||||
feet: Optional[Piece] = None
|
|
||||||
ears: Optional[Piece] = None
|
|
||||||
neck: Optional[Piece] = None
|
|
||||||
wrist: Optional[Piece] = None
|
|
||||||
left_ring: Optional[Piece] = None
|
|
||||||
right_ring: Optional[Piece] = None
|
|
||||||
|
|
||||||
@property
|
|
||||||
def pieces(self) -> List[Piece]:
|
|
||||||
return [piece for piece in self.__dict__.values() if isinstance(piece, Piece)]
|
|
||||||
|
|
||||||
@property
|
|
||||||
def upgrades_required(self) -> Dict[Upgrade, int]:
|
|
||||||
return {
|
|
||||||
upgrade: len(list(pieces))
|
|
||||||
for upgrade, pieces in itertools.groupby(self.pieces, lambda piece: piece.upgrade)
|
|
||||||
}
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def model_properties(cls: Type[Serializable]) -> Dict[str, Any]:
|
|
||||||
return {
|
|
||||||
'weapon': {
|
|
||||||
'description': 'weapon part of BiS',
|
|
||||||
'$ref': cls.model_ref('Piece')
|
|
||||||
},
|
|
||||||
'head': {
|
|
||||||
'description': 'head part of BiS',
|
|
||||||
'$ref': cls.model_ref('Piece')
|
|
||||||
},
|
|
||||||
'body': {
|
|
||||||
'description': 'body part of BiS',
|
|
||||||
'$ref': cls.model_ref('Piece')
|
|
||||||
},
|
|
||||||
'hands': {
|
|
||||||
'description': 'hands part of BiS',
|
|
||||||
'$ref': cls.model_ref('Piece')
|
|
||||||
},
|
|
||||||
'waist': {
|
|
||||||
'description': 'waist part of BiS',
|
|
||||||
'$ref': cls.model_ref('Piece')
|
|
||||||
},
|
|
||||||
'legs': {
|
|
||||||
'description': 'legs part of BiS',
|
|
||||||
'$ref': cls.model_ref('Piece')
|
|
||||||
},
|
|
||||||
'feet': {
|
|
||||||
'description': 'feet part of BiS',
|
|
||||||
'$ref': cls.model_ref('Piece')
|
|
||||||
},
|
|
||||||
'ears': {
|
|
||||||
'description': 'ears part of BiS',
|
|
||||||
'$ref': cls.model_ref('Piece')
|
|
||||||
},
|
|
||||||
'neck': {
|
|
||||||
'description': 'neck part of BiS',
|
|
||||||
'$ref': cls.model_ref('Piece')
|
|
||||||
},
|
|
||||||
'wrist': {
|
|
||||||
'description': 'wrist part of BiS',
|
|
||||||
'$ref': cls.model_ref('Piece')
|
|
||||||
},
|
|
||||||
'left_ring': {
|
|
||||||
'description': 'left_ring part of BiS',
|
|
||||||
'$ref': cls.model_ref('Piece')
|
|
||||||
},
|
|
||||||
'right_ring': {
|
|
||||||
'description': 'right_ring part of BiS',
|
|
||||||
'$ref': cls.model_ref('Piece')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
def has_piece(self, piece: Union[Piece, Upgrade]) -> bool:
|
|
||||||
if isinstance(piece, Piece):
|
|
||||||
return piece in self.pieces
|
|
||||||
elif isinstance(piece, Upgrade):
|
|
||||||
return self.upgrades_required.get(piece) is not None
|
|
||||||
return False
|
|
||||||
|
|
||||||
def set_item(self, piece: Union[Piece, Upgrade]) -> None:
|
|
||||||
setattr(self, piece.name, piece)
|
|
||||||
|
|
||||||
def remove_item(self, piece: Union[Piece, Upgrade]) -> None:
|
|
||||||
setattr(self, piece.name, None)
|
|
@ -1,36 +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 dataclasses import dataclass
|
|
||||||
from typing import Any, Dict, List, Type
|
|
||||||
|
|
||||||
from .serializable import Serializable
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class Error(Serializable):
|
|
||||||
message: str
|
|
||||||
arguments: Dict[str, Any]
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def model_properties(cls: Type[Serializable]) -> Dict[str, Any]:
|
|
||||||
return {
|
|
||||||
'arguments': {
|
|
||||||
'description': 'arguments passed to request',
|
|
||||||
'type': 'object',
|
|
||||||
'additionalProperties': True
|
|
||||||
},
|
|
||||||
'message': {
|
|
||||||
'description': 'error message',
|
|
||||||
'type': 'string'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def model_required(cls: Type[Serializable]) -> List[str]:
|
|
||||||
return ['arguments', 'message']
|
|
@ -1,87 +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 __future__ import annotations
|
|
||||||
|
|
||||||
from enum import auto
|
|
||||||
from typing import Tuple
|
|
||||||
|
|
||||||
from .piece import Piece, PieceAccessory, Weapon
|
|
||||||
from .serializable import SerializableEnum
|
|
||||||
|
|
||||||
|
|
||||||
class Job(SerializableEnum):
|
|
||||||
PLD = auto()
|
|
||||||
WAR = auto()
|
|
||||||
DRK = auto()
|
|
||||||
GNB = auto()
|
|
||||||
WHM = auto()
|
|
||||||
SCH = auto()
|
|
||||||
AST = auto()
|
|
||||||
MNK = auto()
|
|
||||||
DRG = auto()
|
|
||||||
NIN = auto()
|
|
||||||
SAM = auto()
|
|
||||||
BRD = auto()
|
|
||||||
MCH = auto()
|
|
||||||
DNC = auto()
|
|
||||||
BLM = auto()
|
|
||||||
SMN = auto()
|
|
||||||
RDM = auto()
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def group_accs_dex() -> Tuple:
|
|
||||||
return Job.group_ranges() + (Job.NIN,)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def group_accs_str() -> Tuple:
|
|
||||||
return Job.group_mnk() + (Job.DRG,)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def group_casters() -> Tuple:
|
|
||||||
return (Job.BLM, Job.SMN, Job.RDM)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def group_healers() -> Tuple:
|
|
||||||
return (Job.WHM, Job.SCH, Job.AST)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def group_mnk() -> Tuple:
|
|
||||||
return (Job.MNK, Job.SAM)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def group_ranges() -> Tuple:
|
|
||||||
return (Job.BRD, Job.MCH, Job.DNC)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def group_tanks() -> Tuple:
|
|
||||||
return (Job.PLD, Job.WAR, Job.DRK, Job.GNB)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def has_same_loot(left: Job, right: Job, piece: Piece) -> bool:
|
|
||||||
# same jobs, alright
|
|
||||||
if left == right:
|
|
||||||
return True
|
|
||||||
|
|
||||||
# weapons are unique per class always
|
|
||||||
if isinstance(piece, Weapon):
|
|
||||||
return False
|
|
||||||
|
|
||||||
# group comparison
|
|
||||||
for group in (Job.group_casters(), Job.group_healers(), Job.group_mnk(), Job.group_ranges(), Job.group_tanks()):
|
|
||||||
if left in group and right in group:
|
|
||||||
return True
|
|
||||||
|
|
||||||
# accessories group comparison
|
|
||||||
if isinstance(Piece, PieceAccessory):
|
|
||||||
for group in (Job.group_accs_dex(), Job.group_accs_str()):
|
|
||||||
if left in group and right in group:
|
|
||||||
return True
|
|
||||||
|
|
||||||
return False
|
|
||||||
|
|
@ -1,37 +0,0 @@
|
|||||||
#
|
|
||||||
# Copyright (c) 2019 Evgeniy Alekseev.
|
|
||||||
#
|
|
||||||
# This file is part of ffxivbis
|
|
||||||
# (see https://github.com/arcan1s/ffxivbis).
|
|
||||||
#
|
|
||||||
# License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause
|
|
||||||
#
|
|
||||||
from dataclasses import dataclass
|
|
||||||
from typing import Any, Dict, List, Type, Union
|
|
||||||
|
|
||||||
from .piece import Piece
|
|
||||||
from .serializable import Serializable
|
|
||||||
from .upgrade import Upgrade
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class Loot(Serializable):
|
|
||||||
player_id: int
|
|
||||||
piece: Union[Piece, Upgrade]
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def model_properties(cls: Type[Serializable]) -> Dict[str, Any]:
|
|
||||||
return {
|
|
||||||
'piece': {
|
|
||||||
'description': 'player piece',
|
|
||||||
'$ref': cls.model_ref('Piece')
|
|
||||||
},
|
|
||||||
'player_id': {
|
|
||||||
'description': 'player identifier',
|
|
||||||
'$ref': cls.model_ref('PlayerId')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def model_required(cls: Type[Serializable]) -> List[str]:
|
|
||||||
return ['piece', 'player_id']
|
|
@ -1,168 +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 __future__ import annotations
|
|
||||||
|
|
||||||
from dataclasses import dataclass
|
|
||||||
from typing import Any, Dict, List, Mapping, Type, Union
|
|
||||||
|
|
||||||
from ffxivbis.core.exceptions import InvalidDataRow
|
|
||||||
|
|
||||||
from .serializable import Serializable
|
|
||||||
from .upgrade import Upgrade
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class Piece(Serializable):
|
|
||||||
is_tome: bool
|
|
||||||
name: str
|
|
||||||
|
|
||||||
@property
|
|
||||||
def upgrade(self) -> Upgrade:
|
|
||||||
if not self.is_tome:
|
|
||||||
return Upgrade.NoUpgrade
|
|
||||||
elif isinstance(self, Waist) or isinstance(self, PieceAccessory):
|
|
||||||
return Upgrade.AccessoryUpgrade
|
|
||||||
elif isinstance(self, Weapon):
|
|
||||||
return Upgrade.WeaponUpgrade
|
|
||||||
elif isinstance(self, PieceGear):
|
|
||||||
return Upgrade.GearUpgrade
|
|
||||||
return Upgrade.NoUpgrade
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def available() -> List[str]:
|
|
||||||
return [
|
|
||||||
'weapon',
|
|
||||||
'head', 'body', 'hands', 'waist', 'legs', 'feet',
|
|
||||||
'ears', 'neck', 'wrist', 'left_ring', 'right_ring'
|
|
||||||
]
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get(cls: Type[Piece], data: Mapping[str, Any]) -> Union[Piece, Upgrade]:
|
|
||||||
try:
|
|
||||||
piece_type = data.get('piece') or data.get('name')
|
|
||||||
if piece_type is None:
|
|
||||||
raise KeyError
|
|
||||||
is_tome = data['is_tome'] in ('yes', 'on', '1', 1, True)
|
|
||||||
except KeyError:
|
|
||||||
raise InvalidDataRow(data)
|
|
||||||
if piece_type.lower() == 'weapon':
|
|
||||||
return Weapon(is_tome)
|
|
||||||
elif piece_type.lower() == 'head':
|
|
||||||
return Head(is_tome)
|
|
||||||
elif piece_type.lower() == 'body':
|
|
||||||
return Body(is_tome)
|
|
||||||
elif piece_type.lower() == 'hands':
|
|
||||||
return Hands(is_tome)
|
|
||||||
elif piece_type.lower() == 'waist':
|
|
||||||
return Waist(is_tome)
|
|
||||||
elif piece_type.lower() == 'legs':
|
|
||||||
return Legs(is_tome)
|
|
||||||
elif piece_type.lower() == 'feet':
|
|
||||||
return Feet(is_tome)
|
|
||||||
elif piece_type.lower() == 'ears':
|
|
||||||
return Ears(is_tome)
|
|
||||||
elif piece_type.lower() == 'neck':
|
|
||||||
return Neck(is_tome)
|
|
||||||
elif piece_type.lower() == 'wrist':
|
|
||||||
return Wrist(is_tome)
|
|
||||||
elif piece_type.lower() in ('left_ring', 'right_ring', 'ring'):
|
|
||||||
return Ring(is_tome, piece_type.lower())
|
|
||||||
elif piece_type.lower() in Upgrade.dict_types():
|
|
||||||
return Upgrade[piece_type]
|
|
||||||
else:
|
|
||||||
raise InvalidDataRow(data)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def model_properties(cls: Type[Serializable]) -> Dict[str, Any]:
|
|
||||||
return {
|
|
||||||
'is_tome': {
|
|
||||||
'description': 'is this piece tome gear or not',
|
|
||||||
'type': 'boolean'
|
|
||||||
},
|
|
||||||
'name': {
|
|
||||||
'description': 'piece name',
|
|
||||||
'type': 'string'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def model_required(cls: Type[Serializable]) -> List[str]:
|
|
||||||
return ['is_tome', 'name']
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class PieceAccessory(Piece):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class PieceGear(Piece):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class Weapon(Piece):
|
|
||||||
name: str = 'weapon'
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class Head(PieceGear):
|
|
||||||
name: str = 'head'
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class Body(PieceGear):
|
|
||||||
name: str = 'body'
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class Hands(PieceGear):
|
|
||||||
name: str = 'hands'
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class Waist(PieceGear):
|
|
||||||
name: str = 'waist'
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class Legs(PieceGear):
|
|
||||||
name: str = 'legs'
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class Feet(PieceGear):
|
|
||||||
name: str = 'feet'
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class Ears(PieceAccessory):
|
|
||||||
name: str = 'ears'
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class Neck(PieceAccessory):
|
|
||||||
name: str = 'neck'
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class Wrist(PieceAccessory):
|
|
||||||
name: str = 'wrist'
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class Ring(PieceAccessory):
|
|
||||||
name: str = 'ring'
|
|
||||||
|
|
||||||
# override __eq__method to be able to compare left/right rings
|
|
||||||
def __eq__(self, other: Any) -> bool:
|
|
||||||
if not isinstance(other, Ring):
|
|
||||||
return False
|
|
||||||
return self.is_tome == other.is_tome
|
|
@ -1,201 +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 __future__ import annotations
|
|
||||||
|
|
||||||
import re
|
|
||||||
|
|
||||||
from dataclasses import dataclass
|
|
||||||
from typing import Any, Dict, List, Optional, Type, Union
|
|
||||||
|
|
||||||
from .bis import BiS
|
|
||||||
from .job import Job
|
|
||||||
from .piece import Piece
|
|
||||||
from .serializable import Serializable
|
|
||||||
from .upgrade import Upgrade
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class PlayerId(Serializable):
|
|
||||||
job: Job
|
|
||||||
nick: str
|
|
||||||
|
|
||||||
@property
|
|
||||||
def pretty_name(self) -> str:
|
|
||||||
return f'{self.nick} ({self.job.name})'
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def from_pretty_name(cls: Type[PlayerId], value: str) -> Optional[PlayerId]:
|
|
||||||
matches = re.search('^(?P<nick>.*) \((?P<job>[A-Z]+)\)$', value)
|
|
||||||
if matches is None:
|
|
||||||
return None
|
|
||||||
return PlayerId(Job[matches.group('job')], matches.group('nick'))
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def model_properties(cls: Type[Serializable]) -> Dict[str, Any]:
|
|
||||||
return {
|
|
||||||
'job': {
|
|
||||||
'description': 'player job name',
|
|
||||||
'$ref': cls.model_ref('Job')
|
|
||||||
},
|
|
||||||
'nick': {
|
|
||||||
'description': 'player nick name',
|
|
||||||
'type': 'string'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def model_required(cls: Type[Serializable]) -> List[str]:
|
|
||||||
return ['job', 'nick']
|
|
||||||
|
|
||||||
def __hash__(self) -> int:
|
|
||||||
return hash(str(self))
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class PlayerIdWithCounters(PlayerId):
|
|
||||||
is_required: bool
|
|
||||||
priority: int
|
|
||||||
loot_count: int
|
|
||||||
loot_count_bis: int
|
|
||||||
loot_count_total: int
|
|
||||||
bis_count_total: int
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def model_properties(cls: Type[Serializable]) -> Dict[str, Any]:
|
|
||||||
return {
|
|
||||||
'bis_count_total': {
|
|
||||||
'description': 'total savage pieces in BiS',
|
|
||||||
'type': 'integer'
|
|
||||||
},
|
|
||||||
'is_required': {
|
|
||||||
'description': 'is item required by BiS or not',
|
|
||||||
'type': 'boolean'
|
|
||||||
},
|
|
||||||
'job': {
|
|
||||||
'description': 'player job name',
|
|
||||||
'$ref': cls.model_ref('Job')
|
|
||||||
},
|
|
||||||
'loot_count': {
|
|
||||||
'description': 'count of this item which was already looted',
|
|
||||||
'type': 'integer'
|
|
||||||
},
|
|
||||||
'loot_count_bis': {
|
|
||||||
'description': 'count of BiS items which were already looted',
|
|
||||||
'type': 'integer'
|
|
||||||
},
|
|
||||||
'loot_count_total': {
|
|
||||||
'description': 'total count of items which were looted',
|
|
||||||
'type': 'integer'
|
|
||||||
},
|
|
||||||
'nick': {
|
|
||||||
'description': 'player nick name',
|
|
||||||
'type': 'string'
|
|
||||||
},
|
|
||||||
'priority': {
|
|
||||||
'description': 'player loot priority',
|
|
||||||
'type': 'integer'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def model_required(cls: Type[Serializable]) -> List[str]:
|
|
||||||
return ['bis_count_total', 'is_required', 'job', 'loot_count',
|
|
||||||
'loot_count_bis', 'loot_count_total', 'nick', 'priority']
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class Player(Serializable):
|
|
||||||
job: Job
|
|
||||||
nick: str
|
|
||||||
bis: BiS
|
|
||||||
loot: List[Union[Piece, Upgrade]]
|
|
||||||
link: Optional[str] = None
|
|
||||||
priority: int = 0
|
|
||||||
|
|
||||||
@property
|
|
||||||
def player_id(self) -> PlayerId:
|
|
||||||
return PlayerId(self.job, self.nick)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def model_properties(cls: Type[Serializable]) -> Dict[str, Any]:
|
|
||||||
return {
|
|
||||||
'bis': {
|
|
||||||
'description': 'player BiS',
|
|
||||||
'$ref': cls.model_ref('BiS')
|
|
||||||
},
|
|
||||||
'job': {
|
|
||||||
'description': 'player job name',
|
|
||||||
'$ref': cls.model_ref('Job')
|
|
||||||
},
|
|
||||||
'link': {
|
|
||||||
'description': 'link to player BiS',
|
|
||||||
'type': 'string'
|
|
||||||
},
|
|
||||||
'loot': {
|
|
||||||
'description': 'player looted items',
|
|
||||||
'type': 'array',
|
|
||||||
'items': {
|
|
||||||
'anyOf': [
|
|
||||||
{'$ref': cls.model_ref('Piece')},
|
|
||||||
{'$ref': cls.model_ref('Upgrade')}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
'nick': {
|
|
||||||
'description': 'player nick name',
|
|
||||||
'type': 'string'
|
|
||||||
},
|
|
||||||
'priority': {
|
|
||||||
'description': 'player loot priority',
|
|
||||||
'type': 'integer'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def model_required(cls: Type[Serializable]) -> List[str]:
|
|
||||||
return ['bis', 'job', 'loot', 'nick', 'priority']
|
|
||||||
|
|
||||||
def player_id_with_counters(self, piece: Union[Piece, Upgrade, None]) -> PlayerIdWithCounters:
|
|
||||||
return PlayerIdWithCounters(self.job, self.nick, self.is_required(piece), self.priority,
|
|
||||||
abs(self.loot_count(piece)), abs(self.loot_count_bis(piece)),
|
|
||||||
abs(self.loot_count_total(piece)), abs(self.bis_count_total(piece)))
|
|
||||||
|
|
||||||
# ordering methods
|
|
||||||
def is_required(self, piece: Union[Piece, Upgrade, None]) -> bool:
|
|
||||||
if piece is None:
|
|
||||||
return False
|
|
||||||
|
|
||||||
# lets check if it is even in bis
|
|
||||||
if not self.bis.has_piece(piece):
|
|
||||||
return False
|
|
||||||
|
|
||||||
if isinstance(piece, Piece):
|
|
||||||
# alright it is in is, lets check if he even got it
|
|
||||||
return self.loot_count(piece) == 0
|
|
||||||
elif isinstance(piece, Upgrade):
|
|
||||||
# alright it lets check how much upgrades does they need
|
|
||||||
return self.bis.upgrades_required[piece] > self.loot_count(piece)
|
|
||||||
return False
|
|
||||||
|
|
||||||
def loot_count(self, piece: Union[Piece, Upgrade, None]) -> int:
|
|
||||||
if piece is None:
|
|
||||||
return -self.loot_count_total(piece)
|
|
||||||
return -self.loot.count(piece)
|
|
||||||
|
|
||||||
def loot_count_bis(self, _: Union[Piece, Upgrade, None]) -> int:
|
|
||||||
return -len([piece for piece in self.loot if self.bis.has_piece(piece)])
|
|
||||||
|
|
||||||
def loot_count_total(self, _: Union[Piece, Upgrade, None]) -> int:
|
|
||||||
return -len(self.loot)
|
|
||||||
|
|
||||||
def bis_count_total(self, _: Union[Piece, Upgrade, None]) -> int:
|
|
||||||
return len([piece for piece in self.bis.pieces if not piece.is_tome])
|
|
||||||
|
|
||||||
def loot_priority(self, _: Union[Piece, Upgrade, None]) -> int:
|
|
||||||
return self.priority
|
|
@ -1,35 +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 typing import Any, Dict, List, Type
|
|
||||||
|
|
||||||
from .serializable import Serializable
|
|
||||||
|
|
||||||
|
|
||||||
class PlayerEdit(Serializable):
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def model_properties(cls: Type[Serializable]) -> Dict[str, Any]:
|
|
||||||
return {
|
|
||||||
'action': {
|
|
||||||
'description': 'action to perform',
|
|
||||||
'$ref': cls.model_ref('Action')
|
|
||||||
},
|
|
||||||
'job': {
|
|
||||||
'description': 'player job name to edit',
|
|
||||||
'$ref': cls.model_ref('Job')
|
|
||||||
},
|
|
||||||
'nick': {
|
|
||||||
'description': 'player nick name to edit',
|
|
||||||
'type': 'string'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def model_required(cls: Type[Serializable]) -> List[str]:
|
|
||||||
return ['action', 'nick', 'job']
|
|
@ -1,57 +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 __future__ import annotations
|
|
||||||
|
|
||||||
from enum import Enum
|
|
||||||
from typing import Any, Dict, List, Type
|
|
||||||
|
|
||||||
|
|
||||||
class Serializable:
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def model_name(cls: Type[Serializable]) -> str:
|
|
||||||
return cls.__name__
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def model_properties(cls: Type[Serializable]) -> Dict[str, Any]:
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def model_ref(model_name: str, model_group: str = 'schemas') -> str:
|
|
||||||
return f'#/components/{model_group}/{model_name}'
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def model_required(cls: Type[Serializable]) -> List[str]:
|
|
||||||
return []
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def model_spec(cls: Type[Serializable]) -> Dict[str, Any]:
|
|
||||||
return {
|
|
||||||
'type': cls.model_type(),
|
|
||||||
'properties': cls.model_properties(),
|
|
||||||
'required': cls.model_required()
|
|
||||||
}
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def model_type(cls: Type[Serializable]) -> str:
|
|
||||||
return 'object'
|
|
||||||
|
|
||||||
|
|
||||||
class SerializableEnum(Serializable, Enum):
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def model_spec(cls: Type[SerializableEnum]) -> Dict[str, Any]:
|
|
||||||
return {
|
|
||||||
'type': cls.model_type(),
|
|
||||||
'enum': [item.name for item in cls]
|
|
||||||
}
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def model_type(cls: Type[Serializable]) -> str:
|
|
||||||
return 'string'
|
|
@ -1,23 +0,0 @@
|
|||||||
#
|
|
||||||
# Copyright (c) 2019 Evgeniy Alekseev.
|
|
||||||
#
|
|
||||||
# This file is part of ffxivbis
|
|
||||||
# (see https://github.com/arcan1s/ffxivbis).
|
|
||||||
#
|
|
||||||
# License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause
|
|
||||||
#
|
|
||||||
from enum import auto
|
|
||||||
from typing import List
|
|
||||||
|
|
||||||
from .serializable import SerializableEnum
|
|
||||||
|
|
||||||
|
|
||||||
class Upgrade(SerializableEnum):
|
|
||||||
NoUpgrade = auto()
|
|
||||||
AccessoryUpgrade = auto()
|
|
||||||
GearUpgrade = auto()
|
|
||||||
WeaponUpgrade = auto()
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def dict_types() -> List[str]:
|
|
||||||
return list(map(lambda t: t.name.lower(), Upgrade))
|
|
@ -1,42 +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 dataclasses import dataclass
|
|
||||||
from typing import Any, Dict, List, Type
|
|
||||||
|
|
||||||
from .serializable import Serializable
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class User(Serializable):
|
|
||||||
username: str
|
|
||||||
password: str
|
|
||||||
permission: str
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def model_properties(cls: Type[Serializable]) -> Dict[str, Any]:
|
|
||||||
return {
|
|
||||||
'password': {
|
|
||||||
'description': 'user password',
|
|
||||||
'type': 'string'
|
|
||||||
},
|
|
||||||
'permission': {
|
|
||||||
'default': 'get',
|
|
||||||
'description': 'user action permissions',
|
|
||||||
'type': 'string',
|
|
||||||
'enum': ['admin', 'get', 'post']
|
|
||||||
},
|
|
||||||
'username': {
|
|
||||||
'description': 'user name',
|
|
||||||
'type': 'string'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def model_required(cls: Type[Serializable]) -> List[str]:
|
|
||||||
return ['password', 'username']
|
|
36
src/main/resources/db/migration/V1_0__Create_tables.sql
Normal file
36
src/main/resources/db/migration/V1_0__Create_tables.sql
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
create table players (
|
||||||
|
party_id text not null,
|
||||||
|
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);
|
||||||
|
create unique index players_nick_job_idx on players(party_id, nick, job);
|
||||||
|
|
||||||
|
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,
|
||||||
|
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,
|
||||||
|
username text not null,
|
||||||
|
password text not null,
|
||||||
|
permission text not null);
|
||||||
|
create unique index users_username_idx on users(party_id, username);
|
32
src/main/resources/reference.conf
Normal file
32
src/main/resources/reference.conf
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
me.arcanis.ffxivbis {
|
||||||
|
ariyala {
|
||||||
|
ariyala-url = "https://ffxiv.ariyala.com"
|
||||||
|
xivapi-url = "https://xivapi.com"
|
||||||
|
}
|
||||||
|
|
||||||
|
database {
|
||||||
|
mode = "sqlite"
|
||||||
|
|
||||||
|
sqlite {
|
||||||
|
profile = "slick.jdbc.SQLiteProfile$"
|
||||||
|
db {
|
||||||
|
url = "jdbc:sqlite:/home/arcanis/Documents/github/ffxivbis-scala/ffxivbis.db"
|
||||||
|
user = "user"
|
||||||
|
password = "password"
|
||||||
|
}
|
||||||
|
numThreads = 10
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
settings {
|
||||||
|
priority = [
|
||||||
|
"isRequired", "lootCountBiS", "priority", "lootCount", "lootCountTotal"
|
||||||
|
]
|
||||||
|
request-timeout = 10s
|
||||||
|
}
|
||||||
|
|
||||||
|
web {
|
||||||
|
host = "0.0.0.0"
|
||||||
|
port = 8000
|
||||||
|
}
|
||||||
|
}
|
43
src/main/scala/me/arcanis/ffxivbis/Application.scala
Normal file
43
src/main/scala/me/arcanis/ffxivbis/Application.scala
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
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.Ariyala
|
||||||
|
import me.arcanis.ffxivbis.service.impl.DatabaseImpl
|
||||||
|
import me.arcanis.ffxivbis.storage.Migration
|
||||||
|
|
||||||
|
import scala.concurrent.{Await, ExecutionContext}
|
||||||
|
import scala.concurrent.duration.Duration
|
||||||
|
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 http = new RootEndpoint(context.system, storage, 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)
|
||||||
|
}
|
12
src/main/scala/me/arcanis/ffxivbis/ffxivbis.scala
Normal file
12
src/main/scala/me/arcanis/ffxivbis/ffxivbis.scala
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
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")
|
||||||
|
}
|
||||||
|
}
|
16
src/main/scala/me/arcanis/ffxivbis/http/AriyalaHelper.scala
Normal file
16
src/main/scala/me/arcanis/ffxivbis/http/AriyalaHelper.scala
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
package me.arcanis.ffxivbis.http
|
||||||
|
|
||||||
|
import akka.actor.ActorRef
|
||||||
|
import akka.pattern.ask
|
||||||
|
import akka.util.Timeout
|
||||||
|
import me.arcanis.ffxivbis.models.{BiS, Job, Piece}
|
||||||
|
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[Seq[Piece]].map(BiS(_))
|
||||||
|
}
|
53
src/main/scala/me/arcanis/ffxivbis/http/Authorization.scala
Normal file
53
src/main/scala/me/arcanis/ffxivbis/http/Authorization.scala
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
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._
|
||||||
|
import akka.http.scaladsl.server.Directives._
|
||||||
|
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)
|
||||||
|
}
|
29
src/main/scala/me/arcanis/ffxivbis/http/BiSHelper.scala
Normal file
29
src/main/scala/me/arcanis/ffxivbis/http/BiSHelper.scala
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
package me.arcanis.ffxivbis.http
|
||||||
|
|
||||||
|
import akka.actor.ActorRef
|
||||||
|
import akka.pattern.ask
|
||||||
|
import akka.util.Timeout
|
||||||
|
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): Future[Unit] =
|
||||||
|
Future { storage ! DatabaseBiSHandler.AddPieceToBis(playerId, piece) }
|
||||||
|
|
||||||
|
def bis(partyId: String, playerId: Option[PlayerId])
|
||||||
|
(implicit executionContext: ExecutionContext, timeout: Timeout): Future[Seq[Player]] =
|
||||||
|
(storage ? DatabaseBiSHandler.GetBiS(partyId, playerId)).mapTo[Seq[Player]]
|
||||||
|
|
||||||
|
def putBiS(playerId: PlayerId, link: String)
|
||||||
|
(implicit executionContext: ExecutionContext, timeout: Timeout): Future[Unit] =
|
||||||
|
downloadBiS(link, playerId.job).map(_.pieces.map(addPieceBiS(playerId, _)))
|
||||||
|
|
||||||
|
def removePieceBiS(playerId: PlayerId, piece: Piece)
|
||||||
|
(implicit executionContext: ExecutionContext): Future[Unit] =
|
||||||
|
Future { storage ! DatabaseBiSHandler.RemovePieceFromBiS(playerId, piece) }
|
||||||
|
|
||||||
|
}
|
29
src/main/scala/me/arcanis/ffxivbis/http/LootHelper.scala
Normal file
29
src/main/scala/me/arcanis/ffxivbis/http/LootHelper.scala
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
package me.arcanis.ffxivbis.http
|
||||||
|
|
||||||
|
import akka.actor.ActorRef
|
||||||
|
import akka.pattern.ask
|
||||||
|
import akka.util.Timeout
|
||||||
|
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): Future[Unit] =
|
||||||
|
Future { storage ! DatabaseLootHandler.AddPieceTo(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): Future[Unit] =
|
||||||
|
Future { storage ! DatabaseLootHandler.RemovePieceFrom(playerId, piece) }
|
||||||
|
|
||||||
|
def suggestPiece(partyId: String, piece: Piece)
|
||||||
|
(implicit executionContext: ExecutionContext, timeout: Timeout): Future[Seq[PlayerIdWithCounters]] =
|
||||||
|
(storage ? DatabaseLootHandler.SuggestLoot(partyId, piece)).mapTo[LootSelectorResult].map(_.result)
|
||||||
|
}
|
37
src/main/scala/me/arcanis/ffxivbis/http/PlayerHelper.scala
Normal file
37
src/main/scala/me/arcanis/ffxivbis/http/PlayerHelper.scala
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
package me.arcanis.ffxivbis.http
|
||||||
|
|
||||||
|
import akka.actor.ActorRef
|
||||||
|
import akka.pattern.ask
|
||||||
|
import akka.util.Timeout
|
||||||
|
import me.arcanis.ffxivbis.models.{Player, PlayerId}
|
||||||
|
import me.arcanis.ffxivbis.service.Party
|
||||||
|
import me.arcanis.ffxivbis.service.impl.{DatabaseBiSHandler, DatabasePartyHandler}
|
||||||
|
|
||||||
|
import scala.concurrent.{ExecutionContext, Future}
|
||||||
|
import scala.util.{Failure, Success}
|
||||||
|
|
||||||
|
class PlayerHelper(storage: ActorRef, ariyala: ActorRef) extends AriyalaHelper(ariyala) {
|
||||||
|
|
||||||
|
def addPlayer(player: Player)
|
||||||
|
(implicit executionContext: ExecutionContext, timeout: Timeout): Future[Unit] =
|
||||||
|
Future { storage ! DatabasePartyHandler.AddPlayer(player) }.andThen {
|
||||||
|
case Success(_) if player.link.isDefined =>
|
||||||
|
downloadBiS(player.link.get, player.job).map { bis =>
|
||||||
|
bis.pieces.map(storage ! DatabaseBiSHandler.AddPieceToBis(player.playerId, _))
|
||||||
|
}.map(_ => ())
|
||||||
|
case Success(_) => Future.successful(())
|
||||||
|
case Failure(exception) => Future.failed(exception)
|
||||||
|
}
|
||||||
|
|
||||||
|
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[Player].map(Seq(_))
|
||||||
|
case None =>
|
||||||
|
(storage ? DatabasePartyHandler.GetParty(partyId)).mapTo[Party].map(_.players.values.toSeq)
|
||||||
|
}
|
||||||
|
|
||||||
|
def removePlayer(playerId: PlayerId)(implicit executionContext: ExecutionContext): Future[Unit] =
|
||||||
|
Future { storage ! DatabasePartyHandler.RemovePlayer(playerId) }
|
||||||
|
}
|
45
src/main/scala/me/arcanis/ffxivbis/http/RootEndpoint.scala
Normal file
45
src/main/scala/me/arcanis/ffxivbis/http/RootEndpoint.scala
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
package me.arcanis.ffxivbis.http
|
||||||
|
|
||||||
|
import akka.actor.{ActorRef, ActorSystem}
|
||||||
|
import akka.http.scaladsl.model._
|
||||||
|
import akka.http.scaladsl.server.Directives._
|
||||||
|
import akka.http.scaladsl.server._
|
||||||
|
import akka.util.Timeout
|
||||||
|
import com.typesafe.scalalogging.StrictLogging
|
||||||
|
import me.arcanis.ffxivbis.http.api.v1.ApiV1Endpoint
|
||||||
|
|
||||||
|
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 apiV1Endpoint: ApiV1Endpoint = new ApiV1Endpoint(storage, ariyala)
|
||||||
|
|
||||||
|
def route: Route = apiRoute ~ htmlRoute ~ Swagger.routes ~ swaggerUIRoute
|
||||||
|
|
||||||
|
private def apiRoute: Route =
|
||||||
|
ignoreTrailingSlash {
|
||||||
|
pathPrefix("api") {
|
||||||
|
pathPrefix(Segment) {
|
||||||
|
case "v1" => apiV1Endpoint.route
|
||||||
|
case _ => reject
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private def htmlRoute: Route =
|
||||||
|
ignoreTrailingSlash {
|
||||||
|
pathEndOrSingleSlash {
|
||||||
|
complete(StatusCodes.OK)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private def swaggerUIRoute: Route =
|
||||||
|
path("swagger") {
|
||||||
|
getFromResource("swagger/index.html")
|
||||||
|
} ~ getFromResourceDirectory("swagger")
|
||||||
|
}
|
24
src/main/scala/me/arcanis/ffxivbis/http/Swagger.scala
Normal file
24
src/main/scala/me/arcanis/ffxivbis/http/Swagger.scala
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
package me.arcanis.ffxivbis.http
|
||||||
|
|
||||||
|
import com.github.swagger.akka.SwaggerHttpService
|
||||||
|
import com.github.swagger.akka.model.Info
|
||||||
|
import io.swagger.v3.oas.models.security.SecurityScheme
|
||||||
|
|
||||||
|
object Swagger extends SwaggerHttpService {
|
||||||
|
override val apiClasses: Set[Class[_]] = Set(
|
||||||
|
classOf[api.v1.BiSEndpoint], classOf[api.v1.LootEndpoint],
|
||||||
|
classOf[api.v1.PlayerEndpoint], classOf[api.v1.UserEndpoint]
|
||||||
|
)
|
||||||
|
|
||||||
|
override val info: Info = Info()
|
||||||
|
|
||||||
|
private val basicAuth = new SecurityScheme()
|
||||||
|
.description("basic http auth")
|
||||||
|
.`type`(SecurityScheme.Type.HTTP)
|
||||||
|
.in(SecurityScheme.In.HEADER)
|
||||||
|
.scheme("bearer")
|
||||||
|
override def securitySchemes: Map[String, SecurityScheme] = Map("basic auth" -> basicAuth)
|
||||||
|
|
||||||
|
override val unwantedDefinitions: Seq[String] =
|
||||||
|
Seq("Function1", "Function1RequestContextFutureRouteResult")
|
||||||
|
}
|
28
src/main/scala/me/arcanis/ffxivbis/http/UserHelper.scala
Normal file
28
src/main/scala/me/arcanis/ffxivbis/http/UserHelper.scala
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
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.impl.DatabaseUserHandler
|
||||||
|
|
||||||
|
import scala.concurrent.{ExecutionContext, Future}
|
||||||
|
|
||||||
|
class UserHelper(storage: ActorRef) {
|
||||||
|
|
||||||
|
def addUser(user: User, isHashedPassword: Boolean)
|
||||||
|
(implicit executionContext: ExecutionContext): Future[Unit] =
|
||||||
|
Future { storage ! DatabaseUserHandler.InsertUser(user, isHashedPassword) }
|
||||||
|
|
||||||
|
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): Future[Unit] =
|
||||||
|
Future { storage ! DatabaseUserHandler.DeleteUser(partyId, username) }
|
||||||
|
}
|
@ -0,0 +1,17 @@
|
|||||||
|
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
|
||||||
|
|
||||||
|
class ApiV1Endpoint(storage: ActorRef, ariyala: ActorRef)(implicit timeout: Timeout) {
|
||||||
|
|
||||||
|
private val biSEndpoint = new BiSEndpoint(storage, ariyala)
|
||||||
|
private val lootEndpoint = new LootEndpoint(storage)
|
||||||
|
private val playerEndpoint = new PlayerEndpoint(storage, ariyala)
|
||||||
|
private val userEndpoint = new UserEndpoint(storage)
|
||||||
|
|
||||||
|
def route: Route =
|
||||||
|
biSEndpoint.route ~ lootEndpoint.route ~ playerEndpoint.route ~ userEndpoint.route
|
||||||
|
}
|
132
src/main/scala/me/arcanis/ffxivbis/http/api/v1/BiSEndpoint.scala
Normal file
132
src/main/scala/me/arcanis/ffxivbis/http/api/v1/BiSEndpoint.scala
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
package me.arcanis.ffxivbis.http.api.v1
|
||||||
|
|
||||||
|
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 io.swagger.v3.oas.annotations.enums.ParameterIn
|
||||||
|
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, Parameter}
|
||||||
|
import io.swagger.v3.oas.annotations.parameters.RequestBody
|
||||||
|
import io.swagger.v3.oas.annotations.security.SecurityRequirement
|
||||||
|
import javax.ws.rs.{Consumes, GET, POST, PUT, Path, Produces}
|
||||||
|
import me.arcanis.ffxivbis.http.{Authorization, BiSHelper}
|
||||||
|
import me.arcanis.ffxivbis.http.api.v1.json._
|
||||||
|
import me.arcanis.ffxivbis.models.PlayerId
|
||||||
|
|
||||||
|
@Path("api/v1")
|
||||||
|
class BiSEndpoint(override val storage: ActorRef, ariyala: ActorRef)(implicit timeout: Timeout)
|
||||||
|
extends BiSHelper(storage, ariyala) with Authorization with JsonSupport {
|
||||||
|
import spray.json.DefaultJsonProtocol._
|
||||||
|
|
||||||
|
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"),
|
||||||
|
new ApiResponse(responseCode = "401", description = "Supplied authorization is invalid"),
|
||||||
|
new ApiResponse(responseCode = "403", description = "Access is forbidden"),
|
||||||
|
new ApiResponse(responseCode = "500", description = "Internal server error"),
|
||||||
|
),
|
||||||
|
security = Array(new SecurityRequirement(name = "basic auth", scopes = Array("post"))),
|
||||||
|
tags = Array("best in slot"),
|
||||||
|
)
|
||||||
|
def createBiS: Route =
|
||||||
|
path("party" / Segment / "bis") { partyId: String =>
|
||||||
|
extractExecutionContext { implicit executionContext =>
|
||||||
|
authenticateBasicBCrypt(s"party $partyId", authPost(partyId)) { _ =>
|
||||||
|
put {
|
||||||
|
entity(as[PlayerBiSLinkResponse]) { bisLink =>
|
||||||
|
val playerId = bisLink.playerId.withPartyId(partyId)
|
||||||
|
complete(putBiS(playerId, bisLink.link).map(_ => StatusCodes.Created))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@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"),
|
||||||
|
new ApiResponse(responseCode = "403", description = "Access is forbidden"),
|
||||||
|
new ApiResponse(responseCode = "500", description = "Internal server error"),
|
||||||
|
),
|
||||||
|
security = Array(new SecurityRequirement(name = "basic auth", scopes = Array("get"))),
|
||||||
|
tags = Array("best in slot"),
|
||||||
|
)
|
||||||
|
def getBiS: Route =
|
||||||
|
path("party" / Segment / "bis") { partyId: String =>
|
||||||
|
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)
|
||||||
|
complete(bis(partyId, playerId).map(_.map(PlayerResponse.fromPlayer)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@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"),
|
||||||
|
new ApiResponse(responseCode = "401", description = "Supplied authorization is invalid"),
|
||||||
|
new ApiResponse(responseCode = "403", description = "Access is forbidden"),
|
||||||
|
new ApiResponse(responseCode = "500", description = "Internal server error"),
|
||||||
|
),
|
||||||
|
security = Array(new SecurityRequirement(name = "basic auth", scopes = Array("post"))),
|
||||||
|
tags = Array("best in slot"),
|
||||||
|
)
|
||||||
|
def modifyBiS: Route =
|
||||||
|
path("party" / Segment / "bis") { partyId: String =>
|
||||||
|
extractExecutionContext { implicit executionContext =>
|
||||||
|
authenticateBasicBCrypt(s"party $partyId", authPost(partyId)) { _ =>
|
||||||
|
post {
|
||||||
|
entity(as[PieceActionResponse]) { action =>
|
||||||
|
val playerId = action.playerIdResponse.withPartyId(partyId)
|
||||||
|
complete {
|
||||||
|
val result = action.action match {
|
||||||
|
case ApiAction.add => addPieceBiS(playerId, action.piece.toPiece)
|
||||||
|
case ApiAction.remove => removePieceBiS(playerId, action.piece.toPiece)
|
||||||
|
}
|
||||||
|
result.map(_ => StatusCodes.Accepted)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,139 @@
|
|||||||
|
package me.arcanis.ffxivbis.http.api.v1
|
||||||
|
|
||||||
|
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 io.swagger.v3.oas.annotations.enums.ParameterIn
|
||||||
|
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, Parameter}
|
||||||
|
import io.swagger.v3.oas.annotations.parameters.RequestBody
|
||||||
|
import io.swagger.v3.oas.annotations.security.SecurityRequirement
|
||||||
|
import javax.ws.rs.{Consumes, GET, POST, PUT, Path, Produces}
|
||||||
|
import me.arcanis.ffxivbis.http.{Authorization, LootHelper}
|
||||||
|
import me.arcanis.ffxivbis.http.api.v1.json._
|
||||||
|
import me.arcanis.ffxivbis.models.PlayerId
|
||||||
|
|
||||||
|
@Path("api/v1")
|
||||||
|
class LootEndpoint(override val storage: ActorRef)(implicit timeout: Timeout)
|
||||||
|
extends LootHelper(storage) with Authorization with JsonSupport {
|
||||||
|
import spray.json.DefaultJsonProtocol._
|
||||||
|
|
||||||
|
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"),
|
||||||
|
new ApiResponse(responseCode = "403", description = "Access is forbidden"),
|
||||||
|
new ApiResponse(responseCode = "500", description = "Internal server error"),
|
||||||
|
),
|
||||||
|
security = Array(new SecurityRequirement(name = "basic auth", scopes = Array("get"))),
|
||||||
|
tags = Array("loot"),
|
||||||
|
)
|
||||||
|
def getLoot: Route =
|
||||||
|
path("party" / Segment / "loot") { partyId: String =>
|
||||||
|
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)
|
||||||
|
complete(loot(partyId, playerId).map(_.map(PlayerResponse.fromPlayer)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@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"),
|
||||||
|
new ApiResponse(responseCode = "401", description = "Supplied authorization is invalid"),
|
||||||
|
new ApiResponse(responseCode = "403", description = "Access is forbidden"),
|
||||||
|
new ApiResponse(responseCode = "500", description = "Internal server error"),
|
||||||
|
),
|
||||||
|
security = Array(new SecurityRequirement(name = "basic auth", scopes = Array("post"))),
|
||||||
|
tags = Array("loot"),
|
||||||
|
)
|
||||||
|
def modifyLoot: Route =
|
||||||
|
path("party" / Segment / "loot") { partyId: String =>
|
||||||
|
extractExecutionContext { implicit executionContext =>
|
||||||
|
authenticateBasicBCrypt(s"party $partyId", authPost(partyId)) { _ =>
|
||||||
|
post {
|
||||||
|
entity(as[PieceActionResponse]) { action =>
|
||||||
|
val playerId = action.playerIdResponse.withPartyId(partyId)
|
||||||
|
complete {
|
||||||
|
val result = action.action match {
|
||||||
|
case ApiAction.add => addPieceLoot(playerId, action.piece.toPiece)
|
||||||
|
case ApiAction.remove => removePieceLoot(playerId, action.piece.toPiece)
|
||||||
|
}
|
||||||
|
result.map(_ => StatusCodes.Accepted)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@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"),
|
||||||
|
new ApiResponse(responseCode = "401", description = "Supplied authorization is invalid"),
|
||||||
|
new ApiResponse(responseCode = "403", description = "Access is forbidden"),
|
||||||
|
new ApiResponse(responseCode = "500", description = "Internal server error"),
|
||||||
|
),
|
||||||
|
security = Array(new SecurityRequirement(name = "basic auth", scopes = Array("get"))),
|
||||||
|
tags = Array("loot"),
|
||||||
|
)
|
||||||
|
def suggestLoot: Route =
|
||||||
|
path("party" / Segment / "loot") { partyId: String =>
|
||||||
|
extractExecutionContext { implicit executionContext =>
|
||||||
|
authenticateBasicBCrypt(s"party $partyId", authGet(partyId)) { _ =>
|
||||||
|
put {
|
||||||
|
entity(as[PieceResponse]) { piece =>
|
||||||
|
complete {
|
||||||
|
suggestPiece(partyId, piece.toPiece).map { players =>
|
||||||
|
players.map(PlayerIdWithCountersResponse.fromPlayerId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,97 @@
|
|||||||
|
package me.arcanis.ffxivbis.http.api.v1
|
||||||
|
|
||||||
|
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 io.swagger.v3.oas.annotations.enums.ParameterIn
|
||||||
|
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, Parameter}
|
||||||
|
import io.swagger.v3.oas.annotations.parameters.RequestBody
|
||||||
|
import io.swagger.v3.oas.annotations.security.SecurityRequirement
|
||||||
|
import javax.ws.rs.{Consumes, GET, POST, Path, Produces}
|
||||||
|
import me.arcanis.ffxivbis.http.{Authorization, PlayerHelper}
|
||||||
|
import me.arcanis.ffxivbis.http.api.v1.json._
|
||||||
|
import me.arcanis.ffxivbis.models.PlayerId
|
||||||
|
|
||||||
|
@Path("api/v1")
|
||||||
|
class PlayerEndpoint(override val storage: ActorRef, ariyala: ActorRef)(implicit timeout: Timeout)
|
||||||
|
extends PlayerHelper(storage, ariyala) with Authorization with JsonSupport {
|
||||||
|
import spray.json.DefaultJsonProtocol._
|
||||||
|
|
||||||
|
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"),
|
||||||
|
new ApiResponse(responseCode = "403", description = "Access is forbidden"),
|
||||||
|
new ApiResponse(responseCode = "500", description = "Internal server error"),
|
||||||
|
),
|
||||||
|
security = Array(new SecurityRequirement(name = "basic auth", scopes = Array("get"))),
|
||||||
|
tags = Array("party"),
|
||||||
|
)
|
||||||
|
def getParty: Route =
|
||||||
|
path("party" / Segment) { partyId: String =>
|
||||||
|
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)
|
||||||
|
complete(getPlayers(partyId, playerId).map(_.map(PlayerResponse.fromPlayer)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@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"),
|
||||||
|
new ApiResponse(responseCode = "401", description = "Supplied authorization is invalid"),
|
||||||
|
new ApiResponse(responseCode = "403", description = "Access is forbidden"),
|
||||||
|
new ApiResponse(responseCode = "500", description = "Internal server error"),
|
||||||
|
),
|
||||||
|
security = Array(new SecurityRequirement(name = "basic auth", scopes = Array("post"))),
|
||||||
|
tags = Array("party"),
|
||||||
|
)
|
||||||
|
def modifyParty: Route =
|
||||||
|
path("party" / Segment) { partyId: String =>
|
||||||
|
extractExecutionContext { implicit executionContext =>
|
||||||
|
authenticateBasicBCrypt(s"party $partyId", authPost(partyId)) { _ =>
|
||||||
|
entity(as[PlayerActionResponse]) { action =>
|
||||||
|
val player = action.playerIdResponse.toPlayer.copy(partyId = partyId)
|
||||||
|
complete {
|
||||||
|
val result = action.action match {
|
||||||
|
case ApiAction.add => addPlayer(player)
|
||||||
|
case ApiAction.remove => removePlayer(player.playerId)
|
||||||
|
}
|
||||||
|
result.map(_ => StatusCodes.Accepted)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,152 @@
|
|||||||
|
package me.arcanis.ffxivbis.http.api.v1
|
||||||
|
|
||||||
|
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 io.swagger.v3.oas.annotations.enums.ParameterIn
|
||||||
|
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, Parameter}
|
||||||
|
import io.swagger.v3.oas.annotations.parameters.RequestBody
|
||||||
|
import io.swagger.v3.oas.annotations.security.SecurityRequirement
|
||||||
|
import javax.ws.rs.{Consumes, DELETE, GET, POST, PUT, Path, Produces}
|
||||||
|
import me.arcanis.ffxivbis.http.{Authorization, UserHelper}
|
||||||
|
import me.arcanis.ffxivbis.http.api.v1.json._
|
||||||
|
import me.arcanis.ffxivbis.models.Permission
|
||||||
|
|
||||||
|
@Path("api/v1")
|
||||||
|
class UserEndpoint(override val storage: ActorRef)(implicit timeout: Timeout)
|
||||||
|
extends UserHelper(storage) with Authorization with JsonSupport {
|
||||||
|
import spray.json.DefaultJsonProtocol._
|
||||||
|
|
||||||
|
def route: Route = createParty ~ createUser ~ deleteUser ~ getUsers
|
||||||
|
|
||||||
|
@PUT
|
||||||
|
@Path("party/{partyId}")
|
||||||
|
@Consumes(value = Array("application/json"))
|
||||||
|
@Operation(summary = "create new party", description = "Create new party with specified ID",
|
||||||
|
parameters = Array(
|
||||||
|
new Parameter(name = "partyId", in = ParameterIn.PATH, description = "unique party ID", example = "abcdefgh"),
|
||||||
|
),
|
||||||
|
requestBody = new RequestBody(description = "party administrator description", required = true,
|
||||||
|
content = Array(new Content(schema = new Schema(implementation = classOf[UserResponse])))),
|
||||||
|
responses = Array(
|
||||||
|
new ApiResponse(responseCode = "201", description = "Party has been created"),
|
||||||
|
new ApiResponse(responseCode = "400", description = "Invalid parameters were supplied"),
|
||||||
|
new ApiResponse(responseCode = "406", description = "Party with the specified ID already exists"),
|
||||||
|
new ApiResponse(responseCode = "500", description = "Internal server error"),
|
||||||
|
),
|
||||||
|
tags = Array("party"),
|
||||||
|
)
|
||||||
|
def createParty: Route =
|
||||||
|
path("party" / Segment) { partyId: String =>
|
||||||
|
extractExecutionContext { implicit executionContext =>
|
||||||
|
put {
|
||||||
|
entity(as[UserResponse]) { user =>
|
||||||
|
val admin = user.toUser.copy(partyId = partyId, permission = Permission.admin)
|
||||||
|
complete {
|
||||||
|
addUser(admin, isHashedPassword = false).map(_ => StatusCodes.Created)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@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"),
|
||||||
|
new ApiResponse(responseCode = "401", description = "Supplied authorization is invalid"),
|
||||||
|
new ApiResponse(responseCode = "403", description = "Access is forbidden"),
|
||||||
|
new ApiResponse(responseCode = "500", description = "Internal server error"),
|
||||||
|
),
|
||||||
|
security = Array(new SecurityRequirement(name = "basic auth", scopes = Array("admin"))),
|
||||||
|
tags = Array("users"),
|
||||||
|
)
|
||||||
|
def createUser: Route =
|
||||||
|
path("party" / Segment / "users") { partyId: String =>
|
||||||
|
extractExecutionContext { implicit executionContext =>
|
||||||
|
authenticateBasicBCrypt(s"party $partyId", authAdmin(partyId)) { _ =>
|
||||||
|
post {
|
||||||
|
entity(as[UserResponse]) { user =>
|
||||||
|
val withPartyId = user.toUser.copy(partyId = partyId)
|
||||||
|
complete {
|
||||||
|
addUser(withPartyId, isHashedPassword = false).map(_ => StatusCodes.Created)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@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"),
|
||||||
|
new ApiResponse(responseCode = "403", description = "Access is forbidden"),
|
||||||
|
new ApiResponse(responseCode = "500", description = "Internal server error"),
|
||||||
|
),
|
||||||
|
security = Array(new SecurityRequirement(name = "basic auth", scopes = Array("admin"))),
|
||||||
|
tags = Array("users"),
|
||||||
|
)
|
||||||
|
def deleteUser: Route =
|
||||||
|
path("party" / Segment / "users" / Segment) { (partyId: String, username: String) =>
|
||||||
|
extractExecutionContext { implicit executionContext =>
|
||||||
|
authenticateBasicBCrypt(s"party $partyId", authAdmin(partyId)) { _ =>
|
||||||
|
delete {
|
||||||
|
complete {
|
||||||
|
removeUser(partyId, username).map(_ => StatusCodes.Accepted)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@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"),
|
||||||
|
new ApiResponse(responseCode = "403", description = "Access is forbidden"),
|
||||||
|
new ApiResponse(responseCode = "500", description = "Internal server error"),
|
||||||
|
),
|
||||||
|
security = Array(new SecurityRequirement(name = "basic auth", scopes = Array("admin"))),
|
||||||
|
tags = Array("users"),
|
||||||
|
)
|
||||||
|
def getUsers: Route =
|
||||||
|
path("party" / Segment / "users") { partyId: String =>
|
||||||
|
extractExecutionContext { implicit executionContext =>
|
||||||
|
authenticateBasicBCrypt(s"party $partyId", authAdmin(partyId)) { _ =>
|
||||||
|
get {
|
||||||
|
complete {
|
||||||
|
users(partyId).map(_.map(UserResponse.fromUser))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,5 @@
|
|||||||
|
package me.arcanis.ffxivbis.http.api.v1.json
|
||||||
|
|
||||||
|
object ApiAction extends Enumeration {
|
||||||
|
val add, remove = Value
|
||||||
|
}
|
@ -0,0 +1,31 @@
|
|||||||
|
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 {
|
||||||
|
import 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 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 pieceFormat: RootJsonFormat[PieceResponse] = jsonFormat3(PieceResponse.apply)
|
||||||
|
implicit val pieceActionFormat: RootJsonFormat[PieceActionResponse] = jsonFormat3(PieceActionResponse.apply)
|
||||||
|
implicit val playerFormat: RootJsonFormat[PlayerResponse] = jsonFormat7(PlayerResponse.apply)
|
||||||
|
implicit val playerActionFormat: RootJsonFormat[PlayerActionResponse] = jsonFormat2(PlayerActionResponse.apply)
|
||||||
|
implicit val playerBiSLinkFormat: RootJsonFormat[PlayerBiSLinkResponse] = jsonFormat2(PlayerBiSLinkResponse.apply)
|
||||||
|
implicit val playerIdFormat: RootJsonFormat[PlayerIdResponse] = jsonFormat3(PlayerIdResponse.apply)
|
||||||
|
implicit val playerIdWithCountersFormat: RootJsonFormat[PlayerIdWithCountersResponse] =
|
||||||
|
jsonFormat9(PlayerIdWithCountersResponse.apply)
|
||||||
|
implicit val userFormat: RootJsonFormat[UserResponse] = jsonFormat4(UserResponse.apply)
|
||||||
|
}
|
@ -0,0 +1,8 @@
|
|||||||
|
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) playerIdResponse: PlayerIdResponse)
|
@ -0,0 +1,16 @@
|
|||||||
|
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.fromString(job))
|
||||||
|
}
|
||||||
|
|
||||||
|
object PieceResponse {
|
||||||
|
def fromPiece(piece: Piece): PieceResponse =
|
||||||
|
PieceResponse(piece.isTome, piece.job.toString, piece.piece)
|
||||||
|
}
|
@ -0,0 +1,7 @@
|
|||||||
|
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) playerIdResponse: PlayerResponse)
|
@ -0,0 +1,7 @@
|
|||||||
|
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,12 @@
|
|||||||
|
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.fromString(job), nick)
|
||||||
|
}
|
@ -0,0 +1,29 @@
|
|||||||
|
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,25 @@
|
|||||||
|
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") priority: Option[Int]) {
|
||||||
|
def toPlayer: Player =
|
||||||
|
Player(partyId, Job.fromString(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,18 @@
|
|||||||
|
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))
|
||||||
|
}
|
72
src/main/scala/me/arcanis/ffxivbis/models/BiS.scala
Normal file
72
src/main/scala/me/arcanis/ffxivbis/models/BiS.scala
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
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,
|
||||||
|
"leftRing" -> leftRing,
|
||||||
|
"rightRing" -> 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("leftRing").flatten,
|
||||||
|
data.get("rightRing").flatten)
|
||||||
|
|
||||||
|
def apply(): BiS = BiS(Seq.empty)
|
||||||
|
|
||||||
|
def apply(pieces: Seq[Piece]): BiS =
|
||||||
|
BiS(pieces.map { piece => piece.piece -> Some(piece) }.toMap)
|
||||||
|
}
|
65
src/main/scala/me/arcanis/ffxivbis/models/Job.scala
Normal file
65
src/main/scala/me/arcanis/ffxivbis/models/Job.scala
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
package me.arcanis.ffxivbis.models
|
||||||
|
|
||||||
|
object Job {
|
||||||
|
sealed trait Job
|
||||||
|
|
||||||
|
case object AnyJob extends Job {
|
||||||
|
override def equals(obj: Any): Boolean = obj match {
|
||||||
|
case Job => true
|
||||||
|
case _ => false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case object PLD extends Job
|
||||||
|
case object WAR extends Job
|
||||||
|
case object DRK extends Job
|
||||||
|
case object GNB extends Job
|
||||||
|
|
||||||
|
case object WHM extends Job
|
||||||
|
case object SCH extends Job
|
||||||
|
case object AST extends Job
|
||||||
|
|
||||||
|
case object MNK extends Job
|
||||||
|
case object DRG extends Job
|
||||||
|
case object NIN extends Job
|
||||||
|
case object SAM extends Job
|
||||||
|
|
||||||
|
case object BRD extends Job
|
||||||
|
case object MCH extends Job
|
||||||
|
case object DNC extends Job
|
||||||
|
|
||||||
|
case object BLM extends Job
|
||||||
|
case object SMN extends Job
|
||||||
|
case object RDM extends Job
|
||||||
|
|
||||||
|
def groupAccessoriesDex: Seq[Job.Job] = groupRanges :+ NIN
|
||||||
|
def groupAccessoriesStr: Seq[Job.Job] = groupMnk :+ DRG
|
||||||
|
def groupAll: Seq[Job.Job] = groupCasters ++ groupHealers ++ groupRanges ++ groupTanks
|
||||||
|
def groupCasters: Seq[Job.Job] = Seq(BLM, SMN, RDM)
|
||||||
|
def groupHealers: Seq[Job.Job] = Seq(WHM, SCH, AST)
|
||||||
|
def groupMnk: Seq[Job.Job] = Seq(MNK, SAM)
|
||||||
|
def groupRanges: Seq[Job.Job] = Seq(BRD, MCH, DNC)
|
||||||
|
def groupTanks: Seq[Job.Job] = Seq(PLD, WAR, DRK, GNB)
|
||||||
|
|
||||||
|
def groupFull: Seq[Seq[Job.Job]] = Seq(groupCasters, groupHealers, groupMnk, groupRanges, groupTanks)
|
||||||
|
def groupRight: Seq[Seq[Job.Job]] = Seq(groupAccessoriesDex, groupAccessoriesStr)
|
||||||
|
|
||||||
|
def fromString(job: String): Job.Job = groupAll.find(_.toString == job.toUpperCase).orNull
|
||||||
|
|
||||||
|
def hasSameLoot(left: Job, right: Job, piece: Piece): Boolean = {
|
||||||
|
def isAccessory(piece: Piece): Boolean = piece match {
|
||||||
|
case _: PieceAccessory => true
|
||||||
|
case _ => false
|
||||||
|
}
|
||||||
|
def isWeapon(piece: Piece): Boolean = piece match {
|
||||||
|
case _: PieceWeapon => true
|
||||||
|
case _ => false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (left == right) true
|
||||||
|
else if (isWeapon(piece)) false
|
||||||
|
else if (groupFull.exists(group => group.contains(left) && group.contains(right))) true
|
||||||
|
else if (isAccessory(piece) && groupRight.exists(group => group.contains(left) && group.contains(right))) true
|
||||||
|
else false
|
||||||
|
}
|
||||||
|
}
|
3
src/main/scala/me/arcanis/ffxivbis/models/Loot.scala
Normal file
3
src/main/scala/me/arcanis/ffxivbis/models/Loot.scala
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
package me.arcanis.ffxivbis.models
|
||||||
|
|
||||||
|
case class Loot(playerId: Long, piece: Piece)
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user