mirror of
https://github.com/arcan1s/ffxivbis.git
synced 2025-07-10 04:15:52 +00:00
Compare commits
97 Commits
feature/pa
...
0.15.0
Author | SHA1 | Date | |
---|---|---|---|
0bcda3233e | |||
c4be6f12f1 | |||
f3535f6e16 | |||
bdf413d494 | |||
7a1a73592e | |||
b1ac894ccf | |||
6023e86570 | |||
a4ab1e49be | |||
cb99486f8a | |||
0e8b95d0dd | |||
118d8faf6b | |||
448880ed91 | |||
ed3cdd62bd | |||
88617eccdf | |||
ccbf581332 | |||
0ab9162cb5 | |||
d3018998cd | |||
d4553b2e50 | |||
8496d105c0 | |||
ec2cfaea38 | |||
963e84f792 | |||
feea01a47e | |||
fcacd9f15c | |||
b2256784dd | |||
fee87ddbc8 | |||
dc882b74bf | |||
7a6cd84ce3 | |||
33b750123d | |||
d049238dcf | |||
5d72852420 | |||
78a00e2cab | |||
786c3d7d48 | |||
8a1d99b319 | |||
ac0e0ac899 | |||
e88c9d51b0 | |||
ced781bba2 | |||
012cdd2d8b | |||
c5b0832d29 | |||
b36240765a | |||
4e3066e0a3 | |||
eeb5178efc | |||
a6991a0a91 | |||
5ec372be87 | |||
bcdc88fa2c | |||
53b42a6fa8 | |||
99ed2705a2 | |||
0ed9e92441 | |||
1866a1bb12 | |||
08f7f4571e | |||
d9cbb6cf00 | |||
df8e09f02c | |||
df1f28c7ef | |||
8d516cdb15 | |||
2e16a8c1fa | |||
25b05aa289 | |||
534ed98459 | |||
0171b229a1 | |||
10c107d2c2 | |||
16ce0bf61c | |||
1e6064e081 | |||
92e2c1d383 | |||
5eae1d46a2 | |||
eb24019965 | |||
173ea9079f | |||
12c99bd52c | |||
bdfb5aedeb | |||
666a1b8b7a | |||
65a4a25b3a | |||
37c444a5b9 | |||
f5a644747d | |||
ab790e87ff | |||
9faceb4f61 | |||
65b9e53b66 | |||
ad144534a9 | |||
4700768aed | |||
557038c262 | |||
6e8b64feef | |||
0a71a98482 | |||
69d35c95d9 | |||
155790465e | |||
da00a60332 | |||
0bf1edfff8 | |||
50acecd97e | |||
e03f8987b0 | |||
2ad3600da5 | |||
a84b947862 | |||
f84b9cbaba | |||
9f12647fed | |||
2a1eb9430e | |||
4cdcd80d51 | |||
b228595a1b | |||
d1001ffb8e | |||
09c7efec62 | |||
9668a0edd1 | |||
d5233361e5 | |||
eea2f1b04b | |||
49fd33fffc |
40
.github/workflows/release.yml
vendored
Normal file
40
.github/workflows/release.yml
vendored
Normal file
@ -0,0 +1,40 @@
|
||||
name: release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- '*.*.*'
|
||||
|
||||
jobs:
|
||||
make-release:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: extract version
|
||||
id: version
|
||||
run: echo ::set-output name=VERSION::${GITHUB_REF#refs/tags/}
|
||||
- name: create changelog
|
||||
id: changelog
|
||||
uses: jaywcjlove/changelog-generator@main
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
filter: 'Release \d+\.\d+\.\d+'
|
||||
- name: setup JDK
|
||||
uses: actions/setup-java@v2
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: 18
|
||||
- name: create dist
|
||||
run: make dist
|
||||
- name: release
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
body: |
|
||||
${{ steps.changelog.outputs.compareurl }}
|
||||
${{ steps.changelog.outputs.changelog }}
|
||||
files: target/universal/ffxivbis-*.zip
|
||||
fail_on_unmatched_files: true
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
22
.github/workflows/run-tests.yml
vendored
Normal file
22
.github/workflows/run-tests.yml
vendored
Normal file
@ -0,0 +1,22 @@
|
||||
name: tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
|
||||
jobs:
|
||||
run-tests:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: setup JDK
|
||||
uses: actions/setup-java@v2
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: 18
|
||||
- name: run tests
|
||||
run: make tests
|
163
.gitignore
vendored
163
.gitignore
vendored
@ -1,96 +1,89 @@
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
#### joe made this: http://goel.io/joe
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
#### jetbrains ####
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
env/
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
parts/
|
||||
sdist/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm
|
||||
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
# User-specific stuff:
|
||||
.idea
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
## File-based project format:
|
||||
*.iws
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
## Plugin-specific files:
|
||||
|
||||
# IntelliJ
|
||||
/out/
|
||||
|
||||
# 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
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*,cover
|
||||
.hypothesis/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log*
|
||||
local_settings.py
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
.history
|
||||
.lib/
|
||||
dist/*
|
||||
target/
|
||||
lib_managed/
|
||||
src_managed/
|
||||
project/boot/
|
||||
project/plugins/project/
|
||||
.bsp/
|
||||
|
||||
# IPython Notebook
|
||||
.ipynb_checkpoints
|
||||
# Scala-IDE specific
|
||||
.scala_dependencies
|
||||
.worksheet
|
||||
|
||||
# pyenv
|
||||
.python-version
|
||||
|
||||
# celery beat schedule file
|
||||
celerybeat-schedule
|
||||
|
||||
# dotenv
|
||||
.env
|
||||
|
||||
# virtualenv
|
||||
venv/
|
||||
ENV/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
*.deb
|
||||
|
||||
.idea/
|
||||
|
||||
.mypy_cache/
|
||||
|
||||
/cache
|
||||
# ENSIME specific
|
||||
.ensime_cache/
|
||||
.ensime
|
||||
|
||||
*.db
|
||||
*.sc
|
||||
|
35
.scalafmt.conf
Normal file
35
.scalafmt.conf
Normal file
@ -0,0 +1,35 @@
|
||||
version = 3.3.1
|
||||
|
||||
runner.dialect = "scala213"
|
||||
|
||||
maxColumn = 120
|
||||
|
||||
align.preset = none
|
||||
|
||||
continuationIndent {
|
||||
defnSite = 2
|
||||
extendSite = 2
|
||||
}
|
||||
|
||||
rewrite {
|
||||
rules = [
|
||||
AvoidInfix,
|
||||
RedundantBraces,
|
||||
RedundantParens,
|
||||
SortImports,
|
||||
SortModifiers
|
||||
]
|
||||
|
||||
redundantBraces {
|
||||
generalExpressions = yes
|
||||
ifElseExpressions = yes
|
||||
includeUnitMethods = yes
|
||||
methodBodies = yes
|
||||
parensForOneLineApply = yes
|
||||
stringInterpolation = yes
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
importSelectors = singleLine
|
||||
trailingCommas = preserve
|
35
Makefile
Normal file
35
Makefile
Normal file
@ -0,0 +1,35 @@
|
||||
.PHONY: check clean compile dist push tests version
|
||||
.DEFAULT_GOAL := compile
|
||||
|
||||
PROJECT := ffxivbis
|
||||
|
||||
check:
|
||||
sbt scalafmtCheck
|
||||
|
||||
clean:
|
||||
sbt clean
|
||||
|
||||
compile: clean
|
||||
sbt compile
|
||||
|
||||
format:
|
||||
sbt scalafmt
|
||||
|
||||
dist: tests
|
||||
sbt dist
|
||||
|
||||
push: version dist
|
||||
git add version.sbt
|
||||
git commit -m "Release $(VERSION)"
|
||||
git tag "$(VERSION)"
|
||||
git push
|
||||
git push --tags
|
||||
|
||||
tests: compile check
|
||||
sbt test
|
||||
|
||||
version:
|
||||
ifndef VERSION
|
||||
$(error VERSION is required, but not set)
|
||||
endif
|
||||
sed -i '/version := "[0-9.]*/s/[^"][^)]*/version := "$(VERSION)"/' version.sbt
|
90
README.md
90
README.md
@ -1,34 +1,24 @@
|
||||
# FFXIV BiS
|
||||
|
||||
Service which allows to manage savage loot distribution easy.
|
||||
[](https://github.com/arcan1s/ffxivbis/actions/workflows/run-tests.yml) 
|
||||
|
||||
Service which allows managing savage loot distribution easy.
|
||||
|
||||
## Installation and usage
|
||||
|
||||
This service requires python >= 3.7. For other dependencies see `setup.py`.
|
||||
|
||||
In general installation process looks like:
|
||||
In general compilation process looks like:
|
||||
|
||||
```bash
|
||||
python setup.py build install
|
||||
python setup.py test # if you want to run tests
|
||||
sbt dist
|
||||
```
|
||||
|
||||
With virtualenv (make sure that virtualenv package was installed) the process may look like:
|
||||
Or alternatively you can download the latest distribution zip from the releases page. Service can be run by using command:
|
||||
|
||||
```bash
|
||||
virtualenv -p python3.7 env
|
||||
source env/bin/activate
|
||||
python setup.py install
|
||||
pip install aiosqlite # setup.py does not handle extras
|
||||
bin/ffxivbis
|
||||
```
|
||||
|
||||
Service can be run by using command (if you don't use virtualenv, you have to run it from `src` directory):
|
||||
|
||||
```bash
|
||||
python -m ffxivbis.application.application
|
||||
```
|
||||
|
||||
To see all available options type `--help`.
|
||||
from the extracted archive root.
|
||||
|
||||
## Web service
|
||||
|
||||
@ -36,66 +26,6 @@ REST API documentation is available at `http://0.0.0.0:8000/api-docs`. HTML repr
|
||||
|
||||
*Note*: host and port depend on configuration settings.
|
||||
|
||||
### Authorization
|
||||
## Public service
|
||||
|
||||
Default admin user is `admin:qwerty`, but it may be changed by generating new hash, e.g.:
|
||||
|
||||
```python
|
||||
from passlib.hash import md5_crypt
|
||||
md5_crypt.hash('newstrongpassword')
|
||||
```
|
||||
|
||||
and add new password to configuration.
|
||||
|
||||
## Configuration
|
||||
|
||||
* `settings` section
|
||||
|
||||
General project settings.
|
||||
|
||||
* `include`: path to include configuration directory, string, optional.
|
||||
* `logging`: path to logging configuration, see `logging.ini` for reference, string, optional.
|
||||
* `database`: database provide name, string, required. Allowed values: `sqlite`, `postgres`.
|
||||
* `priority`: methods of `Player` class which will be called to sort players for loot priority, space separated list of strings, required.
|
||||
|
||||
* `ariyala` section
|
||||
|
||||
Settings related to ariyala parser.
|
||||
|
||||
* `ariyala_url`: ariyala base url, string, required.
|
||||
* `xivapi_key`: xivapi developer key, string, optional.
|
||||
* `xivapi_url`: xivapi base url, string, required.
|
||||
|
||||
* `auth` section
|
||||
|
||||
Authentication settings.
|
||||
|
||||
* `enabled`: whether authentication enabled or not, boolean, required.
|
||||
* `root_username`: username of administrator, string, required.
|
||||
* `root_password`: md5 hashed password of administrator, string, required.
|
||||
|
||||
* `postgres` section
|
||||
|
||||
Database settings for `postgres` provider.
|
||||
|
||||
* `database`: database name, string, required.
|
||||
* `host`: database host, string, required.
|
||||
* `password`: database password, string, required.
|
||||
* `port`: database port, int, required.
|
||||
* `username`: database username, string, required.
|
||||
* `migrations_path`: path to database migrations, string, required.
|
||||
|
||||
* `sqlite` section
|
||||
|
||||
Database settings for `sqlite` provider.
|
||||
|
||||
* `database_path`: path to sqlite database, string, required.
|
||||
* `migrations_path`: path to database migrations, string, required.
|
||||
|
||||
* `web` section
|
||||
|
||||
Web server related settings.
|
||||
|
||||
* `host`: address to bind, string, required.
|
||||
* `port`: port to bind, int, required.
|
||||
* `templates`: path to directory with jinja templates, string, required.
|
||||
There is also public service which is available at https://ffxivbis.arcanis.me.
|
||||
|
3
TODO.md
3
TODO.md
@ -1,3 +0,0 @@
|
||||
* [ ] items improvements
|
||||
* [ ] multiple parties support
|
||||
* [ ] pretty UI
|
9
build.sbt
Normal file
9
build.sbt
Normal file
@ -0,0 +1,9 @@
|
||||
organization := "me.arcanis"
|
||||
|
||||
name := "ffxivbis"
|
||||
|
||||
scalaVersion := "2.13.12"
|
||||
|
||||
scalacOptions ++= Seq("-deprecation", "-feature")
|
||||
|
||||
enablePlugins(JavaAppPackaging)
|
56
extract_items.py
Normal file
56
extract_items.py
Normal file
@ -0,0 +1,56 @@
|
||||
import json
|
||||
import requests
|
||||
|
||||
# NOTE: it does not cover all items, just workaround to extract most gear pieces from patches
|
||||
MIN_ILVL = 580
|
||||
MAX_ILVL = 605
|
||||
|
||||
TOME = (
|
||||
'radiant',
|
||||
)
|
||||
SAVAGE = (
|
||||
'asphodelos',
|
||||
)
|
||||
|
||||
|
||||
payload = {
|
||||
'queries': [
|
||||
{
|
||||
'slots': []
|
||||
},
|
||||
{
|
||||
'jobs': [],
|
||||
'minItemLevel': 580,
|
||||
'maxItemLevel': 605
|
||||
}
|
||||
],
|
||||
'existing': []
|
||||
}
|
||||
# it does not support application/json
|
||||
r = requests.post('https://ffxiv.ariyala.com/items.app', data=json.dumps(payload))
|
||||
r.raise_for_status()
|
||||
|
||||
result = []
|
||||
|
||||
for item in r.json():
|
||||
item_id = item['itemID']
|
||||
source_dict = item['source']
|
||||
name = item['name']['en']
|
||||
if 'crafting' in source_dict:
|
||||
source = 'Crafted'
|
||||
elif 'gathering' in source_dict:
|
||||
continue # some random shit
|
||||
elif 'purchase' in source_dict:
|
||||
if any(tome in name.lower() for tome in TOME):
|
||||
source = 'Tome'
|
||||
elif any(savage in name.lower() for savage in SAVAGE):
|
||||
source = 'Savage'
|
||||
else:
|
||||
source = None
|
||||
continue
|
||||
else:
|
||||
raise RuntimeError(f'Unknown source {source_dict}')
|
||||
result.append({'id': item_id, 'source': source, 'name': name})
|
||||
|
||||
output = {'cached-items': result}
|
||||
print(json.dumps(output, indent=4, sort_keys=True))
|
33
libraries.sbt
Normal file
33
libraries.sbt
Normal file
@ -0,0 +1,33 @@
|
||||
val AkkaVersion = "2.8.6"
|
||||
val AkkaHttpVersion = "10.5.3"
|
||||
val ScalaTestVersion = "3.2.19"
|
||||
|
||||
libraryDependencies += "ch.qos.logback" % "logback-classic" % "1.5.6"
|
||||
libraryDependencies += "com.typesafe.scala-logging" %% "scala-logging" % "3.9.5"
|
||||
|
||||
libraryDependencies += "com.typesafe.akka" %% "akka-http" % AkkaHttpVersion
|
||||
libraryDependencies += "com.typesafe.akka" %% "akka-http-spray-json" % AkkaHttpVersion
|
||||
libraryDependencies += "com.typesafe.akka" %% "akka-actor-typed" % AkkaVersion
|
||||
libraryDependencies += "com.typesafe.akka" %% "akka-stream" % AkkaVersion
|
||||
libraryDependencies += "com.github.swagger-akka-http" %% "swagger-akka-http" % "2.11.0"
|
||||
libraryDependencies += "jakarta.platform" % "jakarta.jakartaee-web-api" % "10.0.0"
|
||||
libraryDependencies += "ch.megard" %% "akka-http-cors" % "1.2.0"
|
||||
|
||||
libraryDependencies += "io.spray" %% "spray-json" % "1.3.6"
|
||||
|
||||
libraryDependencies += "org.playframework.anorm" %% "anorm" % "2.7.0"
|
||||
libraryDependencies += "com.zaxxer" % "HikariCP" % "5.1.0" exclude("org.slf4j", "slf4j-api")
|
||||
libraryDependencies += "org.flywaydb" % "flyway-core" % "9.16.0"
|
||||
libraryDependencies += "org.xerial" % "sqlite-jdbc" % "3.46.0.0"
|
||||
libraryDependencies += "org.postgresql" % "postgresql" % "42.7.3"
|
||||
|
||||
libraryDependencies += "org.mindrot" % "jbcrypt" % "0.4"
|
||||
libraryDependencies += "com.google.guava" % "guava" % "31.0.1-jre"
|
||||
|
||||
// testing
|
||||
libraryDependencies += "org.scalactic" %% "scalactic" % ScalaTestVersion % "test"
|
||||
libraryDependencies += "org.scalatest" %% "scalatest" % ScalaTestVersion % "test"
|
||||
|
||||
libraryDependencies += "com.typesafe.akka" %% "akka-actor-testkit-typed" % AkkaVersion % "test"
|
||||
libraryDependencies += "com.typesafe.akka" %% "akka-stream-testkit" % AkkaVersion % "test"
|
||||
libraryDependencies += "com.typesafe.akka" %% "akka-http-testkit" % AkkaHttpVersion % "test"
|
@ -1,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/build.properties
Normal file
1
project/build.properties
Normal file
@ -0,0 +1 @@
|
||||
sbt.version = 1.7.1
|
4
project/plugins.sbt
Normal file
4
project/plugins.sbt
Normal file
@ -0,0 +1,4 @@
|
||||
addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.4.3")
|
||||
addSbtPlugin("org.scoverage" % "sbt-scoverage" % "1.9.3")
|
||||
addSbtPlugin("com.typesafe.sbt" % "sbt-native-packager" % "1.3.4")
|
||||
addDependencyTreePlugin
|
@ -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']
|
@ -0,0 +1,36 @@
|
||||
create table players (
|
||||
party_id text not null,
|
||||
player_id bigserial unique,
|
||||
created bigint not null,
|
||||
nick text not null,
|
||||
job text not null,
|
||||
bis_link text,
|
||||
priority integer not null default 1);
|
||||
create unique index players_nick_job_idx on players(party_id, nick, job);
|
||||
|
||||
create table loot (
|
||||
loot_id bigserial unique,
|
||||
player_id bigint not null,
|
||||
created bigint not null,
|
||||
piece text not null,
|
||||
is_tome integer not null,
|
||||
job text not null,
|
||||
foreign key (player_id) references players(player_id) on delete cascade);
|
||||
create index loot_owner_idx on loot(player_id);
|
||||
|
||||
create table bis (
|
||||
player_id bigint not null,
|
||||
created bigint not null,
|
||||
piece text not null,
|
||||
is_tome integer not null,
|
||||
job text not null,
|
||||
foreign key (player_id) references players(player_id) on delete cascade);
|
||||
create unique index bis_piece_player_id_idx on bis(player_id, piece);
|
||||
|
||||
create table users (
|
||||
party_id text not null,
|
||||
user_id bigserial unique,
|
||||
username text not null,
|
||||
password text not null,
|
||||
permission text not null);
|
||||
create unique index users_username_idx on users(party_id, username);
|
@ -0,0 +1,5 @@
|
||||
update loot set piece = 'left ring' where piece = 'leftRing';
|
||||
update loot set piece = 'right ring' where piece = 'rightRing';
|
||||
|
||||
update bis set piece = 'left ring' where piece = 'leftRing';
|
||||
update bis set piece = 'right ring' where piece = 'rightRing';
|
@ -0,0 +1,5 @@
|
||||
create table parties (
|
||||
player_id bigserial unique,
|
||||
party_name text not null,
|
||||
party_alias text);
|
||||
create unique index parties_party_name_idx on parties(party_name);
|
@ -0,0 +1,17 @@
|
||||
-- loot
|
||||
alter table loot add column piece_type text;
|
||||
|
||||
update loot set piece_type = 'Tome' where is_tome = 1;
|
||||
update loot set piece_type = 'Savage' where is_tome = 0;
|
||||
|
||||
alter table loot alter column piece_type set not null;
|
||||
alter table loot drop column is_tome;
|
||||
|
||||
-- bis
|
||||
alter table bis add column piece_type text;
|
||||
|
||||
update bis set piece_type = 'Tome' where is_tome = 1;
|
||||
update bis set piece_type = 'Savage' where is_tome = 0;
|
||||
|
||||
alter table bis alter column piece_type set not null;
|
||||
alter table bis drop column is_tome;
|
@ -0,0 +1 @@
|
||||
alter table loot add column is_free_loot integer not null default 0;
|
@ -0,0 +1,2 @@
|
||||
drop index bis_piece_player_id_idx;
|
||||
create index bis_piece_type_player_id_idx on bis(player_id, piece, piece_type);
|
@ -0,0 +1 @@
|
||||
alter table parties rename column player_id to party_id;
|
@ -0,0 +1,2 @@
|
||||
drop index bis_piece_type_player_id_idx;
|
||||
create unique index bis_piece_type_player_id_idx on bis(player_id, piece, piece_type);
|
@ -0,0 +1,3 @@
|
||||
update parties set party_alias = regexp_replace(party_alias, '[^A-Za-z0-9!@#$%^&*()\-_=+;:'',./? ]', '', 'g');
|
||||
update players set nick = regexp_replace(nick, '[^A-Za-z0-9!@#$%^&*()\-_=+;:'',./? ]', '', 'g');
|
||||
update users set username = regexp_replace(username, '[^A-Za-z0-9!@#$%^&*()\-_=+;:'',./? ]', '', 'g');
|
@ -0,0 +1 @@
|
||||
update players set bis_link = null where bis_link = '';
|
@ -0,0 +1,36 @@
|
||||
create table players (
|
||||
party_id text not null,
|
||||
player_id integer primary key autoincrement,
|
||||
created integer not null,
|
||||
nick text not null,
|
||||
job text not null,
|
||||
bis_link text,
|
||||
priority integer not null default 1);
|
||||
create unique index players_nick_job_idx on players(party_id, nick, job);
|
||||
|
||||
create table loot (
|
||||
loot_id integer primary key autoincrement,
|
||||
player_id integer not null,
|
||||
created integer not null,
|
||||
piece text not null,
|
||||
is_tome integer not null,
|
||||
job text not null,
|
||||
foreign key (player_id) references players(player_id) on delete cascade);
|
||||
create index loot_owner_idx on loot(player_id);
|
||||
|
||||
create table bis (
|
||||
player_id integer not null,
|
||||
created integer not null,
|
||||
piece text not null,
|
||||
is_tome integer not null,
|
||||
job text not null,
|
||||
foreign key (player_id) references players(player_id) on delete cascade);
|
||||
create unique index bis_piece_player_id_idx on bis(player_id, piece);
|
||||
|
||||
create table users (
|
||||
party_id text not null,
|
||||
user_id integer primary key autoincrement,
|
||||
username text not null,
|
||||
password text not null,
|
||||
permission text not null);
|
||||
create unique index users_username_idx on users(party_id, username);
|
@ -0,0 +1,5 @@
|
||||
update loot set piece = 'left ring' where piece = 'leftRing';
|
||||
update loot set piece = 'right ring' where piece = 'rightRing';
|
||||
|
||||
update bis set piece = 'left ring' where piece = 'leftRing';
|
||||
update bis set piece = 'right ring' where piece = 'rightRing';
|
@ -0,0 +1,5 @@
|
||||
create table parties (
|
||||
player_id integer primary key autoincrement,
|
||||
party_name text not null,
|
||||
party_alias text);
|
||||
create unique index parties_party_name_idx on parties(party_name);
|
@ -0,0 +1,42 @@
|
||||
-- loot
|
||||
alter table loot add column piece_type text;
|
||||
|
||||
update loot set piece_type = 'Tome' where is_tome = 1;
|
||||
update loot set piece_type = 'Savage' where is_tome = 0;
|
||||
|
||||
create table loot_new (
|
||||
loot_id integer primary key autoincrement,
|
||||
player_id integer not null,
|
||||
created integer not null,
|
||||
piece text not null,
|
||||
piece_type text not null,
|
||||
job text not null,
|
||||
foreign key (player_id) references players(player_id) on delete cascade);
|
||||
insert into loot_new select loot_id, player_id, created, piece, piece_type, job from loot;
|
||||
|
||||
drop index loot_owner_idx;
|
||||
drop table loot;
|
||||
|
||||
alter table loot_new rename to loot;
|
||||
create index loot_owner_idx on loot(player_id);
|
||||
|
||||
-- bis
|
||||
alter table bis add column piece_type text;
|
||||
|
||||
update bis set piece_type = 'Tome' where is_tome = 1;
|
||||
update bis set piece_type = 'Savage' where is_tome = 0;
|
||||
|
||||
create table bis_new (
|
||||
player_id integer not null,
|
||||
created integer not null,
|
||||
piece text not null,
|
||||
piece_type text not null,
|
||||
job text not null,
|
||||
foreign key (player_id) references players(player_id) on delete cascade);
|
||||
insert into bis_new select player_id, created, piece, piece_type, job from bis;
|
||||
|
||||
drop index bis_piece_player_id_idx;
|
||||
drop table bis;
|
||||
|
||||
alter table bis_new rename to bis;
|
||||
create unique index bis_piece_player_id_idx on bis(player_id, piece);
|
20
src/main/resources/db/migration/sqlite/V5_0__Free_loot.sql
Normal file
20
src/main/resources/db/migration/sqlite/V5_0__Free_loot.sql
Normal file
@ -0,0 +1,20 @@
|
||||
alter table loot add column is_free_loot integer;
|
||||
|
||||
update loot set is_free_loot = 0;
|
||||
|
||||
create table loot_new (
|
||||
loot_id integer primary key autoincrement,
|
||||
player_id integer not null,
|
||||
created integer not null,
|
||||
piece text not null,
|
||||
piece_type text not null,
|
||||
job text not null,
|
||||
is_free_loot integer not null,
|
||||
foreign key (player_id) references players(player_id) on delete cascade);
|
||||
insert into loot_new select loot_id, player_id, created, piece, piece_type, job, is_free_loot from loot;
|
||||
|
||||
drop index loot_owner_idx;
|
||||
drop table loot;
|
||||
|
||||
alter table loot_new rename to loot;
|
||||
create index loot_owner_idx on loot(player_id);
|
@ -0,0 +1,2 @@
|
||||
drop index bis_piece_player_id_idx;
|
||||
create index bis_piece_type_player_id_idx on bis(player_id, piece, piece_type);
|
@ -0,0 +1,11 @@
|
||||
create table parties_new (
|
||||
party_id integer primary key autoincrement,
|
||||
party_name text not null,
|
||||
party_alias text);
|
||||
insert into parties_new select player_id, party_name, party_alias from parties;
|
||||
|
||||
drop index parties_party_name_idx;
|
||||
drop table parties;
|
||||
|
||||
alter table parties_new rename to parties;
|
||||
create unique index parties_party_name_idx on parties(party_name);
|
@ -0,0 +1,2 @@
|
||||
drop index bis_piece_type_player_id_idx;
|
||||
create unique index bis_piece_type_player_id_idx on bis(player_id, piece, piece_type);
|
23
src/main/resources/html/api.html
Normal file
23
src/main/resources/html/api.html
Normal file
@ -0,0 +1,23 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>FFXIV loot helper API</title>
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||
<!-- Embed elements Elements via Web Component -->
|
||||
<script src="https://unpkg.com/@stoplight/elements/web-components.min.js"></script>
|
||||
<link rel="stylesheet" href="https://unpkg.com/@stoplight/elements/styles.min.css" type="text/css">
|
||||
|
||||
<link rel="shortcut icon" href="/static/favicon.ico">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<elements-api
|
||||
apiDescriptionUrl="/api-docs/swagger.json"
|
||||
router="hash"
|
||||
layout="sidebar"
|
||||
/>
|
||||
|
||||
</body>
|
||||
</html>
|
366
src/main/resources/html/bis.html
Normal file
366
src/main/resources/html/bis.html
Normal file
@ -0,0 +1,366 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Best in slot</title>
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
|
||||
<link rel="shortcut icon" href="/static/favicon.ico">
|
||||
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous" type="text/css">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.8.3/font/bootstrap-icons.css" type="text/css">
|
||||
|
||||
<link rel="stylesheet" href="https://unpkg.com/bootstrap-table@1.20.2/dist/bootstrap-table.min.css" type="text/css">
|
||||
|
||||
|
||||
<link rel="stylesheet" href="https://unpkg.com/jquery-resizable-columns@0.2.3/dist/jquery.resizableColumns.css" type="text/css">
|
||||
|
||||
<link rel="stylesheet" href="/static/styles.css" type="text/css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="container">
|
||||
<nav class="navbar navbar-expand-lg justify-content-between align-items-center border-bottom">
|
||||
<a class="navbar-brand" id="navbar-title">Party</a>
|
||||
<ul class="navbar-nav">
|
||||
<a class="nav-item nav-link" id="navbar-bis">best in slot</a>
|
||||
<a class="nav-item nav-link" id="navbar-loot">looted items</a>
|
||||
</ul>
|
||||
<ul class="navbar-nav">
|
||||
<a class="nav-item nav-link" id="navbar-users">users</a>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<div id="alert-placeholder" class="container"></div>
|
||||
|
||||
<div class="container">
|
||||
<h2>Best in slot</h2>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<div id="toolbar">
|
||||
<button id="update-btn" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#update-bis-dialog" hidden>
|
||||
<i class="bi bi-plus"></i> update
|
||||
</button>
|
||||
<button class="btn btn-secondary" onclick="reload()">
|
||||
<i class="bi bi-arrow-clockwise"></i> reload
|
||||
</button>
|
||||
<button id="remove-btn" class="btn btn-danger" onclick="removePiece()" disabled hidden>
|
||||
<i class="bi bi-trash"></i> remove
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<table id="bis" class="table table-striped table-hover"
|
||||
data-click-to-select="true"
|
||||
data-export-options='{"fileName": "bis"}'
|
||||
data-page-list="[25, 50, 100, all]"
|
||||
data-page-size="25"
|
||||
data-pagination="true"
|
||||
data-resizable="true"
|
||||
data-search="true"
|
||||
data-show-columns="true"
|
||||
data-show-columns-search="true"
|
||||
data-show-columns-toggle-all="true"
|
||||
data-show-export="true"
|
||||
data-show-fullscreen="true"
|
||||
data-show-search-clear-button="true"
|
||||
data-single-select="true"
|
||||
data-sortable="true"
|
||||
data-sort-name="nick"
|
||||
data-sort-order="asc"
|
||||
data-sort-reset="true"
|
||||
data-toolbar="#toolbar">
|
||||
<thead class="table-primary">
|
||||
<tr>
|
||||
<th data-checkbox="true"></th>
|
||||
<th data-sortable="true" data-switchable="false" data-field="nick">nick</th>
|
||||
<th data-sortable="true" data-switchable="false" data-field="job">job</th>
|
||||
<th data-sortable="true" data-field="piece">piece</th>
|
||||
<th data-sortable="true" data-field="pieceType">piece type</th>
|
||||
</tr>
|
||||
</thead>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div id="update-bis-dialog" tabindex="-1" role="dialog" class="modal fade">
|
||||
<div class="modal-dialog" role="document">
|
||||
<form class="modal-content" action="javascript:" onsubmit="updateBis()">
|
||||
<div class="modal-header form-group row">
|
||||
<div class="btn-group" role="group" aria-label="Update bis">
|
||||
<input id="add-piece-btn" name="update-bis" type="radio" class="btn-check" autocomplete="off" checked>
|
||||
<label class="btn btn-outline-primary" for="add-piece-btn">add piece</label>
|
||||
|
||||
<input id="update-bis-btn" name="update-bis" type="radio" class="btn-check" autocomplete="off">
|
||||
<label class="btn btn-outline-primary" for="update-bis-btn">update bis</label>
|
||||
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="close"></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-body">
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-4 col-form-label" for="player">player</label>
|
||||
<div class="col-sm-8">
|
||||
<select id="player" name="player" class="form-control" title="player" required></select>
|
||||
</div>
|
||||
</div>
|
||||
<div id="piece-row" class="form-group row">
|
||||
<label class="col-sm-4 col-form-label" for="piece">piece</label>
|
||||
<div class="col-sm-8">
|
||||
<select id="piece" name="piece" class="form-control" title="piece"></select>
|
||||
</div>
|
||||
</div>
|
||||
<div id="piece-type-row" class="form-group row">
|
||||
<label class="col-sm-4 col-form-label" for="piece-type">piece type</label>
|
||||
<div class="col-sm-8">
|
||||
<select id="piece-type" name="pieceType" class="form-control" title="piece-type"></select>
|
||||
</div>
|
||||
</div>
|
||||
<div id="bis-link-row" class="form-group row" style="display: none">
|
||||
<label class="col-sm-4 col-form-label" for="bis-link">link</label>
|
||||
<div class="col-sm-8">
|
||||
<input id="bis-link" name="link" class="form-control" placeholder="link to bis">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-danger" data-bs-dismiss="modal">close</button>
|
||||
<button id="submit-add-bis-btn" type="submit" class="btn btn-primary">add</button>
|
||||
<button id="submit-set-bis-btn" type="submit" class="btn btn-primary" style="display: none">set</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<footer class="d-flex flex-wrap justify-content-between align-items-center border-top">
|
||||
<ul class="nav">
|
||||
<li><a class="nav-link" href="/" title="home">home</a></li>
|
||||
<li><a class="nav-link" href="/api-docs" title="api">api</a></li>
|
||||
</ul>
|
||||
|
||||
<ul class="nav">
|
||||
<li><a id="sources-link" class="nav-link" href="https://github.com/arcan1s/ffxivbis" title="sources">ffxivbis</a></li>
|
||||
<li><a class="nav-link" href="https://github.com/arcan1s/ffxivbis/releases" title="releases list">releases</a></li>
|
||||
<li><a class="nav-link" href="https://github.com/arcan1s/ffxivbis/issues" title="issues tracker">report a bug</a></li>
|
||||
</ul>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/jquery/dist/jquery.min.js"></script>
|
||||
|
||||
<script src="https://unpkg.com/tableexport.jquery.plugin/tableExport.min.js"></script>
|
||||
|
||||
<script src="https://unpkg.com/jquery-resizable-columns@0.2.3/dist/jquery.resizableColumns.min.js"></script>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p" crossorigin="anonymous"></script>
|
||||
<script src="https://unpkg.com/bootstrap-table@1.20.2/dist/bootstrap-table.min.js"></script>
|
||||
|
||||
<script src="https://unpkg.com/bootstrap-table@1.20.2/dist/extensions/export/bootstrap-table-export.min.js"></script>
|
||||
|
||||
<script src="https://unpkg.com/bootstrap-table@1.20.2/dist/extensions/resizable/bootstrap-table-resizable.js"></script>
|
||||
|
||||
<script src="/static/utils.js"></script>
|
||||
<script src="/static/load.js"></script>
|
||||
|
||||
<script>
|
||||
const [partyId, isReadOnly] = getPartyId();
|
||||
const table = $("#bis");
|
||||
const removeButton = $("#remove-btn");
|
||||
const updateButton = $("#update-btn");
|
||||
|
||||
const submitAddBisButton = $("#submit-add-bis-btn");
|
||||
const submitSetBisButton = $("#submit-set-bis-btn");
|
||||
const updateBisDialog = $("#update-bis-dialog");
|
||||
|
||||
const addPieceButton = $("#add-piece-btn");
|
||||
const updateBisButton = $("#update-bis-btn");
|
||||
|
||||
const bisLinkRow = $("#bis-link-row");
|
||||
const pieceRow = $("#piece-row");
|
||||
const pieceTypeRow = $("#piece-type-row");
|
||||
|
||||
const linkInput = $("#bis-link");
|
||||
const pieceInput = $("#piece");
|
||||
const pieceTypeInput = $("#piece-type");
|
||||
const playerInput = $("#player");
|
||||
|
||||
function addPiece() {
|
||||
const player = getCurrentOption(playerInput);
|
||||
$.ajax({
|
||||
url: `/api/v1/party/${partyId}/bis`,
|
||||
data: JSON.stringify({
|
||||
action: "add",
|
||||
piece: {
|
||||
pieceType: pieceTypeInput.val(),
|
||||
job: player.dataset.job,
|
||||
piece: pieceInput.val(),
|
||||
},
|
||||
playerId: {
|
||||
partyId: partyId,
|
||||
nick: player.dataset.nick,
|
||||
job: player.dataset.job,
|
||||
},
|
||||
}),
|
||||
type: "POST",
|
||||
contentType: "application/json",
|
||||
success: _ => { reload(); },
|
||||
error: (jqXHR, _, errorThrown) => { requestAlert(jqXHR, errorThrown); },
|
||||
});
|
||||
updateBisDialog.modal("hide");
|
||||
return true; // action expects boolean result
|
||||
}
|
||||
|
||||
function hideControls() {
|
||||
removeButton.attr("hidden", isReadOnly);
|
||||
updateButton.attr("hidden", isReadOnly);
|
||||
}
|
||||
|
||||
function hideLinkPart() {
|
||||
bisLinkRow.hide();
|
||||
linkInput.prop("required", false);
|
||||
submitSetBisButton.hide();
|
||||
pieceRow.show();
|
||||
pieceTypeRow.show();
|
||||
pieceInput.prop("required", true);
|
||||
pieceTypeInput.prop("required", true);
|
||||
submitAddBisButton.show();
|
||||
}
|
||||
|
||||
function hidePiecePart() {
|
||||
bisLinkRow.show();
|
||||
linkInput.prop("required", true);
|
||||
submitSetBisButton.show();
|
||||
pieceRow.hide();
|
||||
pieceTypeRow.hide();
|
||||
pieceInput.prop("required", false);
|
||||
pieceTypeInput.prop("required", false);
|
||||
submitAddBisButton.hide();
|
||||
}
|
||||
|
||||
function reload() {
|
||||
table.bootstrapTable("showLoading");
|
||||
$.ajax({
|
||||
url: `/api/v1/party/${partyId}`,
|
||||
type: "GET",
|
||||
dataType: "json",
|
||||
success: response => {
|
||||
const items = response.map(player => {
|
||||
return player.bis.map(loot => {
|
||||
return {
|
||||
nick: player.nick,
|
||||
job: player.job,
|
||||
piece: loot.piece,
|
||||
pieceType: loot.pieceType,
|
||||
};
|
||||
});
|
||||
});
|
||||
const payload = items.reduce((left, right) => { return left.concat(right); }, []);
|
||||
table.bootstrapTable("load", payload);
|
||||
table.bootstrapTable("uncheckAll");
|
||||
table.bootstrapTable("hideLoading");
|
||||
|
||||
const options = response.map(player => {
|
||||
const option = document.createElement("option");
|
||||
option.innerText = formatPlayerId(player);
|
||||
option.dataset.nick = player.nick;
|
||||
option.dataset.job = player.job;
|
||||
return option;
|
||||
});
|
||||
playerInput.empty().append(options);
|
||||
},
|
||||
error: (jqXHR, _, errorThrown) => { requestAlert(jqXHR, errorThrown); },
|
||||
});
|
||||
}
|
||||
|
||||
function removePiece() {
|
||||
const pieces = table.bootstrapTable("getSelections");
|
||||
pieces.map(loot => {
|
||||
$.ajax({
|
||||
url: `/api/v1/party/${partyId}/bis`,
|
||||
data: JSON.stringify({
|
||||
action: "remove",
|
||||
piece: {
|
||||
pieceType: loot.pieceType,
|
||||
job: loot.job,
|
||||
piece: loot.piece,
|
||||
},
|
||||
playerId: {
|
||||
partyId: partyId,
|
||||
job: loot.job,
|
||||
nick: loot.nick,
|
||||
},
|
||||
}),
|
||||
type: "POST",
|
||||
contentType: "application/json",
|
||||
success: _ => { reload(); },
|
||||
error: (jqXHR, _, errorThrown) => { requestAlert(jqXHR, errorThrown); },
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function reset() {
|
||||
if (updateBisButton.is(":checked")) {
|
||||
hidePiecePart();
|
||||
}
|
||||
if (addPieceButton.is(":checked")) {
|
||||
hideLinkPart();
|
||||
}
|
||||
}
|
||||
|
||||
function setBis() {
|
||||
const player = getCurrentOption(playerInput);
|
||||
$.ajax({
|
||||
url: `/api/v1/party/${partyId}/bis`,
|
||||
data: JSON.stringify({
|
||||
link: linkInput.val(),
|
||||
playerId: {
|
||||
partyId: partyId,
|
||||
nick: player.dataset.nick,
|
||||
job: player.dataset.job,
|
||||
},
|
||||
}),
|
||||
type: "PUT",
|
||||
contentType: "application/json",
|
||||
success: _ => { reload(); },
|
||||
error: (jqXHR, _, errorThrown) => { requestAlert(jqXHR, errorThrown); },
|
||||
});
|
||||
updateBisDialog.modal("hide");
|
||||
return true; // action expects boolean result
|
||||
}
|
||||
|
||||
function updateBis() {
|
||||
if (updateBisButton.is(":checked")) {
|
||||
return setBis();
|
||||
}
|
||||
if (addPieceButton.is(":checked")) {
|
||||
return addPiece();
|
||||
}
|
||||
return false; // should not happen
|
||||
}
|
||||
|
||||
$(() => {
|
||||
setupFormClear(updateBisDialog, reset);
|
||||
setupRemoveButton(table, removeButton);
|
||||
|
||||
loadHeader(partyId);
|
||||
loadVersion();
|
||||
loadTypes("/api/v1/types/pieces", pieceInput);
|
||||
loadTypes("/api/v1/types/pieces/types", pieceTypeInput);
|
||||
|
||||
hideControls();
|
||||
|
||||
updateBisButton.click(() => { reset(); });
|
||||
addPieceButton.click(() => { reset(); });
|
||||
|
||||
table.bootstrapTable({});
|
||||
reload();
|
||||
reset();
|
||||
});
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
190
src/main/resources/html/index.html
Normal file
190
src/main/resources/html/index.html
Normal file
@ -0,0 +1,190 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>FFXIV loot helper</title>
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
|
||||
<link rel="shortcut icon" href="/static/favicon.ico">
|
||||
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous" type="text/css">
|
||||
|
||||
<link rel="stylesheet" href="/static/styles.css" type="text/css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<div id="alert-placeholder" class="container"></div>
|
||||
|
||||
<div class="container mb-5">
|
||||
<div class="form-group row">
|
||||
<div class="btn-group" role="group" aria-label="Sign in">
|
||||
<input id="signin-btn" name="signin" type="radio" class="btn-check" autocomplete="off" checked>
|
||||
<label class="btn btn-outline-primary" for="signin-btn">login to existing party</label>
|
||||
|
||||
<input id="signup-btn" name="signin" type="radio" class="btn-check" autocomplete="off">
|
||||
<label class="btn btn-outline-primary" for="signup-btn">create a new party</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form id="signup-form" class="container mb-5" style="display: none">
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-2 col-form-label" for="alias">party alias</label>
|
||||
<div class="col-sm-10">
|
||||
<input id="alias" name="alias" class="form-control" placeholder="alias">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-2 col-form-label" for="username">username</label>
|
||||
<div class="col-sm-10">
|
||||
<input id="username" name="username" class="form-control" placeholder="admin user name" onkeyup="disableAddButton()">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label for="password" class="col-sm-2 col-form-label">password</label>
|
||||
<div class="col-sm-10">
|
||||
<input id="password" name="password" type="password" class="form-control" placeholder="admin password" onkeyup="disableAddButton()">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<div class="col-sm-10">
|
||||
<button id="add-btn" type="button" class="btn btn-primary" onclick="createParty()" disabled>add</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<form id="signin-form" class="container mb-5">
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-2 col-form-label" for="party-id">party id</label>
|
||||
<div class="col-sm-10">
|
||||
<input id="party-id" name="partyId" class="form-control" placeholder="id" onkeyup="disableRedirectButton()">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<div class="col-sm-10">
|
||||
<button id="redirect-btn" type="button" class="btn btn-primary" onclick="redirectToParty()" disabled>go</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="container">
|
||||
<footer class="d-flex flex-wrap justify-content-between align-items-center border-top">
|
||||
<ul class="nav">
|
||||
<li><a class="nav-link" href="/api-docs" title="api">api</a></li>
|
||||
</ul>
|
||||
|
||||
<ul class="nav">
|
||||
<li><a id="sources-link" class="nav-link" href="https://github.com/arcan1s/ffxivbis" title="sources">ffxivbis</a></li>
|
||||
<li><a class="nav-link" href="https://github.com/arcan1s/ffxivbis/releases" title="releases list">releases</a></li>
|
||||
<li><a class="nav-link" href="https://github.com/arcan1s/ffxivbis/issues" title="issues tracker">report a bug</a></li>
|
||||
</ul>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/jquery/dist/jquery.min.js"></script>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p" crossorigin="anonymous"></script>
|
||||
|
||||
<script src="/static/utils.js"></script>
|
||||
<script src="/static/load.js"></script>
|
||||
|
||||
<script>
|
||||
const signinButton = $("#signin-btn");
|
||||
const signupButton = $("#signup-btn");
|
||||
|
||||
const addButton = $("#add-btn");
|
||||
const redirectButton = $("#redirect-btn");
|
||||
const signinForm = $("#signin-form");
|
||||
const signupForm = $("#signup-form");
|
||||
|
||||
const aliasInput = $("#alias");
|
||||
const partyIdInput = $("#party-id");
|
||||
const passwordInput = $("#password");
|
||||
const usernameInput = $("#username");
|
||||
|
||||
function createDescription(partyId) {
|
||||
$.ajax({
|
||||
url: `/api/v1/party/${partyId}/description`,
|
||||
data: JSON.stringify({
|
||||
partyId: partyId,
|
||||
partyAlias: aliasInput.val(),
|
||||
}),
|
||||
type: "POST",
|
||||
contentType: "application/json",
|
||||
success: function (_) { doRedirect(partyId); },
|
||||
error: function (jqXHR, _, errorThrown) { requestAlert(jqXHR, errorThrown); },
|
||||
});
|
||||
}
|
||||
|
||||
function createParty() {
|
||||
$.ajax({
|
||||
url: `/api/v1/party`,
|
||||
data: JSON.stringify({
|
||||
partyId: "",
|
||||
username: usernameInput.val(),
|
||||
password: passwordInput.val(),
|
||||
permission: "admin",
|
||||
}),
|
||||
type: "POST",
|
||||
contentType: "application/json",
|
||||
dataType: "json",
|
||||
success: function (data) {
|
||||
if (aliasInput.val()) {
|
||||
createDescription(data.partyId);
|
||||
} else {
|
||||
doRedirect(data.partyId);
|
||||
}
|
||||
},
|
||||
error: function (jqXHR, _, errorThrown) { requestAlert(jqXHR, errorThrown); },
|
||||
});
|
||||
}
|
||||
|
||||
function disableAddButton() {
|
||||
addButton.attr("disabled", !(passwordInput.val() && usernameInput.val()));
|
||||
}
|
||||
|
||||
function disableRedirectButton() {
|
||||
redirectButton.attr("disabled", !partyIdInput.val());
|
||||
}
|
||||
|
||||
function doRedirect(partyId) {
|
||||
location.href = `/party/${partyId}`;
|
||||
}
|
||||
|
||||
function hideSigninPart() {
|
||||
signinForm.hide();
|
||||
signupForm.show();
|
||||
}
|
||||
|
||||
function hideSignupPart() {
|
||||
signinForm.show();
|
||||
signupForm.hide();
|
||||
}
|
||||
|
||||
function redirectToParty() {
|
||||
return doRedirect(partyIdInput.val());
|
||||
}
|
||||
|
||||
function reset() {
|
||||
signinForm.trigger("reset");
|
||||
signupForm.trigger("reset");
|
||||
if (signinButton.is(":checked")) {
|
||||
hideSignupPart();
|
||||
}
|
||||
if (signupButton.is(":checked")) {
|
||||
hideSigninPart();
|
||||
}
|
||||
}
|
||||
|
||||
$(function () {
|
||||
loadVersion();
|
||||
|
||||
signinButton.click(function () { reset(); });
|
||||
signupButton.click(function () { reset(); });
|
||||
});
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
358
src/main/resources/html/loot.html
Normal file
358
src/main/resources/html/loot.html
Normal file
@ -0,0 +1,358 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Loot table</title>
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
|
||||
<link rel="shortcut icon" href="/static/favicon.ico">
|
||||
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous" type="text/css">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.8.3/font/bootstrap-icons.css" type="text/css">
|
||||
|
||||
<link rel="stylesheet" href="https://unpkg.com/bootstrap-table@1.20.2/dist/bootstrap-table.min.css" type="text/css">
|
||||
|
||||
|
||||
<link rel="stylesheet" href="https://unpkg.com/jquery-resizable-columns@0.2.3/dist/jquery.resizableColumns.css" type="text/css">
|
||||
|
||||
<link rel="stylesheet" href="/static/styles.css" type="text/css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="container">
|
||||
<nav class="navbar navbar-expand-lg justify-content-between align-items-center border-bottom">
|
||||
<a class="navbar-brand" id="navbar-title">Party</a>
|
||||
<ul class="navbar-nav">
|
||||
<a class="nav-item nav-link" id="navbar-bis">best in slot</a>
|
||||
<a class="nav-item nav-link" id="navbar-loot">looted items</a>
|
||||
</ul>
|
||||
<ul class="navbar-nav">
|
||||
<a class="nav-item nav-link" id="navbar-users">users</a>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<div id="alert-placeholder" class="container"></div>
|
||||
|
||||
<div class="container">
|
||||
<h2>Looted items</h2>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<div id="toolbar">
|
||||
<button id="add-btn" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#add-loot-dialog" hidden>
|
||||
<i class="bi bi-plus"></i> add
|
||||
</button>
|
||||
<button class="btn btn-secondary" onclick="reload()">
|
||||
<i class="bi bi-arrow-clockwise"></i> reload
|
||||
</button>
|
||||
<button id="remove-btn" class="btn btn-danger" onclick="removeLoot()" disabled hidden>
|
||||
<i class="bi bi-trash"></i> remove
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<table id="loot" class="table table-striped table-hover"
|
||||
data-click-to-select="true"
|
||||
data-export-options='{"fileName": "loot"}'
|
||||
data-page-list="[25, 50, 100, all]"
|
||||
data-page-size="25"
|
||||
data-pagination="true"
|
||||
data-resizable="true"
|
||||
data-search="true"
|
||||
data-show-columns="true"
|
||||
data-show-columns-search="true"
|
||||
data-show-columns-toggle-all="true"
|
||||
data-show-export="true"
|
||||
data-show-fullscreen="true"
|
||||
data-show-search-clear-button="true"
|
||||
data-single-select="true"
|
||||
data-sortable="true"
|
||||
data-sort-name="timestamp"
|
||||
data-sort-order="desc"
|
||||
data-sort-reset="true"
|
||||
data-toolbar="#toolbar">
|
||||
<thead class="table-primary">
|
||||
<tr>
|
||||
<th data-checkbox="true"></th>
|
||||
<th data-sortable="true" data-switchable="false" data-field="nick">nick</th>
|
||||
<th data-sortable="true" data-switchable="false" data-field="job">job</th>
|
||||
<th data-sortable="true" data-field="piece">piece</th>
|
||||
<th data-sortable="true" data-field="pieceType">piece type</th>
|
||||
<th data-sortable="true" data-field="isFreeLoot">is free loot</th>
|
||||
<th data-sortable="true" data-field="timestamp">date</th>
|
||||
</tr>
|
||||
</thead>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div id="add-loot-dialog" tabindex="-1" role="dialog" class="modal fade">
|
||||
<div class="modal-dialog modal-lg" role="document">
|
||||
<form class="modal-content" action="javascript:" onsubmit="addLootModal()">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">add looted piece</h4>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="close"></button>
|
||||
</div>
|
||||
|
||||
<div class="modal-body">
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-4 col-form-label" for="player">player</label>
|
||||
<div class="col-sm-8">
|
||||
<select id="player" name="player" class="form-control" title="player" required></select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-4 col-form-label" for="piece">piece</label>
|
||||
<div class="col-sm-8">
|
||||
<select id="piece" name="piece" class="form-control" title="piece" required></select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-4 col-form-label" for="piece-type">piece type</label>
|
||||
<div class="col-sm-8">
|
||||
<select id="piece-type" name="pieceType" class="form-control" title="pieceType" required></select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-4 col-form-label" for="job">job</label>
|
||||
<div class="col-sm-8">
|
||||
<select id="job" name="job" class="form-control" title="job" required></select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<div class="col-sm-4"></div>
|
||||
<div class="col-sm-8">
|
||||
<div class="form-check">
|
||||
<input id="free-loot" name="freeLoot" type="checkbox" class="form-check-input">
|
||||
<label class="form-check-label" for="free-loot">as free loot</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table id="stats" class="table table-striped table-hover">
|
||||
<thead class="table-primary">
|
||||
<tr>
|
||||
<th data-formatter="addLootFormatter"></th>
|
||||
<th data-field="nick">nick</th>
|
||||
<th data-field="job">job</th>
|
||||
<th data-field="isRequired">required</th>
|
||||
<th data-field="lootCount">these pieces looted</th>
|
||||
<th data-field="lootCountBiS">total bis pieces looted</th>
|
||||
<th data-field="lootCountTotal">total pieces looted</th>
|
||||
</tr>
|
||||
</thead>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-danger" data-bs-dismiss="modal">close</button>
|
||||
<button type="button" class="btn btn-secondary" onclick="suggestLoot()">suggest</button>
|
||||
<button type="submit" class="btn btn-primary">add</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<footer class="d-flex flex-wrap justify-content-between align-items-center border-top">
|
||||
<ul class="nav">
|
||||
<li><a class="nav-link" href="/" title="home">home</a></li>
|
||||
<li><a class="nav-link" href="/api-docs" title="api">api</a></li>
|
||||
</ul>
|
||||
|
||||
<ul class="nav">
|
||||
<li><a id="sources-link" class="nav-link" href="https://github.com/arcan1s/ffxivbis" title="sources">ffxivbis</a></li>
|
||||
<li><a class="nav-link" href="https://github.com/arcan1s/ffxivbis/releases" title="releases list">releases</a></li>
|
||||
<li><a class="nav-link" href="https://github.com/arcan1s/ffxivbis/issues" title="issues tracker">report a bug</a></li>
|
||||
</ul>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/jquery/dist/jquery.min.js"></script>
|
||||
|
||||
<script src="https://unpkg.com/tableexport.jquery.plugin/tableExport.min.js"></script>
|
||||
|
||||
<script src="https://unpkg.com/jquery-resizable-columns@0.2.3/dist/jquery.resizableColumns.min.js"></script>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p" crossorigin="anonymous"></script>
|
||||
<script src="https://unpkg.com/bootstrap-table@1.20.2/dist/bootstrap-table.min.js"></script>
|
||||
|
||||
<script src="https://unpkg.com/bootstrap-table@1.20.2/dist/extensions/export/bootstrap-table-export.min.js"></script>
|
||||
|
||||
<script src="https://unpkg.com/bootstrap-table@1.20.2/dist/extensions/resizable/bootstrap-table-resizable.js"></script>
|
||||
|
||||
<script src="/static/utils.js"></script>
|
||||
<script src="/static/load.js"></script>
|
||||
|
||||
<script>
|
||||
const [partyId, isReadOnly] = getPartyId();
|
||||
const table = $("#loot");
|
||||
const stats = $("#stats");
|
||||
const addButton = $("#add-btn");
|
||||
const removeButton = $("#remove-btn");
|
||||
|
||||
const addLootDialog = $("#add-loot-dialog");
|
||||
|
||||
const freeLootInput = $("#free-loot");
|
||||
const jobInput = $("#job");
|
||||
const pieceInput = $("#piece");
|
||||
const pieceTypeInput = $("#piece-type");
|
||||
const playerInput = $("#player");
|
||||
|
||||
function addLoot(nick, job) {
|
||||
$.ajax({
|
||||
url: `/api/v1/party/${partyId}/loot`,
|
||||
data: JSON.stringify({
|
||||
action: "add",
|
||||
piece: {
|
||||
pieceType: pieceTypeInput.val(),
|
||||
job: job,
|
||||
piece: pieceInput.val(),
|
||||
},
|
||||
playerId: {
|
||||
partyId: partyId,
|
||||
nick: nick,
|
||||
job: job,
|
||||
},
|
||||
isFreeLoot: freeLootInput.is(":checked"),
|
||||
}),
|
||||
type: "POST",
|
||||
contentType: "application/json",
|
||||
success: _ => {
|
||||
addLootDialog.modal("hide");
|
||||
reload();
|
||||
},
|
||||
error: (jqXHR, _, errorThrown) => { requestAlert(jqXHR, errorThrown); },
|
||||
});
|
||||
}
|
||||
|
||||
function addLootFormatter(value, row, index) {
|
||||
return `<button type="button" class="btn btn-primary" onclick="addLoot('${row.nick}', '${row.job}')"><i class="bi bi-plus"></i></button>`;
|
||||
}
|
||||
|
||||
function addLootModal() {
|
||||
const player = getCurrentOption(playerInput);
|
||||
addLoot(player.dataset.nick, player.dataset.job);
|
||||
return true; // action expects boolean result
|
||||
}
|
||||
|
||||
function hideControls() {
|
||||
addButton.attr("hidden", isReadOnly);
|
||||
removeButton.attr("hidden", isReadOnly);
|
||||
}
|
||||
|
||||
function reload() {
|
||||
table.bootstrapTable("showLoading");
|
||||
$.ajax({
|
||||
url: `/api/v1/party/${partyId}`,
|
||||
type: "GET",
|
||||
dataType: "json",
|
||||
success: response => {
|
||||
const items = response.map(player => {
|
||||
return player.loot.map(loot => {
|
||||
return {
|
||||
nick: player.nick,
|
||||
job: player.job,
|
||||
piece: loot.piece.piece,
|
||||
pieceType: loot.piece.pieceType,
|
||||
isFreeLoot: loot.isFreeLoot ? "yes" : "no",
|
||||
timestamp: loot.timestamp,
|
||||
};
|
||||
});
|
||||
});
|
||||
const payload = items.reduce((left, right) => { return left.concat(right); }, []);
|
||||
table.bootstrapTable("load", payload);
|
||||
table.bootstrapTable("uncheckAll");
|
||||
table.bootstrapTable("hideLoading");
|
||||
|
||||
const options = response.map(player => {
|
||||
const option = document.createElement("option");
|
||||
option.innerText = formatPlayerId(player);
|
||||
option.dataset.nick = player.nick;
|
||||
option.dataset.job = player.job;
|
||||
return option;
|
||||
});
|
||||
playerInput.empty().append(options);
|
||||
},
|
||||
error: (jqXHR, _, errorThrown) => { requestAlert(jqXHR, errorThrown); },
|
||||
});
|
||||
}
|
||||
|
||||
function removeLoot() {
|
||||
const pieces = table.bootstrapTable("getSelections");
|
||||
pieces.map(loot => {
|
||||
$.ajax({
|
||||
url: `/api/v1/party/${partyId}/loot`,
|
||||
data: JSON.stringify({
|
||||
action: "remove",
|
||||
piece: {
|
||||
pieceType: loot.pieceType,
|
||||
job: loot.job,
|
||||
piece: loot.piece,
|
||||
},
|
||||
playerId: {
|
||||
partyId: partyId,
|
||||
nick: loot.nick,
|
||||
job: loot.job,
|
||||
},
|
||||
isFreeLoot: loot.isFreeLoot === "yes",
|
||||
}),
|
||||
type: "POST",
|
||||
contentType: "application/json",
|
||||
success: _ => { reload(); },
|
||||
error: (jqXHR, _, errorThrown) => { requestAlert(jqXHR, errorThrown); },
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function suggestLoot() {
|
||||
stats.bootstrapTable("showLoading");
|
||||
$.ajax({
|
||||
url: `/api/v1/party/${partyId}/loot`,
|
||||
data: JSON.stringify({
|
||||
pieceType: pieceTypeInput.val(),
|
||||
job: jobInput.val(),
|
||||
piece: pieceInput.val(),
|
||||
}),
|
||||
type: "PUT",
|
||||
contentType: "application/json",
|
||||
dataType: "json",
|
||||
success: response => {
|
||||
const payload = response.map(stat => {
|
||||
return {
|
||||
nick: stat.nick,
|
||||
job: stat.job,
|
||||
isRequired: stat.isRequired ? "yes" : "no",
|
||||
lootCount: stat.lootCount,
|
||||
lootCountBiS: stat.lootCountBiS,
|
||||
lootCountTotal: stat.lootCountTotal,
|
||||
};
|
||||
});
|
||||
stats.bootstrapTable("load", payload);
|
||||
stats.bootstrapTable("uncheckAll");
|
||||
stats.bootstrapTable("hideLoading");
|
||||
},
|
||||
error: (jqXHR, _, errorThrown) => { requestAlert(jqXHR, errorThrown); },
|
||||
});
|
||||
}
|
||||
|
||||
$(() => {
|
||||
setupFormClear(addLootDialog);
|
||||
setupRemoveButton(table, removeButton);
|
||||
|
||||
loadVersion();
|
||||
loadHeader(partyId);
|
||||
loadTypes("/api/v1/types/jobs/all", jobInput);
|
||||
loadTypes("/api/v1/types/pieces", pieceInput);
|
||||
loadTypes("/api/v1/types/pieces/types", pieceTypeInput);
|
||||
|
||||
hideControls();
|
||||
|
||||
table.bootstrapTable({});
|
||||
stats.bootstrapTable({});
|
||||
reload();
|
||||
});
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
259
src/main/resources/html/party.html
Normal file
259
src/main/resources/html/party.html
Normal file
@ -0,0 +1,259 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>FFXIV loot helper</title>
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
|
||||
<link rel="shortcut icon" href="/static/favicon.ico">
|
||||
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous" type="text/css">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.8.3/font/bootstrap-icons.css" type="text/css">
|
||||
|
||||
<link rel="stylesheet" href="https://unpkg.com/bootstrap-table@1.20.2/dist/bootstrap-table.min.css" type="text/css">
|
||||
|
||||
|
||||
<link rel="stylesheet" href="https://unpkg.com/jquery-resizable-columns@0.2.3/dist/jquery.resizableColumns.css" type="text/css">
|
||||
|
||||
<link rel="stylesheet" href="/static/styles.css" type="text/css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="container">
|
||||
<nav class="navbar navbar-expand-lg justify-content-between align-items-center border-bottom">
|
||||
<a class="navbar-brand" id="navbar-title">Party</a>
|
||||
<ul class="navbar-nav">
|
||||
<a class="nav-item nav-link" id="navbar-bis">best in slot</a>
|
||||
<a class="nav-item nav-link" id="navbar-loot">looted items</a>
|
||||
</ul>
|
||||
<ul class="navbar-nav">
|
||||
<a class="nav-item nav-link" id="navbar-users">users</a>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<div id="alert-placeholder" class="container"></div>
|
||||
|
||||
<div class="container">
|
||||
<div id="toolbar">
|
||||
<button id="add-btn" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#add-player-dialog" hidden>
|
||||
<i class="bi bi-plus"></i> add
|
||||
</button>
|
||||
<button class="btn btn-secondary" onclick="reload()">
|
||||
<i class="bi bi-arrow-clockwise"></i> reload
|
||||
</button>
|
||||
<button id="remove-btn" class="btn btn-danger" onclick="removePlayers()" disabled hidden>
|
||||
<i class="bi bi-trash"></i> remove
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<table id="players" class="table table-striped table-hover"
|
||||
data-click-to-select="true"
|
||||
data-export-options='{"fileName": "players"}'
|
||||
data-page-list="[25, 50, 100, all]"
|
||||
data-page-size="25"
|
||||
data-pagination="true"
|
||||
data-resizable="true"
|
||||
data-search="true"
|
||||
data-show-columns="true"
|
||||
data-show-columns-search="true"
|
||||
data-show-columns-toggle-all="true"
|
||||
data-show-export="true"
|
||||
data-show-fullscreen="true"
|
||||
data-show-search-clear-button="true"
|
||||
data-single-select="true"
|
||||
data-sortable="true"
|
||||
data-sort-name="nick"
|
||||
data-sort-order="asc"
|
||||
data-sort-reset="true"
|
||||
data-toolbar="#toolbar">
|
||||
<thead class="table-primary">
|
||||
<tr>
|
||||
<th data-checkbox="true"></th>
|
||||
<th data-sortable="true" data-switchable="false" data-field="nick">nick</th>
|
||||
<th data-sortable="true" data-field="job">job</th>
|
||||
<th data-sortable="true" data-field="link" data-formatter="bisLinkFormatter">best in slot link</th>
|
||||
<th data-sortable="true" data-field="lootCountBiS">total bis pieces looted</th>
|
||||
<th data-sortable="true" data-field="lootCountTotal">total pieces looted</th>
|
||||
<th data-sortable="true" data-field="priority">priority</th>
|
||||
</tr>
|
||||
</thead>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div id="add-player-dialog" tabindex="-1" role="dialog" class="modal fade">
|
||||
<div class="modal-dialog" role="document">
|
||||
<form class="modal-content" action="javascript:" onsubmit="addPlayer()">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">add new player</h4>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="close"></button>
|
||||
</div>
|
||||
|
||||
<div class="modal-body">
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-4 col-form-label" for="nick">player name</label>
|
||||
<div class="col-sm-8">
|
||||
<input id="nick" name="nick" class="form-control" placeholder="nick" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-4 col-form-label" for="job">player job</label>
|
||||
<div class="col-sm-8">
|
||||
<select id="job" name="job" class="form-control" title="job" required></select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-4 col-form-label" for="link">link to best in slot</label>
|
||||
<div class="col-sm-8">
|
||||
<input id="link" name="link" class="form-control" placeholder="link to bis">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-4 col-form-label" for="priority">priority</label>
|
||||
<div class="col-sm-8">
|
||||
<input id="priority" name="priority" type="number" class="form-control" value="0">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-danger" data-bs-dismiss="modal">close</button>
|
||||
<button type="submit" class="btn btn-primary">add</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<footer class="d-flex flex-wrap justify-content-between align-items-center border-top">
|
||||
<ul class="nav">
|
||||
<li><a class="nav-link" href="/" title="home">home</a></li>
|
||||
<li><a class="nav-link" href="/api-docs" title="api">api</a></li>
|
||||
</ul>
|
||||
|
||||
<ul class="nav">
|
||||
<li><a id="sources-link" class="nav-link" href="https://github.com/arcan1s/ffxivbis" title="sources">ffxivbis</a></li>
|
||||
<li><a class="nav-link" href="https://github.com/arcan1s/ffxivbis/releases" title="releases list">releases</a></li>
|
||||
<li><a class="nav-link" href="https://github.com/arcan1s/ffxivbis/issues" title="issues tracker">report a bug</a></li>
|
||||
</ul>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/jquery/dist/jquery.min.js"></script>
|
||||
|
||||
<script src="https://unpkg.com/tableexport.jquery.plugin/tableExport.min.js"></script>
|
||||
|
||||
<script src="https://unpkg.com/jquery-resizable-columns@0.2.3/dist/jquery.resizableColumns.min.js"></script>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p" crossorigin="anonymous"></script>
|
||||
<script src="https://unpkg.com/bootstrap-table@1.20.2/dist/bootstrap-table.min.js"></script>
|
||||
|
||||
<script src="https://unpkg.com/bootstrap-table@1.20.2/dist/extensions/export/bootstrap-table-export.min.js"></script>
|
||||
|
||||
<script src="https://unpkg.com/bootstrap-table@1.20.2/dist/extensions/resizable/bootstrap-table-resizable.js"></script>
|
||||
|
||||
<script src="/static/utils.js"></script>
|
||||
<script src="/static/load.js"></script>
|
||||
|
||||
<script>
|
||||
const [partyId, isReadOnly] = getPartyId();
|
||||
const table = $("#players");
|
||||
const addButton = $("#add-btn");
|
||||
const removeButton = $("#remove-btn");
|
||||
|
||||
const addPlayerDialog = $("#add-player-dialog");
|
||||
|
||||
const jobInput = $("#job");
|
||||
const linkInput = $("#link");
|
||||
const nickInput = $("#nick");
|
||||
const priorityInput = $("#priority");
|
||||
|
||||
function addPlayer() {
|
||||
$.ajax({
|
||||
url: `/api/v1/party/${partyId}`,
|
||||
data: JSON.stringify({
|
||||
action: "add",
|
||||
playerId: {
|
||||
partyId: partyId,
|
||||
job: jobInput.val(),
|
||||
nick: nickInput.val(),
|
||||
link: linkInput.val() || null,
|
||||
priority: parseInt(priorityInput.val(), 10),
|
||||
},
|
||||
}),
|
||||
type: "POST",
|
||||
contentType: "application/json",
|
||||
success: _ => { reload(); },
|
||||
error: (jqXHR, _, errorThrown) => { requestAlert(jqXHR, errorThrown); },
|
||||
});
|
||||
addPlayerDialog.modal("hide");
|
||||
return true; // action expects boolean result
|
||||
}
|
||||
|
||||
function bisLinkFormatter(link, row) {
|
||||
if (link) {
|
||||
return `<a href="${safe(link)}" title="${safe(row.nick)} best in slot for ${safe(row.job)}">${safe(link)}</a>`;
|
||||
} else {
|
||||
return "-";
|
||||
}
|
||||
}
|
||||
|
||||
function hideControls() {
|
||||
addButton.attr("hidden", isReadOnly);
|
||||
removeButton.attr("hidden", isReadOnly);
|
||||
}
|
||||
|
||||
function reload() {
|
||||
table.bootstrapTable("showLoading");
|
||||
$.ajax({
|
||||
url: `/api/v1/party/${partyId}`,
|
||||
type: "GET",
|
||||
dataType: "json",
|
||||
success: response => {
|
||||
table.bootstrapTable("load", response);
|
||||
table.bootstrapTable("uncheckAll");
|
||||
table.bootstrapTable("hideLoading");
|
||||
},
|
||||
error: (jqXHR, _, errorThrown) => { requestAlert(jqXHR, errorThrown); },
|
||||
});
|
||||
}
|
||||
|
||||
function removePlayers() {
|
||||
const players = table.bootstrapTable("getSelections");
|
||||
players.map(player => {
|
||||
$.ajax({
|
||||
url: `/api/v1/party/${partyId}`,
|
||||
data: JSON.stringify({
|
||||
action: "remove",
|
||||
playerId: {
|
||||
partyId: partyId,
|
||||
job: player.job,
|
||||
nick: player.nick,
|
||||
},
|
||||
}),
|
||||
type: "POST",
|
||||
contentType: "application/json",
|
||||
success: _ => { reload(); },
|
||||
error: (jqXHR, _, errorThrown) => { requestAlert(jqXHR, errorThrown); },
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
$(() => {
|
||||
setupFormClear(addPlayerDialog);
|
||||
setupRemoveButton(table, removeButton);
|
||||
|
||||
loadVersion();
|
||||
loadHeader(partyId);
|
||||
loadTypes("/api/v1/types/jobs", jobInput);
|
||||
|
||||
hideControls();
|
||||
|
||||
table.bootstrapTable({});
|
||||
reload();
|
||||
});
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
231
src/main/resources/html/users.html
Normal file
231
src/main/resources/html/users.html
Normal file
@ -0,0 +1,231 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>User management</title>
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
|
||||
<link rel="shortcut icon" href="/static/favicon.ico">
|
||||
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous" type="text/css">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.8.3/font/bootstrap-icons.css" type="text/css">
|
||||
|
||||
<link rel="stylesheet" href="https://unpkg.com/bootstrap-table@1.20.2/dist/bootstrap-table.min.css" type="text/css">
|
||||
|
||||
|
||||
<link rel="stylesheet" href="https://unpkg.com/jquery-resizable-columns@0.2.3/dist/jquery.resizableColumns.css" type="text/css">
|
||||
|
||||
<link rel="stylesheet" href="/static/styles.css" type="text/css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="container">
|
||||
<nav class="navbar navbar-expand-lg justify-content-between align-items-center border-bottom">
|
||||
<a class="navbar-brand" id="navbar-title">Party</a>
|
||||
<ul class="navbar-nav">
|
||||
<a class="nav-item nav-link" id="navbar-bis">best in slot</a>
|
||||
<a class="nav-item nav-link" id="navbar-loot">looted items</a>
|
||||
</ul>
|
||||
<ul class="navbar-nav">
|
||||
<a class="nav-item nav-link" id="navbar-users">users</a>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<div id="alert-placeholder" class="container"></div>
|
||||
|
||||
<div class="container">
|
||||
<h2>Users</h2>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<div id="toolbar">
|
||||
<button id="add-btn" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#add-user-dialog" hidden>
|
||||
<i class="bi bi-plus"></i> add
|
||||
</button>
|
||||
<button class="btn btn-secondary" onclick="reload()">
|
||||
<i class="bi bi-arrow-clockwise"></i> reload
|
||||
</button>
|
||||
<button id="remove-btn" class="btn btn-danger" onclick="removeUsers()" disabled hidden>
|
||||
<i class="bi bi-trash"></i> remove
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<table id="users" class="table table-striped table-hover"
|
||||
data-click-to-select="true"
|
||||
data-export-options='{"fileName": "users"}'
|
||||
data-page-list="[25, 50, 100, all]"
|
||||
data-page-size="25"
|
||||
data-pagination="true"
|
||||
data-resizable="true"
|
||||
data-search="true"
|
||||
data-show-columns="true"
|
||||
data-show-columns-search="true"
|
||||
data-show-columns-toggle-all="true"
|
||||
data-show-export="true"
|
||||
data-show-fullscreen="true"
|
||||
data-show-search-clear-button="true"
|
||||
data-single-select="true"
|
||||
data-sortable="true"
|
||||
data-sort-name="username"
|
||||
data-sort-order="asc"
|
||||
data-sort-reset="true"
|
||||
data-toolbar="#toolbar">
|
||||
<thead class="table-primary">
|
||||
<tr>
|
||||
<th data-checkbox="true"></th>
|
||||
<th data-sortable="true" data-switchable="false" data-field="username">username</th>
|
||||
<th data-sortable="true" data-field="permission">permission</th>
|
||||
</tr>
|
||||
</thead>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div id="add-user-dialog" tabindex="-1" role="dialog" class="modal fade">
|
||||
<div class="modal-dialog" role="document">
|
||||
<form class="modal-content" action="javascript:" onsubmit="addUser()">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">add new user</h4>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="close"></button>
|
||||
</div>
|
||||
|
||||
<div class="modal-body">
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-4 col-form-label" for="username">login</label>
|
||||
<div class="col-sm-8">
|
||||
<input id="username" name="username" class="form-control" placeholder="username" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-4 col-form-label" for="password">password</label>
|
||||
<div class="col-sm-8">
|
||||
<input id="password" name="password" type="password" class="form-control" placeholder="password" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-4 col-form-label" for="permission">permission</label>
|
||||
<div class="col-sm-8">
|
||||
<select id="permission" name="permission" class="form-control" title="permission" required></select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-danger" data-bs-dismiss="modal">close</button>
|
||||
<button type="submit" class="btn btn-primary">add</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<footer class="d-flex flex-wrap justify-content-between align-items-center border-top">
|
||||
<ul class="nav">
|
||||
<li><a class="nav-link" href="/" title="home">home</a></li>
|
||||
<li><a class="nav-link" href="/api-docs" title="api">api</a></li>
|
||||
</ul>
|
||||
|
||||
<ul class="nav">
|
||||
<li><a id="sources-link" class="nav-link" href="https://github.com/arcan1s/ffxivbis" title="sources">ffxivbis</a></li>
|
||||
<li><a class="nav-link" href="https://github.com/arcan1s/ffxivbis/releases" title="releases list">releases</a></li>
|
||||
<li><a class="nav-link" href="https://github.com/arcan1s/ffxivbis/issues" title="issues tracker">report a bug</a></li>
|
||||
</ul>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/jquery/dist/jquery.min.js"></script>
|
||||
|
||||
<script src="https://unpkg.com/tableexport.jquery.plugin/tableExport.min.js"></script>
|
||||
|
||||
<script src="https://unpkg.com/jquery-resizable-columns@0.2.3/dist/jquery.resizableColumns.min.js"></script>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p" crossorigin="anonymous"></script>
|
||||
<script src="https://unpkg.com/bootstrap-table@1.20.2/dist/bootstrap-table.min.js"></script>
|
||||
|
||||
<script src="https://unpkg.com/bootstrap-table@1.20.2/dist/extensions/export/bootstrap-table-export.min.js"></script>
|
||||
|
||||
<script src="https://unpkg.com/bootstrap-table@1.20.2/dist/extensions/resizable/bootstrap-table-resizable.js"></script>
|
||||
|
||||
<script src="/static/utils.js"></script>
|
||||
<script src="/static/load.js"></script>
|
||||
|
||||
<script>
|
||||
const [partyId, isReadOnly] = getPartyId();
|
||||
const table = $("#users");
|
||||
const addButton = $("#add-btn");
|
||||
const removeButton = $("#remove-btn");
|
||||
|
||||
const addUserDialog = $("#add-user-dialog");
|
||||
|
||||
const usernameInput = $("#username");
|
||||
const passwordInput = $("#password");
|
||||
const permissionInput = $("#permission");
|
||||
|
||||
function addUser() {
|
||||
$.ajax({
|
||||
url: `/api/v1/party/${partyId}/users`,
|
||||
data: JSON.stringify({
|
||||
partyId: partyId,
|
||||
username: usernameInput.val(),
|
||||
password: passwordInput.val(),
|
||||
permission: permissionInput.val(),
|
||||
}),
|
||||
type: "POST",
|
||||
contentType: "application/json",
|
||||
success: _ => { reload(); },
|
||||
error: (jqXHR, _, errorThrown) => { requestAlert(jqXHR, errorThrown); },
|
||||
});
|
||||
addUserDialog.modal("hide");
|
||||
return true; // action expects boolean result
|
||||
}
|
||||
|
||||
function hideControls() {
|
||||
addButton.attr("hidden", isReadOnly);
|
||||
removeButton.attr("hidden", isReadOnly);
|
||||
}
|
||||
|
||||
function reload() {
|
||||
table.bootstrapTable("showLoading");
|
||||
$.ajax({
|
||||
url: `/api/v1/party/${partyId}/users`,
|
||||
type: "GET",
|
||||
dataType: "json",
|
||||
success: response => {
|
||||
table.bootstrapTable("load", response);
|
||||
table.bootstrapTable("uncheckAll");
|
||||
table.bootstrapTable("hideLoading");
|
||||
},
|
||||
error: (jqXHR, _, errorThrown) => { requestAlert(jqXHR, errorThrown); },
|
||||
});
|
||||
}
|
||||
|
||||
function removeUsers() {
|
||||
const users = table.bootstrapTable("getSelections");
|
||||
users.map(user => {
|
||||
$.ajax({
|
||||
url: `/api/v1/party/${partyId}/users/${user.username}`,
|
||||
type: "DELETE",
|
||||
success: _ => { reload(); },
|
||||
error: (jqXHR, _, errorThrown) => { requestAlert(jqXHR, errorThrown); },
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
$(() => {
|
||||
setupFormClear(addUserDialog);
|
||||
setupRemoveButton(table, removeButton);
|
||||
|
||||
loadVersion();
|
||||
loadHeader(partyId);
|
||||
loadTypes("/api/v1/types/permissions", permissionInput);
|
||||
|
||||
hideControls();
|
||||
|
||||
table.bootstrapTable({});
|
||||
reload();
|
||||
});
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user