4 Commits

Author SHA1 Message Date
4ff985bf81 some party impl 2019-10-16 02:48:55 +03:00
2d84459c4d replace requests by aiohttp 2019-09-15 03:27:29 +03:00
28dabcb44e rename service to ffxivbis, add notes about venv 2019-09-15 02:57:53 +03:00
36f0b8151a fix args rename 2019-09-15 01:37:30 +03:00
74 changed files with 531 additions and 381 deletions

View File

@ -9,14 +9,23 @@ This service requires python >= 3.7. For other dependencies see `setup.py`.
In general installation process looks like: In general installation process looks like:
```bash ```bash
python setup.py build python setup.py build install
python setup.py test # if you want to run tests python setup.py test # if you want to run tests
``` ```
Service can be run from `src` directory by using command: With virtualenv (make sure that virtualenv package was installed) the process may look like:
```bash ```bash
python -m service.application.application virtualenv -p python3.7 env
source env/bin/activate
python setup.py install
pip install aiosqlite # setup.py does not handle extras
```
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`. To see all available options type `--help`.
@ -27,6 +36,17 @@ REST API documentation is available at `http://0.0.0.0:8000/api-docs`. HTML repr
*Note*: host and port depend on configuration settings. *Note*: host and port depend on configuration settings.
### Authorization
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 ## Configuration
* `settings` section * `settings` section
@ -43,7 +63,6 @@ REST API documentation is available at `http://0.0.0.0:8000/api-docs`. HTML repr
Settings related to ariyala parser. Settings related to ariyala parser.
* `ariyala_url`: ariyala base url, string, required. * `ariyala_url`: ariyala base url, string, required.
* `request_timeout`: xivapi request timeout, float, optional, default 30.
* `xivapi_key`: xivapi developer key, string, optional. * `xivapi_key`: xivapi developer key, string, optional.
* `xivapi_url`: xivapi base url, string, required. * `xivapi_url`: xivapi base url, string, required.

