mirror of
https://github.com/arcan1s/ffxivbis.git
synced 2025-07-14 22:35:50 +00:00
Compare commits
1 Commits
0.10.0
...
feature/pa
Author | SHA1 | Date | |
---|---|---|---|
4ff985bf81 |
40
.github/workflows/release.yml
vendored
40
.github/workflows/release.yml
vendored
@ -1,40 +0,0 @@
|
||||
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: 17
|
||||
- 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
22
.github/workflows/run-tests.yml
vendored
@ -1,22 +0,0 @@
|
||||
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: 17
|
||||
- name: run tests
|
||||
run: make tests
|
162
.gitignore
vendored
162
.gitignore
vendored
@ -1,88 +1,96 @@
|
||||
#### joe made this: http://goel.io/joe
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
#### jetbrains ####
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm
|
||||
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
env/
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
parts/
|
||||
sdist/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
|
||||
# User-specific stuff:
|
||||
.idea
|
||||
# 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
|
||||
|
||||
## File-based project format:
|
||||
*.iws
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
## 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
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
.history
|
||||
.lib/
|
||||
dist/*
|
||||
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
|
||||
target/
|
||||
lib_managed/
|
||||
src_managed/
|
||||
project/boot/
|
||||
project/plugins/project/
|
||||
|
||||
# Scala-IDE specific
|
||||
.scala_dependencies
|
||||
.worksheet
|
||||
# IPython Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# ENSIME specific
|
||||
.ensime_cache/
|
||||
.ensime
|
||||
# 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
|
||||
|
||||
*.db
|
||||
*.sc
|
||||
|
@ -1,35 +0,0 @@
|
||||
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
35
Makefile
@ -1,35 +0,0 @@
|
||||
.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 version
|
||||
sbt dist
|
||||
|
||||
push: 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
|
92
README.md
92
README.md
@ -1,31 +1,101 @@
|
||||
# FFXIV BiS
|
||||
|
||||
[](https://github.com/arcan1s/ffxivbis/actions/workflows/run-tests.yml) 
|
||||
|
||||
Service which allows managing savage loot distribution easy.
|
||||
Service which allows to manage savage loot distribution easy.
|
||||
|
||||
## Installation and usage
|
||||
|
||||
In general compilation process looks like:
|
||||
This service requires python >= 3.7. For other dependencies see `setup.py`.
|
||||
|
||||
In general installation process looks like:
|
||||
|
||||
```bash
|
||||
sbt dist
|
||||
python setup.py build install
|
||||
python setup.py test # if you want to run tests
|
||||
```
|
||||
|
||||
Or alternatively you can download the latest distribution zip from the releases page. Service can be run by using command:
|
||||
With virtualenv (make sure that virtualenv package was installed) the process may look like:
|
||||
|
||||
```bash
|
||||
bin/ffxivbis
|
||||
virtualenv -p python3.7 env
|
||||
source env/bin/activate
|
||||
python setup.py install
|
||||
pip install aiosqlite # setup.py does not handle extras
|
||||
```
|
||||
|
||||
from the extracted archive root.
|
||||
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`.
|
||||
|
||||
## Web service
|
||||
|
||||
REST API documentation is available at `http://0.0.0.0:8000/swagger`. HTML representation is available at `http://0.0.0.0:8000`.
|
||||
REST API documentation is available at `http://0.0.0.0:8000/api-docs`. HTML representation is available at `http://0.0.0.0:8000`.
|
||||
|
||||
*Note*: host and port depend on configuration settings.
|
||||
|
||||
## Public service
|
||||
### Authorization
|
||||
|
||||
There is also public service which is available at http://ffxivbis.arcanis.me.
|
||||
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.
|
4
TODO.md
4
TODO.md
@ -1,3 +1,3 @@
|
||||
* [x] items improvements
|
||||
* [x] multiple parties support
|
||||
* [ ] items improvements
|
||||
* [ ] multiple parties support
|
||||
* [ ] pretty UI
|
16
build.sbt
16
build.sbt
@ -1,16 +0,0 @@
|
||||
name := "ffxivbis"
|
||||
|
||||
scalaVersion := "2.13.6"
|
||||
|
||||
scalacOptions ++= Seq("-deprecation", "-feature")
|
||||
|
||||
enablePlugins(JavaAppPackaging)
|
||||
|
||||
assemblyMergeStrategy in assembly := {
|
||||
case PathList("META-INF", xs @ _*) => MergeStrategy.discard
|
||||
case "application.conf" => MergeStrategy.concat
|
||||
case "module-info.class" => MergeStrategy.first
|
||||
case x =>
|
||||
val oldStrategy = (assemblyMergeStrategy in assembly).value
|
||||
oldStrategy(x)
|
||||
}
|
@ -1,56 +0,0 @@
|
||||
import json
|
||||
import requests
|
||||
|
||||
# NOTE: it does not cover all items, just workaround to extract most gear pieces from patches
|
||||
MIN_ILVL = 580
|
||||
MAX_ILVL = 605
|
||||
|
||||
TOME = (
|
||||
'radiant',
|
||||
)
|
||||
SAVAGE = (
|
||||
'asphodelos',
|
||||
)
|
||||
|
||||
|
||||
payload = {
|
||||
'queries': [
|
||||
{
|
||||
'slots': []
|
||||
},
|
||||
{
|
||||
'jobs': [],
|
||||
'minItemLevel': 580,
|
||||
'maxItemLevel': 605
|
||||
}
|
||||
],
|
||||
'existing': []
|
||||
}
|
||||
# it does not support application/json
|
||||
r = requests.post('https://ffxiv.ariyala.com/items.app', data=json.dumps(payload))
|
||||
r.raise_for_status()
|
||||
|
||||
result = []
|
||||
|
||||
for item in r.json():
|
||||
item_id = item['itemID']
|
||||
source_dict = item['source']
|
||||
name = item['name']['en']
|
||||
if 'crafting' in source_dict:
|
||||
source = 'Crafted'
|
||||
elif 'gathering' in source_dict:
|
||||
continue # some random shit
|
||||
elif 'purchase' in source_dict:
|
||||
if any(tome in name.lower() for tome in TOME):
|
||||
source = 'Tome'
|
||||
elif any(savage in name.lower() for savage in SAVAGE):
|
||||
source = 'Savage'
|
||||
else:
|
||||
source = None
|
||||
continue
|
||||
else:
|
||||
raise RuntimeError(f'Unknown source {source_dict}')
|
||||
result.append({'id': item_id, 'source': source, 'name': name})
|
||||
|
||||
output = {'cached-items': result}
|
||||
print(json.dumps(output, indent=4, sort_keys=True))
|
@ -1,34 +0,0 @@
|
||||
val AkkaVersion = "2.6.17"
|
||||
val AkkaHttpVersion = "10.2.7"
|
||||
val ScalaTestVersion = "3.2.10"
|
||||
val SlickVersion = "3.3.3"
|
||||
|
||||
libraryDependencies += "ch.qos.logback" % "logback-classic" % "1.2.9"
|
||||
libraryDependencies += "com.typesafe.scala-logging" %% "scala-logging" % "3.9.4"
|
||||
|
||||
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.6.0"
|
||||
libraryDependencies += "jakarta.platform" % "jakarta.jakartaee-web-api" % "9.1.0"
|
||||
|
||||
libraryDependencies += "io.spray" %% "spray-json" % "1.3.6"
|
||||
|
||||
libraryDependencies += "com.typesafe.slick" %% "slick" % SlickVersion
|
||||
libraryDependencies += "com.typesafe.slick" %% "slick-hikaricp" % SlickVersion
|
||||
libraryDependencies += "org.flywaydb" % "flyway-core" % "8.2.2"
|
||||
libraryDependencies += "org.xerial" % "sqlite-jdbc" % "3.36.0.3"
|
||||
libraryDependencies += "org.postgresql" % "postgresql" % "42.3.1"
|
||||
|
||||
libraryDependencies += "org.mindrot" % "jbcrypt" % "0.4"
|
||||
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"
|
38
migrations/20190830_01_sYYZL-init-tables.py
Normal file
38
migrations/20190830_01_sYYZL-init-tables.py
Normal file
@ -0,0 +1,38 @@
|
||||
'''
|
||||
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)''')
|
||||
]
|
17
migrations/20190910_01_tgBmx-users-table.py
Normal file
17
migrations/20190910_01_tgBmx-users-table.py
Normal file
@ -0,0 +1,17 @@
|
||||
'''
|
||||
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)''')
|
||||
]
|
75
migrations/20190916_01_zGTB1-party-id.py
Normal file
75
migrations/20190916_01_zGTB1-party-id.py
Normal file
@ -0,0 +1,75 @@
|
||||
'''
|
||||
party id
|
||||
'''
|
||||
|
||||
import random
|
||||
import string
|
||||
|
||||
from yoyo import step
|
||||
|
||||
__depends__ = {'20190830_01_sYYZL-init-tables', '20190910_01_tgBmx-users-table'}
|
||||
party_id = ''.join(random.sample(string.ascii_letters, 16))
|
||||
|
||||
steps = [
|
||||
step('''create table players2 (
|
||||
party_id text not null,
|
||||
player_id integer primary key,
|
||||
created integer not null,
|
||||
nick text not null,
|
||||
job text not null,
|
||||
bis_link text,
|
||||
priority integer not null default 1
|
||||
)'''),
|
||||
# not safe for injections, but sqlite and psycopg have different placeholders for parameters
|
||||
step('''insert into players2 select '%s' as party_id, players.* from players''' % (party_id,)),
|
||||
step('''drop index if exists players_nick_job_idx'''),
|
||||
step('''create unique index players_nick_job_idx on players2(party_id, nick, job)'''),
|
||||
|
||||
step('''create table loot2 (
|
||||
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 players2(player_id) on delete cascade
|
||||
)'''),
|
||||
step('''insert into loot2 select * from loot'''),
|
||||
step('''drop index if exists loot_owner_idx'''),
|
||||
step('''create index loot_owner_idx on loot(player_id)'''),
|
||||
|
||||
step('''create table bis2 (
|
||||
player_id integer not null,
|
||||
created integer not null,
|
||||
piece text not null,
|
||||
is_tome integer not null,
|
||||
foreign key (player_id) references players2(player_id) on delete cascade
|
||||
)'''),
|
||||
step('''insert into bis2 select * from bis'''),
|
||||
step('''drop index if exists bis_piece_player_id_idx'''),
|
||||
step('''create unique index bis_piece_player_id_idx on bis2(player_id, piece)'''),
|
||||
|
||||
step('''create table users2 (
|
||||
party_id text not null,
|
||||
user_id integer primary key,
|
||||
username text not null,
|
||||
password text not null,
|
||||
permission text not null,
|
||||
foreign key (party_id) references players2(party_id) on delete cascade
|
||||
)'''),
|
||||
# not safe for injections, but sqlite and psycopg have different placeholders for parameters
|
||||
step('''insert into users2 select '%s' as party_id, users.* from users''' % (party_id,)),
|
||||
step('''drop index if exists users_username_idx'''),
|
||||
step('''create unique index users_username_idx on users2(party_id, username)'''),
|
||||
|
||||
step('''drop table users'''),
|
||||
step('''alter table users2 rename to users'''),
|
||||
|
||||
step('''drop table loot'''),
|
||||
step('''alter table loot2 rename to loot'''),
|
||||
|
||||
step('''drop table bis'''),
|
||||
step('''alter table bis2 rename to bis'''),
|
||||
|
||||
step('''drop table players'''),
|
||||
step('''alter table players2 rename to players''')
|
||||
]
|
10
package/ini/ffxivbis.ini
Normal file
10
package/ini/ffxivbis.ini
Normal file
@ -0,0 +1,10 @@
|
||||
[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 = /home/arcanis/Documents/github/ffxivbis/templates
|
3
package/ini/ffxivbis.ini.d/ariyala.ini
Normal file
3
package/ini/ffxivbis.ini.d/ariyala.ini
Normal file
@ -0,0 +1,3 @@
|
||||
[ariyala]
|
||||
ariyala_url = https://ffxiv.ariyala.com
|
||||
xivapi_url = https://xivapi.com
|
4
package/ini/ffxivbis.ini.d/auth.ini
Normal file
4
package/ini/ffxivbis.ini.d/auth.ini
Normal file
@ -0,0 +1,4 @@
|
||||
[auth]
|
||||
enabled = yes
|
||||
root_username = admin
|
||||
root_password = $1$R3j4sym6$HtvrKOJ66f7w3.9Zc3U6h1
|
44
package/ini/ffxivbis.ini.d/logging.ini
Normal file
44
package/ini/ffxivbis.ini.d/logging.ini
Normal file
@ -0,0 +1,44 @@
|
||||
[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
|
3
package/ini/ffxivbis.ini.d/sqlite.ini
Normal file
3
package/ini/ffxivbis.ini.d/sqlite.ini
Normal file
@ -0,0 +1,3 @@
|
||||
[sqlite]
|
||||
database_path = /home/arcanis/Documents/github/ffxivbis/ffxivbis.db
|
||||
migrations_path = /home/arcanis/Documents/github/ffxivbis/migrations
|
@ -1 +0,0 @@
|
||||
sbt.version = 1.3.3
|
@ -1,4 +0,0 @@
|
||||
addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.14.6")
|
||||
addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.4.3")
|
||||
addSbtPlugin("com.typesafe.sbt" % "sbt-native-packager" % "1.3.4")
|
||||
addSbtPlugin("net.virtual-void" % "sbt-dependency-graph" % "0.10.0-RC1")
|
5
setup.cfg
Normal file
5
setup.cfg
Normal file
@ -0,0 +1,5 @@
|
||||
[aliases]
|
||||
test=pytest
|
||||
|
||||
[tool:pytest]
|
||||
addopts = --verbose --pyargs .
|
52
setup.py
Normal file
52
setup.py
Normal file
@ -0,0 +1,52 @@
|
||||
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'],
|
||||
},
|
||||
)
|
0
src/ffxivbis/api/__init__.py
Normal file
0
src/ffxivbis/api/__init__.py
Normal file
60
src/ffxivbis/api/auth.py
Normal file
60
src/ffxivbis/api/auth.py
Normal file
@ -0,0 +1,60 @@
|
||||
#
|
||||
# Copyright (c) 2019 Evgeniy Alekseev.
|
||||
#
|
||||
# This file is part of ffxivbis
|
||||
# (see https://github.com/arcan1s/ffxivbis).
|
||||
#
|
||||
# License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause
|
||||
#
|
||||
from aiohttp.web import middleware, Request, Response
|
||||
from aiohttp_security import AbstractAuthorizationPolicy, check_permission
|
||||
from typing import Callable, Optional, Tuple
|
||||
|
||||
from ffxivbis.core.database import Database
|
||||
|
||||
|
||||
class AuthorizationPolicy(AbstractAuthorizationPolicy):
|
||||
|
||||
def __init__(self, database: Database) -> None:
|
||||
self.database = database
|
||||
|
||||
def split_identity(self, identity: str) -> Tuple[str, str]:
|
||||
# identity is party_id + username
|
||||
party_id, username = identity.split('+')
|
||||
return party_id, username
|
||||
|
||||
async def authorized_userid(self, identity: str) -> Optional[str]:
|
||||
party_id, username = self.split_identity(identity)
|
||||
user = await self.database.get_user(party_id, username)
|
||||
return username if user is not None else None
|
||||
|
||||
async def permits(self, identity: str, permission: str, context: str = None) -> bool:
|
||||
party_id, username = self.split_identity(identity)
|
||||
user = await self.database.get_user(party_id, username)
|
||||
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
|
||||
|
34
src/ffxivbis/api/json.py
Normal file
34
src/ffxivbis/api/json.py
Normal file
@ -0,0 +1,34 @@
|
||||
#
|
||||
# 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
|
66
src/ffxivbis/api/routes.py
Normal file
66
src/ffxivbis/api/routes.py
Normal file
@ -0,0 +1,66 @@
|
||||
#
|
||||
# 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/{party_id}/login/{username}', LoginView)
|
||||
app.router.add_post('/api/v1/{party_id}/login', LoginView)
|
||||
app.router.add_post('/api/v1/{party_id}/logout', LogoutView)
|
||||
app.router.add_put('/admin/api/v1/{party_id}/login', LoginView)
|
||||
|
||||
app.router.add_get('/api/v1/party/{party_id}', PlayerView)
|
||||
app.router.add_post('/api/v1/party/{party_id}', PlayerView)
|
||||
|
||||
app.router.add_get('/api/v1/party/{party_id}/bis', BiSView)
|
||||
app.router.add_post('/api/v1/party/{party_id}/bis', BiSView)
|
||||
app.router.add_put('/api/v1/party/{party_id}/bis', BiSView)
|
||||
|
||||
app.router.add_get('/api/v1/party/{party_id}/loot', LootView)
|
||||
app.router.add_post('/api/v1/party/{party_id}/loot', LootView)
|
||||
app.router.add_put('/api/v1/party/{party_id}/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/{party_id}', PlayerHtmlView)
|
||||
app.router.add_post('/party/{party_id}', PlayerHtmlView)
|
||||
|
||||
app.router.add_get('/bis/{party_id}', BiSHtmlView)
|
||||
app.router.add_post('/bis/{party_id}', BiSHtmlView)
|
||||
|
||||
app.router.add_get('/loot/{party_id}', LootHtmlView)
|
||||
app.router.add_post('/loot/{party_id}', LootHtmlView)
|
||||
|
||||
app.router.add_get('/suggest/{party_id}', LootSuggestHtmlView)
|
||||
app.router.add_post('/suggest/{party_id}', LootSuggestHtmlView)
|
||||
|
||||
app.router.add_get('/admin/users/{party_id}', UsersHtmlView)
|
||||
app.router.add_post('/admin/users/{party_id}', UsersHtmlView)
|
||||
|
||||
|
78
src/ffxivbis/api/spec.py
Normal file
78
src/ffxivbis/api/spec.py
Normal file
@ -0,0 +1,78 @@
|
||||
#
|
||||
# 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
|
42
src/ffxivbis/api/utils.py
Normal file
42
src/ffxivbis/api/utils.py
Normal file
@ -0,0 +1,42 @@
|
||||
#
|
||||
# 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'
|
||||
)
|
0
src/ffxivbis/api/views/__init__.py
Normal file
0
src/ffxivbis/api/views/__init__.py
Normal file
0
src/ffxivbis/api/views/api/__init__.py
Normal file
0
src/ffxivbis/api/views/api/__init__.py
Normal file
159
src/ffxivbis/api/views/api/bis.py
Normal file
159
src/ffxivbis/api/views/api/bis.py
Normal file
@ -0,0 +1,159 @@
|
||||
#
|
||||
# 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)
|
140
src/ffxivbis/api/views/api/login.py
Normal file
140
src/ffxivbis/api/views/api/login.py
Normal file
@ -0,0 +1,140 @@
|
||||
#
|
||||
# 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', 'party_id']
|
||||
if any(param not in data for param in required):
|
||||
return wrap_invalid_param(required, data)
|
||||
|
||||
try:
|
||||
await self.create_user(data['party_id'], 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({})
|
46
src/ffxivbis/api/views/api/logout.py
Normal file
46
src/ffxivbis/api/views/api/logout.py
Normal file
@ -0,0 +1,46 @@
|
||||
# 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({})
|
159
src/ffxivbis/api/views/api/loot.py
Normal file
159
src/ffxivbis/api/views/api/loot.py
Normal file
@ -0,0 +1,159 @@
|
||||
#
|
||||
# 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)
|
195
src/ffxivbis/api/views/api/openapi.py
Normal file
195
src/ffxivbis/api/views/api/openapi.py
Normal file
@ -0,0 +1,195 @@
|
||||
#
|
||||
# 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
|
107
src/ffxivbis/api/views/api/player.py
Normal file
107
src/ffxivbis/api/views/api/player.py
Normal file
@ -0,0 +1,107 @@
|
||||
#
|
||||
# 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)
|
0
src/ffxivbis/api/views/common/__init__.py
Normal file
0
src/ffxivbis/api/views/common/__init__.py
Normal file
49
src/ffxivbis/api/views/common/bis_base.py
Normal file
49
src/ffxivbis/api/views/common/bis_base.py
Normal file
@ -0,0 +1,49 @@
|
||||
#
|
||||
# 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
|
43
src/ffxivbis/api/views/common/login_base.py
Normal file
43
src/ffxivbis/api/views/common/login_base.py
Normal file
@ -0,0 +1,43 @@
|
||||
#
|
||||
# Copyright (c) 2019 Evgeniy Alekseev.
|
||||
#
|
||||
# This file is part of ffxivbis
|
||||
# (see https://github.com/arcan1s/ffxivbis).
|
||||
#
|
||||
# License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause
|
||||
#
|
||||
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, party_id: str, username: str, password: str, permission: str) -> None:
|
||||
await self.request.app['database'].insert_user(party_id, 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)
|
43
src/ffxivbis/api/views/common/loot_base.py
Normal file
43
src/ffxivbis/api/views/common/loot_base.py
Normal file
@ -0,0 +1,43 @@
|
||||
#
|
||||
# Copyright (c) 2019 Evgeniy Alekseev.
|
||||
#
|
||||
# This file is part of ffxivbis
|
||||
# (see https://github.com/arcan1s/ffxivbis).
|
||||
#
|
||||
# License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause
|
||||
#
|
||||
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
|
50
src/ffxivbis/api/views/common/player_base.py
Normal file
50
src/ffxivbis/api/views/common/player_base.py
Normal file
@ -0,0 +1,50 @@
|
||||
#
|
||||
# Copyright (c) 2019 Evgeniy Alekseev.
|
||||
#
|
||||
# This file is part of ffxivbis
|
||||
# (see https://github.com/arcan1s/ffxivbis).
|
||||
#
|
||||
# License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause
|
||||
#
|
||||
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
|
0
src/ffxivbis/api/views/html/__init__.py
Normal file
0
src/ffxivbis/api/views/html/__init__.py
Normal file
29
src/ffxivbis/api/views/html/api.py
Normal file
29
src/ffxivbis/api/views/html/api.py
Normal file
@ -0,0 +1,29 @@
|
||||
# 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 {}
|
82
src/ffxivbis/api/views/html/bis.py
Normal file
82
src/ffxivbis/api/views/html/bis.py
Normal file
@ -0,0 +1,82 @@
|
||||
#
|
||||
# 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)
|
23
src/ffxivbis/api/views/html/index.py
Normal file
23
src/ffxivbis/api/views/html/index.py
Normal file
@ -0,0 +1,23 @@
|
||||
#
|
||||
# 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
|
||||
}
|
70
src/ffxivbis/api/views/html/loot.py
Normal file
70
src/ffxivbis/api/views/html/loot.py
Normal file
@ -0,0 +1,70 @@
|
||||
#
|
||||
# Copyright (c) 2019 Evgeniy Alekseev.
|
||||
#
|
||||
# This file is part of ffxivbis
|
||||
# (see https://github.com/arcan1s/ffxivbis).
|
||||
#
|
||||
# License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause
|
||||
#
|
||||
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)
|
64
src/ffxivbis/api/views/html/loot_suggest.py
Normal file
64
src/ffxivbis/api/views/html/loot_suggest.py
Normal file
@ -0,0 +1,64 @@
|
||||
#
|
||||
# Copyright (c) 2019 Evgeniy Alekseev.
|
||||
#
|
||||
# This file is part of ffxivbis
|
||||
# (see https://github.com/arcan1s/ffxivbis).
|
||||
#
|
||||
# License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause
|
||||
#
|
||||
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
|
||||
]
|
||||
}
|
67
src/ffxivbis/api/views/html/player.py
Normal file
67
src/ffxivbis/api/views/html/player.py
Normal file
@ -0,0 +1,67 @@
|
||||
#
|
||||
# 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)
|
31
src/ffxivbis/api/views/html/static.py
Normal file
31
src/ffxivbis/api/views/html/static.py
Normal file
@ -0,0 +1,31 @@
|
||||
# Copyright (c) 2019 Evgeniy Alekseev.
|
||||
#
|
||||
# This file is part of ffxivbis
|
||||
# (see https://github.com/arcan1s/ffxivbis).
|
||||
#
|
||||
# License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause
|
||||
#
|
||||
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)
|
62
src/ffxivbis/api/views/html/users.py
Normal file
62
src/ffxivbis/api/views/html/users.py
Normal file
@ -0,0 +1,62 @@
|
||||
#
|
||||
# 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)
|
67
src/ffxivbis/api/web.py
Normal file
67
src/ffxivbis/api/web.py
Normal file
@ -0,0 +1,67 @@
|
||||
#
|
||||
# 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.party_aggregator import PartyAggregator
|
||||
|
||||
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, aggregator: PartyAggregator) -> 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 aggregator')
|
||||
app['aggregator'] = aggregator
|
||||
|
||||
return app
|
0
src/ffxivbis/application/__init__.py
Normal file
0
src/ffxivbis/application/__init__.py
Normal file
31
src/ffxivbis/application/application.py
Normal file
31
src/ffxivbis/application/application.py
Normal file
@ -0,0 +1,31 @@
|
||||
#
|
||||
# Copyright (c) 2019 Evgeniy Alekseev.
|
||||
#
|
||||
# This file is part of ffxivbis
|
||||
# (see https://github.com/arcan1s/ffxivbis).
|
||||
#
|
||||
# License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause
|
||||
#
|
||||
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()
|
33
src/ffxivbis/application/core.py
Normal file
33
src/ffxivbis/application/core.py
Normal file
@ -0,0 +1,33 @@
|
||||
#
|
||||
# Copyright (c) 2019 Evgeniy Alekseev.
|
||||
#
|
||||
# This file is part of ffxivbis
|
||||
# (see https://github.com/arcan1s/ffxivbis).
|
||||
#
|
||||
# License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause
|
||||
#
|
||||
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.party_aggregator import PartyAggregator
|
||||
|
||||
|
||||
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()
|
||||
|
||||
aggregator = PartyAggregator(self.config, database)
|
||||
|
||||
web = setup_service(self.config, database, aggregator)
|
||||
run_server(web)
|
0
src/ffxivbis/core/__init__.py
Normal file
0
src/ffxivbis/core/__init__.py
Normal file
83
src/ffxivbis/core/ariyala_parser.py
Normal file
83
src/ffxivbis/core/ariyala_parser.py
Normal file
@ -0,0 +1,83 @@
|
||||
#
|
||||
# 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
|
65
src/ffxivbis/core/config.py
Normal file
65
src/ffxivbis/core/config.py
Normal file
@ -0,0 +1,65 @@
|
||||
#
|
||||
# Copyright (c) 2019 Evgeniy Alekseev.
|
||||
#
|
||||
# This file is part of ffxivbis
|
||||
# (see https://github.com/arcan1s/ffxivbis).
|
||||
#
|
||||
# License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause
|
||||
#
|
||||
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')))
|
113
src/ffxivbis/core/database.py
Normal file
113
src/ffxivbis/core/database.py
Normal file
@ -0,0 +1,113 @@
|
||||
#
|
||||
# Copyright (c) 2019 Evgeniy Alekseev.
|
||||
#
|
||||
# This file is part of ffxivbis
|
||||
# (see https://github.com/arcan1s/ffxivbis).
|
||||
#
|
||||
# License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause
|
||||
#
|
||||
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, party_id: str, player_id: PlayerId, piece: Union[Piece, Upgrade]) -> None:
|
||||
raise NotImplementedError
|
||||
|
||||
async def delete_piece_bis(self, party_id: str, player_id: PlayerId, piece: Piece) -> None:
|
||||
raise NotImplementedError
|
||||
|
||||
async def delete_player(self, party_id: str, player_id: PlayerId) -> None:
|
||||
raise NotImplementedError
|
||||
|
||||
async def delete_user(self, party_id: str, username: str) -> None:
|
||||
raise NotImplementedError
|
||||
|
||||
async def get_party(self, party_id: str) -> List[Player]:
|
||||
raise NotImplementedError
|
||||
|
||||
async def get_player(self, party_id: str, player_id: PlayerId) -> Optional[int]:
|
||||
raise NotImplementedError
|
||||
|
||||
async def get_players(self, party_id: str) -> List[int]:
|
||||
raise NotImplementedError
|
||||
|
||||
async def get_user(self, party_id: str, username: str) -> Optional[User]:
|
||||
raise NotImplementedError
|
||||
|
||||
async def get_users(self, party_id: str) -> List[User]:
|
||||
raise NotImplementedError
|
||||
|
||||
async def insert_piece(self, party_id: str, player_id: PlayerId, piece: Union[Piece, Upgrade]) -> None:
|
||||
raise NotImplementedError
|
||||
|
||||
async def insert_piece_bis(self, party_id: str, player_id: PlayerId, piece: Piece) -> None:
|
||||
raise NotImplementedError
|
||||
|
||||
async def insert_player(self, party_id: str, player: Player) -> None:
|
||||
raise NotImplementedError
|
||||
|
||||
async def insert_user(self, party_id: str, 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())
|
27
src/ffxivbis/core/exceptions.py
Normal file
27
src/ffxivbis/core/exceptions.py
Normal file
@ -0,0 +1,27 @@
|
||||
#
|
||||
# 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}')
|
32
src/ffxivbis/core/loot_selector.py
Normal file
32
src/ffxivbis/core/loot_selector.py
Normal file
@ -0,0 +1,32 @@
|
||||
#
|
||||
# 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)]
|
82
src/ffxivbis/core/party.py
Normal file
82
src/ffxivbis/core/party.py
Normal file
@ -0,0 +1,82 @@
|
||||
#
|
||||
# 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, party_id: str, database: Database) -> None:
|
||||
self.lock = Lock()
|
||||
self.party_id = party_id
|
||||
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], party_id: str, database: Database) -> Party:
|
||||
obj = cls(party_id, database)
|
||||
players = await database.get_party(party_id)
|
||||
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(self.party_id, player)
|
||||
|
||||
async def remove_player(self, player_id: PlayerId) -> Optional[Player]:
|
||||
await self.database.delete_player(self.party_id, 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(self.party_id, 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(self.party_id, 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(self.party_id, 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(self.party_id, 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(self.party_id, player_id, piece)
|
||||
with self.lock:
|
||||
self.players[player_id].bis.remove_item(piece)
|
26
src/ffxivbis/core/party_aggregator.py
Normal file
26
src/ffxivbis/core/party_aggregator.py
Normal file
@ -0,0 +1,26 @@
|
||||
#
|
||||
# Copyright (c) 2019 Evgeniy Alekseev.
|
||||
#
|
||||
# This file is part of ffxivbis
|
||||
# (see https://github.com/arcan1s/ffxivbis).
|
||||
#
|
||||
# License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause
|
||||
#
|
||||
from .config import Configuration
|
||||
from .database import Database
|
||||
from .loot_selector import LootSelector
|
||||
from .party import Party
|
||||
|
||||
|
||||
class PartyAggregator:
|
||||
|
||||
def __init__(self, config: Configuration, database: Database) -> None:
|
||||
self.config = config
|
||||
self.database = database
|
||||
|
||||
async def get_party(self, party_id: str) -> Party:
|
||||
return await Party.get(party_id, self.database)
|
||||
|
||||
async def get_loot_selector(self, party: Party) -> LootSelector:
|
||||
priority = self.config.get('settings', 'priority').split()
|
||||
return LootSelector(party, priority)
|
175
src/ffxivbis/core/postgres.py
Normal file
175
src/ffxivbis/core/postgres.py
Normal file
@ -0,0 +1,175 @@
|
||||
#
|
||||
# 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, party_id: str, player_id: PlayerId, piece: Union[Piece, Upgrade]) -> None:
|
||||
player = await self.get_player(party_id, 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, party_id: str, player_id: PlayerId, piece: Piece) -> None:
|
||||
player = await self.get_player(party_id, 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, party_id: str, player_id: PlayerId) -> None:
|
||||
async with self.pool.acquire() as conn:
|
||||
await conn.execute('''delete from players where nick = $1 and job = $2 and party_id = $3''',
|
||||
player_id.nick, player_id.job.name, party_id)
|
||||
|
||||
async def delete_user(self, party_id: str, username: str) -> None:
|
||||
async with self.pool.acquire() as conn:
|
||||
await conn.execute('''delete from users where username = $1 and party_id = $2''',
|
||||
(username, party_id))
|
||||
|
||||
async def get_party(self, party_id: str) -> List[Player]:
|
||||
players = await self.get_players(party_id)
|
||||
if not players:
|
||||
return []
|
||||
|
||||
async with self.pool.acquire() as conn:
|
||||
rows = await conn.fetch('''select * from bis where player_id in $1''', players)
|
||||
bis_pieces = [Loot(row['player_id'], Piece.get(row)) for row in rows]
|
||||
|
||||
rows = await conn.fetch('''select * from loot where player_id in $1''', players)
|
||||
loot_pieces = [Loot(row['player_id'], Piece.get(row)) for row in rows]
|
||||
|
||||
rows = await conn.fetch('''select * from players where party_id = $1''', party_id)
|
||||
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, party_id: str, 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 and party_id = $3''',
|
||||
player_id.nick, player_id.job.name, party_id)
|
||||
return player['player_id'] if player is not None else None
|
||||
|
||||
async def get_players(self, party_id: str) -> List[int]:
|
||||
async with self.pool.acquire() as conn:
|
||||
players = await conn.fetch('''select player_id from players where party_id = $1''', (party_id,))
|
||||
return [player['player_id'] for player in players]
|
||||
|
||||
async def get_user(self, party_id: str, username: str) -> Optional[User]:
|
||||
async with self.pool.acquire() as conn:
|
||||
user = await conn.fetchrow('''select * from users where username = $1 and party_id = $2''',
|
||||
username, party_id)
|
||||
return User(user['username'], user['password'], user['permission']) if user is not None else None
|
||||
|
||||
async def get_users(self, party_id: str) -> List[User]:
|
||||
async with self.pool.acquire() as conn:
|
||||
users = await conn.fetch('''select * from users where party_id = $1''', party_id)
|
||||
return [User(user['username'], user['password'], user['permission']) for user in users]
|
||||
|
||||
async def insert_piece(self, party_id: str, player_id: PlayerId, piece: Union[Piece, Upgrade]) -> None:
|
||||
player = await self.get_player(party_id, 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, party_id: str, player_id: PlayerId, piece: Piece) -> None:
|
||||
player = await self.get_player(party_id, 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, party_id: str, player: Player) -> None:
|
||||
async with self.pool.acquire() as conn:
|
||||
await conn.execute(
|
||||
'''insert into players
|
||||
(party_id, created, nick, job, bis_link, priority)
|
||||
values
|
||||
($1, $2, $3, $4, $5, $6)
|
||||
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, party_id
|
||||
)
|
||||
|
||||
async def insert_user(self, party_id: str, 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
|
||||
(party_id, username, password, permission)
|
||||
values
|
||||
($1, $2, $3, $4)
|
||||
on conflict on constraint users_username_idx do update set
|
||||
password = $2, permission = $3''',
|
||||
party_id, user.username, password, user.permission
|
||||
)
|
165
src/ffxivbis/core/sqlite.py
Normal file
165
src/ffxivbis/core/sqlite.py
Normal file
@ -0,0 +1,165 @@
|
||||
#
|
||||
# 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, party_id: str, player_id: PlayerId, piece: Union[Piece, Upgrade]) -> None:
|
||||
player = await self.get_player(party_id, 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, party_id: str, player_id: PlayerId, piece: Piece) -> None:
|
||||
player = await self.get_player(party_id, 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, party_id: str, player_id: PlayerId) -> None:
|
||||
async with SQLiteHelper(self.database_path) as cursor:
|
||||
await cursor.execute('''delete from players where nick = ? and job = ? and party_id = ?''',
|
||||
(player_id.nick, player_id.job.name, party_id))
|
||||
|
||||
async def delete_user(self, party_id: str, username: str) -> None:
|
||||
async with SQLiteHelper(self.database_path) as cursor:
|
||||
await cursor.execute('''delete from users where username = ? and party_id = ?''',
|
||||
(username, party_id))
|
||||
|
||||
async def get_party(self, party_id: str) -> List[Player]:
|
||||
players = await self.get_players(party_id)
|
||||
if not players:
|
||||
return []
|
||||
placeholder = ', '.join(['?'] * len(players))
|
||||
|
||||
async with SQLiteHelper(self.database_path) as cursor:
|
||||
await cursor.execute('''select * from bis where player_id in ({})'''.format(placeholder), players)
|
||||
rows = await cursor.fetchall()
|
||||
bis_pieces = [Loot(row['player_id'], Piece.get(row)) for row in rows]
|
||||
|
||||
await cursor.execute('''select * from loot where player_id in ({})'''.format(placeholder), players)
|
||||
rows = await cursor.fetchall()
|
||||
loot_pieces = [Loot(row['player_id'], Piece.get(row)) for row in rows]
|
||||
|
||||
await cursor.execute('''select * from players where party_id = ?''', (party_id,))
|
||||
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, party_id: str, 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 = ? and party_id = ?''',
|
||||
(player_id.nick, player_id.job.name, party_id))
|
||||
player = await cursor.fetchone()
|
||||
return player['player_id'] if player is not None else None
|
||||
|
||||
async def get_players(self, party_id: str) -> List[int]:
|
||||
async with SQLiteHelper(self.database_path) as cursor:
|
||||
await cursor.execute('''select player_id from players where party_id = ?''', (party_id,))
|
||||
players = await cursor.fetchall()
|
||||
return [player['player_id'] for player in players]
|
||||
|
||||
async def get_user(self, party_id: str, username: str) -> Optional[User]:
|
||||
async with SQLiteHelper(self.database_path) as cursor:
|
||||
await cursor.execute('''select * from users where username = ? and party_id = ?''',
|
||||
(username, party_id))
|
||||
user = await cursor.fetchone()
|
||||
return User(user['username'], user['password'], user['permission']) if user is not None else None
|
||||
|
||||
async def get_users(self, party_id: str) -> List[User]:
|
||||
async with SQLiteHelper(self.database_path) as cursor:
|
||||
await cursor.execute('''select * from users where party_id = ?''', (party_id,))
|
||||
users = await cursor.fetchall()
|
||||
return [User(user['username'], user['password'], user['permission']) for user in users]
|
||||
|
||||
async def insert_piece(self, party_id: str, player_id: PlayerId, piece: Union[Piece, Upgrade]) -> None:
|
||||
player = await self.get_player(party_id, 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, party_id: str, player_id: PlayerId, piece: Piece) -> None:
|
||||
player = await self.get_player(party_id, 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, party_id: str, player: Player) -> None:
|
||||
async with SQLiteHelper(self.database_path) as cursor:
|
||||
await cursor.execute(
|
||||
'''replace into players
|
||||
(party_id, created, nick, job, bis_link, priority)
|
||||
values
|
||||
(?, ?, ?, ?, ?, ?)''',
|
||||
(party_id, Database.now(), player.nick, player.job.name, player.link, player.priority)
|
||||
)
|
||||
|
||||
async def insert_user(self, party_id: str, 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
|
||||
(party_id, username, password, permission)
|
||||
values
|
||||
(?, ?, ?, ?)''',
|
||||
(party_id, user.username, password, user.permission)
|
||||
)
|
36
src/ffxivbis/core/sqlite_helper.py
Normal file
36
src/ffxivbis/core/sqlite_helper.py
Normal file
@ -0,0 +1,36 @@
|
||||
#
|
||||
# Copyright (c) 2019 Evgeniy Alekseev.
|
||||
#
|
||||
# This file is part of ffxivbis
|
||||
# (see https://github.com/arcan1s/ffxivbis).
|
||||
#
|
||||
# License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause
|
||||
#
|
||||
# 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()
|
9
src/ffxivbis/core/version.py
Normal file
9
src/ffxivbis/core/version.py
Normal file
@ -0,0 +1,9 @@
|
||||
#
|
||||
# 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'
|
0
src/ffxivbis/models/__init__.py
Normal file
0
src/ffxivbis/models/__init__.py
Normal file
16
src/ffxivbis/models/action.py
Normal file
16
src/ffxivbis/models/action.py
Normal file
@ -0,0 +1,16 @@
|
||||
#
|
||||
# Copyright (c) 2019 Evgeniy Alekseev.
|
||||
#
|
||||
# This file is part of ffxivbis
|
||||
# (see https://github.com/arcan1s/ffxivbis).
|
||||
#
|
||||
# License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause
|
||||
#
|
||||
from enum import auto
|
||||
|
||||
from .serializable import SerializableEnum
|
||||
|
||||
|
||||
class Action(SerializableEnum):
|
||||
add = auto()
|
||||
remove = auto()
|
140
src/ffxivbis/models/bis.py
Normal file
140
src/ffxivbis/models/bis.py
Normal file
@ -0,0 +1,140 @@
|
||||
#
|
||||
# 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)
|
36
src/ffxivbis/models/error.py
Normal file
36
src/ffxivbis/models/error.py
Normal file
@ -0,0 +1,36 @@
|
||||
#
|
||||
# Copyright (c) 2019 Evgeniy Alekseev.
|
||||
#
|
||||
# This file is part of ffxivbis
|
||||
# (see https://github.com/arcan1s/ffxivbis).
|
||||
#
|
||||
# License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause
|
||||
#
|
||||
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']
|
87
src/ffxivbis/models/job.py
Normal file
87
src/ffxivbis/models/job.py
Normal file
@ -0,0 +1,87 @@
|
||||
#
|
||||
# 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
|
||||
|
37
src/ffxivbis/models/loot.py
Normal file
37
src/ffxivbis/models/loot.py
Normal file
@ -0,0 +1,37 @@
|
||||
#
|
||||
# Copyright (c) 2019 Evgeniy Alekseev.
|
||||
#
|
||||
# This file is part of ffxivbis
|
||||
# (see https://github.com/arcan1s/ffxivbis).
|
||||
#
|
||||
# License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause
|
||||
#
|
||||
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']
|
168
src/ffxivbis/models/piece.py
Normal file
168
src/ffxivbis/models/piece.py
Normal file
@ -0,0 +1,168 @@
|
||||
#
|
||||
# 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
|
201
src/ffxivbis/models/player.py
Normal file
201
src/ffxivbis/models/player.py
Normal file
@ -0,0 +1,201 @@
|
||||
#
|
||||
# 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
|
35
src/ffxivbis/models/player_edit.py
Normal file
35
src/ffxivbis/models/player_edit.py
Normal file
@ -0,0 +1,35 @@
|
||||
#
|
||||
# Copyright (c) 2019 Evgeniy Alekseev.
|
||||
#
|
||||
# This file is part of ffxivbis
|
||||
# (see https://github.com/arcan1s/ffxivbis).
|
||||
#
|
||||
# License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause
|
||||
#
|
||||
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']
|
57
src/ffxivbis/models/serializable.py
Normal file
57
src/ffxivbis/models/serializable.py
Normal file
@ -0,0 +1,57 @@
|
||||
#
|
||||
# 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'
|
23
src/ffxivbis/models/upgrade.py
Normal file
23
src/ffxivbis/models/upgrade.py
Normal file
@ -0,0 +1,23 @@
|
||||
#
|
||||
# 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))
|
42
src/ffxivbis/models/user.py
Normal file
42
src/ffxivbis/models/user.py
Normal file
@ -0,0 +1,42 @@
|
||||
#
|
||||
# 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']
|
@ -1,36 +0,0 @@
|
||||
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);
|
@ -1,5 +0,0 @@
|
||||
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';
|
@ -1,5 +0,0 @@
|
||||
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);
|
@ -1,17 +0,0 @@
|
||||
-- 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;
|
@ -1 +0,0 @@
|
||||
alter table loot add column is_free_loot integer not null default 0;
|
@ -1,2 +0,0 @@
|
||||
drop index bis_piece_player_id_idx;
|
||||
create index bis_piece_type_player_id_idx on bis(player_id, piece, piece_type);
|
@ -1 +0,0 @@
|
||||
alter table parties rename column player_id to party_id;
|
@ -1,36 +0,0 @@
|
||||
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);
|
@ -1,5 +0,0 @@
|
||||
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';
|
@ -1,5 +0,0 @@
|
||||
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);
|
@ -1,42 +0,0 @@
|
||||
-- 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);
|
@ -1,20 +0,0 @@
|
||||
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);
|
@ -1,2 +0,0 @@
|
||||
drop index bis_piece_player_id_idx;
|
||||
create index bis_piece_type_player_id_idx on bis(player_id, piece, piece_type);
|
@ -1,11 +0,0 @@
|
||||
create table parties_new (
|
||||
party_id integer primary key autoincrement,
|
||||
party_name text not null,
|
||||
party_alias text);
|
||||
insert into parties_new select player_id, party_name, party_alias from parties;
|
||||
|
||||
drop index parties_party_name_idx;
|
||||
drop table parties;
|
||||
|
||||
alter table parties_new rename to parties;
|
||||
create unique index parties_party_name_idx on parties(party_name);
|
@ -1,349 +0,0 @@
|
||||
<!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 href="/static/favicon.ico" rel="shortcut icon">
|
||||
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous" type="text/css">
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.7.2/font/bootstrap-icons.css" rel="stylesheet" type="text/css">
|
||||
|
||||
<link href="https://unpkg.com/bootstrap-table@1.19.1/dist/bootstrap-table.min.css" rel="stylesheet" type="text/css">
|
||||
|
||||
|
||||
<link href="https://unpkg.com/jquery-resizable-columns@0.2.3/dist/jquery.resizableColumns.css" rel="stylesheet" type="text/css">
|
||||
|
||||
<link href="/static/styles.css" rel="stylesheet" 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-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">
|
||||
<div class="modal-header">
|
||||
<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>
|
||||
</div>
|
||||
<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"></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" onkeyup="disableSubmitBisButton()">
|
||||
</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="button" class="btn btn-primary" data-bs-dismiss="modal" onclick="addPiece()" disabled>add</button>
|
||||
<button id="submit-update-bis-btn" type="button" class="btn btn-primary" data-bs-dismiss="modal" onclick="setBis()" style="display: none" disabled>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>
|
||||
</ul>
|
||||
|
||||
<ul class="nav">
|
||||
<li><a 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.19.1/dist/bootstrap-table.min.js"></script>
|
||||
|
||||
<script src="https://unpkg.com/bootstrap-table@1.19.1/dist/extensions/export/bootstrap-table-export.min.js"></script>
|
||||
|
||||
<script src="https://unpkg.com/bootstrap-table@1.19.1/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 submitUpdateBisButton = $("#submit-update-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: function (_) { reload(); },
|
||||
error: function (jqXHR, _, errorThrown) { requestAlert(jqXHR, errorThrown); },
|
||||
});
|
||||
}
|
||||
|
||||
function disableSubmitBisButton() {
|
||||
const nonEmpty = (playerInput.val() !== null); // well lol
|
||||
submitUpdateBisButton.attr("disabled", !(nonEmpty && linkInput.val()));
|
||||
submitAddBisButton.attr("disabled", !(nonEmpty));
|
||||
}
|
||||
|
||||
function hideControls() {
|
||||
removeButton.attr("hidden", isReadOnly);
|
||||
updateButton.attr("hidden", isReadOnly);
|
||||
}
|
||||
|
||||
function hideLinkPart() {
|
||||
disableSubmitBisButton();
|
||||
bisLinkRow.hide();
|
||||
submitUpdateBisButton.hide();
|
||||
pieceRow.show();
|
||||
pieceTypeRow.show();
|
||||
submitAddBisButton.show();
|
||||
}
|
||||
|
||||
function hidePiecePart() {
|
||||
disableSubmitBisButton();
|
||||
bisLinkRow.show();
|
||||
submitUpdateBisButton.show();
|
||||
pieceRow.hide();
|
||||
pieceTypeRow.hide();
|
||||
submitAddBisButton.hide();
|
||||
}
|
||||
|
||||
function reload() {
|
||||
table.bootstrapTable("showLoading");
|
||||
$.ajax({
|
||||
url: `/api/v1/party/${partyId}`,
|
||||
type: "GET",
|
||||
dataType: "json",
|
||||
success: function (data) {
|
||||
const items = data.map(function (player) {
|
||||
return player.bis.map(function (loot) {
|
||||
return {
|
||||
nick: player.nick,
|
||||
job: player.job,
|
||||
piece: loot.piece,
|
||||
pieceType: loot.pieceType,
|
||||
};
|
||||
});
|
||||
});
|
||||
const payload = items.reduce(function (left, right) { return left.concat(right); }, []);
|
||||
table.bootstrapTable("load", payload);
|
||||
table.bootstrapTable("uncheckAll");
|
||||
table.bootstrapTable("hideLoading");
|
||||
|
||||
const options = data.map(function (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);
|
||||
disableSubmitBisButton();
|
||||
},
|
||||
error: function (jqXHR, _, errorThrown) { requestAlert(jqXHR, errorThrown); },
|
||||
});
|
||||
}
|
||||
|
||||
function removePiece() {
|
||||
const pieces = table.bootstrapTable("getSelections");
|
||||
pieces.map(function (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: function (_) { reload(); },
|
||||
error: function (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: function (_) { reload(); },
|
||||
error: function (jqXHR, _, errorThrown) { requestAlert(jqXHR, errorThrown); },
|
||||
});
|
||||
}
|
||||
|
||||
$(function () {
|
||||
setupFormClear(updateBisDialog, reset);
|
||||
setupRemoveButton(table, removeButton);
|
||||
|
||||
loadHeader(partyId);
|
||||
loadTypes("/api/v1/types/pieces", pieceInput);
|
||||
loadTypes("/api/v1/types/pieces/types", pieceTypeInput);
|
||||
|
||||
hideControls();
|
||||
|
||||
updateBisButton.click(function () { reset(); });
|
||||
addPieceButton.click(function () { reset(); });
|
||||
|
||||
table.bootstrapTable({});
|
||||
reload();
|
||||
});
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
@ -1,183 +0,0 @@
|
||||
<!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 href="/static/favicon.ico" rel="shortcut icon">
|
||||
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous" type="text/css">
|
||||
|
||||
<link href="/static/styles.css" rel="stylesheet" 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"></ul>
|
||||
|
||||
<ul class="nav">
|
||||
<li><a 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>
|
||||
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: "PUT",
|
||||
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 () {
|
||||
signinButton.click(function () { reset(); });
|
||||
signupButton.click(function () { reset(); });
|
||||
});
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
@ -1,338 +0,0 @@
|
||||
<!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 href="/static/favicon.ico" rel="shortcut icon">
|
||||
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous" type="text/css">
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.7.2/font/bootstrap-icons.css" rel="stylesheet" type="text/css">
|
||||
|
||||
<link href="https://unpkg.com/bootstrap-table@1.19.1/dist/bootstrap-table.min.css" rel="stylesheet" type="text/css">
|
||||
|
||||
|
||||
<link href="https://unpkg.com/jquery-resizable-columns@0.2.3/dist/jquery.resizableColumns.css" rel="stylesheet" type="text/css">
|
||||
|
||||
<link href="/static/styles.css" rel="stylesheet" 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-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">
|
||||
<div class="modal-content">
|
||||
<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>
|
||||
|
||||
<form 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"></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"></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"></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"></select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table id="stats" class="table table-striped table-hover">
|
||||
<thead class="table-primary">
|
||||
<tr>
|
||||
<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>
|
||||
</form>
|
||||
|
||||
<div class="modal-footer">
|
||||
<div class="form-check form-switch">
|
||||
<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>
|
||||
<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 id="submit-btn" type="button" class="btn btn-primary" data-bs-dismiss="modal" onclick="addLoot()" disabled>add</button>
|
||||
</div>
|
||||
</div>
|
||||
</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>
|
||||
</ul>
|
||||
|
||||
<ul class="nav">
|
||||
<li><a 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.19.1/dist/bootstrap-table.min.js"></script>
|
||||
|
||||
<script src="https://unpkg.com/bootstrap-table@1.19.1/dist/extensions/export/bootstrap-table-export.min.js"></script>
|
||||
|
||||
<script src="https://unpkg.com/bootstrap-table@1.19.1/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 submitLootButton = $("#submit-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() {
|
||||
const player = getCurrentOption(playerInput);
|
||||
$.ajax({
|
||||
url: `/api/v1/party/${partyId}/loot`,
|
||||
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,
|
||||
},
|
||||
isFreeLoot: freeLootInput.is(":checked"),
|
||||
}),
|
||||
type: "POST",
|
||||
contentType: "application/json",
|
||||
success: function (_) { reload(); },
|
||||
error: function (jqXHR, _, errorThrown) { requestAlert(jqXHR, errorThrown); },
|
||||
});
|
||||
}
|
||||
|
||||
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: function (data) {
|
||||
const items = data.map(function (player) {
|
||||
return player.loot.map(function (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(function (left, right) { return left.concat(right); }, []);
|
||||
table.bootstrapTable("load", payload);
|
||||
table.bootstrapTable("uncheckAll");
|
||||
table.bootstrapTable("hideLoading");
|
||||
|
||||
const options = data.map(function (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);
|
||||
submitLootButton.attr("disabled", options.length === 0);
|
||||
},
|
||||
error: function (jqXHR, _, errorThrown) { requestAlert(jqXHR, errorThrown); },
|
||||
});
|
||||
}
|
||||
|
||||
function removeLoot() {
|
||||
const pieces = table.bootstrapTable("getSelections");
|
||||
pieces.map(function (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: function (_) { reload(); },
|
||||
error: function (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: function (data) {
|
||||
const payload = data.map(function (stat) {
|
||||
return {
|
||||
nick: stat.nick,
|
||||
job: stat.job,
|
||||
isRequired: stat.isRequired,
|
||||
lootCount: stat.lootCount,
|
||||
lootCountBiS: stat.lootCountBiS,
|
||||
lootCountTotal: stat.lootCountTotal,
|
||||
};
|
||||
});
|
||||
stats.bootstrapTable("load", payload);
|
||||
stats.bootstrapTable("uncheckAll");
|
||||
stats.bootstrapTable("hideLoading");
|
||||
},
|
||||
error: function (jqXHR, _, errorThrown) { requestAlert(jqXHR, errorThrown); },
|
||||
});
|
||||
}
|
||||
|
||||
$(function () {
|
||||
setupFormClear(addLootDialog);
|
||||
setupRemoveButton(table, removeButton);
|
||||
|
||||
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>
|
@ -1,258 +0,0 @@
|
||||
<!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 href="/static/favicon.ico" rel="shortcut icon">
|
||||
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous" type="text/css">
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.7.2/font/bootstrap-icons.css" rel="stylesheet" type="text/css">
|
||||
|
||||
<link href="https://unpkg.com/bootstrap-table@1.19.1/dist/bootstrap-table.min.css" rel="stylesheet" type="text/css">
|
||||
|
||||
|
||||
<link href="https://unpkg.com/jquery-resizable-columns@0.2.3/dist/jquery.resizableColumns.css" rel="stylesheet" type="text/css">
|
||||
|
||||
<link href="/static/styles.css" rel="stylesheet" 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-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">
|
||||
<div class="modal-content">
|
||||
<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>
|
||||
|
||||
<form 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" onkeyup="disableAddPlayerForm()">
|
||||
</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"></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>
|
||||
</form>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-danger" data-bs-dismiss="modal">close</button>
|
||||
<button id="submit-player-btn" type="button" class="btn btn-primary" data-bs-dismiss="modal" onclick="addPlayer()" disabled>add</button>
|
||||
</div>
|
||||
</div>
|
||||
</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>
|
||||
</ul>
|
||||
|
||||
<ul class="nav">
|
||||
<li><a 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.19.1/dist/bootstrap-table.min.js"></script>
|
||||
|
||||
<script src="https://unpkg.com/bootstrap-table@1.19.1/dist/extensions/export/bootstrap-table-export.min.js"></script>
|
||||
|
||||
<script src="https://unpkg.com/bootstrap-table@1.19.1/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 submitPlayerButton = $("#submit-player-btn");
|
||||
|
||||
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: function (_) { reload(); },
|
||||
error: function (jqXHR, _, errorThrown) { requestAlert(jqXHR, errorThrown); },
|
||||
});
|
||||
}
|
||||
|
||||
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 disableAddPlayerForm() {
|
||||
submitPlayerButton.attr("disabled", !nickInput.val());
|
||||
}
|
||||
|
||||
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: function (data) {
|
||||
table.bootstrapTable("load", data);
|
||||
table.bootstrapTable("uncheckAll");
|
||||
table.bootstrapTable("hideLoading");
|
||||
},
|
||||
error: function (jqXHR, _, errorThrown) { requestAlert(jqXHR, errorThrown); },
|
||||
});
|
||||
}
|
||||
|
||||
function removePlayers() {
|
||||
const players = table.bootstrapTable("getSelections");
|
||||
players.map(function (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: function (_) { reload(); },
|
||||
error: function (jqXHR, _, errorThrown) { requestAlert(jqXHR, errorThrown); },
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
$(function () {
|
||||
setupFormClear(addPlayerDialog);
|
||||
setupRemoveButton(table, removeButton);
|
||||
|
||||
loadHeader(partyId);
|
||||
loadTypes("/api/v1/types/jobs", jobInput);
|
||||
|
||||
hideControls();
|
||||
|
||||
table.bootstrapTable({});
|
||||
reload();
|
||||
});
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
@ -1,19 +0,0 @@
|
||||
<!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">
|
||||
|
||||
<link href="https://fonts.googleapis.com/css?family=Montserrat:300,400,700|Roboto:300,400,700" rel="stylesheet" type="text/css">
|
||||
|
||||
<link href="/static/favicon.ico" rel="shortcut icon">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<redoc spec-url="/api-docs/swagger.json"></redoc>
|
||||
<script src="https://cdn.jsdelivr.net/npm/redoc@next/bundles/redoc.standalone.js"> </script>
|
||||
|
||||
</body>
|
||||
</html>
|
@ -1,230 +0,0 @@
|
||||
<!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 href="/static/favicon.ico" rel="shortcut icon">
|
||||
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous" type="text/css">
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.7.2/font/bootstrap-icons.css" rel="stylesheet" type="text/css">
|
||||
|
||||
<link href="https://unpkg.com/bootstrap-table@1.19.1/dist/bootstrap-table.min.css" rel="stylesheet" type="text/css">
|
||||
|
||||
|
||||
<link href="https://unpkg.com/jquery-resizable-columns@0.2.3/dist/jquery.resizableColumns.css" rel="stylesheet" type="text/css">
|
||||
|
||||
<link href="/static/styles.css" rel="stylesheet" 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-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">
|
||||
<div class="modal-content">
|
||||
<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>
|
||||
|
||||
<form 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" onkeyup="disableAddUserForm()">
|
||||
</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" onkeyup="disableAddUserForm()">
|
||||
</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"></select>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-danger" data-bs-dismiss="modal">close</button>
|
||||
<button id="submit-btn" type="button" class="btn btn-primary" data-bs-dismiss="modal" onclick="addUser()" disabled>add</button>
|
||||
</div>
|
||||
</div>
|
||||
</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>
|
||||
</ul>
|
||||
|
||||
<ul class="nav">
|
||||
<li><a 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.19.1/dist/bootstrap-table.min.js"></script>
|
||||
|
||||
<script src="https://unpkg.com/bootstrap-table@1.19.1/dist/extensions/export/bootstrap-table-export.min.js"></script>
|
||||
|
||||
<script src="https://unpkg.com/bootstrap-table@1.19.1/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 submitUserButton = $("#submit-btn");
|
||||
|
||||
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: function (_) { reload(); },
|
||||
error: function (jqXHR, _, errorThrown) { requestAlert(jqXHR, errorThrown); },
|
||||
});
|
||||
}
|
||||
|
||||
function disableAddUserForm() {
|
||||
submitUserButton.attr("disabled", !(usernameInput.val() && passwordInput.val()));
|
||||
}
|
||||
|
||||
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: function (data) {
|
||||
table.bootstrapTable("load", data);
|
||||
table.bootstrapTable("uncheckAll");
|
||||
table.bootstrapTable("hideLoading");
|
||||
},
|
||||
error: function (jqXHR, _, errorThrown) { requestAlert(jqXHR, errorThrown); },
|
||||
});
|
||||
}
|
||||
|
||||
function removeUsers() {
|
||||
const users = table.bootstrapTable("getSelections");
|
||||
users.map(function (user) {
|
||||
$.ajax({
|
||||
url: `/api/v1/party/${partyId}/users/${user.username}`,
|
||||
type: "DELETE",
|
||||
success: function (_) { reload(); },
|
||||
error: function (jqXHR, _, errorThrown) { requestAlert(jqXHR, errorThrown); },
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
$(function () {
|
||||
setupFormClear(addUserDialog);
|
||||
setupRemoveButton(table, removeButton);
|
||||
|
||||
loadHeader(partyId);
|
||||
loadTypes("/api/v1/types/permissions", permissionInput);
|
||||
|
||||
hideControls();
|
||||
|
||||
table.bootstrapTable({});
|
||||
reload();
|
||||
});
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user