View 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''')
]

View File

@ -7,4 +7,4 @@ priority = is_required loot_count_bis loot_priority loot_count loot_count_total
[web] [web]
host = 0.0.0.0 host = 0.0.0.0
port = 8000 port = 8000
templates = templates templates = /home/arcanis/Documents/github/ffxivbis/templates

View File

@ -1,4 +1,3 @@
[ariyala] [ariyala]
ariyala_url = https://ffxiv.ariyala.com ariyala_url = https://ffxiv.ariyala.com
request_timeout = 1
xivapi_url = https://xivapi.com xivapi_url = https://xivapi.com

View File

@ -5,7 +5,7 @@ from os import path
here = path.abspath(path.dirname(__file__)) here = path.abspath(path.dirname(__file__))
metadata = dict() metadata = dict()
with open(convert_path('src/service/core/version.py')) as metadata_file: with open(convert_path('src/ffxivbis/core/version.py')) as metadata_file:
exec(metadata_file.read(), metadata) exec(metadata_file.read(), metadata)
@ -22,16 +22,17 @@ setup(
license='BSD', license='BSD',
packages=find_packages(exclude=['contrib', 'docs', 'tests']), package_dir={'': 'src'},
packages=find_packages(where='src', exclude=['contrib', 'docs', 'test']),
install_requires=[ install_requires=[
'aiohttp', 'aiohttp==3.6.0',
'aiohttp_jinja2', 'aiohttp_jinja2',
'aiohttp_security', 'aiohttp_security',
'apispec', 'apispec',
'iniherit',
'Jinja2', 'Jinja2',
'passlib', 'passlib',
'requests',
'yoyo_migrations' 'yoyo_migrations'
], ],
setup_requires=[ setup_requires=[
@ -44,7 +45,7 @@ setup(
include_package_data=True, include_package_data=True,
extras_require={ extras_require={
'Postgresql': ['aiopg'], 'Postgresql': ['asyncpg'],
'SQLite': ['aiosqlite'], 'SQLite': ['aiosqlite'],
'test': ['coverage', 'pytest'], 'test': ['coverage', 'pytest'],
}, },

View File

@ -8,9 +8,9 @@
# #
from aiohttp.web import middleware, Request, Response from aiohttp.web import middleware, Request, Response
from aiohttp_security import AbstractAuthorizationPolicy, check_permission from aiohttp_security import AbstractAuthorizationPolicy, check_permission
from typing import Callable, Optional from typing import Callable, Optional, Tuple
from service.core.database import Database from ffxivbis.core.database import Database
class AuthorizationPolicy(AbstractAuthorizationPolicy): class AuthorizationPolicy(AbstractAuthorizationPolicy):
@ -18,12 +18,19 @@ class AuthorizationPolicy(AbstractAuthorizationPolicy):
def __init__(self, database: Database) -> None: def __init__(self, database: Database) -> None:
self.database = database 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]: async def authorized_userid(self, identity: str) -> Optional[str]:
user = await self.database.get_user(identity) party_id, username = self.split_identity(identity)
return identity if user is not None else None 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: async def permits(self, identity: str, permission: str, context: str = None) -> bool:
user = await self.database.get_user(identity) party_id, username = self.split_identity(identity)
user = await self.database.get_user(party_id, username)
if user is None: if user is None:
return False return False
if user.username != identity: if user.username != identity:

View 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)

View File

@ -9,17 +9,17 @@
from aiohttp.web import Application from aiohttp.web import Application
from apispec import APISpec from apispec import APISpec
from service.core.version import __version__ from ffxivbis.core.version import __version__
from service.models.action import Action from ffxivbis.models.action import Action
from service.models.bis import BiS, BiSLink from ffxivbis.models.bis import BiS, BiSLink
from service.models.error import Error from ffxivbis.models.error import Error
from service.models.job import Job from ffxivbis.models.job import Job
from service.models.loot import Loot from ffxivbis.models.loot import Loot
from service.models.piece import Piece from ffxivbis.models.piece import Piece
from service.models.player import Player, PlayerId, PlayerIdWithCounters from ffxivbis.models.player import Player, PlayerId, PlayerIdWithCounters
from service.models.player_edit import PlayerEdit from ffxivbis.models.player_edit import PlayerEdit
from service.models.upgrade import Upgrade from ffxivbis.models.upgrade import Upgrade
from service.models.user import User from ffxivbis.models.user import User
def get_spec(app: Application) -> APISpec: def get_spec(app: Application) -> APISpec:

View File

@ -9,12 +9,12 @@
from aiohttp.web import Response from aiohttp.web import Response
from typing import Any, Dict, List, Optional, Type from typing import Any, Dict, List, Optional, Type
from service.models.job import Job from ffxivbis.models.job import Job
from service.models.piece import Piece from ffxivbis.models.piece import Piece
from service.models.player import PlayerId from ffxivbis.models.player import PlayerId
from service.api.utils import wrap_exception, wrap_invalid_param, wrap_json from ffxivbis.api.utils import wrap_exception, wrap_invalid_param, wrap_json
from service.api.views.common.bis_base import BiSBaseView from ffxivbis.api.views.common.bis_base import BiSBaseView
from .openapi import OpenApi from .openapi import OpenApi

View File

@ -9,8 +9,8 @@
from aiohttp.web import Response from aiohttp.web import Response
from typing import Any, Dict, List, Optional, Type from typing import Any, Dict, List, Optional, Type
from service.api.utils import wrap_exception, wrap_invalid_param, wrap_json from ffxivbis.api.utils import wrap_exception, wrap_invalid_param, wrap_json
from service.api.views.common.login_base import LoginBaseView from ffxivbis.api.views.common.login_base import LoginBaseView
from .openapi import OpenApi from .openapi import OpenApi
@ -126,12 +126,13 @@ class LoginView(LoginBaseView, OpenApi):
except Exception: except Exception:
data = dict(await self.request.post()) data = dict(await self.request.post())
required = ['username', 'password'] required = ['username', 'password', 'party_id']
if any(param not in data for param in required): if any(param not in data for param in required):
return wrap_invalid_param(required, data) return wrap_invalid_param(required, data)
try: try:
await self.create_user(data['username'], data['password'], data.get('permission', 'get')) await self.create_user(data['party_id'], data['username'],
data['password'], data.get('permission', 'get'))
except Exception as e: except Exception as e:
self.request.app.logger.exception('cannot create user') self.request.app.logger.exception('cannot create user')
return wrap_exception(e, data) return wrap_exception(e, data)

View File

@ -6,8 +6,8 @@
from aiohttp.web import Response from aiohttp.web import Response
from typing import Any, Dict, List, Optional, Type from typing import Any, Dict, List, Optional, Type
from service.api.utils import wrap_exception, wrap_json from ffxivbis.api.utils import wrap_exception, wrap_json
from service.api.views.common.login_base import LoginBaseView from ffxivbis.api.views.common.login_base import LoginBaseView
from .openapi import OpenApi from .openapi import OpenApi

View File

@ -9,12 +9,12 @@
from aiohttp.web import Response from aiohttp.web import Response
from typing import Any, Dict, List, Optional, Type from typing import Any, Dict, List, Optional, Type
from service.models.job import Job from ffxivbis.models.job import Job
from service.models.piece import Piece from ffxivbis.models.piece import Piece
from service.models.player import PlayerId from ffxivbis.models.player import PlayerId
from service.api.utils import wrap_exception, wrap_invalid_param, wrap_json from ffxivbis.api.utils import wrap_exception, wrap_invalid_param, wrap_json
from service.api.views.common.loot_base import LootBaseView from ffxivbis.api.views.common.loot_base import LootBaseView
from .openapi import OpenApi from .openapi import OpenApi

View File

@ -10,7 +10,7 @@ from __future__ import annotations
from typing import Any, Dict, List, Optional, Type from typing import Any, Dict, List, Optional, Type
from service.models.serializable import Serializable from ffxivbis.models.serializable import Serializable
class OpenApi(Serializable): class OpenApi(Serializable):

View File

@ -9,10 +9,10 @@
from aiohttp.web import Response from aiohttp.web import Response
from typing import Any, Dict, List, Optional, Type from typing import Any, Dict, List, Optional, Type
from service.models.job import Job from ffxivbis.models.job import Job
from service.api.utils import wrap_exception, wrap_invalid_param, wrap_json from ffxivbis.api.utils import wrap_exception, wrap_invalid_param, wrap_json
from service.api.views.common.player_base import PlayerBaseView from ffxivbis.api.views.common.player_base import PlayerBaseView
from .openapi import OpenApi from .openapi import OpenApi

View File

@ -9,10 +9,10 @@
from aiohttp.web import View from aiohttp.web import View
from typing import List, Optional from typing import List, Optional
from service.core.ariyala_parser import AriyalaParser from ffxivbis.core.ariyala_parser import AriyalaParser
from service.models.bis import BiS from ffxivbis.models.bis import BiS
from service.models.piece import Piece from ffxivbis.models.piece import Piece
from service.models.player import PlayerId from ffxivbis.models.player import PlayerId
class BiSBaseView(View): class BiSBaseView(View):
@ -38,7 +38,7 @@ class BiSBaseView(View):
async def bis_put(self, player_id: PlayerId, link: str) -> BiS: async def bis_put(self, player_id: PlayerId, link: str) -> BiS:
parser = AriyalaParser(self.request.app['config']) parser = AriyalaParser(self.request.app['config'])
items = parser.get(link, player_id.job.name) items = await parser.get(link, player_id.job.name)
for piece in items: for piece in items:
await self.request.app['party'].set_item_bis(player_id, piece) await self.request.app['party'].set_item_bis(player_id, piece)
await self.request.app['party'].set_bis_link(player_id, link) await self.request.app['party'].set_bis_link(player_id, link)

View File

@ -10,7 +10,7 @@ from aiohttp.web import HTTPFound, HTTPUnauthorized, View
from aiohttp_security import check_authorized, forget, remember from aiohttp_security import check_authorized, forget, remember
from passlib.hash import md5_crypt from passlib.hash import md5_crypt
from service.models.user import User from ffxivbis.models.user import User
class LoginBaseView(View): class LoginBaseView(View):
@ -21,8 +21,8 @@ class LoginBaseView(View):
return False return False
return md5_crypt.verify(password, user.password) return md5_crypt.verify(password, user.password)
async def create_user(self, username: str, password: str, permission: str) -> None: async def create_user(self, party_id: str, username: str, password: str, permission: str) -> None:
await self.request.app['database'].insert_user(User(username, password, permission), False) await self.request.app['database'].insert_user(party_id, User(username, password, permission), False)
async def login(self, username: str, password: str) -> None: async def login(self, username: str, password: str) -> None:
if await self.check_credentials(username, password): if await self.check_credentials(username, password):

View File

@ -9,9 +9,9 @@
from aiohttp.web import View from aiohttp.web import View
from typing import List, Optional, Union from typing import List, Optional, Union
from service.models.piece import Piece from ffxivbis.models.piece import Piece
from service.models.player import PlayerId, PlayerIdWithCounters from ffxivbis.models.player import PlayerId, PlayerIdWithCounters
from service.models.upgrade import Upgrade from ffxivbis.models.upgrade import Upgrade
class LootBaseView(View): class LootBaseView(View):

View File

@ -9,10 +9,10 @@
from aiohttp.web import View from aiohttp.web import View
from typing import List, Optional from typing import List, Optional
from service.core.ariyala_parser import AriyalaParser from ffxivbis.core.ariyala_parser import AriyalaParser
from service.models.bis import BiS from ffxivbis.models.bis import BiS
from service.models.job import Job from ffxivbis.models.job import Job
from service.models.player import Player, PlayerId from ffxivbis.models.player import Player, PlayerId
class PlayerBaseView(View): class PlayerBaseView(View):
@ -24,7 +24,7 @@ class PlayerBaseView(View):
if link: if link:
parser = AriyalaParser(self.request.app['config']) parser = AriyalaParser(self.request.app['config'])
items = parser.get(link, job.name) items = await parser.get(link, job.name)
for piece in items: for piece in items:
await self.request.app['party'].set_item_bis(player_id, piece) await self.request.app['party'].set_item_bis(player_id, piece)

View File

@ -10,12 +10,12 @@ from aiohttp.web import HTTPFound, Response
from aiohttp_jinja2 import template from aiohttp_jinja2 import template
from typing import Any, Dict, List from typing import Any, Dict, List
from service.models.piece import Piece from ffxivbis.models.piece import Piece
from service.models.player import Player, PlayerId from ffxivbis.models.player import Player, PlayerId
from service.api.utils import wrap_exception, wrap_invalid_param from ffxivbis.api.utils import wrap_exception, wrap_invalid_param
from service.api.views.common.bis_base import BiSBaseView from ffxivbis.api.views.common.bis_base import BiSBaseView
from service.api.views.common.player_base import PlayerBaseView from ffxivbis.api.views.common.player_base import PlayerBaseView
class BiSHtmlView(BiSBaseView, PlayerBaseView): class BiSHtmlView(BiSBaseView, PlayerBaseView):

View File

@ -10,13 +10,13 @@ from aiohttp.web import HTTPFound, Response
from aiohttp_jinja2 import template from aiohttp_jinja2 import template
from typing import Any, Dict, List from typing import Any, Dict, List
from service.models.piece import Piece from ffxivbis.models.piece import Piece
from service.models.player import Player, PlayerId from ffxivbis.models.player import Player, PlayerId
from service.models.upgrade import Upgrade from ffxivbis.models.upgrade import Upgrade
from service.api.utils import wrap_exception, wrap_invalid_param from ffxivbis.api.utils import wrap_exception, wrap_invalid_param
from service.api.views.common.loot_base import LootBaseView from ffxivbis.api.views.common.loot_base import LootBaseView
from service.api.views.common.player_base import PlayerBaseView from ffxivbis.api.views.common.player_base import PlayerBaseView
class LootHtmlView(LootBaseView, PlayerBaseView): class LootHtmlView(LootBaseView, PlayerBaseView):

View File

@ -10,13 +10,13 @@ from aiohttp.web import Response
from aiohttp_jinja2 import template from aiohttp_jinja2 import template
from typing import Any, Dict, List, Union from typing import Any, Dict, List, Union
from service.models.piece import Piece from ffxivbis.models.piece import Piece
from service.models.player import PlayerIdWithCounters from ffxivbis.models.player import PlayerIdWithCounters
from service.models.upgrade import Upgrade from ffxivbis.models.upgrade import Upgrade
from service.api.utils import wrap_invalid_param from ffxivbis.api.utils import wrap_invalid_param
from service.api.views.common.loot_base import LootBaseView from ffxivbis.api.views.common.loot_base import LootBaseView
from service.api.views.common.player_base import PlayerBaseView from ffxivbis.api.views.common.player_base import PlayerBaseView
class LootSuggestHtmlView(LootBaseView, PlayerBaseView): class LootSuggestHtmlView(LootBaseView, PlayerBaseView):

View File

@ -10,11 +10,11 @@ from aiohttp.web import HTTPFound, Response
from aiohttp_jinja2 import template from aiohttp_jinja2 import template
from typing import Any, Dict, List from typing import Any, Dict, List
from service.models.job import Job from ffxivbis.models.job import Job
from service.models.player import PlayerIdWithCounters from ffxivbis.models.player import PlayerIdWithCounters
from service.api.utils import wrap_exception, wrap_invalid_param from ffxivbis.api.utils import wrap_exception, wrap_invalid_param
from service.api.views.common.player_base import PlayerBaseView from ffxivbis.api.views.common.player_base import PlayerBaseView
class PlayerHtmlView(PlayerBaseView): class PlayerHtmlView(PlayerBaseView):

View File

@ -10,10 +10,10 @@ from aiohttp.web import HTTPFound, Response
from aiohttp_jinja2 import template from aiohttp_jinja2 import template
from typing import Any, Dict, List from typing import Any, Dict, List
from service.models.user import User from ffxivbis.models.user import User
from service.api.utils import wrap_exception, wrap_invalid_param from ffxivbis.api.utils import wrap_exception, wrap_invalid_param
from service.api.views.common.login_base import LoginBaseView from ffxivbis.api.views.common.login_base import LoginBaseView
class UsersHtmlView(LoginBaseView): class UsersHtmlView(LoginBaseView):

View File

@ -14,10 +14,9 @@ from aiohttp import web
from aiohttp_security import setup as setup_security from aiohttp_security import setup as setup_security
from aiohttp_security import CookiesIdentityPolicy from aiohttp_security import CookiesIdentityPolicy
from service.core.config import Configuration from ffxivbis.core.config import Configuration
from service.core.database import Database from ffxivbis.core.database import Database
from service.core.loot_selector import LootSelector from ffxivbis.core.party_aggregator import PartyAggregator
from service.core.party import Party
from .auth import AuthorizationPolicy, authorize_factory from .auth import AuthorizationPolicy, authorize_factory
from .routes import setup_routes from .routes import setup_routes
@ -35,7 +34,7 @@ def run_server(app: web.Application) -> None:
port=app['config'].getint('web', 'port'), port=app['config'].getint('web', 'port'),
handle_signals=False) handle_signals=False)
def setup_service(config: Configuration, database: Database, loot: LootSelector, party: Party) -> web.Application: def setup_service(config: Configuration, database: Database, aggregator: PartyAggregator) -> web.Application:
app = web.Application(logger=logging.getLogger('http')) app = web.Application(logger=logging.getLogger('http'))
app.on_shutdown.append(on_shutdown) app.on_shutdown.append(on_shutdown)
@ -62,10 +61,7 @@ def setup_service(config: Configuration, database: Database, loot: LootSelector,
app.logger.info('setup database') app.logger.info('setup database')
app['database'] = database app['database'] = database
app.logger.info('setup loot selector') app.logger.info('setup aggregator')
app['loot'] = loot app['aggregator'] = aggregator
app.logger.info('setup party worker')
app['party'] = party
return app return app

View File

@ -6,7 +6,7 @@
# #
# License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause # License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause
# #
from service.core.config import Configuration from ffxivbis.core.config import Configuration
from .core import Application from .core import Application

View 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)

View File

@ -7,11 +7,12 @@
# License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause # License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause
# #
import os import os
import requests import socket
from aiohttp import ClientSession
from typing import Dict, List, Optional from typing import Dict, List, Optional
from service.models.piece import Piece from ffxivbis.models.piece import Piece
from .config import Configuration from .config import Configuration
@ -22,7 +23,6 @@ class AriyalaParser:
self.ariyala_url = config.get('ariyala', 'ariyala_url') self.ariyala_url = config.get('ariyala', 'ariyala_url')
self.xivapi_key = config.get('ariyala', 'xivapi_key', fallback=None) self.xivapi_key = config.get('ariyala', 'xivapi_key', fallback=None)
self.xivapi_url = config.get('ariyala', 'xivapi_url') self.xivapi_url = config.get('ariyala', 'xivapi_url')
self.request_timeout = config.getfloat('ariyala', 'request_timeout', fallback=30)
def __remap_key(self, key: str) -> Optional[str]: def __remap_key(self, key: str) -> Optional[str]:
if key == 'mainhand': if key == 'mainhand':
@ -37,19 +37,20 @@ class AriyalaParser:
return key return key
return None return None
def get(self, url: str, job: str) -> List[Piece]: async def get(self, url: str, job: str) -> List[Piece]:
items = self.get_ids(url, job) items = await self.get_ids(url, job)
return [ return [
Piece.get({'piece': slot, 'is_tome': self.get_is_tome(item_id)}) # type: ignore Piece.get({'piece': slot, 'is_tome': await self.get_is_tome(item_id)}) # type: ignore
for slot, item_id in items.items() for slot, item_id in items.items()
] ]
def get_ids(self, url: str, job: str) -> Dict[str, int]: async def get_ids(self, url: str, job: str) -> Dict[str, int]:
norm_path = os.path.normpath(url) norm_path = os.path.normpath(url)
set_id = os.path.basename(norm_path) set_id = os.path.basename(norm_path)
response = requests.get(f'{self.ariyala_url}/store.app', params={'identifier': set_id}) async with ClientSession() as session:
response.raise_for_status() async with session.get(f'{self.ariyala_url}/store.app', params={'identifier': set_id}) as response:
data = response.json() 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, # 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 # e.g. http://ffxiv.ariyala.com/store.app?identifier=1AJB8
@ -67,13 +68,16 @@ class AriyalaParser:
result[key] = value result[key] = value
return result return result
def get_is_tome(self, item_id: int) -> bool: async def get_is_tome(self, item_id: int) -> bool:
params = {'columns': 'IsEquippable'} params = {'columns': 'IsEquippable'}
if self.xivapi_key is not None: if self.xivapi_key is not None:
params['private_key'] = self.xivapi_key params['private_key'] = self.xivapi_key
response = requests.get(f'{self.xivapi_url}/item/{item_id}', params=params, timeout=self.request_timeout) async with ClientSession() as session:
response.raise_for_status() # for some reasons ipv6 does not work for me
data = response.json() 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 return data['IsEquippable'] == 0 # don't ask

View File

@ -14,11 +14,11 @@ import logging
from yoyo import get_backend, read_migrations from yoyo import get_backend, read_migrations
from typing import List, Mapping, Optional, Type, Union from typing import List, Mapping, Optional, Type, Union
from service.models.loot import Loot from ffxivbis.models.loot import Loot
from service.models.piece import Piece from ffxivbis.models.piece import Piece
from service.models.player import Player, PlayerId from ffxivbis.models.player import Player, PlayerId
from service.models.upgrade import Upgrade from ffxivbis.models.upgrade import Upgrade
from service.models.user import User from ffxivbis.models.user import User
from .config import Configuration from .config import Configuration
from .exceptions import InvalidDatabase from .exceptions import InvalidDatabase
@ -59,40 +59,43 @@ class Database:
async def init(self) -> None: async def init(self) -> None:
pass pass
async def delete_piece(self, player_id: PlayerId, piece: Union[Piece, Upgrade]) -> None: async def delete_piece(self, party_id: str, player_id: PlayerId, piece: Union[Piece, Upgrade]) -> None:
raise NotImplementedError raise NotImplementedError
async def delete_piece_bis(self, player_id: PlayerId, piece: Piece) -> None: async def delete_piece_bis(self, party_id: str, player_id: PlayerId, piece: Piece) -> None:
raise NotImplementedError raise NotImplementedError
async def delete_player(self, player_id: PlayerId) -> None: async def delete_player(self, party_id: str, player_id: PlayerId) -> None:
raise NotImplementedError raise NotImplementedError
async def delete_user(self, username: str) -> None: async def delete_user(self, party_id: str, username: str) -> None:
raise NotImplementedError raise NotImplementedError
async def get_party(self) -> List[Player]: async def get_party(self, party_id: str) -> List[Player]:
raise NotImplementedError raise NotImplementedError
async def get_player(self, player_id: PlayerId) -> Optional[int]: async def get_player(self, party_id: str, player_id: PlayerId) -> Optional[int]:
raise NotImplementedError raise NotImplementedError
async def get_user(self, username: str) -> Optional[User]: async def get_players(self, party_id: str) -> List[int]:
raise NotImplementedError raise NotImplementedError
async def get_users(self) -> List[User]: async def get_user(self, party_id: str, username: str) -> Optional[User]:
raise NotImplementedError raise NotImplementedError
async def insert_piece(self, player_id: PlayerId, piece: Union[Piece, Upgrade]) -> None: async def get_users(self, party_id: str) -> List[User]:
raise NotImplementedError raise NotImplementedError
async def insert_piece_bis(self, player_id: PlayerId, piece: Piece) -> None: async def insert_piece(self, party_id: str, player_id: PlayerId, piece: Union[Piece, Upgrade]) -> None:
raise NotImplementedError raise NotImplementedError
async def insert_player(self, player: Player) -> None: async def insert_piece_bis(self, party_id: str, player_id: PlayerId, piece: Piece) -> None:
raise NotImplementedError raise NotImplementedError
async def insert_user(self, user: User, hashed_password: bool) -> None: 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 raise NotImplementedError
def migration(self) -> None: def migration(self) -> None:

View File

@ -8,9 +8,9 @@
# #
from typing import Iterable, List, Tuple, Union from typing import Iterable, List, Tuple, Union
from service.models.player import Player, PlayerIdWithCounters from ffxivbis.models.player import Player, PlayerIdWithCounters
from service.models.piece import Piece from ffxivbis.models.piece import Piece
from service.models.upgrade import Upgrade from ffxivbis.models.upgrade import Upgrade
from .party import Party from .party import Party

View File

@ -11,17 +11,18 @@ from __future__ import annotations
from threading import Lock from threading import Lock
from typing import Dict, List, Optional, Type, Union from typing import Dict, List, Optional, Type, Union
from service.models.piece import Piece from ffxivbis.models.piece import Piece
from service.models.player import Player, PlayerId from ffxivbis.models.player import Player, PlayerId
from service.models.upgrade import Upgrade from ffxivbis.models.upgrade import Upgrade
from .database import Database from .database import Database
class Party: class Party:
def __init__(self, database: Database) -> None: def __init__(self, party_id: str, database: Database) -> None:
self.lock = Lock() self.lock = Lock()
self.party_id = party_id
self.players: Dict[PlayerId, Player] = {} self.players: Dict[PlayerId, Player] = {}
self.database = database self.database = database
@ -31,9 +32,9 @@ class Party:
return list(self.players.values()) return list(self.players.values())
@classmethod @classmethod
async def get(cls: Type[Party], database: Database) -> Party: async def get(cls: Type[Party], party_id: str, database: Database) -> Party:
obj = Party(database) obj = cls(party_id, database)
players = await database.get_party() players = await database.get_party(party_id)
for player in players: for player in players:
obj.players[player.player_id] = player obj.players[player.player_id] = player
return obj return obj
@ -42,28 +43,28 @@ class Party:
with self.lock: with self.lock:
player = self.players[player_id] player = self.players[player_id]
player.link = link player.link = link
await self.database.insert_player(player) await self.database.insert_player(self.party_id, player)
async def remove_player(self, player_id: PlayerId) -> Optional[Player]: async def remove_player(self, player_id: PlayerId) -> Optional[Player]:
await self.database.delete_player(player_id) await self.database.delete_player(self.party_id, player_id)
with self.lock: with self.lock:
player = self.players.pop(player_id, None) player = self.players.pop(player_id, None)
return player return player
async def set_player(self, player: Player) -> PlayerId: async def set_player(self, player: Player) -> PlayerId:
player_id = player.player_id player_id = player.player_id
await self.database.insert_player(player) await self.database.insert_player(self.party_id, player)
with self.lock: with self.lock:
self.players[player_id] = player self.players[player_id] = player
return player_id return player_id
async def set_item(self, player_id: PlayerId, piece: Union[Piece, Upgrade]) -> None: async def set_item(self, player_id: PlayerId, piece: Union[Piece, Upgrade]) -> None:
await self.database.insert_piece(player_id, piece) await self.database.insert_piece(self.party_id, player_id, piece)
with self.lock: with self.lock:
self.players[player_id].loot.append(piece) self.players[player_id].loot.append(piece)
async def remove_item(self, player_id: PlayerId, piece: Union[Piece, Upgrade]) -> None: async def remove_item(self, player_id: PlayerId, piece: Union[Piece, Upgrade]) -> None:
await self.database.delete_piece(player_id, piece) await self.database.delete_piece(self.party_id, player_id, piece)
with self.lock: with self.lock:
try: try:
self.players[player_id].loot.remove(piece) self.players[player_id].loot.remove(piece)
@ -71,11 +72,11 @@ class Party:
pass pass
async def set_item_bis(self, player_id: PlayerId, piece: Piece) -> None: async def set_item_bis(self, player_id: PlayerId, piece: Piece) -> None:
await self.database.insert_piece_bis(player_id, piece) await self.database.insert_piece_bis(self.party_id, player_id, piece)
with self.lock: with self.lock:
self.players[player_id].bis.set_item(piece) self.players[player_id].bis.set_item(piece)
async def remove_item_bis(self, player_id: PlayerId, piece: Piece) -> None: async def remove_item_bis(self, player_id: PlayerId, piece: Piece) -> None:
await self.database.delete_piece_bis(player_id, piece) await self.database.delete_piece_bis(self.party_id, player_id, piece)
with self.lock: with self.lock:
self.players[player_id].bis.remove_item(piece) self.players[player_id].bis.remove_item(piece)

View 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)

View File

@ -12,13 +12,13 @@ from passlib.hash import md5_crypt
from psycopg2.extras import DictCursor from psycopg2.extras import DictCursor
from typing import List, Optional, Union from typing import List, Optional, Union
from service.models.bis import BiS from ffxivbis.models.bis import BiS
from service.models.job import Job from ffxivbis.models.job import Job
from service.models.loot import Loot from ffxivbis.models.loot import Loot
from service.models.piece import Piece from ffxivbis.models.piece import Piece
from service.models.player import Player, PlayerId from ffxivbis.models.player import Player, PlayerId
from service.models.upgrade import Upgrade from ffxivbis.models.upgrade import Upgrade
from service.models.user import User from ffxivbis.models.user import User
from .database import Database from .database import Database
@ -42,8 +42,8 @@ class PostgresDatabase(Database):
self.pool = await asyncpg.create_pool(host=self.host, port=self.port, username=self.username, self.pool = await asyncpg.create_pool(host=self.host, port=self.port, username=self.username,
password=self.password, database=self.database) password=self.password, database=self.database)
async def delete_piece(self, player_id: PlayerId, piece: Union[Piece, Upgrade]) -> None: async def delete_piece(self, party_id: str, player_id: PlayerId, piece: Union[Piece, Upgrade]) -> None:
player = await self.get_player(player_id) player = await self.get_player(party_id, player_id)
if player is None: if player is None:
return return
@ -57,8 +57,8 @@ class PostgresDatabase(Database):
player, piece.name, getattr(piece, 'is_tome', True) player, piece.name, getattr(piece, 'is_tome', True)
) )
async def delete_piece_bis(self, player_id: PlayerId, piece: Piece) -> None: async def delete_piece_bis(self, party_id: str, player_id: PlayerId, piece: Piece) -> None:
player = await self.get_player(player_id) player = await self.get_player(party_id, player_id)
if player is None: if player is None:
return return
@ -67,24 +67,29 @@ class PostgresDatabase(Database):
'''delete from bis where player_id = $1 and piece = $2''', '''delete from bis where player_id = $1 and piece = $2''',
player, piece.name) player, piece.name)
async def delete_player(self, player_id: PlayerId) -> None: async def delete_player(self, party_id: str, player_id: PlayerId) -> None:
async with self.pool.acquire() as conn: async with self.pool.acquire() as conn:
await conn.execute('''delete from players where nick = $1 and job = $2''', await conn.execute('''delete from players where nick = $1 and job = $2 and party_id = $3''',
player_id.nick, player_id.job.name) player_id.nick, player_id.job.name, party_id)
async def delete_user(self, username: str) -> None: async def delete_user(self, party_id: str, username: str) -> None:
async with self.pool.acquire() as conn: async with self.pool.acquire() as conn:
await conn.execute('''delete from users where username = $1''', username) 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 def get_party(self) -> List[Player]:
async with self.pool.acquire() as conn: async with self.pool.acquire() as conn:
rows = await conn.fetch('''select * from bis''') 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] bis_pieces = [Loot(row['player_id'], Piece.get(row)) for row in rows]
rows = await conn.fetch('''select * from loot''') 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] loot_pieces = [Loot(row['player_id'], Piece.get(row)) for row in rows]
rows = await conn.fetch('''select * from players''') rows = await conn.fetch('''select * from players where party_id = $1''', party_id)
party = { party = {
row['player_id']: Player(Job[row['job']], row['nick'], BiS(), [], row['bis_link'], row['priority']) row['player_id']: Player(Job[row['job']], row['nick'], BiS(), [], row['bis_link'], row['priority'])
for row in rows for row in rows
@ -92,24 +97,30 @@ class PostgresDatabase(Database):
return self.set_loot(party, bis_pieces, loot_pieces) return self.set_loot(party, bis_pieces, loot_pieces)
async def get_player(self, player_id: PlayerId) -> Optional[int]: async def get_player(self, party_id: str, player_id: PlayerId) -> Optional[int]:
async with self.pool.acquire() as conn: async with self.pool.acquire() as conn:
player = await conn.fetchrow('''select player_id from players where nick = $1 and job = $2''', 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) player_id.nick, player_id.job.name, party_id)
return player['player_id'] if player is not None else None return player['player_id'] if player is not None else None
async def get_user(self, username: str) -> Optional[User]: async def get_players(self, party_id: str) -> List[int]:
async with self.pool.acquire() as conn: async with self.pool.acquire() as conn:
user = await conn.fetchrow('''select * from users where username = $1''', username) 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 return User(user['username'], user['password'], user['permission']) if user is not None else None
async def get_users(self) -> List[User]: async def get_users(self, party_id: str) -> List[User]:
async with self.pool.acquire() as conn: async with self.pool.acquire() as conn:
users = await conn.fetch('''select * from users''') 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] return [User(user['username'], user['password'], user['permission']) for user in users]
async def insert_piece(self, player_id: PlayerId, piece: Union[Piece, Upgrade]) -> None: async def insert_piece(self, party_id: str, player_id: PlayerId, piece: Union[Piece, Upgrade]) -> None:
player = await self.get_player(player_id) player = await self.get_player(party_id, player_id)
if player is None: if player is None:
return return
@ -122,8 +133,8 @@ class PostgresDatabase(Database):
Database.now(), piece.name, getattr(piece, 'is_tome', True), player Database.now(), piece.name, getattr(piece, 'is_tome', True), player
) )
async def insert_piece_bis(self, player_id: PlayerId, piece: Piece) -> None: async def insert_piece_bis(self, party_id: str, player_id: PlayerId, piece: Piece) -> None:
player = await self.get_player(player_id) player = await self.get_player(party_id, player_id)
if player is None: if player is None:
return return
@ -138,27 +149,27 @@ class PostgresDatabase(Database):
Database.now(), piece.name, piece.is_tome, player Database.now(), piece.name, piece.is_tome, player
) )
async def insert_player(self, player: Player) -> None: async def insert_player(self, party_id: str, player: Player) -> None:
async with self.pool.acquire() as conn: async with self.pool.acquire() as conn:
await conn.execute( await conn.execute(
'''insert into players '''insert into players
(created, nick, job, bis_link, priority) (party_id, created, nick, job, bis_link, priority)
values values
($1, $2, $3, $4, $5) ($1, $2, $3, $4, $5, $6)
on conflict on constraint players_nick_job_idx do update set on conflict on constraint players_nick_job_idx do update set
created = $1, bis_link = $4, priority = $5''', created = $1, bis_link = $4, priority = $5''',
Database.now(), player.nick, player.job.name, player.link, player.priority Database.now(), player.nick, player.job.name, player.link, player.priority, party_id
) )
async def insert_user(self, user: User, hashed_password: bool) -> None: 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) password = user.password if hashed_password else md5_crypt.hash(user.password)
async with self.pool.acquire() as conn: async with self.pool.acquire() as conn:
await conn.execute( await conn.execute(
'''insert into users '''insert into users
(username, password, permission) (party_id, username, password, permission)
values values
($1, $2, $3) ($1, $2, $3, $4)
on conflict on constraint users_username_idx do update set on conflict on constraint users_username_idx do update set
password = $2, permission = $3''', password = $2, permission = $3''',
user.username, password, user.permission party_id, user.username, password, user.permission
) )

View File

@ -9,13 +9,13 @@
from passlib.hash import md5_crypt from passlib.hash import md5_crypt
from typing import List, Optional, Union from typing import List, Optional, Union
from service.models.bis import BiS from ffxivbis.models.bis import BiS
from service.models.job import Job from ffxivbis.models.job import Job
from service.models.loot import Loot from ffxivbis.models.loot import Loot
from service.models.piece import Piece from ffxivbis.models.piece import Piece
from service.models.player import Player, PlayerId from ffxivbis.models.player import Player, PlayerId
from service.models.upgrade import Upgrade from ffxivbis.models.upgrade import Upgrade
from service.models.user import User from ffxivbis.models.user import User
from .database import Database from .database import Database
from .sqlite_helper import SQLiteHelper from .sqlite_helper import SQLiteHelper
@ -31,8 +31,8 @@ class SQLiteDatabase(Database):
def connection(self) -> str: def connection(self) -> str:
return f'sqlite:///{self.database_path}' return f'sqlite:///{self.database_path}'
async def delete_piece(self, player_id: PlayerId, piece: Union[Piece, Upgrade]) -> None: async def delete_piece(self, party_id: str, player_id: PlayerId, piece: Union[Piece, Upgrade]) -> None:
player = await self.get_player(player_id) player = await self.get_player(party_id, player_id)
if player is None: if player is None:
return return
@ -45,8 +45,8 @@ class SQLiteDatabase(Database):
)''', )''',
(player, piece.name, getattr(piece, 'is_tome', True))) (player, piece.name, getattr(piece, 'is_tome', True)))
async def delete_piece_bis(self, player_id: PlayerId, piece: Piece) -> None: async def delete_piece_bis(self, party_id: str, player_id: PlayerId, piece: Piece) -> None:
player = await self.get_player(player_id) player = await self.get_player(party_id, player_id)
if player is None: if player is None:
return return
@ -55,26 +55,32 @@ class SQLiteDatabase(Database):
'''delete from bis where player_id = ? and piece = ?''', '''delete from bis where player_id = ? and piece = ?''',
(player, piece.name)) (player, piece.name))
async def delete_player(self, player_id: PlayerId) -> None: async def delete_player(self, party_id: str, player_id: PlayerId) -> None:
async with SQLiteHelper(self.database_path) as cursor: async with SQLiteHelper(self.database_path) as cursor:
await cursor.execute('''delete from players where nick = ? and job = ?''', await cursor.execute('''delete from players where nick = ? and job = ? and party_id = ?''',
(player_id.nick, player_id.job.name)) (player_id.nick, player_id.job.name, party_id))
async def delete_user(self, username: str) -> None: async def delete_user(self, party_id: str, username: str) -> None:
async with SQLiteHelper(self.database_path) as cursor: async with SQLiteHelper(self.database_path) as cursor:
await cursor.execute('''delete from users where username = ?''', (username,)) 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 def get_party(self) -> List[Player]:
async with SQLiteHelper(self.database_path) as cursor: async with SQLiteHelper(self.database_path) as cursor:
await cursor.execute('''select * from bis''') await cursor.execute('''select * from bis where player_id in ({})'''.format(placeholder), players)
rows = await cursor.fetchall() rows = await cursor.fetchall()
bis_pieces = [Loot(row['player_id'], Piece.get(row)) for row in rows] bis_pieces = [Loot(row['player_id'], Piece.get(row)) for row in rows]
await cursor.execute('''select * from loot''') await cursor.execute('''select * from loot where player_id in ({})'''.format(placeholder), players)
rows = await cursor.fetchall() rows = await cursor.fetchall()
loot_pieces = [Loot(row['player_id'], Piece.get(row)) for row in rows] loot_pieces = [Loot(row['player_id'], Piece.get(row)) for row in rows]
await cursor.execute('''select * from players''') await cursor.execute('''select * from players where party_id = ?''', (party_id,))
rows = await cursor.fetchall() rows = await cursor.fetchall()
party = { party = {
row['player_id']: Player(Job[row['job']], row['nick'], BiS(), [], row['bis_link'], row['priority']) row['player_id']: Player(Job[row['job']], row['nick'], BiS(), [], row['bis_link'], row['priority'])
@ -83,27 +89,34 @@ class SQLiteDatabase(Database):
return self.set_loot(party, bis_pieces, loot_pieces) return self.set_loot(party, bis_pieces, loot_pieces)
async def get_player(self, player_id: PlayerId) -> Optional[int]: async def get_player(self, party_id: str, player_id: PlayerId) -> Optional[int]:
async with SQLiteHelper(self.database_path) as cursor: async with SQLiteHelper(self.database_path) as cursor:
await cursor.execute('''select player_id from players where nick = ? and job = ?''', await cursor.execute('''select player_id from players where nick = ? and job = ? and party_id = ?''',
(player_id.nick, player_id.job.name)) (player_id.nick, player_id.job.name, party_id))
player = await cursor.fetchone() player = await cursor.fetchone()
return player['player_id'] if player is not None else None return player['player_id'] if player is not None else None
async def get_user(self, username: str) -> Optional[User]: async def get_players(self, party_id: str) -> List[int]:
async with SQLiteHelper(self.database_path) as cursor: async with SQLiteHelper(self.database_path) as cursor:
await cursor.execute('''select * from users where username = ?''', (username,)) 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() user = await cursor.fetchone()
return User(user['username'], user['password'], user['permission']) if user is not None else None return User(user['username'], user['password'], user['permission']) if user is not None else None
async def get_users(self) -> List[User]: async def get_users(self, party_id: str) -> List[User]:
async with SQLiteHelper(self.database_path) as cursor: async with SQLiteHelper(self.database_path) as cursor:
await cursor.execute('''select * from users''') await cursor.execute('''select * from users where party_id = ?''', (party_id,))
users = await cursor.fetchall() users = await cursor.fetchall()
return [User(user['username'], user['password'], user['permission']) for user in users] return [User(user['username'], user['password'], user['permission']) for user in users]
async def insert_piece(self, player_id: PlayerId, piece: Union[Piece, Upgrade]) -> None: async def insert_piece(self, party_id: str, player_id: PlayerId, piece: Union[Piece, Upgrade]) -> None:
player = await self.get_player(player_id) player = await self.get_player(party_id, player_id)
if player is None: if player is None:
return return
@ -116,8 +129,8 @@ class SQLiteDatabase(Database):
(Database.now(), piece.name, getattr(piece, 'is_tome', True), player) (Database.now(), piece.name, getattr(piece, 'is_tome', True), player)
) )
async def insert_piece_bis(self, player_id: PlayerId, piece: Piece) -> None: async def insert_piece_bis(self, party_id: str, player_id: PlayerId, piece: Piece) -> None:
player = await self.get_player(player_id) player = await self.get_player(party_id, player_id)
if player is None: if player is None:
return return
@ -130,23 +143,23 @@ class SQLiteDatabase(Database):
(Database.now(), piece.name, piece.is_tome, player) (Database.now(), piece.name, piece.is_tome, player)
) )
async def insert_player(self, player: Player) -> None: async def insert_player(self, party_id: str, player: Player) -> None:
async with SQLiteHelper(self.database_path) as cursor: async with SQLiteHelper(self.database_path) as cursor:
await cursor.execute( await cursor.execute(
'''replace into players '''replace into players
(created, nick, job, bis_link, priority) (party_id, created, nick, job, bis_link, priority)
values values
(?, ?, ?, ?, ?)''', (?, ?, ?, ?, ?, ?)''',
(Database.now(), player.nick, player.job.name, player.link, player.priority) (party_id, Database.now(), player.nick, player.job.name, player.link, player.priority)
) )
async def insert_user(self, user: User, hashed_password: bool) -> None: 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) password = user.password if hashed_password else md5_crypt.hash(user.password)
async with SQLiteHelper(self.database_path) as cursor: async with SQLiteHelper(self.database_path) as cursor:
await cursor.execute( await cursor.execute(
'''replace into users '''replace into users
(username, password, permission) (party_id, username, password, permission)
values values
(?, ?, ?)''', (?, ?, ?, ?)''',
(user.username, password, user.permission) (party_id, user.username, password, user.permission)
) )

View File

@ -6,4 +6,4 @@
# #
# License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause # License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause
# #
__version__ = '0.1.0' __version__ = '0.1.1'

View File

@ -11,7 +11,7 @@ from __future__ import annotations
from dataclasses import dataclass from dataclasses import dataclass
from typing import Any, Dict, List, Mapping, Type, Union from typing import Any, Dict, List, Mapping, Type, Union
from service.core.exceptions import InvalidDataRow from ffxivbis.core.exceptions import InvalidDataRow
from .serializable import Serializable from .serializable import Serializable
from .upgrade import Upgrade from .upgrade import Upgrade
@ -46,7 +46,9 @@ class Piece(Serializable):
@classmethod @classmethod
def get(cls: Type[Piece], data: Mapping[str, Any]) -> Union[Piece, Upgrade]: def get(cls: Type[Piece], data: Mapping[str, Any]) -> Union[Piece, Upgrade]:
try: try:
piece_type = data['piece'] 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) is_tome = data['is_tome'] in ('yes', 'on', '1', 1, True)
except KeyError: except KeyError:
raise InvalidDataRow(data) raise InvalidDataRow(data)

View File

@ -1,66 +0,0 @@
#
# Copyright (c) 2019 Evgeniy Alekseev.
#
# This file is part of ffxivbis
# (see https://github.com/arcan1s/ffxivbis).
#
# License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause
#
from aiohttp.web import Application
from .views.api.bis import BiSView
from .views.api.login import LoginView
from .views.api.logout import LogoutView
from .views.api.loot import LootView
from .views.api.player import PlayerView
from .views.html.api import ApiDocVIew, ApiHtmlView
from .views.html.bis import BiSHtmlView
from .views.html.index import IndexHtmlView
from .views.html.loot import LootHtmlView
from .views.html.loot_suggest import LootSuggestHtmlView
from .views.html.player import PlayerHtmlView
from .views.html.static import StaticHtmlView
from .views.html.users import UsersHtmlView
def setup_routes(app: Application) -> None:
# api routes
app.router.add_delete('/admin/api/v1/login/{username}', LoginView)
app.router.add_post('/api/v1/login', LoginView)
app.router.add_post('/api/v1/logout', LogoutView)
app.router.add_put('/admin/api/v1/login', LoginView)
app.router.add_get('/api/v1/party', PlayerView)
app.router.add_post('/api/v1/party', PlayerView)
app.router.add_get('/api/v1/party/bis', BiSView)
app.router.add_post('/api/v1/party/bis', BiSView)
app.router.add_put('/api/v1/party/bis', BiSView)
app.router.add_get('/api/v1/party/loot', LootView)
app.router.add_post('/api/v1/party/loot', LootView)
app.router.add_put('/api/v1/party/loot', LootView)
# html routes
app.router.add_get('/', IndexHtmlView)
app.router.add_get('/static/{resource_id}', StaticHtmlView)
app.router.add_get('/api-docs', ApiHtmlView)
app.router.add_get('/api-docs/swagger.json', ApiDocVIew)
app.router.add_get('/party', PlayerHtmlView)
app.router.add_post('/party', PlayerHtmlView)
app.router.add_get('/bis', BiSHtmlView)
app.router.add_post('/bis', BiSHtmlView)
app.router.add_get('/loot', LootHtmlView)
app.router.add_post('/loot', LootHtmlView)
app.router.add_get('/suggest', LootSuggestHtmlView)
app.router.add_post('/suggest', LootSuggestHtmlView)
app.router.add_get('/admin/users', UsersHtmlView)
app.router.add_post('/admin/users', UsersHtmlView)

View File

@ -1,41 +0,0 @@
#
# Copyright (c) 2019 Evgeniy Alekseev.
#
# This file is part of ffxivbis
# (see https://github.com/arcan1s/ffxivbis).
#
# License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause
#
import asyncio
import logging
from service.api.web import run_server, setup_service
from service.core.config import Configuration
from service.core.database import Database
from service.core.loot_selector import LootSelector
from service.core.party import Party
from service.models.user import User
class Application:
def __init__(self, config: Configuration) -> None:
self.config = config
self.logger = logging.getLogger('application')
def run(self) -> None:
loop = asyncio.get_event_loop()
database = loop.run_until_complete(Database.get(self.config))
database.migration()
party = loop.run_until_complete(Party.get(database))
admin = User(self.config.get('auth', 'root_username'), self.config.get('auth', 'root_password'), 'admin')
loop.run_until_complete(database.insert_user(admin, True))
priority = self.config.get('settings', 'priority').split()
loot_selector = LootSelector(party, priority)
web = setup_service(self.config, database, loot_selector, party)
run_server(web)

View File

@ -4,17 +4,17 @@ import tempfile
from typing import Any, List from typing import Any, List
from service.api.web import setup_service from ffxivbis.api.web import setup_service
from service.core.ariyala_parser import AriyalaParser from ffxivbis.core.ariyala_parser import AriyalaParser
from service.core.config import Configuration from ffxivbis.core.config import Configuration
from service.core.database import Database from ffxivbis.core.database import Database
from service.core.loot_selector import LootSelector from ffxivbis.core.loot_selector import LootSelector
from service.core.party import Party from ffxivbis.core.party import Party
from service.core.sqlite import SQLiteDatabase from ffxivbis.core.sqlite import SQLiteDatabase
from service.models.bis import BiS from ffxivbis.models.bis import BiS
from service.models.job import Job from ffxivbis.models.job import Job
from service.models.piece import Head, Piece, Weapon from ffxivbis.models.piece import Head, Piece, Weapon
from service.models.player import Player from ffxivbis.models.player import Player
@pytest.fixture @pytest.fixture

View File

@ -1,11 +1,11 @@
from typing import List from typing import List
from service.core.ariyala_parser import AriyalaParser from ffxivbis.core.ariyala_parser import AriyalaParser
from service.models.piece import Piece from ffxivbis.models.piece import Piece
from service.models.player import Player from ffxivbis.models.player import Player
def test_get(parser: AriyalaParser, player: Player, bis_link: str, bis_set: List[Piece]) -> None: async def test_get(parser: AriyalaParser, player: Player, bis_link: str, bis_set: List[Piece]) -> None:
items = parser.get(bis_link, player.job.name) items = await parser.get(bis_link, player.job.name)
assert items == bis_set assert items == bis_set

View File

@ -1,6 +1,6 @@
from service.models.bis import BiS from ffxivbis.models.bis import BiS
from service.models.piece import Piece from ffxivbis.models.piece import Piece
from service.models.upgrade import Upgrade from ffxivbis.models.upgrade import Upgrade
def test_set_item(bis: BiS, weapon: Piece) -> None: def test_set_item(bis: BiS, weapon: Piece) -> None:

View File

@ -1,6 +1,6 @@
from service.core.loot_selector import LootSelector from ffxivbis.core.loot_selector import LootSelector
from service.models.piece import Piece from ffxivbis.models.piece import Piece
from service.models.player import Player from ffxivbis.models.player import Player
def test_suggest_by_need(selector: LootSelector, player: Player, player2: Player, head_with_upgrade: Piece) -> None: def test_suggest_by_need(selector: LootSelector, player: Player, player2: Player, head_with_upgrade: Piece) -> None:

View File

@ -1,7 +1,7 @@
from service.core.database import Database from ffxivbis.core.database import Database
from service.core.party import Party from ffxivbis.core.party import Party
from service.models.piece import Piece from ffxivbis.models.piece import Piece
from service.models.player import Player from ffxivbis.models.player import Player
async def test_set_player(party: Party, player: Player) -> None: async def test_set_player(party: Party, player: Player) -> None:

View File

@ -1,4 +1,4 @@
from service.models.piece import Piece from ffxivbis.models.piece import Piece
def test_parse_head(head_with_upgrade: Piece) -> None: def test_parse_head(head_with_upgrade: Piece) -> None:

View File

@ -1,5 +1,5 @@
from service.models.piece import Piece from ffxivbis.models.piece import Piece
from service.models.player import Player from ffxivbis.models.player import Player
def test_loot_count(player: Player, head_with_upgrade: Piece, weapon: Piece) -> None: def test_loot_count(player: Player, head_with_upgrade: Piece, weapon: Piece) -> None:

View File

@ -1,9 +1,9 @@
from typing import Any, List from typing import Any, List
from service.api.utils import make_json from ffxivbis.api.utils import make_json
from service.core.party import Party from ffxivbis.core.party import Party
from service.models.piece import Piece from ffxivbis.models.piece import Piece
from service.models.player import Player from ffxivbis.models.player import Player
async def test_bis_get(server: Any, party: Party, player: Player, player2: Player, async def test_bis_get(server: Any, party: Party, player: Player, player2: Player,
@ -35,7 +35,7 @@ async def test_bis_get_with_filter(server: Any, party: Party, player: Player, pl
async def test_bis_post_add(server: Any, player: Player, head_with_upgrade: Piece) -> None: async def test_bis_post_add(server: Any, player: Player, head_with_upgrade: Piece) -> None:
response = await server.post('/api/v1/party/bis', json={ response = await server.post('/api/v1/party/bis', json={
'action': 'add', 'action': 'add',
'piece': head_with_upgrade.name, 'name': head_with_upgrade.name,
'is_tome': head_with_upgrade.is_tome, 'is_tome': head_with_upgrade.is_tome,
'job': player.job.name, 'job': player.job.name,
'nick': player.nick 'nick': player.nick
@ -47,7 +47,7 @@ async def test_bis_post_add(server: Any, player: Player, head_with_upgrade: Piec
async def test_bis_post_remove(server: Any, player: Player, player2: Player, weapon: Piece) -> None: async def test_bis_post_remove(server: Any, player: Player, player2: Player, weapon: Piece) -> None:
response = await server.post('/api/v1/party/bis', json={ response = await server.post('/api/v1/party/bis', json={
'action': 'remove', 'action': 'remove',
'piece': weapon.name, 'name': weapon.name,
'is_tome': weapon.is_tome, 'is_tome': weapon.is_tome,
'job': player.job.name, 'job': player.job.name,
'nick': player.nick 'nick': player.nick

View File

@ -1,9 +1,9 @@
from typing import Any from typing import Any
from service.api.utils import make_json from ffxivbis.api.utils import make_json
from service.core.party import Party from ffxivbis.core.party import Party
from service.models.piece import Piece from ffxivbis.models.piece import Piece
from service.models.player import Player from ffxivbis.models.player import Player
async def test_loot_get(server: Any, party: Party, player: Player, player2: Player, weapon: Piece) -> None: async def test_loot_get(server: Any, party: Party, player: Player, player2: Player, weapon: Piece) -> None:
@ -36,7 +36,7 @@ async def test_loot_post_add(server: Any, player: Player, weapon: Piece) -> None
response = await server.post('/api/v1/party/loot', json={ response = await server.post('/api/v1/party/loot', json={
'action': 'add', 'action': 'add',
'piece': weapon.name, 'name': weapon.name,
'is_tome': weapon.is_tome, 'is_tome': weapon.is_tome,
'job': player.job.name, 'job': player.job.name,
'nick': player.nick 'nick': player.nick
@ -53,7 +53,7 @@ async def test_loot_post_remove(server: Any, player: Player, head_with_upgrade:
response = await server.post('/api/v1/party/loot', json={ response = await server.post('/api/v1/party/loot', json={
'action': 'remove', 'action': 'remove',
'piece': weapon.name, 'name': weapon.name,
'is_tome': weapon.is_tome, 'is_tome': weapon.is_tome,
'job': player.job.name, 'job': player.job.name,
'nick': player.nick 'nick': player.nick
@ -65,7 +65,7 @@ async def test_loot_post_remove(server: Any, player: Player, head_with_upgrade:
response = await server.post('/api/v1/party/loot', json={ response = await server.post('/api/v1/party/loot', json={
'action': 'remove', 'action': 'remove',
'piece': weapon.name, 'name': weapon.name,
'is_tome': weapon.is_tome, 'is_tome': weapon.is_tome,
'job': player.job.name, 'job': player.job.name,
'nick': player.nick 'nick': player.nick
@ -78,7 +78,7 @@ async def test_loot_post_remove(server: Any, player: Player, head_with_upgrade:
async def test_loot_put(server: Any, player: Player, player2: Player, head_with_upgrade: Piece) -> None: async def test_loot_put(server: Any, player: Player, player2: Player, head_with_upgrade: Piece) -> None:
response = await server.put('/api/v1/party/loot', json={ response = await server.put('/api/v1/party/loot', json={
'is_tome': head_with_upgrade.is_tome, 'is_tome': head_with_upgrade.is_tome,
'piece': head_with_upgrade.name 'name': head_with_upgrade.name
}) })
assert response.status == 200 assert response.status == 200
assert await response.text() == make_json( assert await response.text() == make_json(

View File

@ -1,9 +1,9 @@
from typing import Any, List from typing import Any, List
from service.api.utils import make_json from ffxivbis.api.utils import make_json
from service.core.party import Party from ffxivbis.core.party import Party
from service.models.piece import Piece from ffxivbis.models.piece import Piece
from service.models.player import Player from ffxivbis.models.player import Player
async def test_players_get(server: Any, party: Party, player: Player) -> None: async def test_players_get(server: Any, party: Party, player: Player) -> None: