diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..22c2c75 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,13 @@ + DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE + Version 2, December 2004 + +Copyright (C) 2004 Sam Hocevar + +Everyone is permitted to copy and distribute verbatim or modified +copies of this license document, and changing it is allowed as long +as the name is changed. + + DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. You just DO WHAT THE FUCK YOU WANT TO. diff --git a/changelog.md b/changelog.md index 8f088ff..a1e3f85 100644 --- a/changelog.md +++ b/changelog.md @@ -1,9 +1,114 @@ # Changelog Documenting updates to ARTEMiS, to be updated every time the master branch is pushed to. +## 20240811 +### System ++ Change backend from Twisted to Starlette ++ Implement async handlers ++ Reboot times for multiple games have been fixed (thanks zaphkito!) + +### Frontend ++ Edit button changed to View on the user page, and is where you can edit the card memo ++ Add card now works as it should ++ Add event log viewer in the `sys` page for sysadmins ++ Add pages for Pokken, SAO, and maimai + +### AimeDB ++ Now rejects all-zero access codes ++ Stores card IDm (for AmusementIC) and MiFare ID (for old aime/banapass) + + ...unless that MiFare ID is 0x01020304 (the default for segatools) + +### maimai ++ Add support for BUDDiES ++ Rivals and Favorite Music support + +### Wacca ++ Add option to block unregistered serials from accessing the title server + +### DIVA ++ Fix for reading modded content (Thanks ThatzOkay!) + +### CHUNITHM ++ Save net battle info + +## 20240630 +### DIVA ++ Added configurable festa options' + +## 20240629 +### CHUNITHM ++ Add team points + +## 20240628 +### maimai ++ Add present support + +## 20240627 +### SAO ++ Fix ghost items, character and player XP, EX Bonuses, unlocks, and much much more + +## 20240620 +### CHUNITHM ++ CHUNITHM LUMINOUS support + +## 20240616 +### CHUNITHM ++ Support network encryption for Export/International versions + +### DIVA ++ Working frontend with name and level strings edit and playlog + +## 20240530 +### DIVA ++ Fix reader for when dificulty is not a int + +## 20240526 +### DIVA ++ Fixed missing awaits causing coroutine error + +## 20240524 +### DIVA ++ Fixed new profile start request causing coroutine error + +## 20240523 +### DIVA ++ Fixed binary handler & render_POST errors + +## 20240408 +### System ++ Modified the game specific documentation + +## 20240407 +### Maimai ++ Support maimai DX International [#118](https://gitea.tendokyu.moe/Hay1tsme/artemis/pulls/118) (Thanks beerpsi!) ++ Fixed the maimai DX reboot time from config [#120](https://gitea.tendokyu.moe/Hay1tsme/artemis/pulls/120) (Thanks topty!) + +## 20240318 +### CXB ++ Fixing handle_data_shop_list_detail_request for Sunrise S1 + +## 20240302 +### SAO ++ Fixing new profile creation with right heroes and start VP ++ Fix to the Unanalyzed Log responses returning the wrong rewards ++ Documentation revised + +## 20240226 +### CXB ++ Fixing paths for rev.py ++ Changed encoding for handle_data_item_list_icon_request + +## 20240202 +### SAO ++ Added reader assets and edited the game specific documentation + +## 20240118 +### System ++ Added game version names to the readme + ## 20240109 ### System -+ Removed `ADD config config` from dockerfile ++ Removed `ADD config config` from dockerfile [#83](https://gitea.tendokyu.moe/Hay1tsme/artemis/pulls/83) (Thanks zaphkito!) ### Aimedb + Fixed an error that resulted from trying to scan a banned or locked card diff --git a/contributing.md b/contributing.md index 5397f70..d12425a 100644 --- a/contributing.md +++ b/contributing.md @@ -1,8 +1,182 @@ # Contributing to ARTEMiS -If you would like to contribute to artemis, either by adding features, games, or fixing bugs, you can do so by forking the repo and submitting a pull request [here](https://gitea.tendokyu.moe/Hay1tsme/artemis/pulls). Please make sure, if you're submitting a PR for a game or game version, that you're following the n-0/y-1 guidelines, or it will be rejected. +If you would like to contribute to artemis, either by adding features, games, or fixing bugs, you can do so by forking the repo and submitting a pull request [here](https://gitea.tendokyu.moe/Hay1tsme/artemis/pulls). This guide assume you're familiar with both git, python, and the libraries that artemis uses. + +This document is a work in progress. If you have any questions or notice any errors, please report it to the discord. ## Adding games -Guide WIP +### Step 0 ++ Follow the "n-1" rule of thumb. PRs for game versions that are currently active in arcades will be deleted. If you're unsure, ask! ++ Always PR against the `develop` branch. ++ Check to see if somebody else is already PRing the features/games you want to add. If they are, consider contributing to them rather then making an entirely new PR. ++ We don't technically have a written code style guide (TODO) but try to keep your code consistant with code that's already there where possible. + +### Step 1 (Setup) +1) Fork the gitea repo, clone your fork, and checkout the develop branch. +2) Make a new folder in the `titles` folder, name it some recogniseable shorthand for your game (Chunithm becomes chuni, maimai dx is mai2, etc) +3) In this new folder, create a file named `__init__.py`. This is the first thing that will load when your title module is loaded by the core system, and it acts as sort of a directory for where everything lives in your module. This file will contain the following required items: + + `index`: must point to a subclass of `BaseServlet` that will handle setup and dispatching of your game. + + `game_codes`: must be a list of 4 letter SEGA game codes as strings. + + It can also contain the following optional fields: + + `database`: points to a subclass of `Data` that contains one or more subclasses of `BaseData` that act as database transaction handlers. Required for the class to store and retrieve data from the database. + + `reader`: points to a subclass of `BaseReader` that handles importing static data from game files into the database. + + `frontend`: points to a subclass of `FE_Base` that handles frontend routes for your game. + + The next step will focus on `index` + +### Step 2 (Index) +1) Create another file in your game's folder. By convention, it should be called `index.py`. + +2) Inside `index.py`, add the following code, replacing {Game name here} with the name of your game, without spaces or special characters. Look at other titles for examples. +```py +from core.title import BaseServlet +from core import CoreConfig + +class {Game name here}Servlet(BaseServlet): + def __init__(self, core_cfg: CoreConfig, cfg_dir: str) -> None: + pass +``` +3) The `__init__` function should acomplish the following: + + Reading your game's config + + Setting up your games logger + + Instancing your games versions + + It's usually safe to copy and paste the `__init__` functions from other games, just make sure you change everything that needs to be changed! + +4) Go back to the `__init__.py` that you created and add the following: +```py +from .index import {Game name here}Servlet + +index = {Game name here}Servlet +``` + +5) Going back to `index.py`, within the Servlet class, define the following functions from `BaseServlet` as needed (see function documentation): + + `is_game_enabled`: Returns true if the game is enabled and should be served, false otherwise. Returns false by default, so override this to allow your game to be served. + + `get_routes`: Returns a list of Starlette routes that your game will serve. + + `get_allnet_info`: Returns a tuple of strings where the first is the allnet uri and the second is the allnet host. The function takes the game ID, version and keychip ID as parameters, so you can send different responses if need be. + + `get_mucha_info`: Only used by games that use Mucha as authentication. Returns a tuple where the first is a bool that is weather or not the game is enabled, the 2nd is a list of game CDs as strings that this servlet should handle, and the 3rd is a list of netID prefixes that each game CD should use. If your game does not use mucha, do not define this function. + + `setup`: Preforms any setup your servlet requires, such as spinning up matching servers. It is run once when the server starts. If you don't need any setup, do not define. + +6) Make sure any functions you specify to handle routes in `get_routes` are defined as async, as follows: `async def handle_thing(self, request: Request) -> Response:` where Response is whatever kind of Response class you'll be returning. Make sure all paths in this function return some subclass of Response, otherwise you'll get an error when serving. + +### Step 3 (Constants) +1) In your game's folder, create a file to store static values for your game. By convention, we call this `const.py` + +2) Inside, create a class called `{Game name here}Constants`. Do not define an `__init__` function. + +3) Put constants related to your game here. A good example of something to put here is game codes. +```py +class {Game name here}Constants: + GAME_CODE = "SBXX" + CONFIG_NAME = "{game name}.yaml" +``` + +4) If you choose to put game codes in here, add this to your `__init__.py` file: +```py +from .const import {Game name here}Constants +... +game_codes = [{Game name here}Constants.GAME_CODE] +``` + +### Step 4 (Config) +1) Make a file to store your game's config. By convention, it should be called `config.py` + +2) Inside that file, add the following: +```py +from core.config import CoreConfig + +class {game name}ServerConfig: + def __init__(self, parent_config: "{game name}Config") -> None: + self.__config = parent_config + + @property + def enable(self) -> bool: + return CoreConfig.get_config_field( + self.__config, "{game name}", "server", "enable", default=True + ) + + @property + def loglevel(self) -> int: + return CoreConfig.str_to_loglevel( + CoreConfig.get_config_field( + self.__config, "{game name}", "server", "loglevel", default="info" + ) + ) + +class {game name}Config(dict): + def __init__(self) -> None: + self.server = {game name}ServerConfig(self) +``` + +3) In the `example_config` folder, create a yaml file for your game. By convention, it should be called `{game folder name}.ymal`. Add the following: +```yaml +server: + enable: True + loglevel: "info" +``` + +4) Add any additional config options that you feel the game needs. Look to other games for config examples. + +5) In `index.py` import your config and instance it in `__init__` with: +```py +self.game_cfg = {game folder name}Config() +if path.exists(f"{cfg_dir}/{game folder name}Constants.CONFIG_NAME}"): + self.game_cfg.update( + yaml.safe_load(open(f"{cfg_dir}/{game folder name}Constants.CONFIG_NAME}")) + ) +``` +This will attempt to load the config file you specified in your constants, and if not, go with the defaults specified in `config.py`. This game_cfg object can then be passed down to your handlers when you create them. + +At this stage your game should be loaded by allnet, and serve whatever routes you put in `get_routes`. See the next section about adding versions and handlers. + +### Step 5 (Database) +TODO + +### Step 6 (Frontend) +TODO + +### Step 7 (Reader) +TODO ## Adding game versions -Guide WIP \ No newline at end of file +See the above section about code expectations and how to PR. +1) In the game's folder, create a python file to contain the version handlers. By convention, the first version is version 0, and is stored in `base.py`. Versions following that increment the version number, and are stored in `{short version name}.py`. See Wacca's folder for an example of how to name versions. + +2) Internal version numbers should be defined in `const.py`. The version should change any time the game gets a major update (i.e. a new version or plus version.) +```py +# in const.py +VERSION_{game name} = 0 +VERSION_{game name}_PLUS = 1 +``` + +3) Inside `base.py` (or whatever your version is named) add the following: +```py +class {game name}Base: + def __init__(self, cfg: CoreConfig, game_cfg: {game name}Config) -> None: + self.game_config = game_cfg + self.core_config = cfg + self.version = {game name}Constants.VERSION_{game name} + self.data = {game name}Data(cfg) + # Any other initialization stuff +``` + +4) Define your handlers. This will vary wildly by game, but best practice is to keep the naming consistant, so that the main dispatch function in `index.py` can use `getattr` to get the handler, rather then having a static list of what endpoint or request type goes to which handler. See Wacca's `index.py` and `base.py` for examples of how to do this. + +5) If your version is not the base version, make sure it inherits from the base version: +```py +class {game name}Plus({game name}Base): + def __init__(self, cfg: CoreConfig, game_cfg: {game name}Config) -> None: + super().__init__(cfg, game_cfg) + self.version = {game name}Constants.VERSION_{game name}_PLUS +``` + +6) Back in `index.py` make sure to import your new class, and add it to `__init__`. Some games may opt to just a single list called `self.versions` that contains all the version classes at their internal version's index. Others may simply define them as seperate members. See Wacca for an example of `self.versions` + +7) Add your version to your game's dispatching logic. + +8) Test to make sure your game is being handled properly. + +9) Submit a PR. + +## Adding/improving core services +If you intend to submit improvements or additions to core services (allnet, mucha, billing, aimedb, database, etc) please get in touch with a maintainer. diff --git a/core/__init__.py b/core/__init__.py index 185d9bc..f5e306e 100644 --- a/core/__init__.py +++ b/core/__init__.py @@ -1,6 +1,6 @@ from core.config import CoreConfig -from core.allnet import AllnetServlet -from core.aimedb import AimedbFactory +from core.allnet import AllnetServlet, BillingServlet +from core.aimedb import AimedbServlette from core.title import TitleServlet from core.utils import Utils from core.mucha import MuchaServlet diff --git a/core/adb_handlers/__init__.py b/core/adb_handlers/__init__.py index 0c96baf..9a8121f 100644 --- a/core/adb_handlers/__init__.py +++ b/core/adb_handlers/__init__.py @@ -2,5 +2,5 @@ from .base import ADBBaseRequest, ADBBaseResponse, ADBHeader, ADBHeaderException from .base import CompanyCodes, ReaderFwVer, CMD_CODE_GOODBYE, HEADER_SIZE from .lookup import ADBLookupRequest, ADBLookupResponse, ADBLookupExResponse from .campaign import ADBCampaignClearRequest, ADBCampaignClearResponse, ADBCampaignResponse, ADBOldCampaignRequest, ADBOldCampaignResponse -from .felica import ADBFelicaLookupRequest, ADBFelicaLookupResponse, ADBFelicaLookup2Request, ADBFelicaLookup2Response +from .felica import ADBFelicaLookupRequest, ADBFelicaLookupResponse, ADBFelicaLookupExRequest, ADBFelicaLookupExResponse from .log import ADBLogExRequest, ADBLogRequest, ADBStatusLogRequest, ADBLogExResponse diff --git a/core/adb_handlers/base.py b/core/adb_handlers/base.py index 0f208dd..06b5267 100644 --- a/core/adb_handlers/base.py +++ b/core/adb_handlers/base.py @@ -102,7 +102,7 @@ class ADBHeader: magic, protocol_ver, cmd, length, status, game_id, store_id, keychip_id = struct.unpack_from("<5H6sI12s", data) head = cls(magic, protocol_ver, cmd, length, status, game_id, store_id, keychip_id) - if head.length != len(data): + if head.length > len(data): raise ADBHeaderException(f"Length is incorrect! Expect {head.length}, got {len(data)}") return head diff --git a/core/adb_handlers/felica.py b/core/adb_handlers/felica.py index 479e84d..b7fdc2e 100644 --- a/core/adb_handlers/felica.py +++ b/core/adb_handlers/felica.py @@ -10,13 +10,14 @@ class ADBFelicaLookupRequest(ADBBaseRequest): self.pmm = hex(pmm)[2:].upper() class ADBFelicaLookupResponse(ADBBaseResponse): - def __init__(self, access_code: str = None, game_id: str = "SXXX", store_id: int = 1, keychip_id: str = "A69E01A8888", code: int = 0x03, length: int = 0x30, status: int = 1) -> None: + def __init__(self, access_code: str = None, idx: int = 0, game_id: str = "SXXX", store_id: int = 1, keychip_id: str = "A69E01A8888", code: int = 0x03, length: int = 0x30, status: int = 1) -> None: super().__init__(code, length, status, game_id, store_id, keychip_id) self.access_code = access_code if access_code is not None else "00000000000000000000" + self.idx = idx @classmethod - def from_req(cls, req: ADBHeader, access_code: str = None) -> "ADBFelicaLookupResponse": - c = cls(access_code, req.game_id, req.store_id, req.keychip_id) + def from_req(cls, req: ADBHeader, access_code: str = None, idx: int = 0) -> "ADBFelicaLookupResponse": + c = cls(access_code, idx, req.game_id, req.store_id, req.keychip_id) c.head.protocol_ver = req.protocol_ver return c @@ -26,7 +27,7 @@ class ADBFelicaLookupResponse(ADBBaseResponse): "access_code" / Int8ub[10], Padding(2) ).build(dict( - felica_idx = 0, + felica_idx = self.idx, access_code = bytes.fromhex(self.access_code) )) @@ -34,7 +35,7 @@ class ADBFelicaLookupResponse(ADBBaseResponse): return self.head.make() + resp_struct -class ADBFelicaLookup2Request(ADBBaseRequest): +class ADBFelicaLookupExRequest(ADBBaseRequest): def __init__(self, data: bytes) -> None: super().__init__(data) self.random = struct.unpack_from("<16s", data, 0x20)[0] @@ -45,7 +46,7 @@ class ADBFelicaLookup2Request(ADBBaseRequest): self.company = CompanyCodes(int.from_bytes(company, 'little')) self.fw_ver = ReaderFwVer.from_byte(fw_ver) -class ADBFelicaLookup2Response(ADBBaseResponse): +class ADBFelicaLookupExResponse(ADBBaseResponse): def __init__(self, user_id: Union[int, None] = None, access_code: Union[str, None] = None, game_id: str = "SXXX", store_id: int = 1, keychip_id: str = "A69E01A8888", code: int = 0x12, length: int = 0x130, status: int = 1) -> None: super().__init__(code, length, status, game_id, store_id, keychip_id) self.user_id = user_id if user_id is not None else -1 @@ -55,7 +56,7 @@ class ADBFelicaLookup2Response(ADBBaseResponse): self.auth_key = [0] * 256 @classmethod - def from_req(cls, req: ADBHeader, user_id: Union[int, None] = None, access_code: Union[str, None] = None) -> "ADBFelicaLookup2Response": + def from_req(cls, req: ADBHeader, user_id: Union[int, None] = None, access_code: Union[str, None] = None) -> "ADBFelicaLookupExResponse": c = cls(user_id, access_code, req.game_id, req.store_id, req.keychip_id) c.head.protocol_ver = req.protocol_ver return c diff --git a/core/aimedb.py b/core/aimedb.py index 6d6615a..6d5bd57 100644 --- a/core/aimedb.py +++ b/core/aimedb.py @@ -1,9 +1,7 @@ -from twisted.internet.protocol import Factory, Protocol import logging, coloredlogs from Crypto.Cipher import AES -import struct -from typing import Dict, Tuple, Callable, Union -from typing_extensions import Final +from typing import Dict, Tuple, Callable, Union, Optional +import asyncio from logging.handlers import TimedRotatingFileHandler from core.config import CoreConfig @@ -11,15 +9,37 @@ from core.utils import create_sega_auth_key from core.data import Data from .adb_handlers import * - -class AimedbProtocol(Protocol): +class AimedbServlette(): request_list: Dict[int, Tuple[Callable[[bytes, int], Union[ADBBaseResponse, bytes]], int, str]] = {} - - def __init__(self, core_cfg: CoreConfig) -> None: - self.logger = logging.getLogger("aimedb") - self.config = core_cfg + def __init__(self, core_cfg: CoreConfig) -> None: + self.config = core_cfg self.data = Data(core_cfg) - if core_cfg.aimedb.key == "": + + self.logger = logging.getLogger("aimedb") + if not hasattr(self.logger, "initted"): + log_fmt_str = "[%(asctime)s] Aimedb | %(levelname)s | %(message)s" + log_fmt = logging.Formatter(log_fmt_str) + + fileHandler = TimedRotatingFileHandler( + "{0}/{1}.log".format(self.config.server.log_dir, "aimedb"), + when="d", + backupCount=10, + ) + fileHandler.setFormatter(log_fmt) + + consoleHandler = logging.StreamHandler() + consoleHandler.setFormatter(log_fmt) + + self.logger.addHandler(fileHandler) + self.logger.addHandler(consoleHandler) + + self.logger.setLevel(self.config.aimedb.loglevel) + coloredlogs.install( + level=core_cfg.aimedb.loglevel, logger=self.logger, fmt=log_fmt_str + ) + self.logger.initted = True + + if not core_cfg.aimedb.key: self.logger.error("!!!KEY NOT SET!!!") exit(1) @@ -40,27 +60,31 @@ class AimedbProtocol(Protocol): self.register_handler(0x13, 0x14, self.handle_log_ex, 'aime_log_ex') self.register_handler(0x64, 0x65, self.handle_hello, 'hello') - self.register_handler(0x66, 0, self.handle_goodbye, 'goodbye') - + def register_handler(self, cmd: int, resp:int, handler: Callable[[bytes, int], Union[ADBBaseResponse, bytes]], name: str) -> None: self.request_list[cmd] = (handler, resp, name) + + def start(self) -> None: + self.logger.info(f"Start on port {self.config.aimedb.port}") + addr = self.config.aimedb.listen_address if self.config.aimedb.listen_address else self.config.server.listen_address + asyncio.create_task(asyncio.start_server(self.dataReceived, addr, self.config.aimedb.port)) + + async def dataReceived(self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter): + self.logger.debug(f"Connection made from {writer.get_extra_info('peername')[0]}") + while True: + try: + data: bytes = await reader.read(4096) + if len(data) == 0: + self.logger.debug("Connection closed") + return + await self.process_data(data, reader, writer) + await writer.drain() + except ConnectionResetError as e: + self.logger.debug("Connection reset, disconnecting") + return - def append_padding(self, data: bytes): - """Appends 0s to the end of the data until it's at the correct size""" - length = struct.unpack_from(" None: - self.logger.debug(f"{self.transport.getPeer().host} Connected") - - def connectionLost(self, reason) -> None: - self.logger.debug( - f"{self.transport.getPeer().host} Disconnected - {reason.value}" - ) - - def dataReceived(self, data: bytes) -> None: + async def process_data(self, data: bytes, reader: asyncio.StreamReader, writer: asyncio.StreamWriter) -> Optional[bytes]: + addr = writer.get_extra_info('peername')[0] cipher = AES.new(self.config.aimedb.key.encode(), AES.MODE_ECB) try: @@ -68,9 +92,9 @@ class AimedbProtocol(Protocol): except Exception as e: self.logger.error(f"Failed to decrypt {data.hex()} because {e}") - return None + return - self.logger.debug(f"{self.transport.getPeer().host} wrote {decrypted.hex()}") + self.logger.debug(f"{addr} wrote {decrypted.hex()}") try: head = ADBHeader.from_data(decrypted) @@ -79,7 +103,9 @@ class AimedbProtocol(Protocol): self.logger.error(f"Error parsing ADB header: {e}") try: encrypted = cipher.encrypt(ADBBaseResponse().make()) - self.transport.write(encrypted) + writer.write(encrypted) + await writer.drain() + return except Exception as e: self.logger.error(f"Failed to encrypt default response because {e}") @@ -89,46 +115,51 @@ class AimedbProtocol(Protocol): if head.keychip_id == "ABCD1234567" or head.store_id == 0xfff0: self.logger.warning(f"Request from uninitialized AMLib: {vars(head)}") + if head.cmd == 0x66: + self.logger.info("Goodbye") + writer.close() + return + handler, resp_code, name = self.request_list.get(head.cmd, (self.handle_default, None, 'default')) if resp_code is None: self.logger.warning(f"No handler for cmd {hex(head.cmd)}") elif resp_code > 0: - self.logger.info(f"{name} from {head.keychip_id} ({head.game_id}) @ {self.transport.getPeer().host}") + self.logger.info(f"{name} from {head.keychip_id} ({head.game_id}) @ {addr}") - resp = handler(decrypted, resp_code) + resp = await handler(decrypted, resp_code) if type(resp) == ADBBaseResponse or issubclass(type(resp), ADBBaseResponse): resp_bytes = resp.make() - if len(resp_bytes) != resp.head.length: - resp_bytes = self.append_padding(resp_bytes) elif type(resp) == bytes: resp_bytes = resp elif resp is None: # Nothing to send, probably a goodbye + self.logger.warn(f"None return by handler for {name}") return else: + self.logger.error(f"Unsupported type returned by ADB handler for {name}: {type(resp)}") raise TypeError(f"Unsupported type returned by ADB handler for {name}: {type(resp)}") - try: + try: encrypted = cipher.encrypt(resp_bytes) self.logger.debug(f"Response {resp_bytes.hex()}") - self.transport.write(encrypted) + writer.write(encrypted) except Exception as e: self.logger.error(f"Failed to encrypt {resp_bytes.hex()} because {e}") - - def handle_default(self, data: bytes, resp_code: int, length: int = 0x20) -> ADBBaseResponse: + + async def handle_default(self, data: bytes, resp_code: int, length: int = 0x20) -> ADBBaseResponse: req = ADBHeader.from_data(data) return ADBBaseResponse(resp_code, length, 1, req.game_id, req.store_id, req.keychip_id, req.protocol_ver) - def handle_hello(self, data: bytes, resp_code: int) -> ADBBaseResponse: - return self.handle_default(data, resp_code) + async def handle_hello(self, data: bytes, resp_code: int) -> ADBBaseResponse: + return await self.handle_default(data, resp_code) - def handle_campaign(self, data: bytes, resp_code: int) -> ADBBaseResponse: + async def handle_campaign(self, data: bytes, resp_code: int) -> ADBBaseResponse: h = ADBHeader.from_data(data) if h.protocol_ver >= 0x3030: req = h @@ -143,12 +174,18 @@ class AimedbProtocol(Protocol): # We don't currently support campaigns return resp - def handle_lookup(self, data: bytes, resp_code: int) -> ADBBaseResponse: + async def handle_lookup(self, data: bytes, resp_code: int) -> ADBBaseResponse: req = ADBLookupRequest(data) - user_id = self.data.card.get_user_id_from_card(req.access_code) - is_banned = self.data.card.get_card_banned(req.access_code) - is_locked = self.data.card.get_card_locked(req.access_code) - + if req.access_code == "00000000000000000000": + self.logger.warn(f"All-zero access code from {req.head.keychip_id}") + ret = ADBLookupResponse.from_req(req.head, -1) + ret.head.status = ADBStatus.BAN_SYS + return ret + + user_id = await self.data.card.get_user_id_from_card(req.access_code) + is_banned = await self.data.card.get_card_banned(req.access_code) + is_locked = await self.data.card.get_card_locked(req.access_code) + ret = ADBLookupResponse.from_req(req.head, user_id) if is_banned and is_locked: ret.head.status = ADBStatus.BAN_SYS_USER @@ -160,14 +197,26 @@ class AimedbProtocol(Protocol): self.logger.info( f"access_code {req.access_code} -> user_id {ret.user_id}" ) + + if user_id and user_id > 0: + await self.data.card.update_card_last_login(req.access_code) + if (req.access_code.startswith("010") or req.access_code.startswith("3")) and req.serial_number != 0x04030201: # Default segatools sn + await self.data.card.set_chip_id_by_access_code(req.access_code, req.serial_number) + self.logger.info(f"Attempt to set chip id to {req.serial_number:08X} for access code {req.access_code}") return ret - def handle_lookup_ex(self, data: bytes, resp_code: int) -> ADBBaseResponse: + async def handle_lookup_ex(self, data: bytes, resp_code: int) -> ADBBaseResponse: req = ADBLookupRequest(data) - user_id = self.data.card.get_user_id_from_card(req.access_code) + if req.access_code == "00000000000000000000": + self.logger.warn(f"All-zero access code from {req.head.keychip_id}") + ret = ADBLookupExResponse.from_req(req.head, -1) + ret.head.status = ADBStatus.BAN_SYS + return ret + + user_id = await self.data.card.get_user_id_from_card(req.access_code) - is_banned = self.data.card.get_card_banned(req.access_code) - is_locked = self.data.card.get_card_locked(req.access_code) + is_banned = await self.data.card.get_card_banned(req.access_code) + is_locked = await self.data.card.get_card_locked(req.access_code) ret = ADBLookupExResponse.from_req(req.head, user_id) if is_banned and is_locked: @@ -189,40 +238,67 @@ class AimedbProtocol(Protocol): self.logger.debug(f"Generated auth token {auth_key}") ret.auth_key = auth_key_full + if user_id and user_id > 0: + await self.data.card.update_card_last_login(req.access_code) return ret - def handle_felica_lookup(self, data: bytes, resp_code: int) -> bytes: + async def handle_felica_lookup(self, data: bytes, resp_code: int) -> bytes: """ - On official, I think a card has to be registered for this to actually work, but - I'm making the executive decision to not implement that and just kick back our - faux generated access code. The real felica IDm -> access code conversion is done - on the ADB server, which we do not and will not ever have access to. Because we can - assure that all IDms will be unique, this basic 0-padded hex -> int conversion will - be fine. + On official, the IDm is used as a key to look up the stored access code in a large + database. We do not have access to that database so we have to make due with what we got. + Interestingly, namco games are able to read S_PAD0 and send the server the correct access + code, but aimedb doesn't. Until somebody either enters the correct code manually, or scans + on a game that reads it correctly from the card, this will have to do. It's the same conversion + used on the big boy networks. """ req = ADBFelicaLookupRequest(data) - ac = self.data.card.to_access_code(req.idm) + idm = req.idm.zfill(16) + if idm == "0000000000000000": + self.logger.warn(f"All-zero IDm from {req.head.keychip_id}") + ret = ADBFelicaLookupResponse.from_req(req.head, "00000000000000000000") + ret.head.status = ADBStatus.BAN_SYS + return ret + + card = await self.data.card.get_card_by_idm(idm) + if not card: + ac = self.data.card.to_access_code(idm) + test = await self.data.card.get_card_by_access_code(ac) + if test: + await self.data.card.set_idm_by_access_code(ac, idm) + + else: + ac = card['access_code'] + self.logger.info( - f"idm {req.idm} ipm {req.pmm} -> access_code {ac}" + f"idm {idm} ipm {req.pmm.zfill(16)} -> access_code {ac}" ) return ADBFelicaLookupResponse.from_req(req.head, ac) - def handle_felica_register(self, data: bytes, resp_code: int) -> bytes: + async def handle_felica_register(self, data: bytes, resp_code: int) -> bytes: """ - I've never seen this used. + Used to register felica moble access codes. Will never be used on our network + because we don't implement felica_lookup properly. """ req = ADBFelicaLookupRequest(data) + idm = req.idm.zfill(16) + + if idm == "0000000000000000": + self.logger.warn(f"All-zero IDm from {req.head.keychip_id}") + ret = ADBFelicaLookupResponse.from_req(req.head, "00000000000000000000") + ret.head.status = ADBStatus.BAN_SYS + return ret + ac = self.data.card.to_access_code(req.idm) if self.config.server.allow_user_registration: - user_id = self.data.user.create_user() + user_id = await self.data.user.create_user() if user_id is None: self.logger.error("Failed to register user!") user_id = -1 else: - card_id = self.data.card.create_card(user_id, ac) + card_id = await self.data.card.create_card(user_id, ac) if card_id is None: self.logger.error("Failed to register card!") @@ -237,21 +313,49 @@ class AimedbProtocol(Protocol): f"Registration blocked!: access code {ac} (IDm: {req.idm} PMm: {req.pmm})" ) + if user_id > 0: + await self.data.card.update_card_last_login(ac) return ADBFelicaLookupResponse.from_req(req.head, ac) - def handle_felica_lookup_ex(self, data: bytes, resp_code: int) -> bytes: - req = ADBFelicaLookup2Request(data) - access_code = self.data.card.to_access_code(req.idm) - user_id = self.data.card.get_user_id_from_card(access_code=access_code) + async def handle_felica_lookup_ex(self, data: bytes, resp_code: int) -> bytes: + req = ADBFelicaLookupExRequest(data) + user_id = None + idm = req.idm.zfill(16) + + if idm == "0000000000000000": + self.logger.warn(f"All-zero IDm from {req.head.keychip_id}") + ret = ADBFelicaLookupExResponse.from_req(req.head, -1, "00000000000000000000") + ret.head.status = ADBStatus.BAN_SYS + return ret + + card = await self.data.card.get_card_by_idm(idm) + if not card: + access_code = self.data.card.to_access_code(idm) + card = await self.data.card.get_card_by_access_code(access_code) + if card: + user_id = card['user'] + await self.data.card.set_idm_by_access_code(access_code, idm) + + else: + user_id = card['user'] + access_code = card['access_code'] if user_id is None: user_id = -1 self.logger.info( - f"idm {req.idm} ipm {req.pmm} -> access_code {access_code} user_id {user_id}" + f"idm {idm} ipm {req.pmm} -> access_code {access_code} user_id {user_id}" ) - resp = ADBFelicaLookup2Response.from_req(req.head, user_id, access_code) + resp = ADBFelicaLookupExResponse.from_req(req.head, user_id, access_code) + + if user_id > 0: + if card['is_banned'] and card['is_locked']: + resp.head.status = ADBStatus.BAN_SYS_USER + elif card['is_banned']: + resp.head.status = ADBStatus.BAN_SYS + elif card['is_locked']: + resp.head.status = ADBStatus.LOCK_USER if user_id and user_id > 0 and self.config.aimedb.id_secret: auth_key = create_sega_auth_key(user_id, req.head.game_id, req.head.store_id, req.head.keychip_id, self.config.aimedb.id_secret, self.config.aimedb.id_lifetime_seconds) @@ -260,10 +364,12 @@ class AimedbProtocol(Protocol): auth_key_full = auth_key.encode() + (b"\0" * auth_key_extra_len) self.logger.debug(f"Generated auth token {auth_key}") resp.auth_key = auth_key_full - + + if user_id and user_id > 0: + await self.data.card.update_card_last_login(access_code) return resp - def handle_campaign_clear(self, data: bytes, resp_code: int) -> ADBBaseResponse: + async def handle_campaign_clear(self, data: bytes, resp_code: int) -> ADBBaseResponse: req = ADBCampaignClearRequest(data) resp = ADBCampaignClearResponse.from_req(req.head) @@ -271,19 +377,25 @@ class AimedbProtocol(Protocol): # We don't support campaign stuff return resp - def handle_register(self, data: bytes, resp_code: int) -> bytes: + async def handle_register(self, data: bytes, resp_code: int) -> bytes: req = ADBLookupRequest(data) user_id = -1 + + if req.access_code == "00000000000000000000": + self.logger.warn(f"All-zero access code from {req.head.keychip_id}") + ret = ADBLookupResponse.from_req(req.head, -1) + ret.head.status = ADBStatus.BAN_SYS + return ret if self.config.server.allow_user_registration: - user_id = self.data.user.create_user() + user_id = await self.data.user.create_user() if user_id is None: self.logger.error("Failed to register user!") user_id = -1 else: - card_id = self.data.card.create_card(user_id, req.access_code) + card_id = await self.data.card.create_card(user_id, req.access_code) if card_id is None: self.logger.error("Failed to register card!") @@ -297,25 +409,38 @@ class AimedbProtocol(Protocol): self.logger.info( f"Registration blocked!: access code {req.access_code}" ) + + if user_id > 0: + if (req.access_code.startswith("010") or req.access_code.startswith("3")) and req.serial_number != 0x04030201: # Default segatools sn: + await self.data.card.set_chip_id_by_access_code(req.access_code, req.serial_number) + self.logger.info(f"Attempt to set chip id to {req.serial_number} for access code {req.access_code}") + + elif req.access_code.startswith("0008"): + idm = self.data.card.to_idm(req.access_code) + await self.data.card.set_idm_by_access_code(req.access_code, idm) + self.logger.info(f"Attempt to set IDm to {idm} for access code {req.access_code}") resp = ADBLookupResponse.from_req(req.head, user_id) if resp.user_id <= 0: resp.head.status = ADBStatus.BAN_SYS # Closest we can get to a "You cannot register" + else: + await self.data.card.update_card_last_login(req.access_code) + return resp # TODO: Save these in some capacity, as deemed relevant - def handle_status_log(self, data: bytes, resp_code: int) -> bytes: + async def handle_status_log(self, data: bytes, resp_code: int) -> bytes: req = ADBStatusLogRequest(data) self.logger.info(f"User {req.aime_id} logged {req.status.name} event") return ADBBaseResponse(resp_code, 0x20, 1, req.head.game_id, req.head.store_id, req.head.keychip_id, req.head.protocol_ver) - def handle_log(self, data: bytes, resp_code: int) -> bytes: + async def handle_log(self, data: bytes, resp_code: int) -> bytes: req = ADBLogRequest(data) self.logger.info(f"User {req.aime_id} logged {req.status.name} event, credit_ct: {req.credit_ct} bet_ct: {req.bet_ct} won_ct: {req.won_ct}") return ADBBaseResponse(resp_code, 0x20, 1, req.head.game_id, req.head.store_id, req.head.keychip_id, req.head.protocol_ver) - def handle_log_ex(self, data: bytes, resp_code: int) -> bytes: + async def handle_log_ex(self, data: bytes, resp_code: int) -> bytes: req = ADBLogExRequest(data) strs = [] self.logger.info(f"Recieved {req.num_logs} or {len(req.logs)} logs") @@ -324,43 +449,3 @@ class AimedbProtocol(Protocol): self.logger.debug(f"User {req.logs[x].aime_id} logged {req.logs[x].status.name} event, credit_ct: {req.logs[x].credit_ct} bet_ct: {req.logs[x].bet_ct} won_ct: {req.logs[x].won_ct}") return ADBLogExResponse.from_req(req.head) - def handle_goodbye(self, data: bytes, resp_code: int) -> None: - self.logger.info(f"goodbye from {self.transport.getPeer().host}") - self.transport.loseConnection() - return - -class AimedbFactory(Factory): - protocol = AimedbProtocol - - def __init__(self, cfg: CoreConfig) -> None: - self.config = cfg - log_fmt_str = "[%(asctime)s] Aimedb | %(levelname)s | %(message)s" - log_fmt = logging.Formatter(log_fmt_str) - self.logger = logging.getLogger("aimedb") - - fileHandler = TimedRotatingFileHandler( - "{0}/{1}.log".format(self.config.server.log_dir, "aimedb"), - when="d", - backupCount=10, - ) - fileHandler.setFormatter(log_fmt) - - consoleHandler = logging.StreamHandler() - consoleHandler.setFormatter(log_fmt) - - self.logger.addHandler(fileHandler) - self.logger.addHandler(consoleHandler) - - self.logger.setLevel(self.config.aimedb.loglevel) - coloredlogs.install( - level=cfg.aimedb.loglevel, logger=self.logger, fmt=log_fmt_str - ) - - if self.config.aimedb.key == "": - self.logger.error("Please set 'key' field in your config file.") - exit(1) - - self.logger.info(f"Ready on port {self.config.aimedb.port}") - - def buildProtocol(self, addr): - return AimedbProtocol(self.config) diff --git a/core/allnet.py b/core/allnet.py index e83aae0..9eb6595 100644 --- a/core/allnet.py +++ b/core/allnet.py @@ -1,20 +1,24 @@ -from typing import Dict, List, Any, Optional, Tuple, Union, Final -import logging, coloredlogs -from logging.handlers import TimedRotatingFileHandler -from twisted.web.http import Request -from datetime import datetime import pytz import base64 import zlib import json +import yaml +import logging +import coloredlogs +import urllib.parse +import math +from typing import Dict, List, Any, Optional, Union, Final +from logging.handlers import TimedRotatingFileHandler +from starlette.requests import Request +from starlette.responses import PlainTextResponse +from starlette.applications import Starlette +from starlette.routing import Route +from datetime import datetime from enum import Enum from Crypto.PublicKey import RSA from Crypto.Hash import SHA from Crypto.Signature import PKCS1_v1_5 -from time import strptime -from os import path -import urllib.parse -import math +from os import path, environ, mkdir, access, W_OK from .config import CoreConfig from .utils import Utils @@ -90,8 +94,8 @@ class DLI_STATUS(Enum): return cls.UNKNOWN class AllnetServlet: + allnet_registry: Dict[str, Any] = {} def __init__(self, core_cfg: CoreConfig, cfg_folder: str): - super().__init__() self.config = core_cfg self.config_folder = cfg_folder self.data = Data(core_cfg) @@ -120,25 +124,22 @@ class AllnetServlet: ) self.logger.initialized = True - plugins = Utils.get_all_titles() + def startup(self) -> None: + self.logger.info(f"Ready on port {self.config.allnet.port if self.config.allnet.standalone else self.config.server.port}") + if not TitleServlet.title_registry: + TitleServlet(self.config, self.config_folder) - if len(plugins) == 0: - self.logger.error("No games detected!") - - self.logger.info( - f"Serving {len(TitleServlet.title_registry)} game codes port {core_cfg.allnet.port}" - ) - - def handle_poweron(self, request: Request, _: Dict): + async def handle_poweron(self, request: Request): request_ip = Utils.get_ip_addr(request) - pragma_header = request.getHeader('Pragma') - is_dfi = pragma_header is not None and pragma_header == "DFI" + pragma_header = request.headers.get('Pragma', "") + is_dfi = pragma_header == "DFI" + data = await request.body() try: if is_dfi: - req_urlencode = self.from_dfi(request.content.getvalue()) + req_urlencode = self.from_dfi(data) else: - req_urlencode = request.content.getvalue().decode() + req_urlencode = data req_dict = self.allnet_req_to_dict(req_urlencode) if req_dict is None: @@ -155,7 +156,7 @@ class AllnetServlet: except AllnetRequestException as e: if e.message != "": self.logger.error(e) - return b"" + return PlainTextResponse() if req.format_ver == 3: resp = AllnetPowerOnResponse3(req.token) @@ -166,44 +167,54 @@ class AllnetServlet: self.logger.debug(f"Allnet request: {vars(req)}") - machine = self.data.arcade.get_machine(req.serial) + machine = await self.data.arcade.get_machine(req.serial) if machine is None and not self.config.server.allow_unregistered_serials: msg = f"Unrecognised serial {req.serial} attempted allnet auth from {request_ip}." - self.data.base.log_event( - "allnet", "ALLNET_AUTH_UNKNOWN_SERIAL", logging.WARN, msg + await self.data.base.log_event( + "allnet", "ALLNET_AUTH_UNKNOWN_SERIAL", logging.WARN, msg, {"serial": req.serial}, None, None, None, request_ip, req.game_id, req.ver ) self.logger.warning(msg) resp.stat = ALLNET_STAT.bad_machine.value resp_dict = {k: v for k, v in vars(resp).items() if v is not None} - return (urllib.parse.unquote(urllib.parse.urlencode(resp_dict)) + "\n").encode("utf-8") + return PlainTextResponse(urllib.parse.unquote(urllib.parse.urlencode(resp_dict)) + "\n") if machine is not None: - arcade = self.data.arcade.get_arcade(machine["arcade"]) + arcade = await self.data.arcade.get_arcade(machine["arcade"]) if self.config.server.check_arcade_ip: if arcade["ip"] and arcade["ip"] is not None and arcade["ip"] != req.ip: - msg = f"Serial {req.serial} attempted allnet auth from bad IP {req.ip} (expected {arcade['ip']})." - self.data.base.log_event( - "allnet", "ALLNET_AUTH_BAD_IP", logging.ERROR, msg + msg = f"{req.serial} attempted allnet auth from bad IP {req.ip} (expected {arcade['ip']})." + await self.data.base.log_event( + "allnet", "ALLNET_AUTH_BAD_IP", logging.ERROR, msg, {}, None, arcade['id'], machine['id'], request_ip, req.game_id, req.ver ) self.logger.warning(msg) resp.stat = ALLNET_STAT.bad_shop.value resp_dict = {k: v for k, v in vars(resp).items() if v is not None} - return (urllib.parse.unquote(urllib.parse.urlencode(resp_dict)) + "\n").encode("utf-8") + return PlainTextResponse(urllib.parse.unquote(urllib.parse.urlencode(resp_dict)) + "\n") elif (not arcade["ip"] or arcade["ip"] is None) and self.config.server.strict_ip_checking: - msg = f"Serial {req.serial} attempted allnet auth from bad IP {req.ip}, but arcade {arcade['id']} has no IP set! (strict checking enabled)." - self.data.base.log_event( - "allnet", "ALLNET_AUTH_NO_SHOP_IP", logging.ERROR, msg + msg = f"{req.serial} attempted allnet auth from bad IP {req.ip}, but arcade {arcade['id']} has no IP set! (strict checking enabled)." + await self.data.base.log_event( + "allnet", "ALLNET_AUTH_NO_SHOP_IP", logging.ERROR, msg, {}, None, arcade['id'], machine['id'], request_ip, req.game_id, req.ver ) self.logger.warning(msg) resp.stat = ALLNET_STAT.bad_shop.value resp_dict = {k: v for k, v in vars(resp).items() if v is not None} - return (urllib.parse.unquote(urllib.parse.urlencode(resp_dict)) + "\n").encode("utf-8") + return PlainTextResponse(urllib.parse.unquote(urllib.parse.urlencode(resp_dict)) + "\n") + if machine['game'] and machine['game'] != req.game_id: + msg = f"{req.serial} attempted allnet auth with bad game ID {req.game_id} (expected {machine['game']})." + await self.data.base.log_event( + "allnet", "ALLNET_AUTH_BAD_GAME", logging.ERROR, msg, {}, None, arcade['id'], machine['id'], request_ip, req.game_id, req.ver + ) + self.logger.warning(msg) + resp.stat = ALLNET_STAT.bad_game.value + resp_dict = {k: v for k, v in vars(resp).items() if v is not None} + return PlainTextResponse(urllib.parse.unquote(urllib.parse.urlencode(resp_dict)) + "\n") + country = ( arcade["country"] if machine["country"] is None else machine["country"] ) @@ -211,7 +222,7 @@ class AllnetServlet: country = AllnetCountryCode.JAPAN.value resp.country = country - resp.place_id = arcade["id"] + resp.place_id = f"{arcade['id']:04X}" resp.allnet_id = machine["id"] resp.name = arcade["name"] if arcade["name"] is not None else "" resp.nickname = arcade["nickname"] if arcade["nickname"] is not None else "" @@ -235,60 +246,82 @@ class AllnetServlet: arcade["timezone"] if arcade["timezone"] is not None else "+0900" if req.format_ver == 3 else "+09:00" ) + else: + arcade = None + if req.game_id not in TitleServlet.title_registry: if not self.config.server.is_develop: msg = f"Unrecognised game {req.game_id} attempted allnet auth from {request_ip}." - self.data.base.log_event( - "allnet", "ALLNET_AUTH_UNKNOWN_GAME", logging.WARN, msg + await self.data.base.log_event( + "allnet", "ALLNET_AUTH_UNKNOWN_GAME", logging.WARN, msg, {}, None, arcade['id'] if arcade else None, machine['id'] if machine else None, request_ip, req.game_id, req.ver ) self.logger.warning(msg) resp.stat = ALLNET_STAT.bad_game.value resp_dict = {k: v for k, v in vars(resp).items() if v is not None} - return (urllib.parse.unquote(urllib.parse.urlencode(resp_dict)) + "\n").encode("utf-8") + return PlainTextResponse(urllib.parse.unquote(urllib.parse.urlencode(resp_dict)) + "\n") else: self.logger.info( f"Allowed unknown game {req.game_id} v{req.ver} to authenticate from {request_ip} due to 'is_develop' being enabled. S/N: {req.serial}" ) - resp.uri = f"http://{self.config.title.hostname}:{self.config.title.port}/{req.game_id}/{req.ver.replace('.', '')}/" - resp.host = f"{self.config.title.hostname}:{self.config.title.port}" + resp.uri = f"http://{self.config.server.hostname}:{self.config.server.port}/{req.game_id}/{req.ver.replace('.', '')}/" + resp.host = f"{self.config.server.hostname}:{self.config.server.port}" resp_dict = {k: v for k, v in vars(resp).items() if v is not None} resp_str = urllib.parse.unquote(urllib.parse.urlencode(resp_dict)) self.logger.debug(f"Allnet response: {resp_str}") - return (resp_str + "\n").encode("utf-8") + return PlainTextResponse(resp_str + "\n") int_ver = req.ver.replace(".", "") - resp.uri, resp.host = TitleServlet.title_registry[req.game_id].get_allnet_info(req.game_id, int(int_ver), req.serial) + try: + resp.uri, resp.host = TitleServlet.title_registry[req.game_id].get_allnet_info(req.game_id, int(int_ver), req.serial) + except Exception as e: + self.logger.error(f"Error running get_allnet_info for {req.game_id} - {e}") + resp.stat = ALLNET_STAT.bad_game.value + resp_dict = {k: v for k, v in vars(resp).items() if v is not None} + return PlainTextResponse(urllib.parse.unquote(urllib.parse.urlencode(resp_dict)) + "\n") - msg = f"{req.serial} authenticated from {request_ip}: {req.game_id} v{req.ver}" - self.data.base.log_event("allnet", "ALLNET_AUTH_SUCCESS", logging.INFO, msg) + if machine and arcade: + msg = f"{req.serial} authenticated from {request_ip}: {req.game_id} v{req.ver}" + await self.data.base.log_event( + "allnet", "ALLNET_AUTH_SUCCESS", logging.INFO, msg, {}, None, arcade['id'], machine['id'], request_ip, req.game_id, req.ver + ) + else: + msg = f"Allow unregistered serial {req.serial} to authenticate from {request_ip}: {req.game_id} v{req.ver}" + await self.data.base.log_event( + "allnet", "ALLNET_AUTH_SUCCESS_UNREG", logging.INFO, msg, {"serial": req.serial}, None, None, None, request_ip, req.game_id, req.ver + ) + self.logger.info(msg) resp_dict = {k: v for k, v in vars(resp).items() if v is not None} - resp_str = urllib.parse.unquote(urllib.parse.urlencode(resp_dict)) + resp_str = urllib.parse.unquote(urllib.parse.urlencode(resp_dict)) + "\n" self.logger.debug(f"Allnet response: {resp_dict}") - resp_str += "\n" - """if is_dfi: - request.responseHeaders.addRawHeader('Pragma', 'DFI') - return self.to_dfi(resp_str)""" + if is_dfi: + return PlainTextResponse( + content=self.to_dfi(resp_str) + b"\r\n", + headers={ + "Pragma": "DFI", + }, + ) - return resp_str.encode("utf-8") + return PlainTextResponse(resp_str) - def handle_dlorder(self, request: Request, _: Dict): + async def handle_dlorder(self, request: Request): request_ip = Utils.get_ip_addr(request) - pragma_header = request.getHeader('Pragma') - is_dfi = pragma_header is not None and pragma_header == "DFI" + pragma_header = request.headers.get('Pragma', "") + is_dfi = pragma_header == "DFI" + data = await request.body() try: if is_dfi: - req_urlencode = self.from_dfi(request.content.getvalue()) + req_urlencode = self.from_dfi(data) else: - req_urlencode = request.content.getvalue().decode() + req_urlencode = data.decode() req_dict = self.allnet_req_to_dict(req_urlencode) if req_dict is None: @@ -305,7 +338,7 @@ class AllnetServlet: except AllnetRequestException as e: if e.message != "": self.logger.error(e) - return b"" + return PlainTextResponse() self.logger.info( f"DownloadOrder from {request_ip} -> {req.game_id} v{req.ver} serial {req.serial}" @@ -316,54 +349,71 @@ class AllnetServlet: not self.config.allnet.allow_online_updates or not self.config.allnet.update_cfg_folder ): - return urllib.parse.unquote(urllib.parse.urlencode(vars(resp))) + "\n" + return PlainTextResponse(urllib.parse.unquote(urllib.parse.urlencode(vars(resp))) + "\n") + + else: + machine = await self.data.arcade.get_machine(req.serial) + if not machine or not machine['ota_enable'] or not machine['is_cab'] or machine['is_blacklisted']: + return PlainTextResponse(urllib.parse.unquote(urllib.parse.urlencode(vars(resp))) + "\n") - else: # TODO: Keychip check if path.exists( f"{self.config.allnet.update_cfg_folder}/{req.game_id}-{req.ver.replace('.', '')}-app.ini" ): - resp.uri = f"http://{self.config.title.hostname}:{self.config.title.port}/dl/ini/{req.game_id}-{req.ver.replace('.', '')}-app.ini" + resp.uri = f"http://{self.config.server.hostname}:{self.config.server.port}/dl/ini/{req.game_id}-{req.ver.replace('.', '')}-app.ini" if path.exists( f"{self.config.allnet.update_cfg_folder}/{req.game_id}-{req.ver.replace('.', '')}-opt.ini" ): - resp.uri += f"|http://{self.config.title.hostname}:{self.config.title.port}/dl/ini/{req.game_id}-{req.ver.replace('.', '')}-opt.ini" + resp.uri += f"|http://{self.config.server.hostname}:{self.config.server.port}/dl/ini/{req.game_id}-{req.ver.replace('.', '')}-opt.ini" - self.logger.debug(f"Sending download uri {resp.uri}") - self.data.base.log_event("allnet", "DLORDER_REQ_SUCCESS", logging.INFO, f"{Utils.get_ip_addr(request)} requested DL Order for {req.serial} {req.game_id} v{req.ver}") + if resp.uri: + self.logger.info(f"Sending download uri {resp.uri}") + await self.data.base.log_event( + "allnet", "DLORDER_REQ_SUCCESS", logging.INFO, f"Send download URI to {req.serial} for {req.game_id} v{req.ver} from {Utils.get_ip_addr(request)}", {"uri": resp.uri}, None, + machine['arcade'], machine['id'], request_ip, req.game_id, req.ver + ) + # Maybe add a log event for checkin but no url sent? res_str = urllib.parse.unquote(urllib.parse.urlencode(vars(resp))) + "\n" - """if is_dfi: - request.responseHeaders.addRawHeader('Pragma', 'DFI') - return self.to_dfi(res_str)""" + + if is_dfi: + return PlainTextResponse( + content=self.to_dfi(res_str) + b"\r\n", + headers={ + "Pragma": "DFI", + }, + ) - return res_str + return PlainTextResponse(res_str) - def handle_dlorder_ini(self, request: Request, match: Dict) -> bytes: - if "file" not in match: - return b"" + async def handle_dlorder_ini(self, request: Request) -> bytes: + req_file = request.path_params.get("file", "").replace("%0A", "").replace("\n", "") + request_ip = Utils.get_ip_addr(request) - req_file = match["file"].replace("%0A", "") + if not req_file: + return PlainTextResponse(status_code=404) if path.exists(f"{self.config.allnet.update_cfg_folder}/{req_file}"): - self.logger.info(f"Request for DL INI file {req_file} from {Utils.get_ip_addr(request)} successful") - self.data.base.log_event("allnet", "DLORDER_INI_SENT", logging.INFO, f"{Utils.get_ip_addr(request)} successfully recieved {req_file}") + self.logger.info(f"Request for DL INI file {req_file} from {request_ip} successful") + await self.data.base.log_event( + "allnet", "DLORDER_INI_SENT", logging.INFO, f"{request_ip} successfully recieved {req_file}", {"file": req_file}, ip=request_ip + ) - return open( - f"{self.config.allnet.update_cfg_folder}/{req_file}", "rb" - ).read() + return PlainTextResponse(open( + f"{self.config.allnet.update_cfg_folder}/{req_file}", "r", encoding="utf-8" + ).read()) self.logger.info(f"DL INI File {req_file} not found") - return b"" + return PlainTextResponse() - def handle_dlorder_report(self, request: Request, match: Dict) -> bytes: - req_raw = request.content.getvalue() + async def handle_dlorder_report(self, request: Request) -> bytes: + req_raw = await request.body() client_ip = Utils.get_ip_addr(request) try: req_dict: Dict = json.loads(req_raw) except Exception as e: self.logger.warning(f"Failed to parse DL Report: {e}") - return "NG" + return PlainTextResponse("NG") dl_data_type = DLIMG_TYPE.app dl_data = req_dict.get("appimage", {}) @@ -374,24 +424,30 @@ class AllnetServlet: if dl_data is None or not dl_data: self.logger.warning(f"Failed to parse DL Report: Invalid format - contains neither appimage nor optimage") - return "NG" + return PlainTextResponse("NG") rep = DLReport(dl_data, dl_data_type) if not rep.validate(): self.logger.warning(f"Failed to parse DL Report: Invalid format - {rep.err}") - return "NG" + return PlainTextResponse("NG") msg = f"{rep.serial} @ {client_ip} reported {rep.rep_type.name} download state {rep.rf_state.name} for {rep.gd} v{rep.dav}:"\ f" {rep.tdsc}/{rep.tsc} segments downloaded for working files {rep.wfl} with {rep.dfl if rep.dfl else 'none'} complete." - self.data.base.log_event("allnet", "DL_REPORT", logging.INFO, msg, dl_data) + machine = await self.data.arcade.get_machine(rep.serial) + if machine: + await self.data.base.log_event("allnet", "DL_REPORT", logging.INFO, msg, dl_data, None, machine['arcade'], machine['id'], client_ip, rep.gd, rep.dav) + + else: + msg = "Unknown serial " + msg + await self.data.base.log_event("allnet", "DL_REPORT_UNREG", logging.INFO, msg, dl_data, None, None, None, client_ip, rep.gd, rep.dav) self.logger.info(msg) - return "OK" + return PlainTextResponse("OK") - def handle_loaderstaterecorder(self, request: Request, match: Dict) -> bytes: - req_data = request.content.getvalue() + async def handle_loaderstaterecorder(self, request: Request) -> bytes: + req_data = await request.body() sections = req_data.decode("utf-8").split("\r\n") req_dict = dict(urllib.parse.parse_qsl(sections[0])) @@ -403,130 +459,27 @@ class AllnetServlet: ip = Utils.get_ip_addr(request) if serial is None or num_files_dld is None or num_files_to_dl is None or dl_state is None: - return "NG".encode() + return PlainTextResponse("NG") - self.logger.info(f"LoaderStateRecorder Request from {ip} {serial}: {num_files_dld}/{num_files_to_dl} Files download (State: {dl_state})") - return "OK".encode() - - def handle_alive(self, request: Request, match: Dict) -> bytes: - return "OK".encode() - - def handle_billing_request(self, request: Request, _: Dict): - req_raw = request.content.getvalue() + msg = f"LoaderStateRecorder Request from {ip} {serial}: {num_files_dld}/{num_files_to_dl} Files download (State: {dl_state})" + machine = await self.data.arcade.get_machine(serial) + if machine: + await self.data.base.log_event("allnet", "LSR_REPORT", logging.INFO, msg, req_dict, None, machine['arcade'], machine['id'], ip) - if request.getHeader('Content-Type') == "application/octet-stream": - req_unzip = zlib.decompressobj(-zlib.MAX_WBITS).decompress(req_raw) else: - req_unzip = req_raw + msg = "Unregistered " + msg + await self.data.base.log_event("allnet", "LSR_REPORT_UNREG", logging.INFO, msg, req_dict, None, None, None, ip) - req_dict = self.billing_req_to_dict(req_unzip) - request_ip = Utils.get_ip_addr(request) - - if req_dict is None: - self.logger.error(f"Failed to parse request {request.content.getvalue()}") - return b"" - - self.logger.debug(f"request {req_dict}") - - rsa = RSA.import_key(open(self.config.billing.signing_key, "rb").read()) - signer = PKCS1_v1_5.new(rsa) - digest = SHA.new() - traces: List[TraceData] = [] - try: - req = BillingInfo(req_dict[0]) - except KeyError as e: - self.logger.error(f"Billing request failed to parse: {e}") - return f"result=5&linelimit=&message=field is missing or formatting is incorrect\r\n".encode() - - for x in range(1, len(req_dict)): - if not req_dict[x]: - continue - - try: - tmp = TraceData(req_dict[x]) - if tmp.trace_type == TraceDataType.CHARGE: - tmp = TraceDataCharge(req_dict[x]) - elif tmp.trace_type == TraceDataType.EVENT: - tmp = TraceDataEvent(req_dict[x]) - elif tmp.trace_type == TraceDataType.CREDIT: - tmp = TraceDataCredit(req_dict[x]) - - traces.append(tmp) - - except KeyError as e: - self.logger.warn(f"Tracelog failed to parse: {e}") - - kc_serial_bytes = req.keychipid.encode() - - - machine = self.data.arcade.get_machine(req.keychipid) - if machine is None and not self.config.server.allow_unregistered_serials: - msg = f"Unrecognised serial {req.keychipid} attempted billing checkin from {request_ip} for {req.gameid} v{req.gamever}." - self.data.base.log_event( - "allnet", "BILLING_CHECKIN_NG_SERIAL", logging.WARN, msg - ) - self.logger.warning(msg) - - return f"result=1&requestno={req.requestno}&message=Keychip Serial bad\r\n".encode() - - msg = ( - f"Billing checkin from {request_ip}: game {req.gameid} ver {req.gamever} keychip {req.keychipid} playcount " - f"{req.playcnt} billing_type {req.billingtype.name} nearfull {req.nearfull} playlimit {req.playlimit}" - ) self.logger.info(msg) - self.data.base.log_event("billing", "BILLING_CHECKIN_OK", logging.INFO, msg) - if req.traceleft > 0: - self.logger.warn(f"{req.traceleft} unsent tracelogs") - kc_playlimit = req.playlimit - kc_nearfull = req.nearfull + return PlainTextResponse("OK") + + async def handle_alive(self, request: Request) -> bytes: + return PlainTextResponse("OK") - while req.playcnt > req.playlimit: - kc_playlimit += 1024 - kc_nearfull += 1024 - - playlimit = kc_playlimit - nearfull = kc_nearfull + (req.billingtype.value * 0x00010000) - - digest.update(playlimit.to_bytes(4, "little") + kc_serial_bytes) - playlimit_sig = signer.sign(digest).hex() - - digest = SHA.new() - digest.update(nearfull.to_bytes(4, "little") + kc_serial_bytes) - nearfull_sig = signer.sign(digest).hex() - - # TODO: playhistory - - #resp = BillingResponse(playlimit, playlimit_sig, nearfull, nearfull_sig) - resp = BillingResponse(playlimit, playlimit_sig, nearfull, nearfull_sig, req.requestno, req.protocolver) - - resp_str = urllib.parse.unquote(urllib.parse.urlencode(vars(resp))) + "\r\n" - - self.logger.debug(f"response {vars(resp)}") - if req.traceleft > 0: - self.logger.info(f"Requesting 20 more of {req.traceleft} unsent tracelogs") - return f"result=6&waittime=0&linelimit=20\r\n".encode() - - return resp_str.encode("utf-8") - - def handle_naomitest(self, request: Request, _: Dict) -> bytes: - self.logger.info(f"Ping from {Utils.get_ip_addr(request)}") - return b"naomi ok" - - def billing_req_to_dict(self, data: bytes): - """ - Parses an billing request string into a python dictionary - """ - try: - sections = data.decode("ascii").split("\r\n") - - ret = [] - for x in sections: - ret.append(dict(urllib.parse.parse_qsl(x))) - return ret - - except Exception as e: - self.logger.error(f"billing_req_to_dict: {e} while parsing {data}") - return None + async def handle_naomitest(self, request: Request) -> bytes: + # This could be spam-able, removing + #self.logger.info(f"Ping from {Utils.get_ip_addr(request)}") + return PlainTextResponse("naomi ok") def allnet_req_to_dict(self, data: str) -> Optional[List[Dict[str, Any]]]: """ @@ -554,6 +507,167 @@ class AllnetServlet: zipped = zlib.compress(unzipped) return base64.b64encode(zipped) +class BillingServlet: + def __init__(self, core_cfg: CoreConfig, cfg_folder: str) -> None: + self.config = core_cfg + self.config_folder = cfg_folder + self.data = Data(core_cfg) + + self.logger = logging.getLogger("billing") + if not hasattr(self.logger, "initialized"): + log_fmt_str = "[%(asctime)s] Billing | %(levelname)s | %(message)s" + log_fmt = logging.Formatter(log_fmt_str) + + fileHandler = TimedRotatingFileHandler( + "{0}/{1}.log".format(self.config.server.log_dir, "billing"), + when="d", + backupCount=10, + ) + fileHandler.setFormatter(log_fmt) + + consoleHandler = logging.StreamHandler() + consoleHandler.setFormatter(log_fmt) + + self.logger.addHandler(fileHandler) + self.logger.addHandler(consoleHandler) + + self.logger.setLevel(core_cfg.allnet.loglevel) + coloredlogs.install( + level=core_cfg.billing.loglevel, logger=self.logger, fmt=log_fmt_str + ) + self.logger.initialized = True + + def startup(self) -> None: + self.logger.info(f"Ready on port {self.config.billing.port if self.config.billing.standalone else self.config.server.port}") + + def billing_req_to_dict(self, data: bytes): + """ + Parses an billing request string into a python dictionary + """ + try: + sections = data.decode("ascii").split("\r\n") + + ret = [] + for x in sections: + ret.append(dict(urllib.parse.parse_qsl(x))) + return ret + + except Exception as e: + self.logger.error(f"billing_req_to_dict: {e} while parsing {data}") + return None + + async def handle_billing_request(self, request: Request): + req_raw = await request.body() + + if request.headers.get('Content-Type', '') == "application/octet-stream": + req_unzip = zlib.decompressobj(-zlib.MAX_WBITS).decompress(req_raw) + else: + req_unzip = req_raw + + req_dict = self.billing_req_to_dict(req_unzip) + request_ip = Utils.get_ip_addr(request) + + if req_dict is None: + self.logger.error(f"Failed to parse request {req_raw}") + return PlainTextResponse() + + self.logger.debug(f"request {req_dict}") + + rsa = RSA.import_key(open(self.config.billing.signing_key, "rb").read()) + signer = PKCS1_v1_5.new(rsa) + digest = SHA.new() + traces: List[TraceData] = [] + try: + req = BillingInfo(req_dict[0]) + except KeyError as e: + self.logger.error(f"Billing request failed to parse: {e}") + return PlainTextResponse("result=5&linelimit=&message=field is missing or formatting is incorrect\r\n") + + for x in range(1, len(req_dict)): + if not req_dict[x]: + continue + + try: + tmp = TraceData(req_dict[x]) + if tmp.trace_type == TraceDataType.CHARGE: + tmp = TraceDataCharge(req_dict[x]) + elif tmp.trace_type == TraceDataType.EVENT: + tmp = TraceDataEvent(req_dict[x]) + elif tmp.trace_type == TraceDataType.CREDIT: + tmp = TraceDataCredit(req_dict[x]) + + traces.append(tmp) + + except KeyError as e: + self.logger.warn(f"Tracelog failed to parse: {e}") + + kc_serial_bytes = req.keychipid.encode() + + + machine = await self.data.arcade.get_machine(req.keychipid) + if machine is None and not self.config.server.allow_unregistered_serials: + msg = f"Unrecognised serial {req.keychipid} attempted billing checkin from {request_ip} for {req.gameid} v{req.gamever}." + await self.data.base.log_event( + "allnet", "BILLING_CHECKIN_NG_SERIAL", logging.WARN, msg, ip=request_ip, game=req.gameid, version=req.gamever + ) + self.logger.warning(msg) + + return PlainTextResponse(f"result=1&requestno={req.requestno}&message=Keychip Serial bad\r\n") + + log_details = { + "playcount": req.playcnt, + "billing_type": req.billingtype.name, + "nearfull": req.nearfull, + "playlimit": req.playlimit, + } + + if machine is not None: + await self.data.base.log_event("billing", "BILLING_CHECKIN_OK", logging.INFO, "", log_details, None, machine['arcade'], machine['id'], request_ip, req.gameid, req.gamever) + + self.logger.info( + f"Unregistered Billing checkin from {request_ip}: game {req.gameid} ver {req.gamever} keychip {req.keychipid} playcount " + f"{req.playcnt} billing_type {req.billingtype.name} nearfull {req.nearfull} playlimit {req.playlimit}" + ) + else: + log_details['serial'] = req.keychipid + await self.data.base.log_event("billing", "BILLING_CHECKIN_OK_UNREG", logging.INFO, "", log_details, None, None, None, request_ip, req.gameid, req.gamever) + + self.logger.info( + f"Unregistered Billing checkin from {request_ip}: game {req.gameid} ver {req.gamever} keychip {req.keychipid} playcount " + f"{req.playcnt} billing_type {req.billingtype.name} nearfull {req.nearfull} playlimit {req.playlimit}" + ) + + if req.traceleft > 0: + self.logger.warn(f"{req.traceleft} unsent tracelogs") + kc_playlimit = req.playlimit + kc_nearfull = req.nearfull + + while req.playcnt > req.playlimit: + kc_playlimit += 1024 + kc_nearfull += 1024 + + playlimit = kc_playlimit + nearfull = kc_nearfull + (req.billingtype.value * 0x00010000) + + digest.update(playlimit.to_bytes(4, "little") + kc_serial_bytes) + playlimit_sig = signer.sign(digest).hex() + + digest = SHA.new() + digest.update(nearfull.to_bytes(4, "little") + kc_serial_bytes) + nearfull_sig = signer.sign(digest).hex() + + # TODO: playhistory + + resp = BillingResponse(playlimit, playlimit_sig, nearfull, nearfull_sig, req.requestno, req.protocolver) + + resp_str = urllib.parse.unquote(urllib.parse.urlencode(vars(resp))) + "\r\n" + + self.logger.debug(f"response {vars(resp)}") + if req.traceleft > 0: + self.logger.info(f"Requesting 20 more of {req.traceleft} unsent tracelogs") + return PlainTextResponse("result=6&waittime=0&linelimit=20\r\n") + + return PlainTextResponse(resp_str) class AllnetPowerOnRequest: def __init__(self, req: Dict) -> None: @@ -613,7 +727,6 @@ class AllnetPowerOnResponse3(AllnetPowerOnResponse): self.minute = None self.second = None - class AllnetPowerOnResponse2(AllnetPowerOnResponse): def __init__(self) -> None: super().__init__() @@ -623,7 +736,6 @@ class AllnetPowerOnResponse2(AllnetPowerOnResponse): self.timezone = "+09:00" self.res_class = "PowerOnResponseV2" - class AllnetDownloadOrderRequest: def __init__(self, req: Dict) -> None: self.game_id = req.get("game_id", "") @@ -631,7 +743,6 @@ class AllnetDownloadOrderRequest: self.serial = req.get("serial", "") self.encode = req.get("encode", "") - class AllnetDownloadOrderResponse: def __init__(self, stat: int = 1, serial: str = "", uri: str = "") -> None: self.stat = stat @@ -781,7 +892,6 @@ class BillingResponse: # playhistory -> YYYYMM/C:... # YYYY -> 4 digit year, MM -> 2 digit month, C -> Playcount during that period - class AllnetRequestException(Exception): def __init__(self, message="") -> None: self.message = message @@ -849,3 +959,48 @@ class DLReport: return False return True + +cfg_dir = environ.get("ARTEMIS_CFG_DIR", "config") +cfg: CoreConfig = CoreConfig() +if path.exists(f"{cfg_dir}/core.yaml"): + cfg.update(yaml.safe_load(open(f"{cfg_dir}/core.yaml"))) + +if not path.exists(cfg.server.log_dir): + mkdir(cfg.server.log_dir) + +if not access(cfg.server.log_dir, W_OK): + print( + f"Log directory {cfg.server.log_dir} NOT writable, please check permissions" + ) + exit(1) + +billing = BillingServlet(cfg, cfg_dir) +app_billing = Starlette( + cfg.server.is_develop, + [ + Route("/request", billing.handle_billing_request, methods=["POST"]), + Route("/request/", billing.handle_billing_request, methods=["POST"]), + ], + on_startup=[billing.startup] +) + +allnet = AllnetServlet(cfg, cfg_dir) +route_lst = [ + Route("/sys/servlet/PowerOn", allnet.handle_poweron, methods=["GET", "POST"]), + Route("/sys/servlet/DownloadOrder", allnet.handle_dlorder, methods=["GET", "POST"]), + Route("/sys/servlet/LoaderStateRecorder", allnet.handle_loaderstaterecorder, methods=["GET", "POST"]), + Route("/sys/servlet/Alive", allnet.handle_alive, methods=["GET", "POST"]), + Route("/naomitest.html", allnet.handle_naomitest), +] + +if cfg.allnet.allow_online_updates: + route_lst += [ + Route("/report-api/Report", allnet.handle_dlorder_report, methods=["POST"]), + Route("/dl/ini/{file:str}", allnet.handle_dlorder_ini), + ] + +app_allnet = Starlette( + cfg.server.is_develop, + route_lst, + on_startup=[allnet.startup] +) diff --git a/core/app.py b/core/app.py new file mode 100644 index 0000000..e4d3330 --- /dev/null +++ b/core/app.py @@ -0,0 +1,92 @@ +import yaml +import logging +import coloredlogs +from logging.handlers import TimedRotatingFileHandler +from starlette.routing import Route +from starlette.requests import Request +from starlette.applications import Starlette +from starlette.responses import PlainTextResponse +from os import environ, path, mkdir, W_OK, access +from typing import List + +from core import CoreConfig, TitleServlet, MuchaServlet, AllnetServlet, BillingServlet, AimedbServlette +from core.frontend import FrontendServlet + +async def dummy_rt(request: Request): + return PlainTextResponse("Service OK") + +cfg_dir = environ.get("ARTEMIS_CFG_DIR", "config") +cfg: CoreConfig = CoreConfig() +if path.exists(f"{cfg_dir}/core.yaml"): + cfg.update(yaml.safe_load(open(f"{cfg_dir}/core.yaml"))) + +if not path.exists(cfg.server.log_dir): + mkdir(cfg.server.log_dir) + +if not access(cfg.server.log_dir, W_OK): + print( + f"Log directory {cfg.server.log_dir} NOT writable, please check permissions" + ) + exit(1) + +logger = logging.getLogger("core") +log_fmt_str = "[%(asctime)s] Core | %(levelname)s | %(message)s" +log_fmt = logging.Formatter(log_fmt_str) + +fileHandler = TimedRotatingFileHandler( + "{0}/{1}.log".format(cfg.server.log_dir, "core"), when="d", backupCount=10 +) +fileHandler.setFormatter(log_fmt) + +consoleHandler = logging.StreamHandler() +consoleHandler.setFormatter(log_fmt) + +logger.addHandler(fileHandler) +logger.addHandler(consoleHandler) + +log_lv = logging.DEBUG if cfg.server.is_develop else logging.INFO +logger.setLevel(log_lv) +coloredlogs.install(level=log_lv, logger=logger, fmt=log_fmt_str) + +logger.info(f"Artemis starting in {'develop' if cfg.server.is_develop else 'production'} mode") + +title = TitleServlet(cfg, cfg_dir) # This has to be loaded first to load plugins +mucha = MuchaServlet(cfg, cfg_dir) + +route_lst: List[Route] = [ + # Mucha + Route("/mucha_front/boardauth.do", mucha.handle_boardauth, methods=["POST"]), + Route("/mucha_front/updatacheck.do", mucha.handle_updatecheck, methods=["POST"]), + Route("/mucha_front/downloadstate.do", mucha.handle_dlstate, methods=["POST"]), + # General + Route("/", dummy_rt), + Route("/robots.txt", FrontendServlet.robots) +] + +if not cfg.billing.standalone: + billing = BillingServlet(cfg, cfg_dir) + route_lst += [ + Route("/request", billing.handle_billing_request, methods=["POST"]), + Route("/request/", billing.handle_billing_request, methods=["POST"]), + ] + +if not cfg.allnet.standalone: + allnet = AllnetServlet(cfg, cfg_dir) + route_lst += [ + Route("/sys/servlet/PowerOn", allnet.handle_poweron, methods=["GET", "POST"]), + Route("/sys/servlet/DownloadOrder", allnet.handle_dlorder, methods=["GET", "POST"]), + Route("/sys/servlet/LoaderStateRecorder", allnet.handle_loaderstaterecorder, methods=["GET", "POST"]), + Route("/sys/servlet/Alive", allnet.handle_alive, methods=["GET", "POST"]), + Route("/naomitest.html", allnet.handle_naomitest), + ] + + if cfg.allnet.allow_online_updates: + route_lst += [ + Route("/report-api/Report", allnet.handle_dlorder_report, methods=["POST"]), + Route("/dl/ini/{file:str}", allnet.handle_dlorder_ini), + ] + +for code, game in title.title_registry.items(): + route_lst += game.get_routes() + +app = Starlette(cfg.server.is_develop, route_lst) diff --git a/core/config.py b/core/config.py index 68db052..3d7b919 100644 --- a/core/config.py +++ b/core/config.py @@ -1,16 +1,48 @@ import logging, os from typing import Any - class ServerConfig: def __init__(self, parent_config: "CoreConfig") -> None: self.__config = parent_config @property def listen_address(self) -> str: + """ + Address Artemis will bind to and listen on + """ return CoreConfig.get_config_field( self.__config, "core", "server", "listen_address", default="127.0.0.1" ) + + @property + def hostname(self) -> str: + """ + Hostname sent to games + """ + return CoreConfig.get_config_field( + self.__config, "core", "server", "hostname", default="localhost" + ) + + @property + def port(self) -> int: + """ + Port the game will listen on + """ + return CoreConfig.get_config_field( + self.__config, "core", "server", "port", default=80 + ) + + @property + def ssl_key(self) -> str: + return CoreConfig.get_config_field( + self.__config, "core", "server", "ssl_key", default="cert/title.key" + ) + + @property + def ssl_cert(self) -> str: + return CoreConfig.get_config_field( + self.__config, "core", "title", "ssl_cert", default="cert/title.pem" + ) @property def allow_user_registration(self) -> bool: @@ -43,9 +75,23 @@ class ServerConfig: ) @property - def threading(self) -> bool: + def proxy_port(self) -> int: + """ + What port the proxy is listening on. This will be sent instead of 'port' if + is_using_proxy is True and this value is non-zero + """ return CoreConfig.get_config_field( - self.__config, "core", "server", "threading", default=False + self.__config, "core", "server", "proxy_port", default=0 + ) + + @property + def proxy_port_ssl(self) -> int: + """ + What port the proxy is listening for secure connections on. This will be sent + instead of 'port' if is_using_proxy is True and this value is non-zero + """ + return CoreConfig.get_config_field( + self.__config, "core", "server", "proxy_port_ssl", default=0 ) @property @@ -66,7 +112,6 @@ class ServerConfig: self.__config, "core", "server", "strict_ip_checking", default=False ) - class TitleConfig: def __init__(self, parent_config: "CoreConfig") -> None: self.__config = parent_config @@ -79,36 +124,6 @@ class TitleConfig: ) ) - @property - def hostname(self) -> str: - return CoreConfig.get_config_field( - self.__config, "core", "title", "hostname", default="localhost" - ) - - @property - def port(self) -> int: - return CoreConfig.get_config_field( - self.__config, "core", "title", "port", default=8080 - ) - - @property - def port_ssl(self) -> int: - return CoreConfig.get_config_field( - self.__config, "core", "title", "port_ssl", default=0 - ) - - @property - def ssl_key(self) -> str: - return CoreConfig.get_config_field( - self.__config, "core", "title", "ssl_key", default="cert/title.key" - ) - - @property - def ssl_cert(self) -> str: - return CoreConfig.get_config_field( - self.__config, "core", "title", "ssl_cert", default="cert/title.pem" - ) - @property def reboot_start_time(self) -> str: return CoreConfig.get_config_field( @@ -121,7 +136,6 @@ class TitleConfig: self.__config, "core", "title", "reboot_end_time", default="" ) - class DatabaseConfig: def __init__(self, parent_config: "CoreConfig") -> None: self.__config = parent_config @@ -159,7 +173,7 @@ class DatabaseConfig: @property def protocol(self) -> str: return CoreConfig.get_config_field( - self.__config, "core", "database", "type", default="mysql" + self.__config, "core", "database", "protocol", default="mysql" ) @property @@ -176,16 +190,6 @@ class DatabaseConfig: ) ) - @property - def user_table_autoincrement_start(self) -> int: - return CoreConfig.get_config_field( - self.__config, - "core", - "database", - "user_table_autoincrement_start", - default=10000, - ) - @property def enable_memcached(self) -> bool: return CoreConfig.get_config_field( @@ -198,13 +202,12 @@ class DatabaseConfig: self.__config, "core", "database", "memcached_host", default="localhost" ) - class FrontendConfig: def __init__(self, parent_config: "CoreConfig") -> None: self.__config = parent_config @property - def enable(self) -> int: + def enable(self) -> bool: return CoreConfig.get_config_field( self.__config, "core", "frontend", "enable", default=False ) @@ -212,7 +215,7 @@ class FrontendConfig: @property def port(self) -> int: return CoreConfig.get_config_field( - self.__config, "core", "frontend", "port", default=8090 + self.__config, "core", "frontend", "port", default=8080 ) @property @@ -222,20 +225,23 @@ class FrontendConfig: self.__config, "core", "frontend", "loglevel", default="info" ) ) - + + @property + def secret(self) -> str: + return CoreConfig.get_config_field( + self.__config, "core", "frontend", "secret", default="" + ) class AllnetConfig: def __init__(self, parent_config: "CoreConfig") -> None: self.__config = parent_config @property - def loglevel(self) -> int: - return CoreConfig.str_to_loglevel( - CoreConfig.get_config_field( - self.__config, "core", "allnet", "loglevel", default="info" - ) + def standalone(self) -> bool: + return CoreConfig.get_config_field( + self.__config, "core", "allnet", "standalone", default=False ) - + @property def port(self) -> int: return CoreConfig.get_config_field( @@ -243,9 +249,11 @@ class AllnetConfig: ) @property - def ip_check(self) -> bool: - return CoreConfig.get_config_field( - self.__config, "core", "allnet", "ip_check", default=False + def loglevel(self) -> int: + return CoreConfig.str_to_loglevel( + CoreConfig.get_config_field( + self.__config, "core", "allnet", "loglevel", default="info" + ) ) @property @@ -260,10 +268,23 @@ class AllnetConfig: self.__config, "core", "allnet", "update_cfg_folder", default="" ) - class BillingConfig: def __init__(self, parent_config: "CoreConfig") -> None: self.__config = parent_config + + @property + def standalone(self) -> bool: + return CoreConfig.get_config_field( + self.__config, "core", "billing", "standalone", default=True + ) + + @property + def loglevel(self) -> int: + return CoreConfig.str_to_loglevel( + CoreConfig.get_config_field( + self.__config, "core", "billing", "loglevel", default="info" + ) + ) @property def port(self) -> int: @@ -289,11 +310,22 @@ class BillingConfig: self.__config, "core", "billing", "signing_key", default="cert/billing.key" ) - class AimedbConfig: def __init__(self, parent_config: "CoreConfig") -> None: self.__config = parent_config + @property + def enable(self) -> bool: + return CoreConfig.get_config_field( + self.__config, "core", "aimedb", "enable", default=True + ) + + @property + def listen_address(self) -> bool: + return CoreConfig.get_config_field( + self.__config, "core", "aimedb", "listen_address", default="" + ) + @property def loglevel(self) -> int: return CoreConfig.str_to_loglevel( @@ -326,17 +358,10 @@ class AimedbConfig: self.__config, "core", "aimedb", "id_lifetime_seconds", default=86400 ) - class MuchaConfig: def __init__(self, parent_config: "CoreConfig") -> None: self.__config = parent_config - @property - def enable(self) -> int: - return CoreConfig.get_config_field( - self.__config, "core", "mucha", "enable", default=False - ) - @property def loglevel(self) -> int: return CoreConfig.str_to_loglevel( @@ -345,13 +370,6 @@ class MuchaConfig: ) ) - @property - def hostname(self) -> str: - return CoreConfig.get_config_field( - self.__config, "core", "mucha", "hostname", default="localhost" - ) - - class CoreConfig(dict): def __init__(self) -> None: self.server = ServerConfig(self) @@ -373,6 +391,19 @@ class CoreConfig(dict): return logging.DEBUG else: return logging.INFO + + @classmethod + def loglevel_to_str(cls, level: int) -> str: + if level == logging.ERROR: + return "error" + elif level == logging.WARN: + return "warn" + elif level == logging.INFO: + return "info" + elif level == logging.DEBUG: + return "debug" + else: + return "notset" @classmethod def get_config_field( diff --git a/core/const.py b/core/const.py index 98effb6..535a1bb 100644 --- a/core/const.py +++ b/core/const.py @@ -1,16 +1,18 @@ from enum import Enum -class MainboardPlatformCodes: - RINGEDGE = "AALE" - RINGWIDE = "AAML" - NU = "AAVE" - NUSX = "AAWE" - ALLS_UX = "ACAE" - ALLS_HX = "ACAX" +class MainboardPlatformCodes(Enum): + RINGEDGE = "AAL" + RINGEDGE2 = "AAS" + RINGWIDE = "AAM" + NU = "AAV" + NUSX = "AAW" + ALLS = "ACA" + #ALLS_UX = "ACAE" + #ALLS_HX = "ACAX" -class MainboardRevisions: +class MainboardRevisions(Enum): RINGEDGE = 1 RINGEDGE2 = 2 @@ -29,11 +31,10 @@ class MainboardRevisions: ALLS_HX2 = 12 -class KeychipPlatformsCodes: - RING = "A72E" - NU = ("A60E", "A60E", "A60E") - NUSX = ("A61X", "A69X") - ALLS = "A63E" +class KeychipPlatformsCodes(Enum): + RING = "72" + NU = ("60", "61", "69") + ALLS = "63" class AllnetCountryCode(Enum): diff --git a/core/data/alembic/README b/core/data/alembic/README new file mode 100644 index 0000000..98e4f9c --- /dev/null +++ b/core/data/alembic/README @@ -0,0 +1 @@ +Generic single-database configuration. \ No newline at end of file diff --git a/core/data/alembic/alembic.ini b/core/data/alembic/alembic.ini new file mode 100644 index 0000000..26b89ea --- /dev/null +++ b/core/data/alembic/alembic.ini @@ -0,0 +1,64 @@ +# A generic, single database configuration. + +[alembic] +script_location=. + +# template used to generate migration files +# file_template = %%(rev)s_%%(slug)s + +# max length of characters to apply to the +# "slug" field +#truncate_slug_length = 40 + +# set to 'true' to run the environment during +# the 'revision' command, regardless of autogenerate +# revision_environment = false + +# set to 'true' to allow .pyc and .pyo files without +# a source .py file to be detected as revisions in the +# versions/ directory +# sourceless = false + +# version location specification; this defaults +# to migrations//versions. When using multiple version +# directories, initial revisions must be specified with --version-path +# version_locations = %(here)s/bar %(here)s/bat migrations//versions + +# the output encoding used when revision files +# are written from script.py.mako +# output_encoding = utf-8 + +# Logging configuration +[loggers] +keys = root,sqlalchemy,alembic + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console +qualname = + +[logger_sqlalchemy] +level = WARN +handlers = +qualname = sqlalchemy.engine + +[logger_alembic] +level = INFO +handlers = +qualname = alembic + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(levelname)-5.5s [%(name)s] %(message)s +datefmt = %H:%M:%S diff --git a/core/data/alembic/env.py b/core/data/alembic/env.py new file mode 100644 index 0000000..d532093 --- /dev/null +++ b/core/data/alembic/env.py @@ -0,0 +1,81 @@ +from __future__ import with_statement +from alembic import context +from sqlalchemy import engine_from_config, pool +from logging.config import fileConfig + +from core.data.schema.base import metadata + +# this is the Alembic Config object, which provides +# access to the values within the .ini file in use. +config = context.config + +# Interpret the config file for Python logging. +# This line sets up loggers basically. +fileConfig(config.config_file_name) + +# add your model's MetaData object here +# for 'autogenerate' support +# from myapp import mymodel +# target_metadata = mymodel.Base.metadata +target_metadata = metadata + +# other values from the config, defined by the needs of env.py, +# can be acquired: +# my_important_option = config.get_main_option("my_important_option") +# ... etc. + + +def run_migrations_offline(): + """Run migrations in 'offline' mode. + + This configures the context with just a URL + and not an Engine, though an Engine is acceptable + here as well. By skipping the Engine creation + we don't even need a DBAPI to be available. + + Calls to context.execute() here emit the given string to the + script output. + + """ + raise Exception('Not implemented or configured!') + + url = config.get_main_option("sqlalchemy.url") + context.configure( + url=url, target_metadata=target_metadata, literal_binds=True) + + with context.begin_transaction(): + context.run_migrations() + + +def run_migrations_online(): + """Run migrations in 'online' mode. + + In this scenario we need to create an Engine + and associate a connection with the context. + + """ + ini_section = config.get_section(config.config_ini_section) + overrides = context.get_x_argument(as_dictionary=True) + for override in overrides: + ini_section[override] = overrides[override] + + connectable = engine_from_config( + ini_section, + prefix='sqlalchemy.', + poolclass=pool.NullPool) + + with connectable.connect() as connection: + context.configure( + connection=connection, + target_metadata=target_metadata, + compare_type=True, + compare_server_default=True, + ) + + with context.begin_transaction(): + context.run_migrations() + +if context.is_offline_mode(): + run_migrations_offline() +else: + run_migrations_online() diff --git a/core/data/alembic/script.py.mako b/core/data/alembic/script.py.mako new file mode 100644 index 0000000..2c01563 --- /dev/null +++ b/core/data/alembic/script.py.mako @@ -0,0 +1,24 @@ +"""${message} + +Revision ID: ${up_revision} +Revises: ${down_revision | comma,n} +Create Date: ${create_date} + +""" +from alembic import op +import sqlalchemy as sa +${imports if imports else ""} + +# revision identifiers, used by Alembic. +revision = ${repr(up_revision)} +down_revision = ${repr(down_revision)} +branch_labels = ${repr(branch_labels)} +depends_on = ${repr(depends_on)} + + +def upgrade(): + ${upgrades if upgrades else "pass"} + + +def downgrade(): + ${downgrades if downgrades else "pass"} diff --git a/core/data/alembic/versions/1e150d16ab6b_chuni_add_net_battle_uk.py b/core/data/alembic/versions/1e150d16ab6b_chuni_add_net_battle_uk.py new file mode 100644 index 0000000..5fcd5a8 --- /dev/null +++ b/core/data/alembic/versions/1e150d16ab6b_chuni_add_net_battle_uk.py @@ -0,0 +1,27 @@ +"""chuni_add_net_battle_uk + +Revision ID: 1e150d16ab6b +Revises: b23f985100ba +Create Date: 2024-06-21 22:57:18.418488 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import mysql + +# revision identifiers, used by Alembic. +revision = '1e150d16ab6b' +down_revision = 'b23f985100ba' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_unique_constraint(None, 'chuni_profile_net_battle', ['user']) + # ### end Alembic commands ### + + +def downgrade(): + op.drop_constraint(None, 'chuni_profile_net_battle', type_='unique') + # ### end Alembic commands ### diff --git a/core/data/alembic/versions/2bf9f38d9444_add_event_log_info.py b/core/data/alembic/versions/2bf9f38d9444_add_event_log_info.py new file mode 100644 index 0000000..5c2492d --- /dev/null +++ b/core/data/alembic/versions/2bf9f38d9444_add_event_log_info.py @@ -0,0 +1,48 @@ +"""add_event_log_info + +Revision ID: 2bf9f38d9444 +Revises: 81e44dd6047a +Create Date: 2024-05-21 23:00:17.468407 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import mysql + +# revision identifiers, used by Alembic. +revision = '2bf9f38d9444' +down_revision = '81e44dd6047a' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('event_log', sa.Column('user', sa.INTEGER(), nullable=True)) + op.add_column('event_log', sa.Column('arcade', sa.INTEGER(), nullable=True)) + op.add_column('event_log', sa.Column('machine', sa.INTEGER(), nullable=True)) + op.add_column('event_log', sa.Column('ip', sa.TEXT(length=39), nullable=True)) + op.alter_column('event_log', 'when_logged', + existing_type=mysql.TIMESTAMP(), + server_default=sa.text('now()'), + existing_nullable=False) + op.create_foreign_key(None, 'event_log', 'machine', ['machine'], ['id'], onupdate='cascade', ondelete='cascade') + op.create_foreign_key(None, 'event_log', 'arcade', ['arcade'], ['id'], onupdate='cascade', ondelete='cascade') + op.create_foreign_key(None, 'event_log', 'aime_user', ['user'], ['id'], onupdate='cascade', ondelete='cascade') + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_constraint(None, 'event_log', type_='foreignkey') + op.drop_constraint(None, 'event_log', type_='foreignkey') + op.drop_constraint(None, 'event_log', type_='foreignkey') + op.alter_column('event_log', 'when_logged', + existing_type=mysql.TIMESTAMP(), + server_default=sa.text('current_timestamp()'), + existing_nullable=False) + op.drop_column('event_log', 'ip') + op.drop_column('event_log', 'machine') + op.drop_column('event_log', 'arcade') + op.drop_column('event_log', 'user') + # ### end Alembic commands ### diff --git a/core/data/alembic/versions/2d024cf145a1_add_event_log_game_version.py b/core/data/alembic/versions/2d024cf145a1_add_event_log_game_version.py new file mode 100644 index 0000000..b947763 --- /dev/null +++ b/core/data/alembic/versions/2d024cf145a1_add_event_log_game_version.py @@ -0,0 +1,46 @@ +"""add_event_log_game_version + +Revision ID: 2d024cf145a1 +Revises: 2bf9f38d9444 +Create Date: 2024-05-21 23:41:31.445331 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import mysql + +# revision identifiers, used by Alembic. +revision = '2d024cf145a1' +down_revision = '2bf9f38d9444' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('event_log', sa.Column('game', sa.TEXT(length=4), nullable=True)) + op.add_column('event_log', sa.Column('version', sa.TEXT(length=24), nullable=True)) + op.alter_column('event_log', 'ip', + existing_type=mysql.TINYTEXT(), + type_=sa.TEXT(length=39), + existing_nullable=True) + op.alter_column('event_log', 'when_logged', + existing_type=mysql.TIMESTAMP(), + server_default=sa.text('now()'), + existing_nullable=False) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.alter_column('event_log', 'when_logged', + existing_type=mysql.TIMESTAMP(), + server_default=sa.text('current_timestamp()'), + existing_nullable=False) + op.alter_column('event_log', 'ip', + existing_type=sa.TEXT(length=39), + type_=mysql.TINYTEXT(), + existing_nullable=True) + op.drop_column('event_log', 'version') + op.drop_column('event_log', 'game') + # ### end Alembic commands ### diff --git a/core/data/alembic/versions/3657efefc5a4_pokken_fix_pokemon_uk.py b/core/data/alembic/versions/3657efefc5a4_pokken_fix_pokemon_uk.py new file mode 100644 index 0000000..2867fb7 --- /dev/null +++ b/core/data/alembic/versions/3657efefc5a4_pokken_fix_pokemon_uk.py @@ -0,0 +1,54 @@ +"""pokken_fix_pokemon_uk + +Revision ID: 3657efefc5a4 +Revises: 4a02e623e5e6 +Create Date: 2024-06-13 23:50:57.611998 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import mysql + +# revision identifiers, used by Alembic. +revision = '3657efefc5a4' +down_revision = '4a02e623e5e6' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.alter_column('pokken_pokemon_data', 'char_id', + existing_type=mysql.INTEGER(display_width=11), + nullable=True) + op.alter_column('pokken_pokemon_data', 'illustration_book_no', + existing_type=mysql.INTEGER(display_width=11), + nullable=False) + op.drop_constraint('pokken_pokemon_data_ibfk_1', table_name='pokken_pokemon_data', type_='foreignkey') + op.drop_index('pokken_pokemon_data_uk', table_name='pokken_pokemon_data') + op.create_unique_constraint('pokken_pokemon_uk', 'pokken_pokemon_data', ['user', 'illustration_book_no']) + op.create_foreign_key("pokken_pokemon_data_ibfk_1", "pokken_pokemon_data", "aime_user", ['user'], ['id']) + op.alter_column('pokken_profile', 'trainer_name', + existing_type=mysql.VARCHAR(length=16), + type_=sa.String(length=14), + existing_nullable=True) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.alter_column('pokken_profile', 'trainer_name', + existing_type=sa.String(length=14), + type_=mysql.VARCHAR(length=16), + existing_nullable=True) + op.drop_constraint('pokken_pokemon_data_ibfk_1', table_name='pokken_pokemon_data', type_='foreignkey') + op.drop_constraint('pokken_pokemon_uk', 'pokken_pokemon_data', type_='unique') + op.create_index('pokken_pokemon_data_uk', 'pokken_pokemon_data', ['user', 'char_id'], unique=True) + op.create_foreign_key("pokken_pokemon_data_ibfk_1", "pokken_pokemon_data", "aime_user", ['user'], ['id']) + op.alter_column('pokken_pokemon_data', 'illustration_book_no', + existing_type=mysql.INTEGER(display_width=11), + nullable=True) + op.alter_column('pokken_pokemon_data', 'char_id', + existing_type=mysql.INTEGER(display_width=11), + nullable=False) + # ### end Alembic commands ### diff --git a/core/data/alembic/versions/48f4acc43a7e_card_add_idm_chip_id.py b/core/data/alembic/versions/48f4acc43a7e_card_add_idm_chip_id.py new file mode 100644 index 0000000..98872e8 --- /dev/null +++ b/core/data/alembic/versions/48f4acc43a7e_card_add_idm_chip_id.py @@ -0,0 +1,50 @@ +"""card_add_idm_chip_id + +Revision ID: 48f4acc43a7e +Revises: 1e150d16ab6b +Create Date: 2024-06-21 23:53:34.369134 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import mysql + +# revision identifiers, used by Alembic. +revision = '48f4acc43a7e' +down_revision = '1e150d16ab6b' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('aime_card', sa.Column('idm', sa.String(length=16), nullable=True)) + op.add_column('aime_card', sa.Column('chip_id', sa.BIGINT(), nullable=True)) + op.alter_column('aime_card', 'access_code', + existing_type=mysql.VARCHAR(length=20), + nullable=False) + op.alter_column('aime_card', 'created_date', + existing_type=mysql.TIMESTAMP(), + server_default=sa.text('now()'), + existing_nullable=True) + op.create_unique_constraint(None, 'aime_card', ['chip_id']) + op.create_unique_constraint(None, 'aime_card', ['idm']) + op.create_unique_constraint(None, 'aime_card', ['access_code']) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_constraint("chip_id", 'aime_card', type_='unique') + op.drop_constraint("idm", 'aime_card', type_='unique') + op.drop_constraint("access_code", 'aime_card', type_='unique') + op.alter_column('aime_card', 'created_date', + existing_type=mysql.TIMESTAMP(), + server_default=sa.text('CURRENT_TIMESTAMP'), + existing_nullable=True) + op.alter_column('aime_card', 'access_code', + existing_type=mysql.VARCHAR(length=20), + nullable=True) + op.drop_column('aime_card', 'chip_id') + op.drop_column('aime_card', 'idm') + # ### end Alembic commands ### diff --git a/core/data/alembic/versions/4a02e623e5e6_mai2_add_favs_rivals.py b/core/data/alembic/versions/4a02e623e5e6_mai2_add_favs_rivals.py new file mode 100644 index 0000000..d221bb6 --- /dev/null +++ b/core/data/alembic/versions/4a02e623e5e6_mai2_add_favs_rivals.py @@ -0,0 +1,48 @@ +"""mai2_add_favs_rivals + +Revision ID: 4a02e623e5e6 +Revises: 8ad40a6e7be2 +Create Date: 2024-06-08 19:02:43.856395 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import mysql + +# revision identifiers, used by Alembic. +revision = '4a02e623e5e6' +down_revision = '8ad40a6e7be2' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('mai2_item_favorite_music', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('user', sa.Integer(), nullable=False), + sa.Column('musicId', sa.Integer(), nullable=False), + sa.ForeignKeyConstraint(['user'], ['aime_user.id'], onupdate='cascade', ondelete='cascade'), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('user', 'musicId', name='mai2_item_favorite_music_uk'), + mysql_charset='utf8mb4' + ) + op.create_table('mai2_user_rival', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('user', sa.Integer(), nullable=False), + sa.Column('rival', sa.Integer(), nullable=False), + sa.Column('show', sa.Boolean(), server_default='0', nullable=False), + sa.ForeignKeyConstraint(['rival'], ['aime_user.id'], onupdate='cascade', ondelete='cascade'), + sa.ForeignKeyConstraint(['user'], ['aime_user.id'], onupdate='cascade', ondelete='cascade'), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('user', 'rival', name='mai2_user_rival_uk'), + mysql_charset='utf8mb4' + ) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('mai2_user_rival') + op.drop_table('mai2_item_favorite_music') + # ### end Alembic commands ### diff --git a/core/data/alembic/versions/5ea363686347_mai2_presents.py b/core/data/alembic/versions/5ea363686347_mai2_presents.py new file mode 100644 index 0000000..8e07a5c --- /dev/null +++ b/core/data/alembic/versions/5ea363686347_mai2_presents.py @@ -0,0 +1,41 @@ +"""mai2_presents + +Revision ID: 5ea363686347 +Revises: 680789dabab3 +Create Date: 2024-06-28 14:49:07.666879 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import mysql + +# revision identifiers, used by Alembic. +revision = '5ea363686347' +down_revision = '680789dabab3' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('mai2_item_present', + sa.Column('id', sa.BIGINT(), nullable=False), + sa.Column('version', sa.INTEGER(), nullable=True), + sa.Column('user', sa.Integer(), nullable=True), + sa.Column('itemKind', sa.INTEGER(), nullable=False), + sa.Column('itemId', sa.INTEGER(), nullable=False), + sa.Column('stock', sa.INTEGER(), server_default='1', nullable=False), + sa.Column('startDate', sa.TIMESTAMP(), nullable=True), + sa.Column('endDate', sa.TIMESTAMP(), nullable=True), + sa.ForeignKeyConstraint(['user'], ['aime_user.id'], onupdate='cascade', ondelete='cascade'), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('version', 'user', 'itemKind', 'itemId', name='mai2_item_present_uk'), + mysql_charset='utf8mb4' + ) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('mai2_item_present') + # ### end Alembic commands ### diff --git a/core/data/alembic/versions/5ea73f89d982_card_add_memo.py b/core/data/alembic/versions/5ea73f89d982_card_add_memo.py new file mode 100644 index 0000000..84c8a18 --- /dev/null +++ b/core/data/alembic/versions/5ea73f89d982_card_add_memo.py @@ -0,0 +1,28 @@ +"""card_add_memo + +Revision ID: 5ea73f89d982 +Revises: 745448d83696 +Create Date: 2024-07-06 22:46:56.992152 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import mysql + +# revision identifiers, used by Alembic. +revision = '5ea73f89d982' +down_revision = '745448d83696' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('aime_card', sa.Column('memo', sa.VARCHAR(length=16), nullable=True)) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('aime_card', 'memo') + # ### end Alembic commands ### diff --git a/core/data/alembic/versions/680789dabab3_sao_player_changes.py b/core/data/alembic/versions/680789dabab3_sao_player_changes.py new file mode 100644 index 0000000..3e4aef7 --- /dev/null +++ b/core/data/alembic/versions/680789dabab3_sao_player_changes.py @@ -0,0 +1,295 @@ +"""sao_player_changes + +Revision ID: 680789dabab3 +Revises: a616fd164e40 +Create Date: 2024-06-26 23:19:16.863778 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import mysql + +# revision identifiers, used by Alembic. +revision = '680789dabab3' +down_revision = 'a616fd164e40' +branch_labels = None +depends_on = None + + +def upgrade(): + op.add_column('sao_equipment_data', sa.Column('is_shop_purchase', sa.BOOLEAN(), server_default='0', nullable=False)) + op.add_column('sao_equipment_data', sa.Column('is_protect', sa.BOOLEAN(), server_default='0', nullable=False)) + op.add_column('sao_equipment_data', sa.Column('property1_property_id', sa.BIGINT(), server_default='2', nullable=False)) + op.add_column('sao_equipment_data', sa.Column('property1_value1', sa.INTEGER(), server_default='0', nullable=False)) + op.add_column('sao_equipment_data', sa.Column('property1_value2', sa.INTEGER(), server_default='0', nullable=False)) + op.add_column('sao_equipment_data', sa.Column('property2_property_id', sa.BIGINT(), server_default='2', nullable=False)) + op.add_column('sao_equipment_data', sa.Column('property2_value1', sa.INTEGER(), server_default='0', nullable=False)) + op.add_column('sao_equipment_data', sa.Column('property2_value2', sa.INTEGER(), server_default='0', nullable=False)) + op.add_column('sao_equipment_data', sa.Column('property3_property_id', sa.BIGINT(), server_default='2', nullable=False)) + op.add_column('sao_equipment_data', sa.Column('property3_value1', sa.INTEGER(), server_default='0', nullable=False)) + op.add_column('sao_equipment_data', sa.Column('property3_value2', sa.INTEGER(), server_default='0', nullable=False)) + op.add_column('sao_equipment_data', sa.Column('property4_property_id', sa.BIGINT(), server_default='2', nullable=False)) + op.add_column('sao_equipment_data', sa.Column('property4_value1', sa.INTEGER(), server_default='0', nullable=False)) + op.add_column('sao_equipment_data', sa.Column('property4_value2', sa.INTEGER(), server_default='0', nullable=False)) + op.add_column('sao_equipment_data', sa.Column('converted_card_num', sa.INTEGER(), server_default='0', nullable=False)) + op.alter_column('sao_equipment_data', 'equipment_id', + existing_type=mysql.INTEGER(), + type_=sa.BIGINT(), + existing_nullable=False) + op.alter_column('sao_equipment_data', 'get_date', + existing_type=mysql.TIMESTAMP(), + server_default=sa.text('now()'), + existing_nullable=False) + op.create_foreign_key(None, 'sao_equipment_data', 'sao_static_property', ['property2_property_id'], ['PropertyId'], onupdate='cascade', ondelete='cascade') + op.create_foreign_key(None, 'sao_equipment_data', 'sao_static_property', ['property4_property_id'], ['PropertyId'], onupdate='cascade', ondelete='cascade') + op.create_foreign_key(None, 'sao_equipment_data', 'sao_static_property', ['property3_property_id'], ['PropertyId'], onupdate='cascade', ondelete='cascade') + op.create_foreign_key(None, 'sao_equipment_data', 'sao_static_property', ['property1_property_id'], ['PropertyId'], onupdate='cascade', ondelete='cascade') + op.create_foreign_key(None, 'sao_equipment_data', 'sao_static_equipment_list', ['equipment_id'], ['EquipmentId'], onupdate='cascade', ondelete='cascade') + op.add_column('sao_hero_log_data', sa.Column('max_level_extend_num', sa.INTEGER(), server_default='0', nullable=False)) + op.add_column('sao_hero_log_data', sa.Column('is_awakenable', sa.BOOLEAN(), server_default='0', nullable=False)) + op.add_column('sao_hero_log_data', sa.Column('awakening_stage', sa.INTEGER(), server_default='0', nullable=False)) + op.add_column('sao_hero_log_data', sa.Column('awakening_exp', sa.INTEGER(), server_default='0', nullable=False)) + op.add_column('sao_hero_log_data', sa.Column('is_shop_purchase', sa.BOOLEAN(), server_default='0', nullable=False)) + op.add_column('sao_hero_log_data', sa.Column('is_protect', sa.BOOLEAN(), server_default='0', nullable=False)) + op.add_column('sao_hero_log_data', sa.Column('property1_property_id', sa.BIGINT(), server_default='2', nullable=False)) + op.add_column('sao_hero_log_data', sa.Column('property1_value1', sa.INTEGER(), server_default='0', nullable=False)) + op.add_column('sao_hero_log_data', sa.Column('property1_value2', sa.INTEGER(), server_default='0', nullable=False)) + op.add_column('sao_hero_log_data', sa.Column('property2_property_id', sa.BIGINT(), server_default='2', nullable=False)) + op.add_column('sao_hero_log_data', sa.Column('property2_value1', sa.INTEGER(), server_default='0', nullable=False)) + op.add_column('sao_hero_log_data', sa.Column('property2_value2', sa.INTEGER(), server_default='0', nullable=False)) + op.add_column('sao_hero_log_data', sa.Column('property3_property_id', sa.BIGINT(), server_default='2', nullable=False)) + op.add_column('sao_hero_log_data', sa.Column('property3_value1', sa.INTEGER(), server_default='0', nullable=False)) + op.add_column('sao_hero_log_data', sa.Column('property3_value2', sa.INTEGER(), server_default='0', nullable=False)) + op.add_column('sao_hero_log_data', sa.Column('property4_property_id', sa.BIGINT(), server_default='2', nullable=False)) + op.add_column('sao_hero_log_data', sa.Column('property4_value1', sa.INTEGER(), server_default='0', nullable=False)) + op.add_column('sao_hero_log_data', sa.Column('property4_value2', sa.INTEGER(), server_default='0', nullable=False)) + op.add_column('sao_hero_log_data', sa.Column('converted_card_num', sa.INTEGER(), server_default='0', nullable=False)) + op.alter_column('sao_hero_log_data', 'main_weapon', + existing_type=mysql.INTEGER(), + nullable=True) + op.alter_column('sao_hero_log_data', 'sub_equipment', + existing_type=mysql.INTEGER(), + nullable=True) + op.alter_column('sao_hero_log_data', 'skill_slot1_skill_id', + existing_type=mysql.INTEGER(), + type_=sa.BIGINT(), + nullable=True) + op.alter_column('sao_hero_log_data', 'skill_slot2_skill_id', + existing_type=mysql.INTEGER(), + type_=sa.BIGINT(), + nullable=True) + op.alter_column('sao_hero_log_data', 'skill_slot3_skill_id', + existing_type=mysql.INTEGER(), + type_=sa.BIGINT(), + nullable=True) + op.alter_column('sao_hero_log_data', 'skill_slot4_skill_id', + existing_type=mysql.INTEGER(), + type_=sa.BIGINT(), + nullable=True) + op.alter_column('sao_hero_log_data', 'skill_slot5_skill_id', + existing_type=mysql.INTEGER(), + type_=sa.BIGINT(), + nullable=True) + op.alter_column('sao_hero_log_data', 'get_date', + existing_type=mysql.TIMESTAMP(), + server_default=sa.text('now()'), + existing_nullable=False) + op.alter_column("sao_hero_log_data", "user_hero_log_id", + existing_type=sa.Integer(), + new_column_name="hero_log_id", + type_=sa.BIGINT(), + nullable=False) + op.execute(sa.text("UPDATE sao_hero_log_data SET skill_slot1_skill_id = NULL WHERE skill_slot1_skill_id = 0;")) + op.execute(sa.text("UPDATE sao_hero_log_data SET skill_slot2_skill_id = NULL WHERE skill_slot2_skill_id = 0;")) + op.execute(sa.text("UPDATE sao_hero_log_data SET skill_slot3_skill_id = NULL WHERE skill_slot3_skill_id = 0;")) + op.execute(sa.text("UPDATE sao_hero_log_data SET skill_slot4_skill_id = NULL WHERE skill_slot4_skill_id = 0;")) + op.execute(sa.text("UPDATE sao_hero_log_data SET skill_slot5_skill_id = NULL WHERE skill_slot5_skill_id = 0;")) + op.execute(sa.text("UPDATE sao_hero_log_data SET main_weapon = NULL WHERE main_weapon = 0;")) + op.execute(sa.text("UPDATE sao_hero_log_data SET sub_equipment = NULL WHERE sub_equipment = 0;")) + op.execute(sa.text("UPDATE sao_hero_party SET user_hero_log_id_1 = NULL WHERE user_hero_log_id_1 = 0;")) + op.execute(sa.text("UPDATE sao_hero_party SET user_hero_log_id_2 = NULL WHERE user_hero_log_id_2 = 0;")) + op.execute(sa.text("UPDATE sao_hero_party SET user_hero_log_id_3 = NULL WHERE user_hero_log_id_3 = 0;")) + + op.execute(sa.text("UPDATE sao_hero_log_data INNER JOIN sao_equipment_data ON sao_hero_log_data.main_weapon = sao_equipment_data.equipment_id SET sao_hero_log_data.main_weapon = sao_equipment_data.id;")) + op.execute(sa.text("UPDATE sao_hero_log_data INNER JOIN sao_equipment_data ON sao_hero_log_data.sub_equipment = sao_equipment_data.equipment_id SET sao_hero_log_data.sub_equipment = sao_equipment_data.id;")) + + op.execute(sa.text("UPDATE sao_hero_party INNER JOIN sao_hero_log_data ON sao_hero_party.user_hero_log_id_1 = sao_hero_log_data.hero_log_id SET sao_hero_party.user_hero_log_id_1 = sao_hero_log_data.id;")) + op.execute(sa.text("UPDATE sao_hero_party INNER JOIN sao_hero_log_data ON sao_hero_party.user_hero_log_id_2 = sao_hero_log_data.hero_log_id SET sao_hero_party.user_hero_log_id_2 = sao_hero_log_data.id;")) + op.execute(sa.text("UPDATE sao_hero_party INNER JOIN sao_hero_log_data ON sao_hero_party.user_hero_log_id_3 = sao_hero_log_data.hero_log_id SET sao_hero_party.user_hero_log_id_3 = sao_hero_log_data.id;")) + + op.create_foreign_key(None, 'sao_hero_log_data', 'sao_static_property', ['property4_property_id'], ['PropertyId'], onupdate='cascade', ondelete='cascade') + op.create_foreign_key(None, 'sao_hero_log_data', 'sao_static_skill', ['skill_slot1_skill_id'], ['SkillId'], onupdate='set null', ondelete='set null') + op.create_foreign_key(None, 'sao_hero_log_data', 'sao_static_skill', ['skill_slot5_skill_id'], ['SkillId'], onupdate='set null', ondelete='set null') + op.create_foreign_key(None, 'sao_hero_log_data', 'sao_static_skill', ['skill_slot2_skill_id'], ['SkillId'], onupdate='set null', ondelete='set null') + op.create_foreign_key(None, 'sao_hero_log_data', 'sao_static_skill', ['skill_slot3_skill_id'], ['SkillId'], onupdate='set null', ondelete='set null') + op.create_foreign_key(None, 'sao_hero_log_data', 'sao_equipment_data', ['main_weapon'], ['id'], onupdate='set null', ondelete='set null') + op.create_foreign_key(None, 'sao_hero_log_data', 'sao_static_property', ['property3_property_id'], ['PropertyId'], onupdate='cascade', ondelete='cascade') + op.create_foreign_key(None, 'sao_hero_log_data', 'sao_static_skill', ['skill_slot4_skill_id'], ['SkillId'], onupdate='set null', ondelete='set null') + op.create_foreign_key(None, 'sao_hero_log_data', 'sao_equipment_data', ['sub_equipment'], ['id'], onupdate='set null', ondelete='set null') + op.create_foreign_key(None, 'sao_hero_log_data', 'sao_static_property', ['property1_property_id'], ['PropertyId'], onupdate='cascade', ondelete='cascade') + op.create_foreign_key(None, 'sao_hero_log_data', 'sao_static_hero_list', ['hero_log_id'], ['HeroLogId'], onupdate='cascade', ondelete='cascade') + op.create_foreign_key(None, 'sao_hero_log_data', 'sao_static_property', ['property2_property_id'], ['PropertyId'], onupdate='cascade', ondelete='cascade') + op.create_foreign_key(None, 'sao_hero_party', 'sao_hero_log_data', ['user_hero_log_id_3'], ['id'], onupdate='cascade', ondelete='cascade') + op.create_foreign_key(None, 'sao_hero_party', 'sao_hero_log_data', ['user_hero_log_id_1'], ['id'], onupdate='cascade', ondelete='cascade') + op.create_foreign_key(None, 'sao_hero_party', 'sao_hero_log_data', ['user_hero_log_id_2'], ['id'], onupdate='cascade', ondelete='cascade') + op.alter_column('sao_item_data', 'get_date', + existing_type=mysql.TIMESTAMP(), + server_default=sa.text('now()'), + existing_nullable=False) + op.alter_column('sao_play_sessions', 'play_date', + existing_type=mysql.TIMESTAMP(), + server_default=sa.text('now()'), + existing_nullable=False) + op.add_column('sao_player_quest', sa.Column('quest_type', sa.INTEGER(), server_default='1', nullable=False)) + op.alter_column('sao_player_quest', 'play_date', + existing_type=mysql.TIMESTAMP(), + server_default=sa.text('now()'), + existing_nullable=False) + op.alter_column('sao_player_quest', 'episode_id', + existing_type=mysql.INTEGER(), + new_column_name="quest_scene_id", + type_=sa.BIGINT(), + nullable=False) + op.create_foreign_key(None, 'sao_player_quest', 'sao_static_quest', ['quest_scene_id'], ['QuestSceneId'], onupdate='cascade', ondelete='cascade') + op.add_column('sao_profile', sa.Column('my_shop', sa.INTEGER(), nullable=True)) + op.add_column('sao_profile', sa.Column('fav_hero', sa.INTEGER(), nullable=True)) + op.add_column('sao_profile', sa.Column('when_register', sa.TIMESTAMP(), server_default=sa.text('now()'), nullable=True)) + op.add_column('sao_profile', sa.Column('last_login_date', sa.TIMESTAMP(), nullable=True)) + op.add_column('sao_profile', sa.Column('last_yui_medal_date', sa.TIMESTAMP(), nullable=True)) + op.add_column('sao_profile', sa.Column('last_bonus_yui_medal_date', sa.TIMESTAMP(), nullable=True)) + op.add_column('sao_profile', sa.Column('last_comeback_date', sa.TIMESTAMP(), nullable=True)) + op.add_column('sao_profile', sa.Column('last_login_bonus_date', sa.TIMESTAMP(), nullable=True)) + op.add_column('sao_profile', sa.Column('ad_confirm_date', sa.TIMESTAMP(), nullable=True)) + op.add_column('sao_profile', sa.Column('login_ct', sa.INTEGER(), server_default='0', nullable=True)) + op.create_foreign_key(None, 'sao_profile', 'sao_hero_log_data', ['fav_hero'], ['id'], onupdate='cascade', ondelete='set null') + + +def downgrade(): + op.drop_constraint("sao_profile_ibfk_2", 'sao_profile', type_='foreignkey') + op.drop_column('sao_profile', 'login_ct') + op.drop_column('sao_profile', 'ad_confirm_date') + op.drop_column('sao_profile', 'last_login_bonus_date') + op.drop_column('sao_profile', 'last_comeback_date') + op.drop_column('sao_profile', 'last_bonus_yui_medal_date') + op.drop_column('sao_profile', 'last_yui_medal_date') + op.drop_column('sao_profile', 'last_login_date') + op.drop_column('sao_profile', 'when_register') + op.drop_column('sao_profile', 'fav_hero') + op.drop_column('sao_profile', 'my_shop') + op.alter_column('sao_player_quest', 'quest_scene_id', + existing_type=mysql.BIGINT(), + new_column_name="episode_id", + type_=sa.INTEGER(), + nullable=False) + op.drop_constraint("sao_player_quest_ibfk_2", 'sao_player_quest', type_='foreignkey') + op.alter_column('sao_player_quest', 'play_date', + existing_type=mysql.TIMESTAMP(), + server_default=sa.text('CURRENT_TIMESTAMP'), + existing_nullable=False) + op.drop_column('sao_player_quest', 'quest_scene_id') + op.drop_column('sao_player_quest', 'quest_type') + op.alter_column('sao_play_sessions', 'play_date', + existing_type=mysql.TIMESTAMP(), + server_default=sa.text('CURRENT_TIMESTAMP'), + existing_nullable=False) + op.alter_column('sao_item_data', 'get_date', + existing_type=mysql.TIMESTAMP(), + server_default=sa.text('CURRENT_TIMESTAMP'), + existing_nullable=False) + op.drop_constraint("sao_hero_party_ibfk_2", 'sao_hero_party', type_='foreignkey') + op.drop_constraint("sao_hero_party_ibfk_3", 'sao_hero_party', type_='foreignkey') + op.drop_constraint("sao_hero_party_ibfk_4", 'sao_hero_party', type_='foreignkey') + op.alter_column("sao_hero_log_data", "hero_log_id", + existing_type=sa.BIGINT(), + new_column_name="user_hero_log_id", + type_=sa.Integer(), + nullable=False) + op.drop_constraint("sao_hero_log_data_ibfk_2", 'sao_hero_log_data', type_='foreignkey') + op.drop_constraint("sao_hero_log_data_ibfk_3", 'sao_hero_log_data', type_='foreignkey') + op.drop_constraint("sao_hero_log_data_ibfk_4", 'sao_hero_log_data', type_='foreignkey') + op.drop_constraint("sao_hero_log_data_ibfk_5", 'sao_hero_log_data', type_='foreignkey') + op.drop_constraint("sao_hero_log_data_ibfk_6", 'sao_hero_log_data', type_='foreignkey') + op.drop_constraint("sao_hero_log_data_ibfk_7", 'sao_hero_log_data', type_='foreignkey') + op.drop_constraint("sao_hero_log_data_ibfk_8", 'sao_hero_log_data', type_='foreignkey') + op.drop_constraint("sao_hero_log_data_ibfk_9", 'sao_hero_log_data', type_='foreignkey') + op.drop_constraint("sao_hero_log_data_ibfk_10", 'sao_hero_log_data', type_='foreignkey') + op.drop_constraint("sao_hero_log_data_ibfk_11", 'sao_hero_log_data', type_='foreignkey') + op.drop_constraint("sao_hero_log_data_ibfk_12", 'sao_hero_log_data', type_='foreignkey') + op.drop_constraint("sao_hero_log_data_ibfk_13", 'sao_hero_log_data', type_='foreignkey') + op.alter_column('sao_hero_log_data', 'get_date', + existing_type=mysql.TIMESTAMP(), + server_default=sa.text('CURRENT_TIMESTAMP'), + existing_nullable=False) + op.alter_column('sao_hero_log_data', 'skill_slot5_skill_id', + existing_type=sa.BIGINT(), + type_=mysql.INTEGER(), + nullable=False) + op.alter_column('sao_hero_log_data', 'skill_slot4_skill_id', + existing_type=sa.BIGINT(), + type_=mysql.INTEGER(), + nullable=False) + op.alter_column('sao_hero_log_data', 'skill_slot3_skill_id', + existing_type=sa.BIGINT(), + type_=mysql.INTEGER(), + nullable=False) + op.alter_column('sao_hero_log_data', 'skill_slot2_skill_id', + existing_type=sa.BIGINT(), + type_=mysql.INTEGER(), + nullable=False) + op.alter_column('sao_hero_log_data', 'skill_slot1_skill_id', + existing_type=sa.BIGINT(), + type_=mysql.INTEGER(), + nullable=False) + op.alter_column('sao_hero_log_data', 'sub_equipment', + existing_type=mysql.INTEGER(), + nullable=False) + op.alter_column('sao_hero_log_data', 'main_weapon', + existing_type=mysql.INTEGER(), + nullable=False) + op.drop_column('sao_hero_log_data', 'converted_card_num') + op.drop_column('sao_hero_log_data', 'property4_value2') + op.drop_column('sao_hero_log_data', 'property4_value1') + op.drop_column('sao_hero_log_data', 'property4_property_id') + op.drop_column('sao_hero_log_data', 'property3_value2') + op.drop_column('sao_hero_log_data', 'property3_value1') + op.drop_column('sao_hero_log_data', 'property3_property_id') + op.drop_column('sao_hero_log_data', 'property2_value2') + op.drop_column('sao_hero_log_data', 'property2_value1') + op.drop_column('sao_hero_log_data', 'property2_property_id') + op.drop_column('sao_hero_log_data', 'property1_value2') + op.drop_column('sao_hero_log_data', 'property1_value1') + op.drop_column('sao_hero_log_data', 'property1_property_id') + op.drop_column('sao_hero_log_data', 'is_protect') + op.drop_column('sao_hero_log_data', 'is_shop_purchase') + op.drop_column('sao_hero_log_data', 'awakening_exp') + op.drop_column('sao_hero_log_data', 'awakening_stage') + op.drop_column('sao_hero_log_data', 'is_awakenable') + op.drop_column('sao_hero_log_data', 'max_level_extend_num') + op.drop_constraint("sao_equipment_data_ibfk_2", 'sao_equipment_data', type_='foreignkey') + op.drop_constraint("sao_equipment_data_ibfk_3", 'sao_equipment_data', type_='foreignkey') + op.drop_constraint("sao_equipment_data_ibfk_4", 'sao_equipment_data', type_='foreignkey') + op.drop_constraint("sao_equipment_data_ibfk_5", 'sao_equipment_data', type_='foreignkey') + op.drop_constraint("sao_equipment_data_ibfk_6", 'sao_equipment_data', type_='foreignkey') + op.alter_column('sao_equipment_data', 'get_date', + existing_type=mysql.TIMESTAMP(), + server_default=sa.text('CURRENT_TIMESTAMP'), + existing_nullable=False) + op.alter_column('sao_equipment_data', 'equipment_id', + existing_type=sa.BIGINT(), + type_=mysql.INTEGER(), + existing_nullable=False) + op.drop_column('sao_equipment_data', 'converted_card_num') + op.drop_column('sao_equipment_data', 'property4_value2') + op.drop_column('sao_equipment_data', 'property4_value1') + op.drop_column('sao_equipment_data', 'property4_property_id') + op.drop_column('sao_equipment_data', 'property3_value2') + op.drop_column('sao_equipment_data', 'property3_value1') + op.drop_column('sao_equipment_data', 'property3_property_id') + op.drop_column('sao_equipment_data', 'property2_value2') + op.drop_column('sao_equipment_data', 'property2_value1') + op.drop_column('sao_equipment_data', 'property2_property_id') + op.drop_column('sao_equipment_data', 'property1_value2') + op.drop_column('sao_equipment_data', 'property1_value1') + op.drop_column('sao_equipment_data', 'property1_property_id') + op.drop_column('sao_equipment_data', 'is_protect') + op.drop_column('sao_equipment_data', 'is_shop_purchase') diff --git a/core/data/alembic/versions/6a7e8277763b_gekichu_rating_tables.py b/core/data/alembic/versions/6a7e8277763b_gekichu_rating_tables.py new file mode 100644 index 0000000..2d4074a --- /dev/null +++ b/core/data/alembic/versions/6a7e8277763b_gekichu_rating_tables.py @@ -0,0 +1,56 @@ +"""GekiChu rating tables + +Revision ID: 6a7e8277763b +Revises: d8950c7ce2fc +Create Date: 2024-03-13 12:18:53.210018 + +""" +from alembic import op +from sqlalchemy import Column, Integer, String + + +# revision identifiers, used by Alembic. +revision = '6a7e8277763b' +down_revision = 'd8950c7ce2fc' +branch_labels = None +depends_on = None + +GEKICHU_RATING_TABLE_NAMES = [ + "chuni_profile_rating", + "ongeki_profile_rating", +] + +def upgrade(): + for table_name in GEKICHU_RATING_TABLE_NAMES: + op.create_table( + table_name, + Column("id", Integer, primary_key=True, nullable=False), + Column("user", Integer, nullable=False), + Column("version", Integer, nullable=False), + Column("type", String(255), nullable=False), + Column("index", Integer, nullable=False), + Column("musicId", Integer), + Column("difficultId", Integer), + Column("romVersionCode", Integer), + Column("score", Integer), + mysql_charset="utf8mb4", + ) + op.create_foreign_key( + None, + table_name, + "aime_user", + ["user"], + ["id"], + ondelete="cascade", + onupdate="cascade", + ) + op.create_unique_constraint( + f"{table_name}_uk", + table_name, + ["user", "version", "type", "index"], + ) + + +def downgrade(): + for table_name in GEKICHU_RATING_TABLE_NAMES: + op.drop_table(table_name) diff --git a/core/data/alembic/versions/745448d83696_chuni_team_points.py b/core/data/alembic/versions/745448d83696_chuni_team_points.py new file mode 100644 index 0000000..2a8465a --- /dev/null +++ b/core/data/alembic/versions/745448d83696_chuni_team_points.py @@ -0,0 +1,28 @@ +"""chuni_team_points + +Revision ID: 745448d83696 +Revises: 5ea363686347 +Create Date: 2024-06-29 00:05:22.479187 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import mysql + +# revision identifiers, used by Alembic. +revision = '745448d83696' +down_revision = '5ea363686347' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('chuni_profile_team', sa.Column('userTeamPoint', sa.JSON(), nullable=True)) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('chuni_profile_team', 'userTeamPoint') + # ### end Alembic commands ### diff --git a/core/data/alembic/versions/7dc13e364e53_cxb_add_playlog_grade.py b/core/data/alembic/versions/7dc13e364e53_cxb_add_playlog_grade.py new file mode 100644 index 0000000..abde273 --- /dev/null +++ b/core/data/alembic/versions/7dc13e364e53_cxb_add_playlog_grade.py @@ -0,0 +1,28 @@ +"""cxb_add_playlog_grade + +Revision ID: 7dc13e364e53 +Revises: 2d024cf145a1 +Create Date: 2024-05-28 22:31:22.264926 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import mysql + +# revision identifiers, used by Alembic. +revision = '7dc13e364e53' +down_revision = '2d024cf145a1' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('cxb_playlog', sa.Column('grade', sa.Integer(), nullable=True)) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('cxb_playlog', 'grade') + # ### end Alembic commands ### diff --git a/core/data/alembic/versions/81e44dd6047a_mai2_buddies_support.py b/core/data/alembic/versions/81e44dd6047a_mai2_buddies_support.py new file mode 100644 index 0000000..0a2267c --- /dev/null +++ b/core/data/alembic/versions/81e44dd6047a_mai2_buddies_support.py @@ -0,0 +1,68 @@ +"""mai2_buddies_support + +Revision ID: 81e44dd6047a +Revises: 6a7e8277763b +Create Date: 2024-03-12 19:10:37.063907 + +""" + +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import mysql + +# revision identifiers, used by Alembic. +revision = "81e44dd6047a" +down_revision = "6a7e8277763b" +branch_labels = None +depends_on = None + + +def upgrade(): + op.create_table( + "mai2_playlog_2p", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("user", sa.Integer(), nullable=False), + sa.Column("userId1", sa.Integer(), nullable=True), + sa.Column("userId2", sa.Integer(), nullable=True), + sa.Column("userName1", sa.String(length=25), nullable=True), + sa.Column("userName2", sa.String(length=25), nullable=True), + sa.Column("regionId", sa.Integer(), nullable=True), + sa.Column("placeId", sa.Integer(), nullable=True), + sa.Column("user2pPlaylogDetailList", sa.JSON(), nullable=True), + sa.ForeignKeyConstraint( + ["user"], ["aime_user.id"], onupdate="cascade", ondelete="cascade" + ), + sa.PrimaryKeyConstraint("id"), + mysql_charset="utf8mb4", + ) + + op.add_column( + "mai2_playlog", + sa.Column( + "extBool1", sa.Boolean(), nullable=True, server_default=sa.text("NULL") + ), + ) + + op.add_column( + "mai2_profile_detail", + sa.Column( + "renameCredit", sa.Integer(), nullable=True, server_default=sa.text("NULL") + ), + ) + op.add_column( + "mai2_profile_detail", + sa.Column( + "currentPlayCount", + sa.Integer(), + nullable=True, + server_default=sa.text("NULL"), + ), + ) + + +def downgrade(): + op.drop_table("mai2_playlog_2p") + + op.drop_column("mai2_playlog", "extBool1") + op.drop_column("mai2_profile_detail", "renameCredit") + op.drop_column("mai2_profile_detail", "currentPlayCount") diff --git a/core/data/alembic/versions/835b862f9bf0_initial_migration.py b/core/data/alembic/versions/835b862f9bf0_initial_migration.py new file mode 100644 index 0000000..bea17d7 --- /dev/null +++ b/core/data/alembic/versions/835b862f9bf0_initial_migration.py @@ -0,0 +1,24 @@ +"""Initial Migration + +Revision ID: 835b862f9bf0 +Revises: +Create Date: 2024-01-09 13:06:10.787432 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '835b862f9bf0' +down_revision = None +branch_labels = None +depends_on = None + + +def upgrade(): + pass + + +def downgrade(): + pass diff --git a/core/data/alembic/versions/8ad40a6e7be2_ongeki_fix_clearstatus.py b/core/data/alembic/versions/8ad40a6e7be2_ongeki_fix_clearstatus.py new file mode 100644 index 0000000..8b6ec51 --- /dev/null +++ b/core/data/alembic/versions/8ad40a6e7be2_ongeki_fix_clearstatus.py @@ -0,0 +1,30 @@ +"""ongeki: fix clearStatus + +Revision ID: 8ad40a6e7be2 +Revises: 7dc13e364e53 +Create Date: 2024-05-29 19:03:30.062157 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import mysql + +# revision identifiers, used by Alembic. +revision = '8ad40a6e7be2' +down_revision = '7dc13e364e53' +branch_labels = None +depends_on = None + + +def upgrade(): + op.alter_column('ongeki_score_best', 'clearStatus', + existing_type=mysql.TINYINT(display_width=1), + type_=sa.Integer(), + existing_nullable=False) + + +def downgrade(): + op.alter_column('ongeki_score_best', 'clearStatus', + existing_type=sa.Integer(), + type_=mysql.TINYINT(display_width=1), + existing_nullable=False) diff --git a/core/data/alembic/versions/a616fd164e40_sao_backport.py b/core/data/alembic/versions/a616fd164e40_sao_backport.py new file mode 100644 index 0000000..5228e0b --- /dev/null +++ b/core/data/alembic/versions/a616fd164e40_sao_backport.py @@ -0,0 +1,437 @@ +"""sao_backport + +Revision ID: a616fd164e40 +Revises: 48f4acc43a7e +Create Date: 2024-06-24 20:28:34.471282 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import mysql + +# revision identifiers, used by Alembic. +revision = 'a616fd164e40' +down_revision = '48f4acc43a7e' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('sao_static_quest') + op.create_table('sao_static_quest', + sa.Column('QuestSceneId', sa.BIGINT(), nullable=False), + sa.Column('SortNo', sa.INTEGER(), nullable=False), + sa.Column('Tutorial', sa.BOOLEAN(), nullable=False), + sa.Column('ColRate', sa.DECIMAL(), nullable=False), + sa.Column('LimitDefault', sa.INTEGER(), nullable=False), + sa.Column('LimitResurrection', sa.INTEGER(), nullable=False), + sa.Column('RewardTableSubId', sa.INTEGER(), nullable=False), + sa.Column('PlayerTraceTableSubId', sa.INTEGER(), nullable=False), + sa.Column('SuccessPlayerExp', sa.INTEGER(), nullable=False), + sa.Column('FailedPlayerExp', sa.INTEGER(), nullable=False), + sa.Column('PairExpRate', sa.INTEGER(), nullable=False), + sa.Column('TrioExpRate', sa.INTEGER(), nullable=False), + sa.Column('SingleRewardVp', sa.INTEGER(), nullable=False), + sa.Column('PairRewardVp', sa.INTEGER(), nullable=False), + sa.Column('TrioRewardVp', sa.INTEGER(), nullable=False), + sa.PrimaryKeyConstraint('QuestSceneId'), + mysql_charset='utf8mb4' + ) + + op.create_table('sao_static_property', + sa.Column('PropertyId', sa.BIGINT(), nullable=False), + sa.Column('PropertyTargetType', sa.INTEGER(), nullable=False), + sa.Column('PropertyName', sa.VARCHAR(length=255), nullable=False), + sa.Column('PropertyName_en', sa.VARCHAR(length=255), nullable=True), + sa.Column('PropertyNameFormat', sa.VARCHAR(length=255), nullable=False), + sa.Column('PropertyNameFormat_en', sa.VARCHAR(length=255), nullable=True), + sa.Column('PropertyTypeId', sa.INTEGER(), nullable=False), + sa.Column('Value1Min', sa.INTEGER(), nullable=False), + sa.Column('Value1Max', sa.INTEGER(), nullable=False), + sa.Column('Value2Min', sa.INTEGER(), nullable=False), + sa.Column('Value2Max', sa.INTEGER(), nullable=False), + sa.PrimaryKeyConstraint('PropertyId'), + mysql_charset='utf8mb4' + ) + op.create_table('sao_static_reward', + sa.Column('RewardTableId', sa.BIGINT(), nullable=False), + sa.Column('RewardTableSubId', sa.INTEGER(), nullable=False), + sa.Column('UnanalyzedLogGradeId', sa.INTEGER(), nullable=False), + sa.Column('CommonRewardType', sa.INTEGER(), nullable=False), + sa.Column('CommonRewardId', sa.INTEGER(), nullable=False), + sa.Column('CommonRewardNum', sa.INTEGER(), nullable=False), + sa.Column('StrengthMin', sa.INTEGER(), nullable=False), + sa.Column('StrengthMax', sa.INTEGER(), nullable=False), + sa.Column('PropertyTableSubId', sa.INTEGER(), nullable=False), + sa.Column('QuestInfoDisplayFlag', sa.BOOLEAN(), nullable=False), + sa.Column('Rate', sa.INTEGER(), nullable=False), + sa.PrimaryKeyConstraint('RewardTableId'), + mysql_charset='utf8mb4' + ) + op.create_table('sao_static_skill', + sa.Column('SkillId', sa.BIGINT(), nullable=False), + sa.Column('WeaponTypeId', sa.INTEGER(), nullable=False), + sa.Column('Name', sa.VARCHAR(length=255), nullable=False), + sa.Column('Name_en', sa.VARCHAR(length=255), nullable=True), + sa.Column('Attack', sa.BOOLEAN(), nullable=False), + sa.Column('Passive', sa.BOOLEAN(), nullable=False), + sa.Column('Pet', sa.BOOLEAN(), nullable=False), + sa.Column('Level', sa.INTEGER(), nullable=False), + sa.Column('SkillCondition', sa.INTEGER(), nullable=False), + sa.Column('CoolTime', sa.INTEGER(), nullable=False), + sa.Column('SkillIcon', sa.VARCHAR(length=255), nullable=False), + sa.Column('FriendSkillIcon', sa.VARCHAR(length=255), nullable=False), + sa.Column('InfoText', sa.VARCHAR(length=255), nullable=False), + sa.Column('InfoText_en', sa.VARCHAR(length=255), nullable=True), + sa.PrimaryKeyConstraint('SkillId'), + mysql_charset='utf8mb4' + ) + op.create_table('sao_static_trace_table', + sa.Column('PlayerTraceTableId', sa.BIGINT(), nullable=False), + sa.Column('PlayerTraceTableSubId', sa.INTEGER(), nullable=False), + sa.Column('CommonRewardType', sa.INTEGER(), nullable=False), + sa.Column('CommonRewardId', sa.INTEGER(), nullable=False), + sa.Column('CommonRewardNum', sa.INTEGER(), nullable=False), + sa.Column('Rate', sa.INTEGER(), nullable=False), + sa.PrimaryKeyConstraint('PlayerTraceTableId'), + mysql_charset='utf8mb4' + ) + op.create_table('sao_player_beginner_mission', + sa.Column('id', sa.BIGINT(), nullable=False), + sa.Column('user', sa.INTEGER(), nullable=False), + sa.Column('beginner_mission_id', sa.INTEGER(), nullable=False), + sa.Column('condition_id', sa.INTEGER(), nullable=False), + sa.Column('is_seat', sa.BOOLEAN(), server_default='0', nullable=False), + sa.Column('achievement_num', sa.INTEGER(), nullable=False), + sa.Column('complete_flag', sa.BOOLEAN(), server_default='0', nullable=False), + sa.Column('complete_date', sa.TIMESTAMP(), nullable=True), + sa.Column('reward_received_flag', sa.BOOLEAN(), server_default='0', nullable=False), + sa.Column('reward_received_date', sa.TIMESTAMP(), nullable=True), + sa.ForeignKeyConstraint(['user'], ['aime_user.id'], onupdate='cascade', ondelete='cascade'), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('user'), + sa.UniqueConstraint('user', 'condition_id', name='sao_player_beginner_mission_uk'), + mysql_charset='utf8mb4' + ) + op.create_table('sao_player_resource_card', + sa.Column('id', sa.BIGINT(), nullable=False), + sa.Column('user', sa.INTEGER(), nullable=False), + sa.Column('common_reward_type', sa.INTEGER(), nullable=False), + sa.Column('common_reward_id', sa.INTEGER(), nullable=False), + sa.Column('holographic_flag', sa.BOOLEAN(), server_default='0', nullable=False), + sa.Column('serial', sa.VARCHAR(length=20), nullable=True), + sa.ForeignKeyConstraint(['user'], ['aime_user.id'], onupdate='cascade', ondelete='cascade'), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('serial'), + mysql_charset='utf8mb4' + ) + op.create_table('sao_player_tutorial', + sa.Column('id', sa.BIGINT(), nullable=False), + sa.Column('user', sa.INTEGER(), nullable=False), + sa.Column('tutorial_byte', sa.INTEGER(), nullable=False), + sa.ForeignKeyConstraint(['user'], ['aime_user.id'], onupdate='cascade', ondelete='cascade'), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('user', 'tutorial_byte', name='sao_player_tutorial_uk'), + mysql_charset='utf8mb4' + ) + op.create_table('sao_static_episode', + sa.Column('EpisodeId', sa.BIGINT(), nullable=False), + sa.Column('EpisodeChapterId', sa.INTEGER(), nullable=False), + sa.Column('ReleaseEpisodeId', sa.INTEGER(), nullable=False), + sa.Column('Title', sa.VARCHAR(length=255), nullable=False), + sa.Column('CommentSummary', sa.VARCHAR(length=255), nullable=False), + sa.Column('ExBonusTableSubId', sa.INTEGER(), nullable=False), + sa.Column('QuestSceneId', sa.BIGINT(), nullable=True), + sa.ForeignKeyConstraint(['QuestSceneId'], ['sao_static_quest.QuestSceneId'], onupdate='cascade', ondelete='cascade'), + sa.PrimaryKeyConstraint('EpisodeId'), + mysql_charset='utf8mb4' + ) + op.create_table('sao_static_ex_bonus', + sa.Column('ExBonusTableId', sa.BIGINT(), nullable=False), + sa.Column('ExBonusTableSubId', sa.INTEGER(), nullable=False), + sa.Column('ExBonusConditionId', sa.INTEGER(), nullable=False), + sa.Column('ConditionValue1', sa.INTEGER(), nullable=False), + sa.Column('ConditionValue2', sa.INTEGER(), nullable=False), + sa.Column('CommonRewardType', sa.INTEGER(), nullable=False), + sa.Column('CommonRewardId', sa.INTEGER(), nullable=False), + sa.Column('CommonRewardNum', sa.INTEGER(), nullable=False), + sa.Column('Strength', sa.INTEGER(), nullable=False), + sa.Column('Property1PropertyId', sa.BIGINT(), nullable=False), + sa.Column('Property1Value1', sa.INTEGER(), nullable=False), + sa.Column('Property1Value2', sa.INTEGER(), nullable=False), + sa.Column('Property2PropertyId', sa.BIGINT(), nullable=False), + sa.Column('Property2Value1', sa.INTEGER(), nullable=False), + sa.Column('Property2Value2', sa.INTEGER(), nullable=False), + sa.Column('Property3PropertyId', sa.BIGINT(), nullable=False), + sa.Column('Property3Value1', sa.INTEGER(), nullable=False), + sa.Column('Property3Value2', sa.INTEGER(), nullable=False), + sa.Column('Property4PropertyId', sa.BIGINT(), nullable=False), + sa.Column('Property4Value1', sa.INTEGER(), nullable=False), + sa.Column('Property4Value2', sa.INTEGER(), nullable=False), + sa.ForeignKeyConstraint(['Property1PropertyId'], ['sao_static_property.PropertyId'], onupdate='cascade', ondelete='cascade'), + sa.ForeignKeyConstraint(['Property2PropertyId'], ['sao_static_property.PropertyId'], onupdate='cascade', ondelete='cascade'), + sa.ForeignKeyConstraint(['Property3PropertyId'], ['sao_static_property.PropertyId'], onupdate='cascade', ondelete='cascade'), + sa.ForeignKeyConstraint(['Property4PropertyId'], ['sao_static_property.PropertyId'], onupdate='cascade', ondelete='cascade'), + sa.PrimaryKeyConstraint('ExBonusTableId'), + mysql_charset='utf8mb4' + ) + op.create_table('sao_static_ex_tower', + sa.Column('ExTowerQuestId', sa.BIGINT(), nullable=False), + sa.Column('ExTowerId', sa.INTEGER(), nullable=False), + sa.Column('ReleaseExTowerQuestId', sa.INTEGER(), nullable=False), + sa.Column('Title', sa.VARCHAR(length=255), nullable=False), + sa.Column('Title_en', sa.VARCHAR(length=255), nullable=True), + sa.Column('ExBonusTableSubId', sa.INTEGER(), nullable=False), + sa.Column('QuestSceneId', sa.BIGINT(), nullable=False), + sa.ForeignKeyConstraint(['QuestSceneId'], ['sao_static_quest.QuestSceneId'], onupdate='cascade', ondelete='cascade'), + sa.PrimaryKeyConstraint('ExTowerQuestId'), + mysql_charset='utf8mb4' + ) + op.create_table('sao_static_side_quest', + sa.Column('SideQuestId', sa.BIGINT(), nullable=False), + sa.Column('DisplayName', sa.VARCHAR(length=255), nullable=False), + sa.Column('DisplayName_en', sa.VARCHAR(length=255), nullable=True), + sa.Column('EpisodeNum', sa.INTEGER(), nullable=False), + sa.Column('ExBonusTableSubId', sa.INTEGER(), nullable=False), + sa.Column('QuestSceneId', sa.BIGINT(), nullable=False), + sa.ForeignKeyConstraint(['QuestSceneId'], ['sao_static_quest.QuestSceneId'], onupdate='cascade', ondelete='cascade'), + sa.PrimaryKeyConstraint('SideQuestId'), + sa.UniqueConstraint('SideQuestId'), + mysql_charset='utf8mb4' + ) + op.create_table('sao_static_skill_table', + sa.Column('SkillTableId', sa.BIGINT(), nullable=False), + sa.Column('SkillId', sa.BIGINT(), nullable=False), + sa.Column('SkillTableSubId', sa.INTEGER(), nullable=False), + sa.Column('LevelObtained', sa.INTEGER(), nullable=False), + sa.Column('AwakeningId', sa.INTEGER(), nullable=False), + sa.ForeignKeyConstraint(['SkillId'], ['sao_static_skill.SkillId'], onupdate='cascade', ondelete='cascade'), + sa.PrimaryKeyConstraint('SkillTableId'), + mysql_charset='utf8mb4' + ) + op.create_table('sao_static_tower', + sa.Column('TowerId', sa.BIGINT(), nullable=False), + sa.Column('ReleaseTowerId', sa.INTEGER(), nullable=False), + sa.Column('ExBonusTableSubId', sa.INTEGER(), nullable=False), + sa.Column('QuestSceneId', sa.BIGINT(), nullable=False), + sa.ForeignKeyConstraint(['QuestSceneId'], ['sao_static_quest.QuestSceneId'], onupdate='cascade', ondelete='cascade'), + sa.PrimaryKeyConstraint('TowerId'), + mysql_charset='utf8mb4' + ) + op.create_table('sao_player_ex_bonus', + sa.Column('id', sa.BIGINT(), nullable=False), + sa.Column('user', sa.INTEGER(), nullable=False), + sa.Column('quest_scene_id', sa.BIGINT(), nullable=False), + sa.Column('ex_bonus_table_id', sa.BIGINT(), nullable=False), + sa.Column('quest_clear_flag', sa.BOOLEAN(), server_default='0', nullable=False), + sa.ForeignKeyConstraint(['ex_bonus_table_id'], ['sao_static_ex_bonus.ExBonusTableId'], onupdate='cascade', ondelete='cascade'), + sa.ForeignKeyConstraint(['quest_scene_id'], ['sao_static_quest.QuestSceneId'], onupdate='cascade', ondelete='cascade'), + sa.ForeignKeyConstraint(['user'], ['aime_user.id'], onupdate='cascade', ondelete='cascade'), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('user', 'quest_scene_id', 'ex_bonus_table_id', name='sao_player_ex_bonus_uk'), + mysql_charset='utf8mb4' + ) + op.create_table('sao_player_hero_card', + sa.Column('id', sa.BIGINT(), nullable=False), + sa.Column('user', sa.INTEGER(), nullable=False), + sa.Column('user_hero_id', sa.INTEGER(), nullable=False), + sa.Column('holographic_flag', sa.BOOLEAN(), server_default='0', nullable=False), + sa.Column('serial', sa.VARCHAR(length=20), nullable=True), + sa.ForeignKeyConstraint(['user'], ['aime_user.id'], onupdate='cascade', ondelete='cascade'), + sa.ForeignKeyConstraint(['user_hero_id'], ['sao_hero_log_data.id'], onupdate='cascade', ondelete='cascade'), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('serial'), + mysql_charset='utf8mb4' + ) + op.alter_column('sao_end_sessions', 'play_date', + existing_type=mysql.TIMESTAMP(), + server_default=sa.text('now()'), + existing_nullable=False) + + op.drop_table('sao_static_equipment_list') + op.create_table("sao_static_equipment_list", + sa.Column("EquipmentId", sa.BIGINT, primary_key=True, nullable=False), + sa.Column("EquipmentType", sa.INTEGER, nullable=False), + sa.Column("WeaponTypeId", sa.INTEGER, nullable=False), + sa.Column("Name", sa.VARCHAR(255), nullable=False), + sa.Column("Name_en", sa.VARCHAR(255)), + sa.Column("Rarity", sa.INTEGER, nullable=False), + sa.Column("Power", sa.INTEGER, nullable=False), + sa.Column("StrengthIncrement", sa.INTEGER, nullable=False), + sa.Column("SkillCondition", sa.INTEGER, nullable=False), + sa.Column("Property1PropertyId", sa.BIGINT, sa.ForeignKey("sao_static_property.PropertyId", ondelete="cascade", onupdate="cascade"), nullable=False), + sa.Column("Property1Value1", sa.INTEGER, nullable=False), + sa.Column("Property1Value2", sa.INTEGER, nullable=False), + sa.Column("Property2PropertyId", sa.BIGINT, sa.ForeignKey("sao_static_property.PropertyId", ondelete="cascade", onupdate="cascade"), nullable=False), + sa.Column("Property2Value1", sa.INTEGER, nullable=False), + sa.Column("Property2Value2", sa.INTEGER, nullable=False), + sa.Column("Property3PropertyId", sa.BIGINT, sa.ForeignKey("sao_static_property.PropertyId", ondelete="cascade", onupdate="cascade"), nullable=False), + sa.Column("Property3Value1", sa.INTEGER, nullable=False), + sa.Column("Property3Value2", sa.INTEGER, nullable=False), + sa.Column("Property4PropertyId", sa.BIGINT, sa.ForeignKey("sao_static_property.PropertyId", ondelete="cascade", onupdate="cascade"), nullable=False), + sa.Column("Property4Value1", sa.INTEGER, nullable=False), + sa.Column("Property4Value2", sa.INTEGER, nullable=False), + sa.Column("SalePrice", sa.INTEGER, nullable=False), + sa.Column("CompositionExp", sa.INTEGER, nullable=False), + sa.Column("AwakeningExp", sa.INTEGER, nullable=False), + sa.Column("FlavorText", sa.VARCHAR(255), nullable=False), + sa.Column("FlavorText_en", sa.VARCHAR(255)), + mysql_charset="utf8mb4" + ) + + op.drop_table('sao_static_hero_list') + op.create_table("sao_static_hero_list", + sa.Column("HeroLogId", sa.BIGINT, primary_key=True, nullable=False), + sa.Column("CharaId", sa.INTEGER, nullable=False), + sa.Column("Name", sa.VARCHAR(255), nullable=False), + sa.Column("Nickname", sa.VARCHAR(255), nullable=False), + sa.Column("Name_en", sa.VARCHAR(255)), + sa.Column("Nickname_en", sa.VARCHAR(255)), + sa.Column("Rarity", sa.INTEGER, nullable=False), + sa.Column("WeaponTypeId", sa.INTEGER, nullable=False), + sa.Column("HeroLogRoleId", sa.INTEGER, nullable=False), + sa.Column("CostumeTypeId", sa.INTEGER, nullable=False), + sa.Column("UnitId", sa.INTEGER, nullable=False), + sa.Column("DefaultEquipmentId1", sa.BIGINT, sa.ForeignKey("sao_static_equipment_list.EquipmentId", ondelete="cascade", onupdate="cascade")), + sa.Column("DefaultEquipmentId2", sa.BIGINT, sa.ForeignKey("sao_static_equipment_list.EquipmentId", ondelete="cascade", onupdate="cascade")), + sa.Column("SkillTableSubId", sa.INTEGER, nullable=False), + sa.Column("HpMin", sa.INTEGER, nullable=False), + sa.Column("HpMax", sa.INTEGER, nullable=False), + sa.Column("StrMin", sa.INTEGER, nullable=False), + sa.Column("StrMax", sa.INTEGER, nullable=False), + sa.Column("VitMin", sa.INTEGER, nullable=False), + sa.Column("VitMax", sa.INTEGER, nullable=False), + sa.Column("IntMin", sa.INTEGER, nullable=False), + sa.Column("IntMax", sa.INTEGER, nullable=False), + sa.Column("Property1PropertyId", sa.BIGINT, sa.ForeignKey("sao_static_property.PropertyId", ondelete="cascade", onupdate="cascade"), nullable=False), + sa.Column("Property1Value1", sa.INTEGER, nullable=False), + sa.Column("Property1Value2", sa.INTEGER, nullable=False), + sa.Column("Property2PropertyId", sa.BIGINT, sa.ForeignKey("sao_static_property.PropertyId", ondelete="cascade", onupdate="cascade"), nullable=False), + sa.Column("Property2Value1", sa.INTEGER, nullable=False), + sa.Column("Property2Value2", sa.INTEGER, nullable=False), + sa.Column("Property3PropertyId", sa.BIGINT, sa.ForeignKey("sao_static_property.PropertyId", ondelete="cascade", onupdate="cascade"), nullable=False), + sa.Column("Property3Value1", sa.INTEGER, nullable=False), + sa.Column("Property3Value2", sa.INTEGER, nullable=False), + sa.Column("Property4PropertyId", sa.BIGINT, sa.ForeignKey("sao_static_property.PropertyId", ondelete="cascade", onupdate="cascade"), nullable=False), + sa.Column("Property4Value1", sa.INTEGER, nullable=False), + sa.Column("Property4Value2", sa.INTEGER, nullable=False), + sa.Column("FlavorText", sa.VARCHAR(255), nullable=False), + sa.Column("FlavorText_en", sa.VARCHAR(255)), + sa.Column("SalePrice", sa.INTEGER, nullable=False), + sa.Column("CompositionExp", sa.INTEGER, nullable=False), + sa.Column("AwakeningExp", sa.INTEGER, nullable=False), + sa.Column("Slot4UnlockLevel", sa.INTEGER, nullable=False), + sa.Column("Slot5UnlockLevel", sa.INTEGER, nullable=False), + sa.Column("CollectionEmptyFrameDisplayFlag", sa.BOOLEAN, nullable=False), + mysql_charset="utf8mb4" + ) + + op.drop_table('sao_static_item_list') + op.create_table("sao_static_item_list", + sa.Column("ItemId", sa.INTEGER, nullable=False, primary_key=True), + sa.Column("ItemTypeId", sa.INTEGER, nullable=False), + sa.Column("Name", sa.VARCHAR(255), nullable=False), + sa.Column("Name_en", sa.VARCHAR(255)), + sa.Column("Rarity", sa.INTEGER, nullable=False), + sa.Column("Value", sa.INTEGER, nullable=False), + sa.Column("PropertyId", sa.BIGINT, sa.ForeignKey("sao_static_property.PropertyId", ondelete="cascade", onupdate="cascade"), nullable=False), + sa.Column("PropertyValue1Min", sa.INTEGER, nullable=False), + sa.Column("PropertyValue1Max", sa.INTEGER, nullable=False), + sa.Column("PropertyValue2Min", sa.INTEGER, nullable=False), + sa.Column("PropertyValue2Max", sa.INTEGER, nullable=False), + sa.Column("FlavorText", sa.VARCHAR(255), nullable=False), + sa.Column("FlavorText_en", sa.VARCHAR(255)), + sa.Column("SalePrice", sa.INTEGER, nullable=False), + sa.Column("ItemIcon", sa.VARCHAR(255), nullable=False), + mysql_charset="utf8mb4" + ) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('sao_static_item_list') + op.create_table("sao_static_item_list", + sa.Column("id", sa.Integer, primary_key=True, nullable=False), + sa.Column("version", sa.Integer), + sa.Column("itemId", sa.Integer), + sa.Column("itemTypeId", sa.Integer), + sa.Column("name", sa.String(255)), + sa.Column("rarity", sa.Integer), + sa.Column("flavorText", sa.String(255)), + sa.Column("enabled", sa.Boolean), + sa.UniqueConstraint( + "version", "itemId", name="sao_static_item_list_uk" + ), + mysql_charset="utf8mb4" + ) + + op.drop_table('sao_static_hero_list') + op.create_table("sao_static_hero_list", + sa.Column("id", sa.Integer, primary_key=True, nullable=False), + sa.Column("version", sa.Integer), + sa.Column("heroLogId", sa.Integer), + sa.Column("name", sa.String(255)), + sa.Column("nickname", sa.String(255)), + sa.Column("rarity", sa.Integer), + sa.Column("skillTableSubId", sa.Integer), + sa.Column("awakeningExp", sa.Integer), + sa.Column("flavorText", sa.String(255)), + sa.Column("enabled", sa.Boolean), + sa.UniqueConstraint( + "version", "heroLogId", name="sao_static_hero_list_uk" + ), + mysql_charset="utf8mb4", + ) + + op.drop_table('sao_static_equipment_list') + op.create_table("sao_static_equipment_list", + sa.Column("id", sa.Integer, primary_key=True, nullable=False), + sa.Column("version", sa.Integer), + sa.Column("equipmentId", sa.Integer), + sa.Column("equipmentType", sa.Integer), + sa.Column("weaponTypeId", sa.Integer), + sa.Column("name", sa.String(255)), + sa.Column("rarity", sa.Integer), + sa.Column("flavorText", sa.String(255)), + sa.Column("enabled", sa.Boolean), + sa.UniqueConstraint( + "version", "equipmentId", name="sao_static_equipment_list_uk" + ), + mysql_charset="utf8mb4" + + ) + + op.alter_column('sao_end_sessions', 'play_date', + existing_type=mysql.TIMESTAMP(), + server_default=sa.text('CURRENT_TIMESTAMP'), + existing_nullable=False) + op.drop_table('sao_player_hero_card') + op.drop_table('sao_player_ex_bonus') + op.drop_table('sao_static_tower') + op.drop_table('sao_static_skill_table') + op.drop_table('sao_static_side_quest') + op.drop_table('sao_static_ex_tower') + op.drop_table('sao_static_ex_bonus') + op.drop_table('sao_static_episode') + op.drop_table('sao_player_tutorial') + op.drop_table('sao_player_resource_card') + op.drop_table('sao_player_beginner_mission') + op.drop_table('sao_static_trace_table') + op.drop_table('sao_static_skill') + op.drop_table('sao_static_reward') + op.drop_table('sao_static_property') + op.drop_table('sao_static_quest') + op.create_table('sao_static_quest', + sa.Column('id', mysql.INTEGER(), autoincrement=True, nullable=False), + sa.Column('enabled', mysql.TINYINT(display_width=1), autoincrement=False, nullable=True), + sa.Column('version', mysql.INTEGER(), autoincrement=False, nullable=True), + sa.Column('questSceneId', mysql.INTEGER(), autoincrement=False, nullable=True), + sa.Column('sortNo', mysql.INTEGER(), autoincrement=False, nullable=True), + sa.Column('name', mysql.VARCHAR(charset='utf8mb4', collation='utf8mb4_general_ci', length=255), nullable=True), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint("version", "questSceneId", name="sao_static_quest_uk"), + mysql_charset='utf8mb4' + ) + # ### end Alembic commands ### diff --git a/core/data/alembic/versions/b23f985100ba_chunithm_luminous.py b/core/data/alembic/versions/b23f985100ba_chunithm_luminous.py new file mode 100644 index 0000000..dd52974 --- /dev/null +++ b/core/data/alembic/versions/b23f985100ba_chunithm_luminous.py @@ -0,0 +1,87 @@ +"""CHUNITHM LUMINOUS + +Revision ID: b23f985100ba +Revises: 3657efefc5a4 +Create Date: 2024-06-20 08:08:08.759261 + +""" +from alembic import op +from sqlalchemy import Column, Integer, Boolean, UniqueConstraint + + +# revision identifiers, used by Alembic. +revision = 'b23f985100ba' +down_revision = '3657efefc5a4' +branch_labels = None +depends_on = None + + +def upgrade(): + op.create_table( + "chuni_profile_net_battle", + Column("id", Integer, primary_key=True, nullable=False), + Column("user", Integer, nullable=False), + Column("isRankUpChallengeFailed", Boolean), + Column("highestBattleRankId", Integer), + Column("battleIconId", Integer), + Column("battleIconNum", Integer), + Column("avatarEffectPoint", Integer), + mysql_charset="utf8mb4", + ) + op.create_foreign_key( + None, + "chuni_profile_net_battle", + "aime_user", + ["user"], + ["id"], + ondelete="cascade", + onupdate="cascade", + ) + + op.create_table( + "chuni_item_cmission", + Column("id", Integer, primary_key=True, nullable=False), + Column("user", Integer, nullable=False), + Column("missionId", Integer, nullable=False), + Column("point", Integer), + UniqueConstraint("user", "missionId", name="chuni_item_cmission_uk"), + mysql_charset="utf8mb4", + ) + op.create_foreign_key( + None, + "chuni_item_cmission", + "aime_user", + ["user"], + ["id"], + ondelete="cascade", + onupdate="cascade", + ) + + op.create_table( + "chuni_item_cmission_progress", + Column("id", Integer, primary_key=True, nullable=False), + Column("user", Integer, nullable=False), + Column("missionId", Integer, nullable=False), + Column("order", Integer), + Column("stage", Integer), + Column("progress", Integer), + UniqueConstraint( + "user", "missionId", "order", name="chuni_item_cmission_progress_uk" + ), + mysql_charset="utf8mb4", + ) + op.create_foreign_key( + None, + "chuni_item_cmission_progress", + "aime_user", + ["user"], + ["id"], + ondelete="cascade", + onupdate="cascade", + ) + + +def downgrade(): + op.drop_table("chuni_profile_net_battle") + op.drop_table("chuni_item_cmission") + op.drop_table("chuni_item_cmission_progress") diff --git a/core/data/alembic/versions/d8950c7ce2fc_remove_old_db_mgmt_system.py b/core/data/alembic/versions/d8950c7ce2fc_remove_old_db_mgmt_system.py new file mode 100644 index 0000000..61990bc --- /dev/null +++ b/core/data/alembic/versions/d8950c7ce2fc_remove_old_db_mgmt_system.py @@ -0,0 +1,29 @@ +"""Remove old db mgmt system + +Revision ID: d8950c7ce2fc +Revises: 835b862f9bf0 +Create Date: 2024-01-09 13:43:51.381175 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = 'd8950c7ce2fc' +down_revision = '835b862f9bf0' +branch_labels = None +depends_on = None + + +def upgrade(): + op.drop_table("schema_versions") + + +def downgrade(): + op.create_table( + "schema_versions", + sa.Column("game", sa.String(4), primary_key=True, nullable=False), + sa.Column("version", sa.Integer, nullable=False, server_default="1"), + mysql_charset="utf8mb4", + ) diff --git a/core/data/database.py b/core/data/database.py index e39d864..bd6c4f2 100644 --- a/core/data/database.py +++ b/core/data/database.py @@ -1,13 +1,14 @@ import logging, coloredlogs -from typing import Optional, Dict, List +from typing import Optional from sqlalchemy.orm import scoped_session, sessionmaker -from sqlalchemy.exc import SQLAlchemyError from sqlalchemy import create_engine from logging.handlers import TimedRotatingFileHandler -import importlib, os +import os import secrets, string import bcrypt from hashlib import sha256 +import alembic.config +import glob from core.config import CoreConfig from core.data.schema import * @@ -15,7 +16,6 @@ from core.utils import Utils class Data: - current_schema_version = 6 engine = None session = None user = None @@ -77,281 +77,177 @@ class Data: ) self.logger.handler_set = True # type: ignore + def __alembic_cmd(self, command: str, *args: str) -> None: + old_dir = os.path.abspath(os.path.curdir) + base_dir = os.path.join(os.path.abspath(os.path.curdir), 'core', 'data', 'alembic') + alembicArgs = [ + "-c", + os.path.join(base_dir, "alembic.ini"), + "-x", + f"script_location={base_dir}", + "-x", + f"sqlalchemy.url={self.__url}", + command, + ] + alembicArgs.extend(args) + os.chdir(base_dir) + alembic.config.main(argv=alembicArgs) + os.chdir(old_dir) + def create_database(self): self.logger.info("Creating databases...") - try: - metadata.create_all(self.__engine.connect()) - except SQLAlchemyError as e: - self.logger.error(f"Failed to create databases! {e}") - return - - games = Utils.get_all_titles() - for game_dir, game_mod in games.items(): - try: - if hasattr(game_mod, "database") and hasattr( - game_mod, "current_schema_version" - ): - game_mod.database(self.config) - metadata.create_all(self.__engine.connect()) - - self.base.touch_schema_ver( - game_mod.current_schema_version, game_mod.game_codes[0] - ) - - except Exception as e: - self.logger.warning( - f"Could not load database schema from {game_dir} - {e}" - ) - - self.logger.info(f"Setting base_schema_ver to {self.current_schema_version}") - self.base.set_schema_ver(self.current_schema_version) - - self.logger.info( - f"Setting user auto_incrememnt to {self.config.database.user_table_autoincrement_start}" - ) - self.user.reset_autoincrement( - self.config.database.user_table_autoincrement_start + metadata.create_all( + self.engine, + checkfirst=True, ) - def recreate_database(self): - self.logger.info("Dropping all databases...") - self.base.execute("SET FOREIGN_KEY_CHECKS=0") - try: - metadata.drop_all(self.__engine.connect()) - except SQLAlchemyError as e: - self.logger.error(f"Failed to drop databases! {e}") - return + for _, mod in Utils.get_all_titles().items(): + if hasattr(mod, "database"): + mod.database(self.config) + metadata.create_all( + self.engine, + checkfirst=True, + ) - for root, dirs, files in os.walk("./titles"): - for dir in dirs: - if not dir.startswith("__"): - try: - mod = importlib.import_module(f"titles.{dir}") + # Stamp the end revision as if alembic had created it, so it can take off after this. + self.__alembic_cmd( + "stamp", + "head", + ) - try: - if hasattr(mod, "database"): - mod.database(self.config) - metadata.drop_all(self.__engine.connect()) + def schema_upgrade(self, ver: str = None): + self.__alembic_cmd( + "upgrade", + "head" if not ver else ver, + ) - except Exception as e: - self.logger.warning( - f"Could not load database schema from {dir} - {e}" - ) + def schema_downgrade(self, ver: str): + self.__alembic_cmd( + "downgrade", + ver, + ) - except ImportError as e: - self.logger.warning( - f"Failed to load database schema dir {dir} - {e}" - ) - break - - self.base.execute("SET FOREIGN_KEY_CHECKS=1") - - self.create_database() - - def migrate_database(self, game: str, version: Optional[int], action: str) -> None: - old_ver = self.base.get_schema_ver(game) - sql = "" - if version is None: - if not game == "CORE": - titles = Utils.get_all_titles() - - for folder, mod in titles.items(): - if not mod.game_codes[0] == game: - continue - - if hasattr(mod, "current_schema_version"): - version = mod.current_schema_version - - else: - self.logger.warning( - f"current_schema_version not found for {folder}" - ) - - else: - version = self.current_schema_version - - if version is None: - self.logger.warning( - f"Could not determine latest version for {game}, please specify --version" - ) - - if old_ver is None: - self.logger.error( - f"Schema for game {game} does not exist, did you run the creation script?" - ) - return - - if old_ver == version: - self.logger.info( - f"Schema for game {game} is already version {old_ver}, nothing to do" - ) - return - - if action == "upgrade": - for x in range(old_ver, version): - if not os.path.exists( - f"core/data/schema/versions/{game.upper()}_{x + 1}_{action}.sql" - ): - self.logger.error( - f"Could not find {action} script {game.upper()}_{x + 1}_{action}.sql in core/data/schema/versions folder" - ) - return - - with open( - f"core/data/schema/versions/{game.upper()}_{x + 1}_{action}.sql", - "r", - encoding="utf-8", - ) as f: - sql = f.read() - - result = self.base.execute(sql) - if result is None: - self.logger.error("Error execuing sql script!") - return None - - else: - for x in range(old_ver, version, -1): - if not os.path.exists( - f"core/data/schema/versions/{game.upper()}_{x - 1}_{action}.sql" - ): - self.logger.error( - f"Could not find {action} script {game.upper()}_{x - 1}_{action}.sql in core/data/schema/versions folder" - ) - return - - with open( - f"core/data/schema/versions/{game.upper()}_{x - 1}_{action}.sql", - "r", - encoding="utf-8", - ) as f: - sql = f.read() - - result = self.base.execute(sql) - if result is None: - self.logger.error("Error execuing sql script!") - return None - - result = self.base.set_schema_ver(version, game) - if result is None: - self.logger.error("Error setting version in schema_version table!") - return None - - self.logger.info(f"Successfully migrated {game} to schema version {version}") - - def create_owner(self, email: Optional[str] = None) -> None: + async def create_owner(self, email: Optional[str] = None, code: Optional[str] = "00000000000000000000") -> None: pw = "".join( secrets.choice(string.ascii_letters + string.digits) for i in range(20) ) hash = bcrypt.hashpw(pw.encode(), bcrypt.gensalt()) - user_id = self.user.create_user(email=email, permission=255, password=hash) + user_id = await self.user.create_user(username="sysowner", email=email, password=hash.decode(), permission=255) if user_id is None: self.logger.error(f"Failed to create owner with email {email}") return - card_id = self.card.create_card(user_id, "00000000000000000000") + card_id = await self.card.create_card(user_id, code) if card_id is None: self.logger.error(f"Failed to create card for owner with id {user_id}") return self.logger.warning( - f"Successfully created owner with email {email}, access code 00000000000000000000, and password {pw} Make sure to change this password and assign a real card ASAP!" + f"Successfully created owner with email {email}, access code {code}, and password {pw} Make sure to change this password and assign a real card ASAP!" ) - - def migrate_card(self, old_ac: str, new_ac: str, should_force: bool) -> None: - if old_ac == new_ac: - self.logger.error("Both access codes are the same!") - return - - new_card = self.card.get_card_by_access_code(new_ac) - if new_card is None: - self.card.update_access_code(old_ac, new_ac) - return - - if not should_force: - self.logger.warning( - f"Card already exists for access code {new_ac} (id {new_card['id']}). If you wish to continue, rerun with the '--force' flag." - f" All exiting data on the target card {new_ac} will be perminently erased and replaced with data from card {old_ac}." - ) - return - - self.logger.info( - f"All exiting data on the target card {new_ac} will be perminently erased and replaced with data from card {old_ac}." - ) - self.card.delete_card(new_card["id"]) - self.card.update_access_code(old_ac, new_ac) - - hanging_user = self.user.get_user(new_card["user"]) - if hanging_user["password"] is None: - self.logger.info(f"Delete hanging user {hanging_user['id']}") - self.user.delete_user(hanging_user["id"]) - - def delete_hanging_users(self) -> None: - """ - Finds and deletes users that have not registered for the webui that have no cards assocated with them. - """ - unreg_users = self.user.get_unregistered_users() - if unreg_users is None: - self.logger.error("Error occoured finding unregistered users") - - for user in unreg_users: - cards = self.card.get_user_cards(user["id"]) - if cards is None: - self.logger.error(f"Error getting cards for user {user['id']}") - continue - - if not cards: - self.logger.info(f"Delete hanging user {user['id']}") - self.user.delete_user(user["id"]) - - def autoupgrade(self) -> None: - all_game_versions = self.base.get_all_schema_vers() - if all_game_versions is None: - self.logger.warning("Failed to get schema versions") - return - - all_games = Utils.get_all_titles() - all_games_list: Dict[str, int] = {} - for _, mod in all_games.items(): - if hasattr(mod, "current_schema_version"): - all_games_list[mod.game_codes[0]] = mod.current_schema_version - - for x in all_game_versions: - failed = False - game = x["game"].upper() - update_ver = int(x["version"]) - latest_ver = all_games_list.get(game, 1) - if game == "CORE": - latest_ver = self.current_schema_version - - if update_ver == latest_ver: - self.logger.info(f"{game} is already latest version") - continue - - for y in range(update_ver + 1, latest_ver + 1): - if os.path.exists(f"core/data/schema/versions/{game}_{y}_upgrade.sql"): - with open( - f"core/data/schema/versions/{game}_{y}_upgrade.sql", - "r", - encoding="utf-8", - ) as f: - sql = f.read() - - result = self.base.execute(sql) - if result is None: - self.logger.error( - f"Error execuing sql script for game {game} v{y}!" - ) - failed = True - break - else: - self.logger.warning(f"Could not find script {game}_{y}_upgrade.sql") - failed = True - - if not failed: - self.base.set_schema_ver(latest_ver, game) - def show_versions(self) -> None: - all_game_versions = self.base.get_all_schema_vers() - for ver in all_game_versions: - self.logger.info(f"{ver['game']} -> v{ver['version']}") + async def migrate(self) -> None: + exist = await self.base.execute("SELECT * FROM alembic_version") + if exist is not None: + self.logger.warn("No need to migrate as you have already migrated to alembic. If you are trying to upgrade the schema, use `upgrade` instead!") + return + + self.logger.info("Upgrading to latest with legacy system") + if not await self.legacy_upgrade(): + self.logger.warn("No need to migrate as you have already deleted the old schema_versions system. If you are trying to upgrade the schema, use `upgrade` instead!") + return + self.logger.info("Done") + + self.logger.info("Stamp with initial revision") + self.__alembic_cmd( + "stamp", + "835b862f9bf0", + ) + + self.logger.info("Upgrade") + self.__alembic_cmd( + "upgrade", + "head", + ) + + async def legacy_upgrade(self) -> bool: + vers = await self.base.execute("SELECT * FROM schema_versions") + if vers is None: + self.logger.warn("Cannot legacy upgrade, schema_versions table unavailable!") + return False + + db_vers = {} + vers_list = vers.fetchall() + for x in vers_list: + db_vers[x['game']] = x['version'] + + core_now_ver = int(db_vers['CORE']) + 1 + while os.path.exists(f"core/data/schema/versions/CORE_{core_now_ver}_upgrade.sql"): + with open(f"core/data/schema/versions/CORE_{core_now_ver}_upgrade.sql", "r") as f: + result = await self.base.execute(f.read()) + + if result is None: + self.logger.error(f"Invalid upgrade script CORE_{core_now_ver}_upgrade.sql") + break + + result = await self.base.execute(f"UPDATE schema_versions SET version = {core_now_ver} WHERE game = 'CORE'") + if result is None: + self.logger.error(f"Failed to update schema version for CORE to {core_now_ver}") + break + + self.logger.info(f"Upgrade CORE to version {core_now_ver}") + core_now_ver += 1 + + for _, mod in Utils.get_all_titles().items(): + game_codes = getattr(mod, "game_codes", []) + for game in game_codes: + if game not in db_vers: + self.logger.warn(f"{game} does not have an antry in schema_versions, skipping") + continue + + now_ver = int(db_vers[game]) + 1 + while os.path.exists(f"core/data/schema/versions/{game}_{now_ver}_upgrade.sql"): + with open(f"core/data/schema/versions/{game}_{now_ver}_upgrade.sql", "r") as f: + result = await self.base.execute(f.read()) + + if result is None: + self.logger.error(f"Invalid upgrade script {game}_{now_ver}_upgrade.sql") + break + + result = await self.base.execute(f"UPDATE schema_versions SET version = {now_ver} WHERE game = '{game}'") + if result is None: + self.logger.error(f"Failed to update schema version for {game} to {now_ver}") + break + + self.logger.info(f"Upgrade {game} to version {now_ver}") + now_ver += 1 + + return True + + async def create_revision(self, message: str) -> None: + if not message: + self.logger.info("Message is required for create-revision") + return + + self.__alembic_cmd( + "revision", + "-m", + message, + ) + + async def create_revision_auto(self, message: str) -> None: + if not message: + self.logger.info("Message is required for create-revision") + return + + for _, mod in Utils.get_all_titles().items(): + if hasattr(mod, "database"): + mod.database(self.config) + + self.__alembic_cmd( + "revision", + "--autogenerate", + "-m", + message, + ) diff --git a/core/data/schema/arcade.py b/core/data/schema/arcade.py index 2fb8e43..3e83bc5 100644 --- a/core/data/schema/arcade.py +++ b/core/data/schema/arcade.py @@ -69,7 +69,7 @@ arcade_owner = Table( class ArcadeData(BaseData): - def get_machine(self, serial: str = None, id: int = None) -> Optional[Row]: + async def get_machine(self, serial: str = None, id: int = None) -> Optional[Row]: if serial is not None: serial = serial.replace("-", "") if len(serial) == 11: @@ -89,12 +89,12 @@ class ArcadeData(BaseData): self.logger.error(f"{__name__ }: Need either serial or ID to look up!") return None - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchone() - def put_machine( + async def create_machine( self, arcade_id: int, serial: str = "", @@ -102,21 +102,21 @@ class ArcadeData(BaseData): game: str = None, is_cab: bool = False, ) -> Optional[int]: - if arcade_id: + if not arcade_id: self.logger.error(f"{__name__ }: Need arcade id!") return None sql = machine.insert().values( - arcade=arcade_id, keychip=serial, board=board, game=game, is_cab=is_cab + arcade=arcade_id, serial=serial, board=board, game=game, is_cab=is_cab ) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.lastrowid - def set_machine_serial(self, machine_id: int, serial: str) -> None: - result = self.execute( + async def set_machine_serial(self, machine_id: int, serial: str) -> None: + result = await self.execute( machine.update(machine.c.id == machine_id).values(keychip=serial) ) if result is None: @@ -125,8 +125,8 @@ class ArcadeData(BaseData): ) return result.lastrowid - def set_machine_boardid(self, machine_id: int, boardid: str) -> None: - result = self.execute( + async def set_machine_boardid(self, machine_id: int, boardid: str) -> None: + result = await self.execute( machine.update(machine.c.id == machine_id).values(board=boardid) ) if result is None: @@ -134,29 +134,29 @@ class ArcadeData(BaseData): f"Failed to update board id for machine {machine_id} -> {boardid}" ) - def get_arcade(self, id: int) -> Optional[Row]: + async def get_arcade(self, id: int) -> Optional[Row]: sql = arcade.select(arcade.c.id == id) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchone() - def get_arcade_machines(self, id: int) -> Optional[List[Row]]: + async def get_arcade_machines(self, id: int) -> Optional[List[Row]]: sql = machine.select(machine.c.arcade == id) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchall() - def put_arcade( + async def create_arcade( self, - name: str, + name: str = None, nickname: str = None, country: str = "JPN", country_id: int = 1, state: str = "", city: str = "", - regional_id: int = 1, + region_id: int = 1, ) -> Optional[int]: if nickname is None: nickname = name @@ -168,65 +168,104 @@ class ArcadeData(BaseData): country_id=country_id, state=state, city=city, - regional_id=regional_id, + region_id=region_id, ) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.lastrowid - def get_arcades_managed_by_user(self, user_id: int) -> Optional[List[Row]]: + async def get_arcades_managed_by_user(self, user_id: int) -> Optional[List[Row]]: sql = select(arcade).join(arcade_owner, arcade_owner.c.arcade == arcade.c.id).where(arcade_owner.c.user == user_id) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return False return result.fetchall() - def get_manager_permissions(self, user_id: int, arcade_id: int) -> Optional[int]: + async def get_manager_permissions(self, user_id: int, arcade_id: int) -> Optional[int]: sql = select(arcade_owner.c.permissions).where(and_(arcade_owner.c.user == user_id, arcade_owner.c.arcade == arcade_id)) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return False return result.fetchone() - def get_arcade_owners(self, arcade_id: int) -> Optional[Row]: + async def get_arcade_owners(self, arcade_id: int) -> Optional[Row]: sql = select(arcade_owner).where(arcade_owner.c.arcade == arcade_id) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchall() - def add_arcade_owner(self, arcade_id: int, user_id: int) -> None: + async def add_arcade_owner(self, arcade_id: int, user_id: int) -> None: sql = insert(arcade_owner).values(arcade=arcade_id, user=user_id) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.lastrowid + async def get_arcade_by_name(self, name: str) -> Optional[List[Row]]: + sql = arcade.select(or_(arcade.c.name.like(f"%{name}%"), arcade.c.nickname.like(f"%{name}%"))) + result = await self.execute(sql) + if result is None: + return None + return result.fetchall() + + async def get_arcades_by_ip(self, ip: str) -> Optional[List[Row]]: + sql = arcade.select().where(arcade.c.ip == ip) + result = await self.execute(sql) + if result is None: + return None + return result.fetchall() + + async def get_num_generated_keychips(self) -> Optional[int]: + result = await self.execute(select(func.count("serial LIKE 'A69A%'")).select_from(machine)) + if result: + return result.fetchone()['count_1'] + self.logger.error("Failed to count machine serials that start with A69A!") + def format_serial( - self, platform_code: str, platform_rev: int, serial_num: int, append: int = 4152 + self, platform_code: str, platform_rev: int, serial_letter: str, serial_num: int, append: int, dash: bool = False ) -> str: - return f"{platform_code}{platform_rev:02d}A{serial_num:04d}{append:04d}" # 0x41 = A, 0x52 = R + return f"{platform_code}{'-' if dash else ''}{platform_rev:02d}{serial_letter}{serial_num:04d}{append:04d}" def validate_keychip_format(self, serial: str) -> bool: - if re.fullmatch(r"^A[0-9]{2}[E|X][-]?[0-9]{2}[A-HJ-NP-Z][0-9]{4}([0-9]{4})?$", serial) is None: + # For the 2nd letter, E and X are the only "real" values that have been observed + if re.fullmatch(r"^A[0-9]{2}[A-Z][-]?[0-9]{2}[A-HJ-NP-Z][0-9]{4}([0-9]{4})?$", serial) is None: return False return True + + # Thanks bottersnike! + def get_keychip_suffix(self, year: int, month: int) -> str: + assert year > 1957 + assert 1 <= month <= 12 - def get_arcade_by_name(self, name: str) -> Optional[List[Row]]: - sql = arcade.select(or_(arcade.c.name.like(f"%{name}%"), arcade.c.nickname.like(f"%{name}%"))) - result = self.execute(sql) - if result is None: - return None - return result.fetchall() + year -= 1957 + # Jan/Feb/Mar are from the previous tax year + if month < 4: + year -= 1 + assert year >= 1 and year <= 99 - def get_arcades_by_ip(self, ip: str) -> Optional[List[Row]]: - sql = arcade.select().where(arcade.c.ip == ip) - result = self.execute(sql) - if result is None: - return None - return result.fetchall() + month = ((month - 1) + 9) % 12 # Offset so April=0 + return f"{year:02}{month // 6:01}{month % 6 + 1:01}" + + + def parse_keychip_suffix(self, suffix: str) -> tuple[int, int]: + year = int(suffix[0:2]) + half = int(suffix[2]) + assert half in (0, 1) + period = int(suffix[3]) + assert period in (1, 2, 3, 4, 5, 6) + + month = half * 6 + (period - 1) + month = ((month + 3) % 12) + 1 # Offset so Jan=1 + + # Jan/Feb/Mar are from the previous tax year + if month < 4: + year += 1 + year += 1957 + + return (year, month) diff --git a/core/data/schema/base.py b/core/data/schema/base.py index ef980e5..d74198b 100644 --- a/core/data/schema/base.py +++ b/core/data/schema/base.py @@ -8,21 +8,14 @@ from sqlalchemy.engine.base import Connection from sqlalchemy.sql import text, func, select from sqlalchemy.exc import SQLAlchemyError from sqlalchemy import MetaData, Table, Column -from sqlalchemy.types import Integer, String, TIMESTAMP, JSON +from sqlalchemy.types import Integer, String, TIMESTAMP, JSON, INTEGER, TEXT +from sqlalchemy.schema import ForeignKey from sqlalchemy.dialects.mysql import insert from core.config import CoreConfig metadata = MetaData() -schema_ver = Table( - "schema_versions", - metadata, - Column("game", String(4), primary_key=True, nullable=False), - Column("version", Integer, nullable=False, server_default="1"), - mysql_charset="utf8mb4", -) - event_log = Table( "event_log", metadata, @@ -30,6 +23,12 @@ event_log = Table( Column("system", String(255), nullable=False), Column("type", String(255), nullable=False), Column("severity", Integer, nullable=False), + Column("user", INTEGER, ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade")), + Column("arcade", INTEGER, ForeignKey("arcade.id", ondelete="cascade", onupdate="cascade")), + Column("machine", INTEGER, ForeignKey("machine.id", ondelete="cascade", onupdate="cascade")), + Column("ip", TEXT(39)), + Column("game", TEXT(4)), + Column("version", TEXT(24)), Column("message", String(1000), nullable=False), Column("details", JSON, nullable=False), Column("when_logged", TIMESTAMP, nullable=False, server_default=func.now()), @@ -43,11 +42,11 @@ class BaseData: self.conn = conn self.logger = logging.getLogger("database") - def execute(self, sql: str, opts: Dict[str, Any] = {}) -> Optional[CursorResult]: + async def execute(self, sql: str, opts: Dict[str, Any] = {}) -> Optional[CursorResult]: res = None try: - self.logger.info(f"SQL Execute: {''.join(str(sql).splitlines())}") + self.logger.debug(f"SQL Execute: {''.join(str(sql).splitlines())}") res = self.conn.execute(text(sql), opts) except SQLAlchemyError as e: @@ -82,62 +81,24 @@ class BaseData: """ return randrange(10000, 9999999) - def get_all_schema_vers(self) -> Optional[List[Row]]: - sql = select(schema_ver) - - result = self.execute(sql) - if result is None: - return None - return result.fetchall() - - def get_schema_ver(self, game: str) -> Optional[int]: - sql = select(schema_ver).where(schema_ver.c.game == game) - - result = self.execute(sql) - if result is None: - return None - - row = result.fetchone() - if row is None: - return None - - return row["version"] - - def touch_schema_ver(self, ver: int, game: str = "CORE") -> Optional[int]: - sql = insert(schema_ver).values(game=game, version=ver) - conflict = sql.on_duplicate_key_update(version=schema_ver.c.version) - - result = self.execute(conflict) - if result is None: - self.logger.error( - f"Failed to update schema version for game {game} (v{ver})" - ) - return None - return result.lastrowid - - def set_schema_ver(self, ver: int, game: str = "CORE") -> Optional[int]: - sql = insert(schema_ver).values(game=game, version=ver) - conflict = sql.on_duplicate_key_update(version=ver) - - result = self.execute(conflict) - if result is None: - self.logger.error( - f"Failed to update schema version for game {game} (v{ver})" - ) - return None - return result.lastrowid - - def log_event( - self, system: str, type: str, severity: int, message: str, details: Dict = {} + async def log_event( + self, system: str, type: str, severity: int, message: str, details: Dict = {}, user: int = None, + arcade: int = None, machine: int = None, ip: str = None, game: str = None, version: str = None ) -> Optional[int]: sql = event_log.insert().values( system=system, type=type, severity=severity, + user=user, + arcade=arcade, + machine=machine, + ip=ip, + game=game, + version=version, message=message, details=json.dumps(details), ) - result = self.execute(sql) + result = await self.execute(sql) if result is None: self.logger.error( @@ -147,9 +108,9 @@ class BaseData: return result.lastrowid - def get_event_log(self, entries: int = 100) -> Optional[List[Dict]]: - sql = event_log.select().limit(entries).all() - result = self.execute(sql) + async def get_event_log(self, entries: int = 100) -> Optional[List[Row]]: + sql = event_log.select().order_by(event_log.c.id.desc()).limit(entries) + result = await self.execute(sql) if result is None: return None diff --git a/core/data/schema/card.py b/core/data/schema/card.py index a95684e..1865539 100644 --- a/core/data/schema/card.py +++ b/core/data/schema/card.py @@ -1,6 +1,6 @@ from typing import Dict, List, Optional from sqlalchemy import Table, Column, UniqueConstraint -from sqlalchemy.types import Integer, String, Boolean, TIMESTAMP +from sqlalchemy.types import Integer, String, Boolean, TIMESTAMP, BIGINT, VARCHAR from sqlalchemy.sql.schema import ForeignKey from sqlalchemy.sql import func from sqlalchemy.engine import Row @@ -11,107 +11,149 @@ aime_card = Table( "aime_card", metadata, Column("id", Integer, primary_key=True, nullable=False), - Column( - "user", - ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), - nullable=False, - ), - Column("access_code", String(20)), + Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False), + Column("access_code", String(20), nullable=False, unique=True), + Column("idm", String(16), unique=True), + Column("chip_id", BIGINT, unique=True), Column("created_date", TIMESTAMP, server_default=func.now()), Column("last_login_date", TIMESTAMP, onupdate=func.now()), Column("is_locked", Boolean, server_default="0"), Column("is_banned", Boolean, server_default="0"), + Column("memo", VARCHAR(16)), UniqueConstraint("user", "access_code", name="aime_card_uk"), mysql_charset="utf8mb4", ) class CardData(BaseData): - def get_card_by_access_code(self, access_code: str) -> Optional[Row]: + moble_os_codes = set([0x06, 0x07, 0x10, 0x12, 0x13, 0x14, 0x15, 0x17, 0x18]) + card_os_codes = set([0x20, 0xF0, 0xF1, 0xF2, 0xF3, 0xF4, 0xF5, 0xF6, 0xF7]) + + async def get_card_by_access_code(self, access_code: str) -> Optional[Row]: sql = aime_card.select(aime_card.c.access_code == access_code) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchone() - def get_card_by_id(self, card_id: int) -> Optional[Row]: + async def get_card_by_id(self, card_id: int) -> Optional[Row]: sql = aime_card.select(aime_card.c.id == card_id) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchone() - def update_access_code(self, old_ac: str, new_ac: str) -> None: + async def update_access_code(self, old_ac: str, new_ac: str) -> None: sql = aime_card.update(aime_card.c.access_code == old_ac).values( access_code=new_ac ) - result = self.execute(sql) + result = await self.execute(sql) if result is None: self.logger.error( f"Failed to change card access code from {old_ac} to {new_ac}" ) - def get_user_id_from_card(self, access_code: str) -> Optional[int]: + async def get_user_id_from_card(self, access_code: str) -> Optional[int]: """ Given a 20 digit access code as a string, get the user id associated with that card """ - card = self.get_card_by_access_code(access_code) + card = await self.get_card_by_access_code(access_code) if card is None: return None return int(card["user"]) - def get_card_banned(self, access_code: str) -> Optional[bool]: + async def get_card_banned(self, access_code: str) -> Optional[bool]: """ Given a 20 digit access code as a string, check if the card is banned """ - card = self.get_card_by_access_code(access_code) + card = await self.get_card_by_access_code(access_code) if card is None: return None if card["is_banned"]: return True return False - def get_card_locked(self, access_code: str) -> Optional[bool]: + + async def get_card_locked(self, access_code: str) -> Optional[bool]: """ Given a 20 digit access code as a string, check if the card is locked """ - card = self.get_card_by_access_code(access_code) + card = await self.get_card_by_access_code(access_code) if card is None: return None if card["is_locked"]: return True return False - def delete_card(self, card_id: int) -> None: + async def delete_card(self, card_id: int) -> None: sql = aime_card.delete(aime_card.c.id == card_id) - result = self.execute(sql) + result = await self.execute(sql) if result is None: self.logger.error(f"Failed to delete card with id {card_id}") - def get_user_cards(self, aime_id: int) -> Optional[List[Row]]: + async def get_user_cards(self, aime_id: int) -> Optional[List[Row]]: """ Returns all cards owned by a user """ sql = aime_card.select(aime_card.c.user == aime_id) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchall() - def create_card(self, user_id: int, access_code: str) -> Optional[int]: + async def create_card(self, user_id: int, access_code: str) -> Optional[int]: """ Given a aime_user id and a 20 digit access code as a string, create a card and return the ID if successful """ sql = aime_card.insert().values(user=user_id, access_code=access_code) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.lastrowid + async def update_card_last_login(self, access_code: str) -> None: + sql = aime_card.update(aime_card.c.access_code == access_code).values( + last_login_date=func.now() + ) + + result = await self.execute(sql) + if result is None: + self.logger.warn(f"Failed to update last login time for {access_code}") + + async def get_card_by_idm(self, idm: str) -> Optional[Row]: + result = await self.execute(aime_card.select(aime_card.c.idm == idm)) + if result: + return result.fetchone() + + async def get_card_by_chip_id(self, chip_id: int) -> Optional[Row]: + result = await self.execute(aime_card.select(aime_card.c.chip_id == chip_id)) + if result: + return result.fetchone() + + async def set_chip_id_by_access_code(self, access_code: str, chip_id: int) -> Optional[Row]: + result = await self.execute(aime_card.update(aime_card.c.access_code == access_code).values(chip_id=chip_id)) + if not result: + self.logger.error(f"Failed to update chip ID to {chip_id} for {access_code}") + + async def set_idm_by_access_code(self, access_code: str, idm: str) -> Optional[Row]: + result = await self.execute(aime_card.update(aime_card.c.access_code == access_code).values(idm=idm)) + if not result: + self.logger.error(f"Failed to update IDm to {idm} for {access_code}") + + async def set_access_code_by_access_code(self, old_ac: str, new_ac: str) -> None: + result = await self.execute(aime_card.update(aime_card.c.access_code == old_ac).values(access_code=new_ac)) + if not result: + self.logger.error(f"Failed to change card access code from {old_ac} to {new_ac}") + + async def set_memo_by_access_code(self, access_code: str, memo: str) -> None: + result = await self.execute(aime_card.update(aime_card.c.access_code == access_code).values(memo=memo)) + if not result: + self.logger.error(f"Failed to add memo to card {access_code}") + def to_access_code(self, luid: str) -> str: """ Given a felica cards internal 16 hex character luid, convert it to a 0-padded 20 digit access code as a string @@ -122,4 +164,4 @@ class CardData(BaseData): """ Given a 20 digit access code as a string, return the 16 hex character luid """ - return f"{int(access_code):0{16}x}" + return f"{int(access_code):0{16}X}" diff --git a/core/data/schema/user.py b/core/data/schema/user.py index 221ba81..8c3695c 100644 --- a/core/data/schema/user.py +++ b/core/data/schema/user.py @@ -1,4 +1,3 @@ -from enum import Enum from typing import Optional, List from sqlalchemy import Table, Column from sqlalchemy.types import Integer, String, TIMESTAMP @@ -24,15 +23,8 @@ aime_user = Table( mysql_charset="utf8mb4", ) - -class PermissionBits(Enum): - PermUser = 1 - PermMod = 2 - PermSysAdmin = 4 - - class UserData(BaseData): - def create_user( + async def create_user( self, id: int = None, username: str = None, @@ -60,20 +52,20 @@ class UserData(BaseData): username=username, email=email, password=password, permissions=permission ) - result = self.execute(conflict) + result = await self.execute(conflict) if result is None: return None return result.lastrowid - def get_user(self, user_id: int) -> Optional[Row]: + async def get_user(self, user_id: int) -> Optional[Row]: sql = select(aime_user).where(aime_user.c.id == user_id) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return False return result.fetchone() - def check_password(self, user_id: int, passwd: bytes = None) -> bool: - usr = self.get_user(user_id) + async def check_password(self, user_id: int, passwd: bytes = None) -> bool: + usr = await self.get_user(user_id) if usr is None: return False @@ -85,39 +77,50 @@ class UserData(BaseData): return bcrypt.checkpw(passwd, usr["password"].encode()) - def reset_autoincrement(self, ai_value: int) -> None: - # ALTER TABLE isn't in sqlalchemy so we do this the ugly way - sql = f"ALTER TABLE aime_user AUTO_INCREMENT={ai_value}" - self.execute(sql) - - def delete_user(self, user_id: int) -> None: + async def delete_user(self, user_id: int) -> None: sql = aime_user.delete(aime_user.c.id == user_id) - result = self.execute(sql) + result = await self.execute(sql) if result is None: self.logger.error(f"Failed to delete user with id {user_id}") - def get_unregistered_users(self) -> List[Row]: + async def get_unregistered_users(self) -> List[Row]: """ Returns a list of users who have not registered with the webui. They may or may not have cards. """ sql = select(aime_user).where(aime_user.c.password == None) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchall() - def find_user_by_email(self, email: str) -> Row: + async def find_user_by_email(self, email: str) -> Row: sql = select(aime_user).where(aime_user.c.email == email) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return False return result.fetchone() - def find_user_by_username(self, username: str) -> List[Row]: + async def find_user_by_username(self, username: str) -> List[Row]: sql = aime_user.select(aime_user.c.username.like(f"%{username}%")) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return False return result.fetchall() + + async def change_password(self, user_id: int, new_passwd: str) -> bool: + sql = aime_user.update(aime_user.c.id == user_id).values(password = new_passwd) + + result = await self.execute(sql) + return result is not None + + async def change_username(self, user_id: int, new_name: str) -> bool: + sql = aime_user.update(aime_user.c.id == user_id).values(username = new_name) + + result = await self.execute(sql) + return result is not None + + async def get_user_by_username(self, username: str) -> Optional[Row]: + result = await self.execute(aime_user.select(aime_user.c.username == username)) + if result: return result.fetchone() diff --git a/core/data/schema/versions/SDDT_5_rollback.sql b/core/data/schema/versions/SDDT_5_rollback.sql index 007716c..61bb352 100644 --- a/core/data/schema/versions/SDDT_5_rollback.sql +++ b/core/data/schema/versions/SDDT_5_rollback.sql @@ -1,8 +1,8 @@ SET FOREIGN_KEY_CHECKS=0; ALTER TABLE ongeki_user_event_point DROP COLUMN version; -ALTER TABLE ongeki_user_event_point DROP COLUMN rank; -ALTER TABLE ongeki_user_event_point DROP COLUMN type; +ALTER TABLE ongeki_user_event_point DROP COLUMN `rank`; +ALTER TABLE ongeki_user_event_point DROP COLUMN `type`; ALTER TABLE ongeki_user_event_point DROP COLUMN date; ALTER TABLE ongeki_user_tech_event DROP COLUMN version; @@ -19,4 +19,4 @@ DROP TABLE ongeki_static_tech_music; DROP TABLE ongeki_static_client_testmode; DROP TABLE ongeki_static_game_point; -SET FOREIGN_KEY_CHECKS=1; +SET FOREIGN_KEY_CHECKS=1; \ No newline at end of file diff --git a/core/data/schema/versions/SDDT_6_upgrade.sql b/core/data/schema/versions/SDDT_6_upgrade.sql index 82d5336..1afa186 100644 --- a/core/data/schema/versions/SDDT_6_upgrade.sql +++ b/core/data/schema/versions/SDDT_6_upgrade.sql @@ -1,8 +1,8 @@ SET FOREIGN_KEY_CHECKS=0; ALTER TABLE ongeki_user_event_point ADD COLUMN version INTEGER NOT NULL; -ALTER TABLE ongeki_user_event_point ADD COLUMN rank INTEGER; -ALTER TABLE ongeki_user_event_point ADD COLUMN type INTEGER NOT NULL; +ALTER TABLE ongeki_user_event_point ADD COLUMN `rank` INTEGER; +ALTER TABLE ongeki_user_event_point ADD COLUMN `type` INTEGER NOT NULL; ALTER TABLE ongeki_user_event_point ADD COLUMN date VARCHAR(25); ALTER TABLE ongeki_user_tech_event ADD COLUMN version INTEGER NOT NULL; @@ -12,87 +12,87 @@ ALTER TABLE ongeki_user_mission_point ADD COLUMN version INTEGER NOT NULL; ALTER TABLE ongeki_static_events ADD COLUMN endDate TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP; CREATE TABLE ongeki_tech_event_ranking ( - id INT PRIMARY KEY NOT NULL AUTO_INCREMENT, - user INT NOT NULL, - version INT NOT NULL, - date VARCHAR(25), - eventId INT NOT NULL, - rank INT, - totalPlatinumScore INT NOT NULL, - totalTechScore INT NOT NULL, - UNIQUE KEY ongeki_tech_event_ranking_uk (user, eventId), - CONSTRAINT ongeki_tech_event_ranking_ibfk1 FOREIGN KEY (user) REFERENCES aime_user(id) ON DELETE CASCADE ON UPDATE CASCADE + id INT PRIMARY KEY NOT NULL AUTO_INCREMENT, + user INT NOT NULL, + version INT NOT NULL, + date VARCHAR(25), + eventId INT NOT NULL, + `rank` INT, + totalPlatinumScore INT NOT NULL, + totalTechScore INT NOT NULL, + UNIQUE KEY ongeki_tech_event_ranking_uk (user, eventId), + CONSTRAINT ongeki_tech_event_ranking_ibfk1 FOREIGN KEY (user) REFERENCES aime_user(id) ON DELETE CASCADE ON UPDATE CASCADE ); CREATE TABLE ongeki_static_music_ranking_list ( - id INT PRIMARY KEY NOT NULL AUTO_INCREMENT, - version INT NOT NULL, - musicId INT NOT NULL, - point INT NOT NULL, - userName VARCHAR(255), - UNIQUE KEY ongeki_static_music_ranking_list_uk (version, musicId) + id INT PRIMARY KEY NOT NULL AUTO_INCREMENT, + version INT NOT NULL, + musicId INT NOT NULL, + point INT NOT NULL, + userName VARCHAR(255), + UNIQUE KEY ongeki_static_music_ranking_list_uk (version, musicId) ); CREATE TABLE ongeki_static_rewards ( - id INT PRIMARY KEY NOT NULL AUTO_INCREMENT, - version INT NOT NULL, - rewardId INT NOT NULL, - rewardName VARCHAR(255) NOT NULL, - itemKind INT NOT NULL, - itemId INT NOT NULL, - UNIQUE KEY ongeki_tech_event_ranking_uk (version, rewardId) + id INT PRIMARY KEY NOT NULL AUTO_INCREMENT, + version INT NOT NULL, + rewardId INT NOT NULL, + rewardName VARCHAR(255) NOT NULL, + itemKind INT NOT NULL, + itemId INT NOT NULL, + UNIQUE KEY ongeki_tech_event_ranking_uk (version, rewardId) ); CREATE TABLE ongeki_static_present_list ( - id INT PRIMARY KEY NOT NULL AUTO_INCREMENT, - version INT NOT NULL, - presentId INT NOT NULL, - presentName VARCHAR(255) NOT NULL, - rewardId INT NOT NULL, - stock INT NOT NULL, - message VARCHAR(255), - startDate VARCHAR(25) NOT NULL, - endDate VARCHAR(25) NOT NULL, - UNIQUE KEY ongeki_static_present_list_uk (version, presentId, rewardId) + id INT PRIMARY KEY NOT NULL AUTO_INCREMENT, + version INT NOT NULL, + presentId INT NOT NULL, + presentName VARCHAR(255) NOT NULL, + rewardId INT NOT NULL, + stock INT NOT NULL, + message VARCHAR(255), + startDate VARCHAR(25) NOT NULL, + endDate VARCHAR(25) NOT NULL, + UNIQUE KEY ongeki_static_present_list_uk (version, presentId, rewardId) ); CREATE TABLE ongeki_static_tech_music ( - id INT PRIMARY KEY NOT NULL AUTO_INCREMENT, - version INT NOT NULL, - eventId INT NOT NULL, - musicId INT NOT NULL, - level INT NOT NULL, - UNIQUE KEY ongeki_static_tech_music_uk (version, musicId, eventId) + id INT PRIMARY KEY NOT NULL AUTO_INCREMENT, + version INT NOT NULL, + eventId INT NOT NULL, + musicId INT NOT NULL, + level INT NOT NULL, + UNIQUE KEY ongeki_static_tech_music_uk (version, musicId, eventId) ); CREATE TABLE ongeki_static_client_testmode ( - id INT PRIMARY KEY NOT NULL AUTO_INCREMENT, - regionId INT NOT NULL, - placeId INT NOT NULL, - clientId VARCHAR(11) NOT NULL, - updateDate TIMESTAMP NOT NULL, - isDelivery BOOLEAN NOT NULL, - groupId INT NOT NULL, - groupRole INT NOT NULL, - continueMode INT NOT NULL, - selectMusicTime INT NOT NULL, - advertiseVolume INT NOT NULL, - eventMode INT NOT NULL, - eventMusicNum INT NOT NULL, - patternGp INT NOT NULL, - limitGp INT NOT NULL, - maxLeverMovable INT NOT NULL, - minLeverMovable INT NOT NULL, - UNIQUE KEY ongeki_static_client_testmode_uk (clientId) + id INT PRIMARY KEY NOT NULL AUTO_INCREMENT, + regionId INT NOT NULL, + placeId INT NOT NULL, + clientId VARCHAR(11) NOT NULL, + updateDate TIMESTAMP NOT NULL, + isDelivery BOOLEAN NOT NULL, + groupId INT NOT NULL, + groupRole INT NOT NULL, + continueMode INT NOT NULL, + selectMusicTime INT NOT NULL, + advertiseVolume INT NOT NULL, + eventMode INT NOT NULL, + eventMusicNum INT NOT NULL, + patternGp INT NOT NULL, + limitGp INT NOT NULL, + maxLeverMovable INT NOT NULL, + minLeverMovable INT NOT NULL, + UNIQUE KEY ongeki_static_client_testmode_uk (clientId) ); CREATE TABLE ongeki_static_game_point ( - id INT PRIMARY KEY NOT NULL AUTO_INCREMENT, - type INT NOT NULL, - cost INT NOT NULL, - startDate VARCHAR(25) NOT NULL DEFAULT "2000-01-01 05:00:00.0", - endDate VARCHAR(25) NOT NULL DEFAULT "2099-01-01 05:00:00.0", - UNIQUE KEY ongeki_static_game_point_uk (type) + id INT PRIMARY KEY NOT NULL AUTO_INCREMENT, + `type` INT NOT NULL, + cost INT NOT NULL, + startDate VARCHAR(25) NOT NULL DEFAULT "2000-01-01 05:00:00.0", + endDate VARCHAR(25) NOT NULL DEFAULT "2099-01-01 05:00:00.0", + UNIQUE KEY ongeki_static_game_point_uk (`type`) ); -SET FOREIGN_KEY_CHECKS=1; +SET FOREIGN_KEY_CHECKS=1; \ No newline at end of file diff --git a/core/frontend.py b/core/frontend.py index 0ee2211..c593828 100644 --- a/core/frontend.py +++ b/core/frontend.py @@ -1,28 +1,26 @@ import logging, coloredlogs -from typing import Any, Dict, List -from twisted.web import resource -from twisted.web.util import redirectTo -from twisted.web.http import Request +from typing import Any, Dict, List, Union, Optional +from starlette.requests import Request +from starlette.routing import Route, Mount +from starlette.responses import Response, PlainTextResponse, RedirectResponse +from starlette.applications import Starlette from logging.handlers import TimedRotatingFileHandler -from twisted.web.server import Session -from zope.interface import Interface, Attribute, implementer -from twisted.python.components import registerAdapter import jinja2 import bcrypt import re +import jwt +import yaml +import secrets +import string +import random +from base64 import b64decode from enum import Enum -from urllib import parse +from datetime import datetime, timezone +from os import path, environ, mkdir, W_OK, access from core import CoreConfig, Utils from core.data import Data - -class IUserSession(Interface): - userId = Attribute("User's ID") - current_ip = Attribute("User's current ip address") - permissions = Attribute("User's permission level") - ongeki_version = Attribute("User's selected Ongeki Version") - class PermissionOffset(Enum): USER = 0 # Regular user USERMOD = 1 # Can moderate other users @@ -31,248 +29,424 @@ class PermissionOffset(Enum): # 4 - 6 reserved for future use OWNER = 7 # Can do anything -@implementer(IUserSession) -class UserSession(object): - def __init__(self, session): - self.userId = 0 - self.current_ip = "0.0.0.0" - self.permissions = 0 - self.ongeki_version = 7 +class ShopPermissionOffset(Enum): + VIEW = 0 # View info and cabs + BOOKKEEP = 1 # View bookeeping info + EDITOR = 2 # Can edit name, settings + REGISTRAR = 3 # Can add cabs + # 4 - 6 reserved for future use + OWNER = 7 # Can do anything +class ShopOwner(): + def __init__(self, usr_id: int = 0, usr_name: str = "", perms: int = 0) -> None: + self.user_id = usr_id + self.username = usr_name + self.permissions = perms -class FrontendServlet(resource.Resource): - def getChild(self, name: bytes, request: Request): - self.logger.debug(f"{Utils.get_ip_addr(request)} -> {name.decode()}") - if name == b"": - return self - return resource.Resource.getChild(self, name, request) +class UserSession(): + def __init__(self, usr_id: int = 0, ip: str = "", perms: int = 0, ongeki_ver: int = 7, chunithm_ver: int = -1, maimai_version: int = -1): + self.user_id = usr_id + self.current_ip = ip + self.permissions = perms + self.ongeki_version = ongeki_ver + self.chunithm_version = chunithm_ver + self.maimai_version = maimai_version +class FrontendServlet(): def __init__(self, cfg: CoreConfig, config_dir: str) -> None: self.config = cfg log_fmt_str = "[%(asctime)s] Frontend | %(levelname)s | %(message)s" log_fmt = logging.Formatter(log_fmt_str) - self.logger = logging.getLogger("frontend") self.environment = jinja2.Environment(loader=jinja2.FileSystemLoader(".")) - self.game_list: List[Dict[str, str]] = [] - self.children: Dict[str, Any] = {} + self.game_list: Dict[str, Dict[str, Any]] = {} + self.sn_cvt: Dict[str, str] = {} + + self.logger = logging.getLogger("frontend") + if not hasattr(self.logger, "inited"): + fileHandler = TimedRotatingFileHandler( + "{0}/{1}.log".format(self.config.server.log_dir, "frontend"), + when="d", + backupCount=10, + ) + fileHandler.setFormatter(log_fmt) - fileHandler = TimedRotatingFileHandler( - "{0}/{1}.log".format(self.config.server.log_dir, "frontend"), - when="d", - backupCount=10, - ) - fileHandler.setFormatter(log_fmt) + consoleHandler = logging.StreamHandler() + consoleHandler.setFormatter(log_fmt) - consoleHandler = logging.StreamHandler() - consoleHandler.setFormatter(log_fmt) + self.logger.addHandler(fileHandler) + self.logger.addHandler(consoleHandler) - self.logger.addHandler(fileHandler) - self.logger.addHandler(consoleHandler) + self.logger.setLevel(cfg.frontend.loglevel) + coloredlogs.install( + level=cfg.frontend.loglevel, logger=self.logger, fmt=log_fmt_str + ) - self.logger.setLevel(cfg.frontend.loglevel) - coloredlogs.install( - level=cfg.frontend.loglevel, logger=self.logger, fmt=log_fmt_str - ) - registerAdapter(UserSession, Session, IUserSession) - - fe_game = FE_Game(cfg, self.environment) + self.logger.inited = True + games = Utils.get_all_titles() for game_dir, game_mod in games.items(): - if hasattr(game_mod, "frontend"): + if hasattr(game_mod, "frontend") and hasattr(game_mod, "index") and hasattr(game_mod, "game_codes"): try: - game_fe = game_mod.frontend(cfg, self.environment, config_dir) - self.game_list.append({"url": game_dir, "name": game_fe.nav_name}) - fe_game.putChild(game_dir.encode(), game_fe) + if game_mod.index.is_game_enabled(game_mod.game_codes[0], self.config, config_dir): + game_fe = game_mod.frontend(cfg, self.environment, config_dir) + self.game_list[game_fe.nav_name] = {"url": f"/{game_dir}", "class": game_fe } + + if hasattr(game_fe, "SN_PREFIX") and hasattr(game_fe, "NETID_PREFIX"): + if len(game_fe.SN_PREFIX) == len(game_fe.NETID_PREFIX): + for x in range(len(game_fe.SN_PREFIX)): + self.sn_cvt[game_fe.SN_PREFIX[x]] = game_fe.NETID_PREFIX[x] except Exception as e: self.logger.error( f"Failed to import frontend from {game_dir} because {e}" ) + self.environment.globals["game_list"] = self.game_list - self.putChild(b"gate", FE_Gate(cfg, self.environment)) - self.putChild(b"user", FE_User(cfg, self.environment)) - self.putChild(b"sys", FE_System(cfg, self.environment)) - self.putChild(b"arcade", FE_Arcade(cfg, self.environment)) - self.putChild(b"cab", FE_Machine(cfg, self.environment)) - self.putChild(b"game", fe_game) + self.environment.globals["sn_cvt"] = self.sn_cvt + self.base = FE_Base(cfg, self.environment) + self.gate = FE_Gate(cfg, self.environment) + self.user = FE_User(cfg, self.environment) + self.system = FE_System(cfg, self.environment) + self.arcade = FE_Arcade(cfg, self.environment) + self.machine = FE_Machine(cfg, self.environment) + + def get_routes(self) -> List[Route]: + g_routes = [] + for nav_name, g_data in self.environment.globals["game_list"].items(): + g_routes.append(Mount(g_data['url'], routes=g_data['class'].get_routes())) + return [ + Route("/", self.base.render_GET, methods=['GET']), + Mount("/user", routes=[ + Route("/", self.user.render_GET, methods=['GET']), + Route("/{user_id:int}", self.user.render_GET, methods=['GET']), + Route("/update.pw", self.user.render_POST, methods=['POST']), + Route("/update.name", self.user.update_username, methods=['POST']), + Route("/edit.card", self.user.edit_card, methods=['POST']), + Route("/add.card", self.user.add_card, methods=['POST']), + Route("/logout", self.user.render_logout, methods=['GET']), + ]), + Mount("/gate", routes=[ + Route("/", self.gate.render_GET, methods=['GET', 'POST']), + Route("/gate.login", self.gate.render_login, methods=['POST']), + Route("/gate.create", self.gate.render_create, methods=['POST']), + Route("/create", self.gate.render_create_get, methods=['GET']), + ]), + Mount("/sys", routes=[ + Route("/", self.system.render_GET, methods=['GET']), + Route("/logs", self.system.render_logs, methods=['GET']), + Route("/lookup.user", self.system.lookup_user, methods=['GET']), + Route("/lookup.shop", self.system.lookup_shop, methods=['GET']), + Route("/add.user", self.system.add_user, methods=['POST']), + Route("/add.card", self.system.add_card, methods=['POST']), + Route("/add.shop", self.system.add_shop, methods=['POST']), + Route("/add.cab", self.system.add_cab, methods=['POST']), + ]), + Mount("/shop", routes=[ + Route("/", self.arcade.render_GET, methods=['GET']), + Route("/{shop_id:int}", self.arcade.render_GET, methods=['GET']), + ]), + Mount("/cab", routes=[ + Route("/", self.machine.render_GET, methods=['GET']), + Route("/{machine_id:int}", self.machine.render_GET, methods=['GET']), + ]), + Mount("/game", routes=g_routes), + Route("/robots.txt", self.robots) + ] + + def startup(self) -> None: + self.config.update({ + "frontend": { + "standalone": True, + "loglevel": CoreConfig.loglevel_to_str(self.config.frontend.loglevel), + "secret": self.config.frontend.secret + } + }) + self.logger.info(f"Serving {len(self.game_list)} games") + + @classmethod + async def robots(cls, request: Request) -> PlainTextResponse: + return PlainTextResponse("User-agent: *\nDisallow: /\n\nUser-agent: AdsBot-Google\nDisallow: /") - self.logger.info( - f"Ready on port {self.config.frontend.port} serving {len(fe_game.children)} games" - ) - - def render_GET(self, request): - self.logger.debug(f"{Utils.get_ip_addr(request)} -> {request.uri.decode()}") - template = self.environment.get_template("core/frontend/index.jinja") - return template.render( - server_name=self.config.server.name, - title=self.config.server.name, - game_list=self.game_list, - sesh=vars(IUserSession(request.getSession())), - ).encode("utf-16") - - -class FE_Base(resource.Resource): +class FE_Base(): """ A Generic skeleton class that all frontend handlers should inherit from Initializes the environment, data, logger, config, and sets isLeaf to true It is expected that game implementations of this class overwrite many of these """ - - isLeaf = True - def __init__(self, cfg: CoreConfig, environment: jinja2.Environment) -> None: self.core_config = cfg self.data = Data(cfg) self.logger = logging.getLogger("frontend") self.environment = environment - self.nav_name = "nav_name" + self.nav_name = "index" + + async def render_GET(self, request: Request): + self.logger.debug(f"{Utils.get_ip_addr(request)} -> {request.url}") + template = self.environment.get_template("core/templates/index.jinja") + sesh = self.validate_session(request) + resp = Response(template.render( + server_name=self.core_config.server.name, + title=self.core_config.server.name, + game_list=self.environment.globals["game_list"], + sesh=vars(sesh) if sesh is not None else vars(UserSession()), + ), media_type="text/html; charset=utf-8") + + if sesh is None: + resp.delete_cookie("ARTEMIS_SESH") + return resp + + def get_routes(self) -> List[Route]: + return [] + + @classmethod + def test_perm(cls, permission: int, offset: Union[PermissionOffset, ShopPermissionOffset]) -> bool: + logging.getLogger('frontend').debug(f"{permission} vs {1 << offset.value}") + return permission & 1 << offset.value == 1 << offset.value + + @classmethod + def test_perm_minimum(cls, permission: int, offset: Union[PermissionOffset, ShopPermissionOffset]) -> bool: + return permission >= 1 << offset.value + + def decode_session(self, token: str) -> UserSession: + sesh = UserSession() + if not token: return sesh + try: + tk = jwt.decode(token, b64decode(self.core_config.frontend.secret), options={"verify_signature": True}, algorithms=["HS256"]) + sesh.user_id = tk['user_id'] + sesh.current_ip = tk['current_ip'] + sesh.permissions = tk['permissions'] + sesh.chunithm_version = tk['chunithm_version'] + sesh.maimai_version = tk['maimai_version'] + sesh.ongeki_version = tk['ongeki_version'] + if sesh.user_id <= 0: + self.logger.error("User session failed to validate due to an invalid ID!") + return UserSession() + return sesh + except jwt.ExpiredSignatureError: + self.logger.error("User session failed to validate due to an expired signature!") + return sesh + except jwt.InvalidSignatureError: + self.logger.error("User session failed to validate due to an invalid signature!") + return sesh + except jwt.DecodeError as e: + self.logger.error(f"User session failed to decode! {e}") + return sesh + except jwt.InvalidTokenError as e: + self.logger.error(f"User session is invalid! {e}") + return sesh + except KeyError as e: + self.logger.error(f"{e} missing from User session!") + return UserSession() + except Exception as e: + self.logger.error(f"Unknown exception occoured when decoding User session! {e}") + return UserSession() + + def validate_session(self, request: Request) -> Optional[UserSession]: + sesh = request.cookies.get('ARTEMIS_SESH', "") + if not sesh: + return None + + usr_sesh = self.decode_session(sesh) + req_ip = Utils.get_ip_addr(request) + + if usr_sesh.current_ip != req_ip: + self.logger.error(f"User session failed to validate due to mismatched IPs! {usr_sesh.current_ip} -> {req_ip}") + return None + + if usr_sesh.permissions <= 0 or usr_sesh.permissions > 255: + self.logger.error(f"User session failed to validate due to an invalid permission value! {usr_sesh.permissions}") + return None + + return usr_sesh + + def encode_session(self, sesh: UserSession, exp_seconds: int = 86400) -> str: + try: + return jwt.encode({ + "user_id": sesh.user_id, + "current_ip": sesh.current_ip, + "permissions": sesh.permissions, + "ongeki_version": sesh.ongeki_version, + "chunithm_version": sesh.chunithm_version, + "maimai_version": sesh.maimai_version, + "exp": int(datetime.now(tz=timezone.utc).timestamp()) + exp_seconds }, + b64decode(self.core_config.frontend.secret), + algorithm="HS256" + ) + except jwt.InvalidKeyError: + self.logger.error("Failed to encode User session because the secret is invalid!") + return "" + except Exception as e: + self.logger.error(f"Unknown exception occoured when encoding User session! {e}") + return "" class FE_Gate(FE_Base): - def render_GET(self, request: Request): - self.logger.debug(f"{Utils.get_ip_addr(request)} -> {request.uri.decode()}") - uri: str = request.uri.decode() + async def render_GET(self, request: Request): + self.logger.debug(f"{Utils.get_ip_addr(request)} -> {request.url.path}") - sesh = request.getSession() - usr_sesh = IUserSession(sesh) - if usr_sesh.userId > 0: - return redirectTo(b"/user", request) - - if uri.startswith("/gate/create"): - return self.create_user(request) - - if b"e" in request.args: + usr_sesh = self.validate_session(request) + if usr_sesh and usr_sesh.user_id > 0: + return RedirectResponse("/user/", 303) + + + if "e" in request.query_params: try: - err = int(request.args[b"e"][0].decode()) + err = int(request.query_params.get("e", ["0"])[0]) except Exception: err = 0 else: err = 0 - template = self.environment.get_template("core/frontend/gate/gate.jinja") - return template.render( + template = self.environment.get_template("core/templates/gate/gate.jinja") + resp = Response(template.render( title=f"{self.core_config.server.name} | Login Gate", error=err, - sesh=vars(usr_sesh), - ).encode("utf-16") - - def render_POST(self, request: Request): - uri = request.uri.decode() + sesh=vars(UserSession()), + ), media_type="text/html; charset=utf-8") + resp.delete_cookie("ARTEMIS_SESH") + return resp + + async def render_login(self, request: Request): ip = Utils.get_ip_addr(request) - - if uri == "/gate/gate.login": - access_code: str = request.args[b"access_code"][0].decode() - passwd: bytes = request.args[b"passwd"][0] - if passwd == b"": - passwd = None - - uid = self.data.card.get_user_id_from_card(access_code) - user = self.data.user.get_user(uid) - if uid is None: - return redirectTo(b"/gate?e=1", request) - - if passwd is None: - sesh = self.data.user.check_password(uid) - - if sesh is not None: - return redirectTo( - f"/gate/create?ac={access_code}".encode(), request - ) - return redirectTo(b"/gate?e=1", request) - - if not self.data.user.check_password(uid, passwd): - return redirectTo(b"/gate?e=1", request) - - self.logger.info(f"Successful login of user {uid} at {ip}") - - sesh = request.getSession() - usr_sesh = IUserSession(sesh) - usr_sesh.userId = uid - usr_sesh.current_ip = ip - usr_sesh.permissions = user['permissions'] - - return redirectTo(b"/user", request) - - elif uri == "/gate/gate.create": - access_code: str = request.args[b"access_code"][0].decode() - username: str = request.args[b"username"][0] - email: str = request.args[b"email"][0].decode() - passwd: bytes = request.args[b"passwd"][0] - - uid = self.data.card.get_user_id_from_card(access_code) - if uid is None: - return redirectTo(b"/gate?e=1", request) - - salt = bcrypt.gensalt() - hashed = bcrypt.hashpw(passwd, salt) - - result = self.data.user.create_user( - uid, username, email.lower(), hashed.decode(), 1 - ) - if result is None: - return redirectTo(b"/gate?e=3", request) - - if not self.data.user.check_password(uid, passwd): - return redirectTo(b"/gate", request) - - return redirectTo(b"/user", request) - - else: - return b"" - - def create_user(self, request: Request): - if b"ac" not in request.args or len(request.args[b"ac"][0].decode()) != 20: - return redirectTo(b"/gate?e=2", request) - - ac = request.args[b"ac"][0].decode() - card = self.data.card.get_card_by_access_code(ac) - if card is None: - return redirectTo(b"/gate?e=1", request) + frm = await request.form() + access_code: str = frm.get("access_code", None) + if not access_code: + return RedirectResponse("/gate/?e=1", 303) - user = self.data.user.get_user(card['user']) + passwd: bytes = frm.get("passwd", "").encode() + if passwd == b"": + passwd = None + + uid = await self.data.card.get_user_id_from_card(access_code) + if uid is None: + user = await self.data.user.get_user_by_username(access_code) # Lookup as username + if not user: + self.logger.debug(f"Failed to find user for card/username {access_code}") + return RedirectResponse("/gate/?e=1", 303) + + uid = user['id'] + + user = await self.data.user.get_user(uid) + if user is None: + self.logger.error(f"Failed to load user {uid}") + return RedirectResponse("/gate/?e=1", 303) + + if passwd is None: + sesh = await self.data.user.check_password(uid) + + if sesh is not None: + return RedirectResponse(f"/gate/create?ac={access_code}", 303) + + return RedirectResponse("/gate/?e=1", 303) + + if not await self.data.user.check_password(uid, passwd): + self.logger.debug(f"Failed password for access code {access_code}") + return RedirectResponse("/gate/?e=1", 303) + + self.logger.info(f"Successful login of user {uid} at {ip}") + + sesh = UserSession() + sesh.user_id = uid + sesh.current_ip = ip + sesh.permissions = user['permissions'] + + usr_sesh = self.encode_session(sesh) + self.logger.debug(f"Created session with JWT {usr_sesh}") + resp = RedirectResponse("/user/", 303) + resp.set_cookie("ARTEMIS_SESH", usr_sesh) + + return resp + + async def render_create(self, request: Request): + ip = Utils.get_ip_addr(request) + frm = await request.form() + access_code: str = frm.get("access_code", "") + username: str = frm.get("username", "") + email: str = frm.get("email", "") + passwd: bytes = frm.get("passwd", "").encode() + + if not access_code or not username or not email or not passwd: + return RedirectResponse("/gate/?e=1", 303) + + uid = await self.data.card.get_user_id_from_card(access_code) + if uid is None: + return RedirectResponse("/gate/?e=1", 303) + + salt = bcrypt.gensalt() + hashed = bcrypt.hashpw(passwd, salt) + + result = await self.data.user.create_user( + uid, username, email.lower(), hashed.decode(), 1 + ) + if result is None: + return RedirectResponse("/gate/?e=3", 303) + + if not await self.data.user.check_password(uid, passwd): + return RedirectResponse("/gate/", 303) + + sesh = UserSession() + sesh.user_id = uid + sesh.current_ip = ip + sesh.permissions = 1 + + usr_sesh = self.encode_session(sesh) + self.logger.debug(f"Created session with JWT {usr_sesh}") + resp = RedirectResponse("/user/", 303) + resp.set_cookie("ARTEMIS_SESH", usr_sesh) + + return resp + + async def render_create_get(self, request: Request): + ac = request.query_params.get("ac", "") + if len(ac) != 20: + return RedirectResponse("/gate/?e=2", 303) + + card = await self.data.card.get_card_by_access_code(ac) + if card is None: + return RedirectResponse("/gate/?e=1", 303) + + user = await self.data.user.get_user(card['user']) if user is None: self.logger.warning(f"Card {ac} exists with no/invalid associated user ID {card['user']}") - return redirectTo(b"/gate?e=0", request) + return RedirectResponse("/gate/?e=0", 303) if user['password'] is not None: - return redirectTo(b"/gate?e=1", request) + return RedirectResponse("/gate/?e=1", 303) - template = self.environment.get_template("core/frontend/gate/create.jinja") - return template.render( + template = self.environment.get_template("core/templates/gate/create.jinja") + return Response(template.render( title=f"{self.core_config.server.name} | Create User", code=ac, - sesh={"userId": 0, "permissions": 0}, - ).encode("utf-16") - + sesh={"user_id": 0, "permissions": 0}, + ), media_type="text/html; charset=utf-8") class FE_User(FE_Base): - def render_GET(self, request: Request): - uri = request.uri.decode() - template = self.environment.get_template("core/frontend/user/index.jinja") + async def render_GET(self, request: Request): + uri = request.url.path + user_id = request.path_params.get('user_id', None) + self.logger.debug(f"{Utils.get_ip_addr(request)} -> {uri}") + template = self.environment.get_template("core/templates/user/index.jinja") - sesh: Session = request.getSession() - usr_sesh = IUserSession(sesh) - if usr_sesh.userId == 0: - return redirectTo(b"/gate", request) + usr_sesh = self.validate_session(request) + if not usr_sesh: + return RedirectResponse("/gate/", 303) - m = re.match("\/user\/(\d*)", uri) - - if m is not None: - usrid = m.group(1) - if usr_sesh.permissions < 1 << PermissionOffset.USERMOD.value or not usrid == usr_sesh.userId: - return redirectTo(b"/user", request) + if user_id: + if not self.test_perm(usr_sesh.permissions, PermissionOffset.USERMOD) and user_id != usr_sesh.user_id: + self.logger.warn(f"User {usr_sesh.user_id} does not have permission to view user {user_id}") + return RedirectResponse("/user/", 303) else: - usrid = usr_sesh.userId + user_id = usr_sesh.user_id - user = self.data.user.get_user(usrid) + user = await self.data.user.get_user(user_id) if user is None: - return redirectTo(b"/user", request) + self.logger.debug(f"User {user_id} not found") + return RedirectResponse("/user/", 303) - cards = self.data.card.get_user_cards(usrid) - arcades = self.data.arcade.get_arcades_managed_by_user(usrid) + cards = await self.data.card.get_user_cards(user_id) card_data = [] arcade_data = [] @@ -285,176 +459,558 @@ class FE_User(FE_Base): else: status = 'Active' - card_data.append({'access_code': c['access_code'], 'status': status}) - - for a in arcades: - arcade_data.append({'id': a['id'], 'name': a['name']}) + #idm = c['idm'] + ac = c['access_code'] - return template.render( + if ac.startswith("5"): #or idm is not None: + c_type = "AmusementIC" + elif ac.startswith("3"): + c_type = "Banapass" + elif ac.startswith("010"): + c_type = "Aime" # TODO: Aime verification + elif ac.startswith("0008"): + c_type = "Generated AIC" + else: + c_type = "Unknown" + + card_data.append({ + 'access_code': ac, + 'status': status, + 'chip_id': c['chip_id'], + 'idm': c['idm'], + 'type': c_type, + "memo": c['memo'], + "id": c['id'], + }) + + if "e" in request.query_params: + try: + err = int(request.query_params.get("e", 0)) + except Exception: + err = 0 + + else: + err = 0 + + if "s" in request.query_params: + try: + succ = int(request.query_params.get("s", 0)) + except Exception: + succ = 0 + + else: + succ = 0 + + return Response(template.render( title=f"{self.core_config.server.name} | Account", sesh=vars(usr_sesh), - cards=card_data, + cards=card_data, + error=err, + success=succ, username=user['username'], arcades=arcade_data - ).encode("utf-16") + ), media_type="text/html; charset=utf-8") + + async def render_logout(self, request: Request): + resp = RedirectResponse("/gate/", 303) + resp.delete_cookie("ARTEMIS_SESH") + return resp + + async def edit_card(self, request: Request) -> RedirectResponse: + frm = await request.form() + usr_sesh = self.validate_session(request) + if not usr_sesh or not self.test_perm(usr_sesh.permissions, PermissionOffset.USERMOD): + return RedirectResponse("/gate/", 303) - def render_POST(self, request: Request): - pass + frm = await request.form() + cid = frm.get("card_edit_frm_card_id", None) + if not cid: + return RedirectResponse("/user/?e=999", 303) + + ac = frm.get("card_edit_frm_access_code", None) + if not ac: + return RedirectResponse("/user/?e=999", 303) + + card = await self.data.card.get_card_by_id(cid) + if not card: + return RedirectResponse("/user/?e=2", 303) + + if card['user'] != usr_sesh.user_id and not self.test_perm_minimum(usr_sesh.permissions, PermissionOffset.USERMOD): + return RedirectResponse("/user/?e=11", 303) + if frm.get("add_memo", None) or frm.get("add_memo", None) == "": + memo = frm.get("add_memo") + if len(memo) > 16: + return RedirectResponse("/user/?e=4", 303) + await self.data.card.set_memo_by_access_code(ac, memo) + + if False: # Saving this in case I want to allow editing idm/chip ID down the line + if frm.get("add_felica_idm", None): + idm = frm.get('add_felica_idm') + if not all(c in string.hexdigits for c in idm): + return RedirectResponse("/user/?e=4", 303) + await self.data.card.set_idm_by_access_code(ac, idm) + + if frm.get("add_mifare_chip_id", None): + chip_id: str = frm.get('add_mifare_chip_id') + if not all(c in string.hexdigits for c in idm): + return RedirectResponse("/user/?e=4", 303) + await self.data.card.set_chip_id_by_access_code(ac, int(chip_id, 16)) + + return RedirectResponse("/user/?s=4", 303) + + async def add_card(self, request: Request) -> RedirectResponse: + frm = await request.form() + card_type = frm.get("card_add_frm_type", None) + access_code = frm.get("add_access_code", None) + idm = frm.get("add_idm", None) + idm_caps = None + + usr_sesh = self.validate_session(request) + if not usr_sesh or not self.test_perm(usr_sesh.permissions, PermissionOffset.USERMOD): + return RedirectResponse("/gate/", 303) + + if not len(access_code) == 20 or (not access_code.startswith("5") and not access_code.startswith("3") \ + and not access_code.startswith("010") and not access_code.startswith("0008")): + return RedirectResponse("/user/?e=4", 303) + + if card_type == "0" and access_code.startswith("5") and len(idm) == 16: + idm_caps = idm.upper() + + if not all([x in string.hexdigits for x in idm_caps]): + return RedirectResponse("/user/?e=4", 303) + + if access_code.startswith("5") and not idm_caps: + return RedirectResponse("/user/?e=13", 303) + + test = await self.data.card.get_card_by_access_code(access_code) + if test: + return RedirectResponse("/user/?e=12", 303) + + if idm_caps: + test = await self.data.card.get_card_by_idm(idm_caps) + if test and test['user'] != usr_sesh.user_id: + return RedirectResponse("/user/?e=12", 303) + + test = await self.data.card.get_card_by_access_code(self.data.card.to_access_code(idm_caps)) + if test: + if test['user'] != usr_sesh.user_id: + return RedirectResponse("/user/?e=12", 303) + + await self.data.card.set_access_code_by_access_code(test['access_code'], access_code) + self.logger.info(f"Update card {test['id']} from {test['access_code']} to {access_code} for user {usr_sesh.user_id}") + + await self.data.card.set_idm_by_access_code(access_code, idm_caps) + self.logger.info(f"Set IDm for card {access_code} to {idm_caps}") + return RedirectResponse("/user/?s=1", 303) + + if card_type == "0" and access_code.startswith("0008"): + test = await self.data.card.get_card_by_idm(self.data.card.to_idm(access_code)) + if test: + return RedirectResponse("/user/?e=12", 303) + + new_card = await self.data.card.create_card(usr_sesh.user_id, access_code) + self.logger.info(f"Created new card {new_card} with access code {access_code} for user {usr_sesh.user_id}") + + if idm_caps: + await self.data.card.set_idm_by_access_code(access_code, idm_caps) + self.logger.info(f"Set IDm for card {access_code} to {idm_caps}") + + return RedirectResponse("/user/?s=1", 303) + + async def render_POST(self, request: Request): + frm = await request.form() + usr_sesh = self.validate_session(request) + if not usr_sesh or not self.test_perm(usr_sesh.permissions, PermissionOffset.USERMOD): + return RedirectResponse("/gate/", 303) + + old_pw: str = frm.get('current_pw', None) + pw1: str = frm.get('password1', None) + pw2: str = frm.get('password2', None) + + if old_pw is None or pw1 is None or pw2 is None: + return RedirectResponse("/user/?e=4", 303) + + if pw1 != pw2: + return RedirectResponse("/user/?e=6", 303) + + if not await self.data.user.check_password(usr_sesh.user_id, old_pw.encode()): + return RedirectResponse("/user/?e=5", 303) + + if len(pw1) < 10 or not any(ele.isupper() for ele in pw1) or not any(ele.islower() for ele in pw1) \ + or not any(ele.isdigit() for ele in pw1) or not any(not ele.isalnum() for ele in pw1): + return RedirectResponse("/user/?e=7", 303) + + salt = bcrypt.gensalt() + hashed = bcrypt.hashpw(pw1.encode(), salt) + if not await self.data.user.change_password(usr_sesh.user_id, hashed.decode()): + return RedirectResponse("/gate/?e=1", 303) + + return RedirectResponse("/user/?s=1", 303) + + async def update_username(self, request: Request): + frm = await request.form() + new_name: bytes = frm.get('new_name', "") + usr_sesh = self.validate_session(request) + if not usr_sesh or not self.test_perm(usr_sesh.permissions, PermissionOffset.USERMOD): + return RedirectResponse("/gate/", 303) + + if new_name is None or not new_name: + return RedirectResponse("/user/?e=4", 303) + + if len(new_name) > 10: + return RedirectResponse("/user/?e=8", 303) + + if not await self.data.user.change_username(usr_sesh.user_id, new_name): + return RedirectResponse("/user/?e=8", 303) + + return RedirectResponse("/user/?s=2", 303) class FE_System(FE_Base): - def render_GET(self, request: Request): - uri = request.uri.decode() - template = self.environment.get_template("core/frontend/sys/index.jinja") + async def render_GET(self, request: Request): + template = self.environment.get_template("core/templates/sys/index.jinja") + self.logger.debug(f"{Utils.get_ip_addr(request)} -> {request.url.path}") + + usr_sesh = self.validate_session(request) + if not usr_sesh or not self.test_perm_minimum(usr_sesh.permissions, PermissionOffset.USERMOD): + return RedirectResponse("/gate/", 303) + + if request.query_params.get("e", None): + err = int(request.query_params.get("e")) + else: + err = 0 + + return Response(template.render( + title=f"{self.core_config.server.name} | System", + sesh=vars(usr_sesh), + usrlist=[], + error = err + ), media_type="text/html; charset=utf-8") + + async def lookup_user(self, request: Request): + template = self.environment.get_template("core/templates/sys/index.jinja") usrlist: List[Dict] = [] - aclist: List[Dict] = [] - cablist: List[Dict] = [] - - sesh: Session = request.getSession() - usr_sesh = IUserSession(sesh) - if usr_sesh.userId == 0 or usr_sesh.permissions < 1 << PermissionOffset.USERMOD.value: - return redirectTo(b"/gate", request) + usr_sesh = self.validate_session(request) + if not usr_sesh or not self.test_perm(usr_sesh.permissions, PermissionOffset.USERMOD): + return RedirectResponse("/gate/", 303) - if uri.startswith("/sys/lookup.user?"): - uri_parse = parse.parse_qs(uri.replace("/sys/lookup.user?", "")) # lop off the first bit - uid_search = uri_parse.get("usrId") - email_search = uri_parse.get("usrEmail") - uname_search = uri_parse.get("usrName") + uid_search = request.query_params.get("usrId", None) + email_search = request.query_params.get("usrEmail", None) + uname_search = request.query_params.get("usrName", None) - if uid_search is not None: - u = self.data.user.get_user(uid_search[0]) - if u is not None: - usrlist.append(u._asdict()) + if uid_search: + u = await self.data.user.get_user(uid_search) + if u is not None: + usrlist.append(u._asdict()) - elif email_search is not None: - u = self.data.user.find_user_by_email(email_search[0]) - if u is not None: - usrlist.append(u._asdict()) + elif email_search: + u = await self.data.user.find_user_by_email(email_search) + if u is not None: + usrlist.append(u._asdict()) - elif uname_search is not None: - ul = self.data.user.find_user_by_username(uname_search[0]) - for u in ul: - usrlist.append(u._asdict()) + elif uname_search: + ul = await self.data.user.find_user_by_username(uname_search) + for u in ul: + usrlist.append(u._asdict()) - elif uri.startswith("/sys/lookup.arcade?"): - uri_parse = parse.parse_qs(uri.replace("/sys/lookup.arcade?", "")) # lop off the first bit - ac_id_search = uri_parse.get("arcadeId") - ac_name_search = uri_parse.get("arcadeName") - ac_user_search = uri_parse.get("arcadeUser") - ac_ip_search = uri_parse.get("arcadeIp") - - if ac_id_search is not None: - u = self.data.arcade.get_arcade(ac_id_search[0]) - if u is not None: - aclist.append(u._asdict()) - - elif ac_name_search is not None: - ul = self.data.arcade.get_arcade_by_name(ac_name_search[0]) - if ul is not None: - for u in ul: - aclist.append(u._asdict()) - - elif ac_user_search is not None: - ul = self.data.arcade.get_arcades_managed_by_user(ac_user_search[0]) - if ul is not None: - for u in ul: - aclist.append(u._asdict()) - - elif ac_ip_search is not None: - ul = self.data.arcade.get_arcades_by_ip(ac_ip_search[0]) - if ul is not None: - for u in ul: - aclist.append(u._asdict()) - - elif uri.startswith("/sys/lookup.cab?"): - uri_parse = parse.parse_qs(uri.replace("/sys/lookup.cab?", "")) # lop off the first bit - cab_id_search = uri_parse.get("cabId") - cab_serial_search = uri_parse.get("cabSerial") - cab_acid_search = uri_parse.get("cabAcId") - - if cab_id_search is not None: - u = self.data.arcade.get_machine(id=cab_id_search[0]) - if u is not None: - cablist.append(u._asdict()) - - elif cab_serial_search is not None: - u = self.data.arcade.get_machine(serial=cab_serial_search[0]) - if u is not None: - cablist.append(u._asdict()) - - elif cab_acid_search is not None: - ul = self.data.arcade.get_arcade_machines(cab_acid_search[0]) - for u in ul: - cablist.append(u._asdict()) - - return template.render( + return Response(template.render( title=f"{self.core_config.server.name} | System", sesh=vars(usr_sesh), usrlist=usrlist, - aclist=aclist, - cablist=cablist, - ).encode("utf-16") + shoplist=[], + ), media_type="text/html; charset=utf-8") + async def lookup_shop(self, request: Request): + shoplist = [] + template = self.environment.get_template("core/templates/sys/index.jinja") + + usr_sesh = self.validate_session(request) + if not usr_sesh or not self.test_perm(usr_sesh.permissions, PermissionOffset.ACMOD): + return RedirectResponse("/gate/", 303) + + shopid_search = request.query_params.get("shopId", None) + sn_search = request.query_params.get("serialNum", None) + + if shopid_search: + if shopid_search.isdigit(): + shopid_search = int(shopid_search) + try: + sinfo = await self.data.arcade.get_arcade(shopid_search) + except Exception as e: + self.logger.error(f"Failed to fetch shop info for shop {shopid_search} in lookup_shop - {e}") + sinfo = None + if sinfo: + shoplist.append({ + "name": sinfo['name'], + "id": sinfo['id'] + }) + + else: + return Response(template.render( + title=f"{self.core_config.server.name} | System", + sesh=vars(usr_sesh), + usrlist=[], + shoplist=shoplist, + error=4 + ), media_type="text/html; charset=utf-8") + + if sn_search: + sn_search = sn_search.upper().replace("-", "").strip() + if sn_search.isdigit() and len(sn_search) == 12: + prefix = sn_search[:4] + suffix = sn_search[5:] + + netid_prefix = self.environment.globals["sn_cvt"].get(prefix, "") + sn_search = netid_prefix + suffix + + if re.match(r"^AB[DGL]N\d{7}$", sn_search) or re.match(r"^A\d{2}[EX]\d{2}[A-Z]\d{4,8}$", sn_search): + cabinfo = await self.data.arcade.get_machine(sn_search) + if cabinfo is None: sinfo = None + else: + sinfo = await self.data.arcade.get_arcade(cabinfo['arcade']) + if sinfo: + shoplist.append({ + "name": sinfo['name'], + "id": sinfo['id'] + }) + + else: + return Response(template.render( + title=f"{self.core_config.server.name} | System", + sesh=vars(usr_sesh), + usrlist=[], + shoplist=shoplist, + error=10 + ), media_type="text/html; charset=utf-8") + + + return Response(template.render( + title=f"{self.core_config.server.name} | System", + sesh=vars(usr_sesh), + usrlist=[], + shoplist=shoplist, + ), media_type="text/html; charset=utf-8") -class FE_Game(FE_Base): - isLeaf = False - children: Dict[str, Any] = {} + async def add_user(self, request: Request): + template = self.environment.get_template("core/templates/sys/index.jinja") + + usr_sesh = self.validate_session(request) + if not usr_sesh or not self.test_perm(usr_sesh.permissions, PermissionOffset.ACMOD): + return RedirectResponse("/gate/", 303) + + frm = await request.form() + username = frm.get("userName", None) + email = frm.get("userEmail", None) + perm = frm.get("usrPerm", "1") + passwd = "".join( + secrets.choice(string.ascii_letters + string.digits) for i in range(20) + ) + hash = bcrypt.hashpw(passwd.encode(), bcrypt.gensalt()) - def getChild(self, name: bytes, request: Request): - if name == b"": - return self - return resource.Resource.getChild(self, name, request) + if not email: + return RedirectResponse("/sys/?e=4", 303) - def render_GET(self, request: Request) -> bytes: - return redirectTo(b"/user", request) + uid = await self.data.user.create_user(username=username if username else None, email=email, password=hash.decode(), permission=int(perm)) + return Response(template.render( + title=f"{self.core_config.server.name} | System", + sesh=vars(usr_sesh), + usradd={"id": uid, "username": username, "password": passwd}, + ), media_type="text/html; charset=utf-8") + async def add_card(self, request: Request): + template = self.environment.get_template("core/templates/sys/index.jinja") + + usr_sesh = self.validate_session(request) + if not usr_sesh or not self.test_perm(usr_sesh.permissions, PermissionOffset.ACMOD): + return RedirectResponse("/gate/", 303) + + frm = await request.form() + userid = frm.get("cardUsr", None) + access_code = frm.get("cardAc", None) + idm = frm.get("cardIdm", None) + if userid is None or access_code is None or not userid.isdigit() or not len(access_code) == 20 or not access_code.isdigit: + return RedirectResponse("/sys/?e=4", 303) + + cardid = await self.data.card.create_card(int(userid), access_code) + if not cardid: + return RedirectResponse("/sys/?e=99", 303) + + if idm is not None: + # TODO: save IDM + pass + + return Response(template.render( + title=f"{self.core_config.server.name} | System", + sesh=vars(usr_sesh), + cardadd={"id": cardid, "user": userid, "access_code": access_code}, + ), media_type="text/html; charset=utf-8") + + async def add_shop(self, request: Request): + template = self.environment.get_template("core/templates/sys/index.jinja") + + usr_sesh = self.validate_session(request) + if not usr_sesh or not self.test_perm(usr_sesh.permissions, PermissionOffset.ACMOD): + return RedirectResponse("/gate/", 303) + + frm = await request.form() + name = frm.get("shopName", None) + country = frm.get("shopCountry", "JPN") + ip = frm.get("shopIp", None) + + acid = await self.data.arcade.create_arcade(name if name else None, name if name else None, country) + if not acid: + return RedirectResponse("/sys/?e=99", 303) + + if ip: + # TODO: set IP + pass + + return Response(template.render( + title=f"{self.core_config.server.name} | System", + sesh=vars(usr_sesh), + shopadd={"id": acid}, + ), media_type="text/html; charset=utf-8") + + async def add_cab(self, request: Request): + template = self.environment.get_template("core/templates/sys/index.jinja") + + usr_sesh = self.validate_session(request) + if not usr_sesh or not self.test_perm(usr_sesh.permissions, PermissionOffset.ACMOD): + return RedirectResponse("/gate/", 303) + + frm = await request.form() + shopid = frm.get("cabShop", None) + serial = frm.get("cabSerial", None) + game_code = frm.get("cabGame", None) + + if not shopid or not shopid.isdigit(): + return RedirectResponse("/sys/?e=4", 303) + + if not serial: + append = self.data.arcade.get_keychip_suffix(datetime.now().year, datetime.now().month) + generated = await self.data.arcade.get_num_generated_keychips() + if not generated: + generated = 0 + serial = self.data.arcade.format_serial("A69A", 1, "A", generated + 1, int(append)) + serial_dash = self.data.arcade.format_serial("A69A", 1, "A", generated + 1, int(append), True) + + cab_id = await self.data.arcade.create_machine(int(shopid), serial, None, game_code if game_code else None) + + return Response(template.render( + title=f"{self.core_config.server.name} | System", + sesh=vars(usr_sesh), + cabadd={"id": cab_id, "serial": serial_dash}, + ), media_type="text/html; charset=utf-8") + + async def render_logs(self, request: Request): + template = self.environment.get_template("core/templates/sys/logs.jinja") + events = [] + + usr_sesh = self.validate_session(request) + if not usr_sesh or not self.test_perm(usr_sesh.permissions, PermissionOffset.SYSADMIN): + return RedirectResponse("/sys/?e=11", 303) + + logs = await self.data.base.get_event_log() + if not logs: + logs = [] + + for log in logs: + evt = log._asdict() + if not evt['user']: evt["user"] = "NONE" + if not evt['arcade']: evt["arcade"] = "NONE" + if not evt['machine']: evt["machine"] = "NONE" + if not evt['ip']: evt["ip"] = "NONE" + if not evt['game']: evt["game"] = "NONE" + if not evt['version']: evt["version"] = "NONE" + evt['when_logged'] = evt['when_logged'].strftime("%x %X") + events.append(evt) + + return Response(template.render( + title=f"{self.core_config.server.name} | Event Logs", + sesh=vars(usr_sesh), + events=events + ), media_type="text/html; charset=utf-8") + class FE_Arcade(FE_Base): - def render_GET(self, request: Request): - uri = request.uri.decode() - template = self.environment.get_template("core/frontend/arcade/index.jinja") - managed = [] + async def render_GET(self, request: Request): + template = self.environment.get_template("core/templates/arcade/index.jinja") + shop_id = request.path_params.get('shop_id', None) - sesh: Session = request.getSession() - usr_sesh = IUserSession(sesh) - if usr_sesh.userId == 0: - return redirectTo(b"/gate", request) + usr_sesh = self.validate_session(request) + if not usr_sesh or not self.test_perm(usr_sesh.permissions, PermissionOffset.ACMOD): + self.logger.warn(f"User {usr_sesh.user_id} does not have permission to view shops!") + return RedirectResponse("/gate/", 303) - m = re.match("\/arcade\/(\d*)", uri) + if not shop_id: + return Response(template.render( + title=f"{self.core_config.server.name} | Arcade", + sesh=vars(usr_sesh), + ), media_type="text/html; charset=utf-8") - if m is not None: - arcadeid = m.group(1) - perms = self.data.arcade.get_manager_permissions(usr_sesh.userId, arcadeid) - arcade = self.data.arcade.get_arcade(arcadeid) - - if perms is None: - perms = 0 + sinfo = await self.data.arcade.get_arcade(shop_id) + if not sinfo: + return Response(template.render( + title=f"{self.core_config.server.name} | Arcade", + sesh=vars(usr_sesh), + ), media_type="text/html; charset=utf-8") - else: - return redirectTo(b"/user", request) + cabs = await self.data.arcade.get_arcade_machines(shop_id) + cablst = [] + if cabs: + for x in cabs: + cablst.append({ + "id": x['id'], + "serial": x['serial'], + "game": x['game'], + }) - return template.render( + return Response(template.render( title=f"{self.core_config.server.name} | Arcade", sesh=vars(usr_sesh), - error=0, - perms=perms, - arcade=arcade._asdict() - ).encode("utf-16") - + arcade={ + "name": sinfo['name'], + "id": sinfo['id'], + "cabs": cablst + } + + ), media_type="text/html; charset=utf-8") class FE_Machine(FE_Base): - def render_GET(self, request: Request): - uri = request.uri.decode() - template = self.environment.get_template("core/frontend/machine/index.jinja") + async def render_GET(self, request: Request): + template = self.environment.get_template("core/templates/machine/index.jinja") + cab_id = request.path_params.get('cab_id', None) - sesh: Session = request.getSession() - usr_sesh = IUserSession(sesh) - if usr_sesh.userId == 0: - return redirectTo(b"/gate", request) + usr_sesh = self.validate_session(request) + if not usr_sesh or not self.test_perm(usr_sesh.permissions, PermissionOffset.ACMOD): + self.logger.warn(f"User {usr_sesh.user_id} does not have permission to view shops!") + return RedirectResponse("/gate/", 303) - return template.render( + if not cab_id: + return Response(template.render( + title=f"{self.core_config.server.name} | Machine", + sesh=vars(usr_sesh), + ), media_type="text/html; charset=utf-8") + + return Response(template.render( title=f"{self.core_config.server.name} | Machine", - sesh=vars(usr_sesh), - arcade={}, - error=0, - ).encode("utf-16") \ No newline at end of file + sesh=vars(usr_sesh), + arcade={} + ), media_type="text/html; charset=utf-8") + +cfg_dir = environ.get("ARTEMIS_CFG_DIR", "config") +cfg: CoreConfig = CoreConfig() +if path.exists(f"{cfg_dir}/core.yaml"): + cfg.update(yaml.safe_load(open(f"{cfg_dir}/core.yaml"))) + +if not path.exists(cfg.server.log_dir): + mkdir(cfg.server.log_dir) + +if not access(cfg.server.log_dir, W_OK): + print( + f"Log directory {cfg.server.log_dir} NOT writable, please check permissions" + ) + exit(1) + +fe = FrontendServlet(cfg, cfg_dir) +app = Starlette(cfg.server.is_develop, fe.get_routes(), on_startup=[fe.startup]) diff --git a/core/frontend/arcade/index.jinja b/core/frontend/arcade/index.jinja deleted file mode 100644 index 20a1f46..0000000 --- a/core/frontend/arcade/index.jinja +++ /dev/null @@ -1,4 +0,0 @@ -{% extends "core/frontend/index.jinja" %} -{% block content %} -

{{ arcade.name }}

-{% endblock content %} \ No newline at end of file diff --git a/core/frontend/gate/gate.jinja b/core/frontend/gate/gate.jinja deleted file mode 100644 index 90abb98..0000000 --- a/core/frontend/gate/gate.jinja +++ /dev/null @@ -1,32 +0,0 @@ -{% extends "core/frontend/index.jinja" %} -{% block content %} -

Gate

-{% include "core/frontend/widgets/err_banner.jinja" %} - -
-
-
- -
-
-
- -
-

- -
-
*To register for the webui, type in the access code of your card, as shown in a game, and leave the password field blank.
-
*If you have not registered a card with this server, you cannot create a webui account.
-{% endblock content %} \ No newline at end of file diff --git a/core/frontend/machine/index.jinja b/core/frontend/machine/index.jinja deleted file mode 100644 index 01e90a0..0000000 --- a/core/frontend/machine/index.jinja +++ /dev/null @@ -1,5 +0,0 @@ -{% extends "core/frontend/index.jinja" %} -{% block content %} -{% include "core/frontend/widgets/err_banner.jinja" %} -

Machine Management

-{% endblock content %} \ No newline at end of file diff --git a/core/frontend/sys/index.jinja b/core/frontend/sys/index.jinja deleted file mode 100644 index 120051a..0000000 --- a/core/frontend/sys/index.jinja +++ /dev/null @@ -1,103 +0,0 @@ -{% extends "core/frontend/index.jinja" %} -{% block content %} -

System Management

- -
- {% if sesh.permissions >= 2 %} -
-
-

User Search

-
- - -
- OR -
- - -
- OR -
- - -
-
- -
-
- {% endif %} - {% if sesh.permissions >= 4 %} -
-
-

Arcade Search

-
- - -
- OR -
- - -
- OR -
- - -
- OR -
- - -
-
- -
-
-
-
-

Machine Search

-
- - -
- OR -
- - -
- OR -
- - -
-
- -
-
- {% endif %} -
-
- {% if sesh.permissions >= 2 %} - - {% endif %} - {% if sesh.permissions >= 4 %} - - {% endif %} -
-
- -
-{% endblock content %} \ No newline at end of file diff --git a/core/frontend/user/index.jinja b/core/frontend/user/index.jinja deleted file mode 100644 index 2f76b14..0000000 --- a/core/frontend/user/index.jinja +++ /dev/null @@ -1,41 +0,0 @@ -{% extends "core/frontend/index.jinja" %} -{% block content %} -

Management for {{ username }}

-

Cards

- - -{% if arcades is defined %} -

Arcades

- -{% endif %} - - - -{% endblock content %} \ No newline at end of file diff --git a/core/frontend/widgets/err_banner.jinja b/core/frontend/widgets/err_banner.jinja deleted file mode 100644 index 6d239f6..0000000 --- a/core/frontend/widgets/err_banner.jinja +++ /dev/null @@ -1,18 +0,0 @@ -{% if error > 0 %} -
-

Error

-{% if error == 1 %} -Card not registered, or wrong password -{% elif error == 2 %} -Missing or malformed access code -{% elif error == 3 %} -Failed to create user -{% elif error == 4 %} -Arcade not found -{% elif error == 5 %} -Machine not found -{% else %} -An unknown error occoured -{% endif %} -
-{% endif %} \ No newline at end of file diff --git a/core/mucha.py b/core/mucha.py index 7c6f0ab..22e4789 100644 --- a/core/mucha.py +++ b/core/mucha.py @@ -1,8 +1,8 @@ -from typing import Dict, Any, Optional, List +from typing import Dict, Any, Optional import logging, coloredlogs from logging.handlers import TimedRotatingFileHandler -from twisted.web import resource -from twisted.web.http import Request +from starlette.requests import Request +from starlette.responses import PlainTextResponse from datetime import datetime from Crypto.Cipher import Blowfish import pytz @@ -10,9 +10,11 @@ import pytz from .config import CoreConfig from .utils import Utils from .title import TitleServlet +from .data import Data +from .const import * class MuchaServlet: - mucha_registry: List[str] = [] + mucha_registry: Dict[str, Dict[str, str]] = {} def __init__(self, cfg: CoreConfig, cfg_dir: str) -> None: self.config = cfg self.config_dir = cfg_dir @@ -36,90 +38,148 @@ class MuchaServlet: self.logger.setLevel(cfg.mucha.loglevel) coloredlogs.install(level=cfg.mucha.loglevel, logger=self.logger, fmt=log_fmt_str) + + self.data = Data(cfg) for _, mod in TitleServlet.title_registry.items(): - if hasattr(mod, "get_mucha_info"): - enabled, game_cd = mod.get_mucha_info( - self.config, self.config_dir - ) - if enabled: - self.mucha_registry.append(game_cd) + enabled, game_cds, netids = mod.get_mucha_info(self.config, self.config_dir) + if enabled: + for x in range(len(game_cds)): + self.mucha_registry[game_cds[x]] = { "netid_prefix": netids[x] } self.logger.info(f"Serving {len(self.mucha_registry)} games") - def handle_boardauth(self, request: Request, _: Dict) -> bytes: - req_dict = self.mucha_preprocess(request.content.getvalue()) + async def handle_boardauth(self, request: Request) -> bytes: + bod = await request.body() + req_dict = self.mucha_preprocess(bod) client_ip = Utils.get_ip_addr(request) if req_dict is None: self.logger.error( - f"Error processing mucha request {request.content.getvalue()}" + f"Error processing mucha request {bod}" ) - return b"RESULTS=000" + return PlainTextResponse("RESULTS=000") req = MuchaAuthRequest(req_dict) - self.logger.info(f"Boardauth request from {client_ip} for {req.gameVer}") self.logger.debug(f"Mucha request {vars(req)}") + + if not req.gameCd or not req.gameVer or not req.sendDate or not req.countryCd or not req.serialNum: + self.logger.warn(f"Missing required fields - {vars(req)}") + return PlainTextResponse("RESULTS=000") - if req.gameCd not in self.mucha_registry: - self.logger.warning(f"Unknown gameCd {req.gameCd}") - return b"RESULTS=000" + minfo = self.mucha_registry.get(req.gameCd, {}) + + if not minfo: + self.logger.warning(f"Unknown gameCd {req.gameCd} from {client_ip}") + return PlainTextResponse("RESULTS=000") - # TODO: Decrypt S/N b_key = b"" for x in range(8): b_key += req.sendDate[(x - 1) & 7].encode() + + b_iv = b_key # what the fuck namco - cipher = Blowfish.new(b_key, Blowfish.MODE_ECB) - sn_decrypt = cipher.decrypt(bytes.fromhex(req.serialNum)) - self.logger.debug(f"Decrypt SN to {sn_decrypt.hex()}") + cipher = Blowfish.new(b_key, Blowfish.MODE_CBC, b_iv) + try: + sn_decrypt = cipher.decrypt(bytes.fromhex(req.serialNum))[:12].decode() + except Exception as e: + self.logger.error(f"Decrypt SN {req.serialNum} failed! - {e}") + return PlainTextResponse("RESULTS=000") + + self.logger.info(f"Boardauth request from {sn_decrypt} ({client_ip}) for {req.gameVer}") resp = MuchaAuthResponse( - f"{self.config.mucha.hostname}{':' + str(self.config.allnet.port) if self.config.server.is_develop else ''}" + f"{self.config.server.hostname}{':' + str(self.config.server.port) if not self.config.server.is_using_proxy else ''}" ) + netid = minfo.get('netid_prefix', "ABxN") + sn_decrypt[5:] + + cab = await self.data.arcade.get_machine(netid) + if cab: + arcade = await self.data.arcade.get_arcade(cab['id']) + if not arcade: + self.logger.error(f"Failed to get arcade with id {cab['id']}") + return PlainTextResponse("RESULTS=000") + + resp.AREA_0 = arcade["region_id"] or AllnetJapanRegionId.AICHI.name + resp.AREA_0_EN = arcade["region_id"] or AllnetJapanRegionId.AICHI.name + resp.AREA_FULL_0 = arcade["region_id"] or AllnetJapanRegionId.AICHI.name + resp.AREA_FULL_0_EN = arcade["region_id"] or AllnetJapanRegionId.AICHI.name + + resp.AREA_1 = arcade["country"] or cab['country'] or AllnetCountryCode.JAPAN.value + resp.AREA_1_EN = arcade["country"] or cab['country'] or AllnetCountryCode.JAPAN.value + resp.AREA_FULL_1 = arcade["country"] or cab['country'] or AllnetCountryCode.JAPAN.value + resp.AREA_FULL_1_EN = arcade["country"] or cab['country'] or AllnetCountryCode.JAPAN.value + + resp.AREA_2 = arcade["city"] if arcade["city"] else "" + resp.AREA_2_EN = arcade["city"] if arcade["city"] else "" + resp.AREA_FULL_2 = arcade["city"] if arcade["city"] else "" + resp.AREA_FULL_2_EN = arcade["city"] if arcade["city"] else "" + + resp.AREA_3 = "" + resp.AREA_3_EN = "" + resp.AREA_FULL_3 = "" + resp.AREA_FULL_3_EN = "" + + resp.PREFECTURE_ID = arcade['region_id'] + resp.COUNTRY_CD = arcade['country'] or cab['country'] or AllnetCountryCode.JAPAN.value + resp.PLACE_ID = req.placeId if req.placeId else f"{arcade['country'] or cab['country'] or AllnetCountryCode.JAPAN.value}{arcade['id']:04X}" + resp.SHOP_NAME = arcade['name'] + resp.SHOP_NAME_EN = arcade['name'] + resp.SHOP_NICKNAME = arcade['nickname'] + resp.SHOP_NICKNAME_EN = arcade['nickname'] + + elif self.config.server.allow_unregistered_serials: + self.logger.info(f"Allow unknown serial {netid} ({sn_decrypt}) to auth") + + else: + self.logger.warn(f'Auth failed for NetID {netid}') + return PlainTextResponse("RESULTS=000") + self.logger.debug(f"Mucha response {vars(resp)}") - return self.mucha_postprocess(vars(resp)) + return PlainTextResponse(self.mucha_postprocess(vars(resp))) - def handle_updatecheck(self, request: Request, _: Dict) -> bytes: - req_dict = self.mucha_preprocess(request.content.getvalue()) + async def handle_updatecheck(self, request: Request) -> bytes: + bod = await request.body() + req_dict = self.mucha_preprocess(bod) client_ip = Utils.get_ip_addr(request) if req_dict is None: self.logger.error( - f"Error processing mucha request {request.content.getvalue()}" + f"Error processing mucha request {bod}" ) - return b"RESULTS=000" + return PlainTextResponse("RESULTS=000") req = MuchaUpdateRequest(req_dict) - self.logger.info(f"Updatecheck request from {client_ip} for {req.gameVer}") + self.logger.info(f"Updatecheck request from {req.serialNum} ({client_ip}) for {req.gameVer}") self.logger.debug(f"Mucha request {vars(req)}") if req.gameCd not in self.mucha_registry: self.logger.warning(f"Unknown gameCd {req.gameCd}") - return b"RESULTS=000" + return PlainTextResponse("RESULTS=000") - resp = MuchaUpdateResponse(req.gameVer, f"{self.config.mucha.hostname}{':' + str(self.config.allnet.port) if self.config.server.is_develop else ''}") + resp = MuchaUpdateResponse(req.gameVer, f"{self.config.server.hostname}{':' + str(self.config.server.port) if not self.config.server.is_using_proxy else ''}") self.logger.debug(f"Mucha response {vars(resp)}") - return self.mucha_postprocess(vars(resp)) + return PlainTextResponse(self.mucha_postprocess(vars(resp))) - def handle_dlstate(self, request: Request, _: Dict) -> bytes: - req_dict = self.mucha_preprocess(request.content.getvalue()) + async def handle_dlstate(self, request: Request) -> bytes: + bod = await request.body() + req_dict = self.mucha_preprocess(bod) client_ip = Utils.get_ip_addr(request) if req_dict is None: self.logger.error( - f"Error processing mucha request {request.content.getvalue()}" + f"Error processing mucha request {bod}" ) - return b"" + return PlainTextResponse("RESULTS=000") req = MuchaDownloadStateRequest(req_dict) - self.logger.info(f"DownloadState request from {client_ip} for {req.gameCd} -> {req.updateVer}") + self.logger.info(f"DownloadState request from {req.serialNum} ({client_ip}) for {req.gameCd} -> {req.updateVer}") self.logger.debug(f"request {vars(req)}") - return b"RESULTS=001" + return PlainTextResponse("RESULTS=001") def mucha_preprocess(self, data: bytes) -> Optional[Dict]: try: @@ -169,7 +229,7 @@ class MuchaAuthResponse: self.RESULTS = "001" self.AUTH_INTERVAL = "86400" self.SERVER_TIME = datetime.strftime(datetime.now(), "%Y%m%d%H%M") - self.UTC_SERVER_TIME = datetime.strftime(datetime.now(pytz.UTC), "%Y%m%d%H%M") + self.SERVER_TIME_UTC = datetime.strftime(datetime.now(pytz.UTC), "%Y%m%d%H%M") self.CHARGE_URL = f"https://{mucha_url}/charge/" self.FILE_URL = f"https://{mucha_url}/file/" diff --git a/core/templates/arcade/index.jinja b/core/templates/arcade/index.jinja new file mode 100644 index 0000000..393443a --- /dev/null +++ b/core/templates/arcade/index.jinja @@ -0,0 +1,19 @@ +{% extends "core/templates/index.jinja" %} +{% block content %} +{% if arcade is defined %} +

{{ arcade.name }}

+

PCBs assigned to this arcade

+{% if success is defined and success == 3 %} +
+Cab added successfully +
+{% endif %} + +{% else %} +

Arcade Not Found

+{% endif %} +{% endblock content %} \ No newline at end of file diff --git a/core/frontend/gate/create.jinja b/core/templates/gate/create.jinja similarity index 94% rename from core/frontend/gate/create.jinja rename to core/templates/gate/create.jinja index f8b5e51..1dfa2f8 100644 --- a/core/frontend/gate/create.jinja +++ b/core/templates/gate/create.jinja @@ -1,4 +1,4 @@ -{% extends "core/frontend/index.jinja" %} +{% extends "core/templates/index.jinja" %} {% block content %}

Create User

diff --git a/core/templates/gate/gate.jinja b/core/templates/gate/gate.jinja new file mode 100644 index 0000000..d398cbd --- /dev/null +++ b/core/templates/gate/gate.jinja @@ -0,0 +1,32 @@ +{% extends "core/templates/index.jinja" %} +{% block content %} +

Gate

+{% include "core/templates/widgets/err_banner.jinja" %} + + +
+
+ +
20 Digit access code from a card registered to your account, or your account username. (NOT your username from a game!)
+
+
+
+ +
Leave blank if registering for the webui. Your card must have been used on a game connected to this server to register.
+
+

+ +
+{% endblock content %} \ No newline at end of file diff --git a/core/frontend/index.jinja b/core/templates/index.jinja similarity index 98% rename from core/frontend/index.jinja rename to core/templates/index.jinja index 3dacbe5..c8accb9 100644 --- a/core/frontend/index.jinja +++ b/core/templates/index.jinja @@ -84,7 +84,7 @@ - {% include "core/frontend/widgets/topbar.jinja" %} + {% include "core/templates/widgets/topbar.jinja" %} {% block content %}

{{ server_name }}

{% endblock content %} diff --git a/core/templates/machine/index.jinja b/core/templates/machine/index.jinja new file mode 100644 index 0000000..3e122f3 --- /dev/null +++ b/core/templates/machine/index.jinja @@ -0,0 +1,4 @@ +{% extends "core/templates/index.jinja" %} +{% block content %} +

Machine Management

+{% endblock content %} \ No newline at end of file diff --git a/core/templates/sys/index.jinja b/core/templates/sys/index.jinja new file mode 100644 index 0000000..637b8c9 --- /dev/null +++ b/core/templates/sys/index.jinja @@ -0,0 +1,192 @@ +{% extends "core/templates/index.jinja" %} +{% block content %} +

System Management

+{% if error is defined %} +{% include "core/templates/widgets/err_banner.jinja" %} +{% endif %} +

Search

+
+ {% if "{:08b}".format(sesh.permissions)[6] == "1" %} +
+
+

User Search

+
+ + +
+ OR +
+ + +
+ OR +
+ + +
+ OR +
+ + +
+
+ +
+
+ {% endif %} + {% if "{:08b}".format(sesh.permissions)[5] == "1" %} +
+
+

Shop search

+
+ + +
+ OR +
+ + +
+
+ +
+
+ + {% endif %} +
+
+ {% if "{:08b}".format(sesh.permissions)[6] == "1" %} +
+ {% for usr in usrlist %} +
{{ usr.username if usr.username is not none else "No Name Set"}}
+ {% endfor %} +
+ {% endif %} + {% if "{:08b}".format(sesh.permissions)[5] == "1" %} +
+ {% for shop in shoplist %} +
{{ shop.name if shop.name else "No Name Set"}}
+ {% endfor %} +
+ {% endif %} +
+

Add

+
+ {% if "{:08b}".format(sesh.permissions)[6] == "1" %} +
+
+

Add User

+
+ + +
+
+
+ + +
+
+
+ + +
+
+ +
+
+
+
+

Add Card

+
+ + +
+
+
+ + +
+
+
+ + +
+
+ +
+
+ {% endif %} + {% if "{:08b}".format(sesh.permissions)[5] == "1" %} +
+
+

Add Shop

+
+ + +
+
+
+ + +
+
+
+ + +
+
+ +
+
+
+
+

Add Machine

+
+ + +
+
+
+ + +
+
+
+ + +
+
+ +
+
+ {% endif %} +
+
+ {% if "{:08b}".format(sesh.permissions)[6] == "1" %} +
+ {% if usradd is defined %} +
Added user {{ usradd.username if usradd.username is not none else "with no name"}} with id {{usradd.id}} and password {{ usradd.password }}
+ {% endif %} +
+
+ {% if cardadd is defined %} +
Added {{ cardadd.access_code }} with id {{cardadd.id}} to user {{ cardadd.user }}
+ {% endif %} +
+ {% endif %} + {% if "{:08b}".format(sesh.permissions)[5] == "1" %} +
+ {% if shopadd is defined %} +
Added Shop {{ shopadd.id }}
+ {% endif %} +
+
+ {% if cabadd is defined %} +
Added Machine {{ cabadd.id }} with serial {{ cabadd.serial }}
+ {% endif %} +
+ {% endif %} +
+{% endblock content %} \ No newline at end of file diff --git a/core/templates/sys/logs.jinja b/core/templates/sys/logs.jinja new file mode 100644 index 0000000..2370f6f --- /dev/null +++ b/core/templates/sys/logs.jinja @@ -0,0 +1,202 @@ +{% extends "core/templates/index.jinja" %} +{% block content %} +

Event Logs

+ + + + + + + + + + + + + + + + + + {% if events is not defined or events|length == 0 %} + + + + {% endif %} +
Viewing last 100 logs
SeverityTimestampSystemNameUserArcadeMachineGameVersionMessageParams
No Events
+
+ +  + + +
+ +{% endblock content %} \ No newline at end of file diff --git a/core/templates/user/index.jinja b/core/templates/user/index.jinja new file mode 100644 index 0000000..88b91c9 --- /dev/null +++ b/core/templates/user/index.jinja @@ -0,0 +1,213 @@ +{% extends "core/templates/index.jinja" %} +{% block content %} + +

Management for {{ username }} 

+{% if error is defined %} +{% include "core/templates/widgets/err_banner.jinja" %} +{% endif %} +{% if success is defined and success == 2 %} +
+Update successful +
+{% endif %} + +

+

Cards

+{% if success is defined and success == 3 %} +
+Card added successfully +
+{% endif %} + +{% if success is defined and success == 4 %} +
+Update successful +
+{% endif %} + + +

Reset Password

+{% if success is defined and success == 1 %} +
+Update successful +
+{% endif %} +
+
+ + +
+
+ + +
Password must be at least 10 characters long, contain an upper and lowercase character, number, and special character
+
+
+ + +
+ +
+ +{% if arcades is defined and arcades|length > 0 %} +

Arcades

+ +{% endif %} + + + +{% endblock content %} \ No newline at end of file diff --git a/core/templates/widgets/err_banner.jinja b/core/templates/widgets/err_banner.jinja new file mode 100644 index 0000000..f1f4899 --- /dev/null +++ b/core/templates/widgets/err_banner.jinja @@ -0,0 +1,34 @@ +{% if error > 0 %} +
+

Error

+{% if error == 1 %} +Card not registered, or wrong password +{% elif error == 2 %} +Missing or malformed access code +{% elif error == 3 %} +Failed to create user +{% elif error == 4 %} +Required field not filled or invalid +{% elif error == 5 %} +Incorrect old password +{% elif error == 6 %} +Passwords don't match +{% elif error == 7 %} +New password not acceptable +{% elif error == 8 %} +New Nickname too long +{% elif error == 9 %} +You must be logged in to preform this action +{% elif error == 10 %} +Invalid serial number +{% elif error == 11 %} +Access Denied +{% elif error == 12 %} +Card already registered +{% elif error == 13 %} +AmusementIC Access Codes beginning with 5 must have IDm +{% else %} +An unknown error occoured +{% endif %} +
+{% endif %} \ No newline at end of file diff --git a/core/frontend/widgets/topbar.jinja b/core/templates/widgets/topbar.jinja similarity index 55% rename from core/frontend/widgets/topbar.jinja rename to core/templates/widgets/topbar.jinja index 1a85873..332173d 100644 --- a/core/frontend/widgets/topbar.jinja +++ b/core/templates/widgets/topbar.jinja @@ -3,19 +3,20 @@
  - {% for game in game_list %} -   + {% for game, data in game_list|items %} +   {% endfor %}
{% if sesh is defined and sesh["permissions"] >= 2 %} - + {% endif %} - {% if sesh is defined and sesh["userId"] > 0 %} - + {% if sesh is defined and sesh["user_id"] > 0 %} + + {% else %} - + {% endif %}
\ No newline at end of file diff --git a/core/title.py b/core/title.py index 3fdb30c..016e09a 100644 --- a/core/title.py +++ b/core/title.py @@ -1,12 +1,24 @@ -from typing import Dict, List, Tuple +from typing import Dict, List, Tuple, Any +import json import logging, coloredlogs from logging.handlers import TimedRotatingFileHandler -from twisted.web.http import Request +from starlette.requests import Request +from starlette.responses import Response +from starlette.routing import Route from core.config import CoreConfig from core.data import Data from core.utils import Utils +class JSONResponseNoASCII(Response): + media_type = "application/json" + + def render(self, content: Any) -> bytes: + return json.dumps( + content, + ensure_ascii=False, + ).encode("utf-8") + class BaseServlet: def __init__(self, core_cfg: CoreConfig, cfg_dir: str) -> None: self.core_cfg = core_cfg @@ -28,18 +40,16 @@ class BaseServlet: """ return False - def get_endpoint_matchers(self) -> Tuple[List[Tuple[str, str, Dict]], List[Tuple[str, str, Dict]]]: + def get_routes(self) -> List[Route]: """Called during boot to get all matcher endpoints this title servlet handles Returns: - Tuple[List[Tuple[str, str, Dict]], List[Tuple[str, str, Dict]]]: A 2-length tuple where offset 0 is GET and offset 1 is POST, - containing a list of 3-length tuples where offset 0 is the name of the function in the handler that should be called, offset 1 - is the matching string, and offset 2 is a dict containing rules for the matcher. + List[Route]: A list of Routes, WebSocketRoutes, or similar classes """ - return ( - [("render_GET", "/{game}/{version}/{endpoint}", {'game': R'S...'})], - [("render_POST", "/{game}/{version}/{endpoint}", {'game': R'S...'})] - ) + return [ + Route("/{game}/{version}/{endpoint}", self.render_POST, methods=["POST"]), + Route("/{game}/{version}/{endpoint}", self.render_GET, methods=["GET"]), + ] def setup(self) -> None: """Called once during boot, should contain any additional setup the handler must do, such as starting any sub-services @@ -58,11 +68,11 @@ class BaseServlet: Tuple[str, str]: A tuple where offset 0 is the allnet uri field, and offset 1 is the allnet host field """ if not self.core_cfg.server.is_using_proxy and Utils.get_title_port(self.core_cfg) != 80: - return (f"http://{self.core_cfg.title.hostname}:{Utils.get_title_port(self.core_cfg)}/{game_code}/{game_ver}/", "") + return (f"http://{self.core_cfg.server.hostname}:{Utils.get_title_port(self.core_cfg)}/{game_code}/{game_ver}/", "") - return (f"http://{self.core_cfg.title.hostname}/{game_code}/{game_ver}/", "") + return (f"http://{self.core_cfg.server.hostname}/{game_code}/{game_ver}/", "") - def get_mucha_info(self, core_cfg: CoreConfig, cfg_dir: str) -> Tuple[bool, str]: + def get_mucha_info(self, core_cfg: CoreConfig, cfg_dir: str) -> Tuple[bool, List[str], List[str]]: """Called once during boot to check if this game is a mucha game Args: @@ -70,17 +80,18 @@ class BaseServlet: cfg_dir (str): Config directory Returns: - Tuple[bool, str]: Tuple where offset 0 is true if the game is enabled, false otherwise, and offset 1 is the game CD + Tuple[bool, List[str], List[str]]: Tuple where offset 0 is true if the game is enabled, false otherwise, and offset 1 is the game CDs handled + by this servlette, and offset 2 is mucha netID prefixes that should be used for each game CD. """ - return (False, "") + return (False, [], []) - def render_POST(self, request: Request, game_code: str, matchers: Dict) -> bytes: - self.logger.warn(f"{game_code} Does not dispatch POST") - return None + async def render_POST(self, request: Request) -> bytes: + self.logger.warn(f"Game Does not dispatch POST") + return Response() - def render_GET(self, request: Request, game_code: str, matchers: Dict) -> bytes: - self.logger.warn(f"{game_code} Does not dispatch GET") - return None + async def render_GET(self, request: Request) -> bytes: + self.logger.warn(f"Game Does not dispatch GET") + return Response() class TitleServlet: title_registry: Dict[str, BaseServlet] = {} @@ -136,7 +147,7 @@ class TitleServlet: self.logger.error(f"{folder} missing game_code or index in __init__.py, or is_game_enabled in index") self.logger.info( - f"Serving {len(self.title_registry)} game codes {'on port ' + str(core_cfg.title.port) if core_cfg.title.port > 0 else ''}" + f"Serving {len(self.title_registry)} game codes {'on port ' + str(core_cfg.server.port) if core_cfg.server.port > 0 else ''}" ) def render_GET(self, request: Request, endpoints: dict) -> bytes: diff --git a/core/utils.py b/core/utils.py index 8264213..24c174c 100644 --- a/core/utils.py +++ b/core/utils.py @@ -1,6 +1,6 @@ from typing import Dict, Any, Optional from types import ModuleType -from twisted.web.http import Request +from starlette.requests import Request import logging import importlib from os import walk @@ -34,33 +34,22 @@ class Utils: @classmethod def get_ip_addr(cls, req: Request) -> str: - return ( - req.getAllHeaders()[b"x-forwarded-for"].decode() - if b"x-forwarded-for" in req.getAllHeaders() - else req.getClientAddress().host - ) + ip = req.headers.get("x-forwarded-for", req.client.host) + return ip.split(", ")[0] @classmethod def get_title_port(cls, cfg: CoreConfig): if cls.real_title_port is not None: return cls.real_title_port - if cfg.title.port == 0: - cls.real_title_port = cfg.allnet.port - - else: - cls.real_title_port = cfg.title.port + cls.real_title_port = cfg.server.proxy_port if cfg.server.is_using_proxy and cfg.server.proxy_port else cfg.server.port return cls.real_title_port - + @classmethod def get_title_port_ssl(cls, cfg: CoreConfig): if cls.real_title_port_ssl is not None: return cls.real_title_port_ssl - if cfg.title.port_ssl == 0: - cls.real_title_port_ssl = 443 - - else: - cls.real_title_port_ssl = cfg.title.port_ssl + cls.real_title_port_ssl = cfg.server.proxy_port_ssl if cfg.server.is_using_proxy and cfg.server.proxy_port_ssl else 443 return cls.real_title_port_ssl diff --git a/dbutils.py b/dbutils.py index 85b18a0..21b5c9d 100644 --- a/dbutils.py +++ b/dbutils.py @@ -1,9 +1,12 @@ -import yaml +#!/usr/bin/env python3 import argparse import logging -from core.config import CoreConfig +from os import mkdir, path, access, W_OK +import yaml +import asyncio + from core.data import Data -from os import path, mkdir, access, W_OK +from core.config import CoreConfig if __name__ == "__main__": parser = argparse.ArgumentParser(description="Database utilities") @@ -16,19 +19,10 @@ if __name__ == "__main__": type=str, help="Version of the database to upgrade/rollback to", ) - parser.add_argument( - "--game", - "-g", - type=str, - help="Game code of the game who's schema will be updated/rolled back. Ex. SDFE", - ) parser.add_argument("--email", "-e", type=str, help="Email for the new user") - parser.add_argument("--old_ac", "-o", type=str, help="Access code to transfer from") - parser.add_argument("--new_ac", "-n", type=str, help="Access code to transfer to") - parser.add_argument("--force", "-f", type=bool, help="Force the action to happen") - parser.add_argument( - "action", type=str, help="DB Action, create, recreate, upgrade, or rollback" - ) + parser.add_argument("--access_code", "-a", type=str, help="Access code for new/transfer user", default="00000000000000000000") + parser.add_argument("--message", "-m", type=str, help="Revision message") + parser.add_argument("action", type=str, help="create, upgrade, downgrade, create-owner, migrate, create-revision, create-autorevision") args = parser.parse_args() cfg = CoreConfig() @@ -48,44 +42,31 @@ if __name__ == "__main__": data = Data(cfg) + loop = asyncio.get_event_loop() + if args.action == "create": data.create_database() + + elif args.action == "upgrade": + data.schema_upgrade(args.version) - elif args.action == "recreate": - data.recreate_database() - - elif args.action == "upgrade" or args.action == "rollback": - if args.version is None: - data.logger.warning("No version set, upgrading to latest") - - if args.game is None: - data.logger.warning("No game set, upgrading core schema") - data.migrate_database( - "CORE", - int(args.version) if args.version is not None else None, - args.action, - ) - - else: - data.migrate_database( - args.game, - int(args.version) if args.version is not None else None, - args.action, - ) - - elif args.action == "autoupgrade": - data.autoupgrade() + elif args.action == "downgrade": + if not args.version: + logging.getLogger("database").error(f"Version argument required for downgrade") + exit(1) + data.schema_downgrade(args.version) elif args.action == "create-owner": - data.create_owner(args.email) + loop.run_until_complete(data.create_owner(args.email, args.access_code)) - elif args.action == "migrate-card": - data.migrate_card(args.old_ac, args.new_ac, args.force) + elif args.action == "migrate": + loop.run_until_complete(data.migrate()) - elif args.action == "cleanup": - data.delete_hanging_users() - - elif args.action == "version": - data.show_versions() + elif args.action == "create-revision": + loop.run_until_complete(data.create_revision(args.message)) - data.logger.info("Done") + elif args.action == "create-autorevision": + loop.run_until_complete(data.create_revision_auto(args.message)) + + else: + logging.getLogger("database").info(f"Unknown action {args.action}") diff --git a/docs/INSTALL_LINUX.md b/docs/INSTALL_LINUX.md new file mode 100644 index 0000000..08054e1 --- /dev/null +++ b/docs/INSTALL_LINUX.md @@ -0,0 +1,107 @@ +# Installing ARTEMiS on Linux +This guide assumes a fresh install of Debian 12 or Rasperry Pi OS. If you're using a different distrubution, your package manager commands and package names may be different then what's listed below. Please check with your repository's package manager for package names. + +## Install prerequisits +### Python +Some installs may come with python already installed. You can verify this by trying the following commands: +- `python --version` +- `python3 --version` +- `python3. --version` where `` is a python 3 release (eg 11, 10) + +If your python version is at least 3.7, you can move to the next step + +### Libraries and other software +ARTEMiS depends on mysql and memcached. As stated above, package names may vary by distrubution, but this is generally what you should expect to install. +#### Rasperry Pi OS +`sudo apt install git mariadb-server python3-pip memcached libmemcached-dev ` + +#### Debian 12 +`sudo apt install git mariadb-server python3-pip memcached libmemcached-dev default-libmysqlclient-dev pkg-config` + +### Optional: Install proxy +If you intend to use a proxy (recomended for public-facing production setups), we recomend nginx +`sudo apt install nginx` + +## Database setup +### mysql_secure_installation +If you already have your database installed and configured, and are able to log in, skip down to the [Creating the database](#creating-the-database) section below. Otherwise, setup your newly installed database. + +`sudo mysql_secure_installation` + +Leave the root password blank, do not switch to unix socket, do reset the root password to something secure, and answer yes to the rest of the prompts. You can then log into your database with `sudo mysql` + +### Creating the database +Once you're logged in, run the following commands, as root, to set up our database. Make sure you note down whatever you decide to make the password for the aime account, as you will need it to configure artemis. + +```sql +CREATE USER 'aime'@'localhost' IDENTIFIED BY ''; +CREATE DATABASE aime; +GRANT Alter,Create,Delete,Drop,Index,Insert,References,Select,Update ON aime.* TO 'aime'@'localhost'; +quit +``` +We have now set up our new user, `aime`, created a database called `aime` and given our user all the permissions it needs on every table of that database. + +### Configure memcached +Under the file /etc/memcached.conf, please make sure the following parameters are set: + +``` +# Start with a cap of 64 megs of memory. It's reasonable, and the daemon default +# Note that the daemon will grow to this size, but does not start out holding this much +# memory + +-I 128m +-m 1024 +``` + +** This is mandatory to avoid memcached overload caused by Crossbeats or by massive profiles + +Restart memcached using: sudo systemctl restart memcached + +## Getting ARTEMiS +### Clone from gitea +use `git clone https://gitea.tendokyu.moe/Hay1tsme/artemis.git` to pull down ARTEMiS into a folder called `artemis` created at wherever your current working directory is. `cd` into `artemis`. + +### Optional: Create a venv +Python venvs are a way to install and manage packages on a per-project basis and are recomended on systems that will have multiple python scripts running on them to avoid dependancy issues. If this server will be running ARTEMiS and ONLY ARTEMiS, then it is possible to get away without creating one. If you do want to create one, you will have to install an additional package: + +`sudo apt install python3-venv` (like above, package name may vary depending on distro and python version) + +Now, simply run `python -m venv .venv` (may have to use python3 or python 3.11 instead of python) to create your virtual environment in the folder `.venv`. In order to install packages and run scripts in this environment, you have to 'activate' it by running `source .venv/bin/activate`. Your terminal should now have (venv) appended to it. + +### Optional: Use the develop branch +By default, pulling down ARTEMiS from gitea will pull the `master` branch. This branch is updated less frequently, but is considered stable and ready for production use. If you'd rather have more updates, but a possibility for instability or bugs, you can switch to the develop branch by running `git checkout develop`. You can run `git checkout master` to switch back to stable. + +## Install python libraries +Run `pip install -r requirements.txt` to install all of ARTEMiS' dependencies. If any installs fail, you may have missed a step in the [Install prerequisits](#install-prerequisits) section above. If you're absolutly sure you didn't, submit an issue on gitea. + +## Configuration +### Copy example configs +From the `artemis` directory, run `cp -r example_config config` to copy the example configuration files to a new folder called `config`. All of the config changes you make will be done in the `config` folder. + +### Optional: Generate AimeDB and Frontend JWT Secrets +AimeDB and the frontend utalize JSON Web Tokens (JWT) for card authentication and session cookies respectivly. While generating a secret for AimeDB is optional, if you intend to run the frontend, a secret is required. You can generate a secret easily by running: + +`openssl rand --base64 64` + +With 64 being the number of bytes. You shouldn't need to go higher then 64, but you can if desired. **NOTE: When pasting secrets into the config file, make sure you remove any newlines!** + +### Edit `core.yaml` +Before editing `core.yaml`, you should familiarize yourself with the name and function of each of the config options. You can find a full list in [config.md](config.md) + +Open `core.yaml` in the `config` folder in your prefered text editor. The only configuration option that it is absolutly mandatory to change is `aimedb`->`key`. This key must be set for the server to start, and the key must be correct, otherwise you will not be able to process aimedb requests. The correct key is floating around online, and finding it is left as an excersie to the reader. + +Another option that should be changed is `database`->`password` to be the password you set when you created your database user. You did write it down somewhere, right? + +Since you are presumably not running the games on the same computer you're installing this server on, you're going to want to change `server`->`hostname` to be whatever hostname or IP address other PCs can reach this server by. Note that some games reject IPs and require hostnames, so setting a hostname is always recomended over an IP. + +### Edit game configs +Every game has their own yaml file with settings that you may want to tweek. `InitialD Zero` and `Pokken` both have `hostname` fields in their config file that you should edit, and some games support encryption, if supplied with proper keys. + +### A note about IDZ +InitialD Zero is currently the only game where it is required to specify encryption information (the AES key and at least one RSA key) for the game to start. These keys are, like the aimedb key, floating around online and will not be provided. If you don't have the keys, and don't plan on anybody connecting to your server playing InitialD Zero, it's best to set `enabled` to `False` in idz.yaml to disable the game. + +## Create database tables +ARTEMiS uses alembic to manage datbase versioning. `dbutils.py` acts as a wrapper for alembic, and can execute some necessassary database functions. To create the database tables, run `python dbutils.py create`. Confirm that there are no errors, and you're good to go. If you intend to use the frontend, you may also want to run `python dbutils.py create-owner -a ` to create a superuser account to log in with. + +## Run ARTEMiS +Once you have everything configured properly, simply run `python index.py` to start ARTEMiS. Verify that clients can connect to all services (allnet, billing, aimedb, and game servers) and setup is complete. \ No newline at end of file diff --git a/docs/INSTALL_UBUNTU.md b/docs/INSTALL_UBUNTU.md deleted file mode 100644 index 710c757..0000000 --- a/docs/INSTALL_UBUNTU.md +++ /dev/null @@ -1,129 +0,0 @@ -# ARTEMiS - Ubuntu 20.04 LTS Guide -This step-by-step guide assumes that you are using a fresh install of Ubuntu 20.04 LTS, some of the steps can be skipped if you already have an installation with MySQL 5.7 or even some of the modules already present on your environment - -# Setup -## Install memcached module -1. sudo apt-get install memcached -2. Under the file /etc/memcached.conf, please make sure the following parameters are set: - -``` -# Start with a cap of 64 megs of memory. It's reasonable, and the daemon default -# Note that the daemon will grow to this size, but does not start out holding this much -# memory - --I 128m --m 1024 -``` - -** This is mandatory to avoid memcached overload caused by Crossbeats or by massive profiles - -3. Restart memcached using: sudo systemctl restart memcached - -## Install MySQL 5.7 -``` -sudo apt update -sudo apt install wget -y -wget https://dev.mysql.com/get/mysql-apt-config_0.8.12-1_all.deb -sudo dpkg -i mysql-apt-config_0.8.12-1_all.deb -``` - 1. During the first prompt, select Ubuntu Bionic - 2. Select the default option - 3. Select MySQL 5.7 - 4. Select the last option -``` -sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 467B942D3A79BD29 -sudo apt-get update -sudo apt-cache policy mysql-server -sudo apt install -f mysql-client=5.7* mysql-community-server=5.7* mysql-server=5.7* -``` - -## Default Configuration for MySQL Server -1. sudo mysql_secure_installation -> Make sure to follow the steps that will be prompted such as changing the mysql root password and such - -2. Test your MySQL Server login by doing the following command : -> mysql -u root -p - -## Create the default ARTEMiS database and user -1. mysql -u root -p -2. Please change the password indicated in the next line for a custom secure one and continue with the next commands - -``` -CREATE USER 'aime'@'localhost' IDENTIFIED BY 'MyStrongPass.'; -CREATE DATABASE aime; -GRANT Alter,Create,Delete,Drop,Index,Insert,References,Select,Update ON aime.* TO 'aime'@'localhost'; -FLUSH PRIVILEGES; -exit; -``` - -3. sudo systemctl restart mysql - -## Install Python modules -``` -sudo apt-get install python3-dev default-libmysqlclient-dev build-essential mysql-client libmysqlclient-dev libmemcached-dev -sudo apt install libpython3.8-dev -sudo apt-get install python3-software-properties -sudo apt install python3-pip -sudo pip3 install --upgrade pip testresources -sudo pip3 install --upgrade pip setuptools -sudo apt-get install python3-tk -``` -7. Change your work path to the ARTEMiS root folder using 'cd' and install the requirements: -> sudo python3 -m pip install -r requirements.txt - -## Copy/Rename the folder example_config to config - -## Adjust /config/core.yaml -1. Make sure to change the server listen_address to be set to your local machine IP (ex.: 192.168.1.xxx) -2. Adjust the proper MySQL information you created earlier -3. Add the AimeDB key at the bottom of the file - -## Create the database tables for ARTEMiS -1. sudo python3 dbutils.py create - -2. If you get "No module named Crypto", run the following command: -``` -sudo pip uninstall crypto -sudo pip uninstall pycrypto -sudo pip install pycrypto -``` - -## Firewall Adjustements -``` -sudo ufw allow 80 -sudo ufw allow 443 -sudo ufw allow 8443 -sudo ufw allow 22345 -sudo ufw allow 8090 -sudo ufw allow 8444 -sudo ufw allow 8080 -``` - -## Running the ARTEMiS instance -1. sudo python3 index.py - -# Troubleshooting - -## Game does not connect to ARTEMiS Allnet server -1. Double-check your core.yaml, the listen_address is most likely either not binded to the proper IP or the port is not opened - -## Game does not connect to Title Server -1. Verify that your core.yaml is setup properly for both the server listen_address and title hostname -2. Boot your game and verify that an AllNet response does show and if it does, attempt to open the URI that is shown under a browser such as Edge, Chrome & Firefox. -3. If a page is shown, the server is working properly and if it doesn't, double check your port forwarding and also that you have entered the proper local IP under the Title hostname in core.yaml. - -## Unhandled command under AimeDB -1. Double check your AimeDB key under core.yaml, it is incorrect. - -## Memcache failed, error 3 -1. Make sure memcached is properly installed and running. You can check the status of the service using the following command: -> sudo systemctl status memcached -2. If it is failing, double check the /etc/memcached.conf file, it may have duplicated arguments like the -I and -m -3. If it is still not working afterward, you can proceed with a workaround by manually editing the /core/data/cache.py file. -``` -# Make memcache optional -try: - has_mc = False -except ModuleNotFoundError: - has_mc = False -``` diff --git a/docs/INSTALL_WINDOWS.md b/docs/INSTALL_WINDOWS.md index b976b26..d17886a 100644 --- a/docs/INSTALL_WINDOWS.md +++ b/docs/INSTALL_WINDOWS.md @@ -1,102 +1,77 @@ -# ARTEMiS - Windows 10/11 Guide -This step-by-step guide assumes that you are using a fresh install of Windows 10/11 without MySQL installed, some of the steps can be skipped if you already have an installation with MySQL 8.0 or even some of the modules already present on your environment +# Installing ARTEMiS on Windows +This guide assumes a fresh install of Windows 10. Please be aware that due to the lack of memcached and the general woes of running a server on Windows, this is only recommended for local setups or small hosting-for-the-homies type servers. -# Setup -## Install Python Python 3.9 (recommended) or 3.10 -1. Download Python 3.9 : [Link](https://www.python.org/ftp/python/3.9.13/python-3.9.13-amd64.exe) -2. Install python-3.9.13-amd64.exe - 1. Select Customize installation - 2. Make sure that pip, tcl/tk, and the for all users are checked and hit Next - 3. Make sure that you enable "Create shortcuts for installed applications" and "Add Python to environment variables" and hit Install +## Install prerequisites +### Python +- Python versions from 3.8 to 3.11 work with ARTEMiS. We recommend 3.11. + - https://www.python.org/ftp/python/3.11.7/python-3.11.7-amd64.exe +- Install using whichever options best suit your environment, making sure that the Python executable is on path, such that you can open CMD, type `python --version` and see the version of Python you have installed. +- If you already have a working version of Python installed, skip this step. -## Install MySQL 8.0 -1. Download MySQL 8.0 Server : [Link](https://dev.mysql.com/get/Downloads/MySQLInstaller/mysql-installer-community-8.0.34.0.msi) -2. Install mysql-installer-web-community-8.0.34.0.msi - 1. Click on "Add ..." on the side - 2. Click on the "+" next to MySQL Servers - 3. Make sure MySQL Server 8.0.34 - X64 is under the products to be installed. - 4. Hit Next and Next once installed - 5. Select the configuration type "Development Computer" - 6. Hit Next - 7. Select "Use Legacy Authentication Method (Retain MySQL 5.x compatibility)" and hit Next - 8. Enter a root password and then hit Next > - 9. Leave everything under Windows Service as default and hit Next > - 10. Click on Execute and for it to finish and hit Next> and then Finish -3. Open MySQL 8.0 Command Line Client and login as your root user -4. Change `` to a new password for the user aime, type those commands to create your user and the database +### MariaDB +- It is always recommended to use MariaDB over MySQL because Oracle is a terrible company. +- While the latest release of v10 is recommended, as it is an LTS release, v11 should work fine. + - https://ftp.osuosl.org/pub/mariadb//mariadb-10.11.6/winx64-packages/mariadb-10.11.6-winx64.msi +- REMEMBER YOUR ROOT PASSWORD SO YOU CAN LOG IN IN FUTURE STEPS. -```sql -CREATE USER 'aime'@'localhost' IDENTIFIED BY ''; +### Git +- While technically optional, it is strongly recommended to obtain ARTEMiS via git clone instead of just downloading it. + - https://git-scm.com/download/win +- It is recommended to use Notepad++ as the default editor (if you have it installed), other than that, the default settings should be fine. + +### Optional: GUI database viewer +- Having a GUI database editor is recommended but not required. +- MariaDB will try to install HeidiSQL, but we recommend DBeaver. + - https://dbeaver.io/download/ + +## Obtain ARTEMiS +### Via git (recommended) +- `git clone https://gitea.tendokyu.moe/Hay1tsme/artemis.git` via cmd in whatever folder you want to install ARTEMiS. + - You can switch to the develop branch for latest changes via `git checkout develop`. + +### Via http download +- Download [here](https://gitea.tendokyu.moe/Hay1tsme/artemis/archive/master.zip). + - Develop branch can be found [here](https://gitea.tendokyu.moe/Hay1tsme/artemis/archive/develop.zip). +- Extract the zip file somewhere. + +## Database setup +- Log into your server as root, either via GUI (recommended) or CMD +- Create the `aime` user, replace `` with a password you choose. Remember it! +``` +CREATE USER 'aime'@'localhost' IDENTIFIED BY ''; CREATE DATABASE aime; GRANT Alter,Create,Delete,Drop,Index,Insert,References,Select,Update ON aime.* TO 'aime'@'localhost'; -FLUSH PRIVILEGES; -exit; ``` +- If you create the database via a GUI, make sure you grant all the above permissions. -## Install Python modules -1. Change your work path to the artemis-master folder using 'cd' and install the requirements: +## Create a venv +- Python virtual environments are a good way to manage packages and make dealing with python and pip easier. +- `python -m pip venv venv` +- `venv\Scripts\activate.bat` to activate the venv whenever you need to interact with ARTEMiS. +- All the rest of the steps assume your venv is activated. -```shell -pip install -r requirements.txt -``` +## Install pip modules +- `pip install -r requirements.txt` -## Copy/Rename the folder `example_config` to `config` +## Setup configuration +- Create a new `config` folder and copy the files in `example_config` over. +- edit `core.yaml` + - Put the password you created for the aime user into the `database` section. + - Put in the aimedb key (YOU DO NOT GENERATE THIS KEY, FIND IT SOMEWHERE). + - Set your hostname to be whatever hostname or IP address games can reach your server at (many games reject localhost and 127.0.0.1). + - Optional: generate base64-encoded secrets for aimedb and frontend using something like `openssl rand -base64 64`. It is advised to make all secrets different. + - See [config.md](docs/config.md) for a full list of options. +- edit `idz.yaml` + - If you don't plan on anyone using your server to play Initial D Zero, it is best to disable it to cut down on console spam on boot. +- Edit other game yamls + - Add keys, set hostnames, ports, etc. Specific settings will depend on the game. See [game_specific_info](docs/game_specific_info.md). -## Adjust `config/core.yaml` +## Create Database Tables +- `python dbutils.py create` -1. Make sure to change the server `hostname` to be set to your local machine IP (ex.: 192.168.xxx.xxx) - - In case you want to run this only locally, set the following values: +## Firewall +- If you're planning on serving games not on your PC, open at least ports 80, 8443, and 22345 in windows firewall + - Also set `listen_address` to either your local IP to serve on your LAN, or `0.0.0.0` for all interfaces, to accept connections from other places. -```yaml -server: - listen_address: 0.0.0.0 -title: - hostname: 192.168.xxx.xxx -``` - -1. Adjust the proper MySQL information you created earlier -```yaml -database: - host: "localhost" - username: "aime" - password: "" - name: "aime" -``` -3. Add the AimeDB key at the bottom of the file -4. If the webui is needed, change the flag from False to True - -## Create the database tables for ARTEMiS - -```shell -python dbutils.py create -``` - -## Firewall Adjustements -Make sure the following ports are open both on your router and local Windows firewall in case you want to use this for public use (NOT recommended): -> Port 80 (TCP), 443 (TCP), 8443 (TCP), 22345 (TCP), 8080 (TCP), 8090 (TCP) **webui, 8444 (TCP) **mucha - -## Running the ARTEMiS instance -```shell -python index.py -``` - -# Troubleshooting - -## Game does not connect to ARTEMiS Allnet server -1. Double-check your core.yaml, the listen_address is most likely either not binded to the proper IP or the port is not opened - -## Game does not connect to Title Server -1. Verify that your core.yaml is setup properly for both the server listen_address and title hostname -2. Boot your game and verify that an AllNet response does show and if it does, attempt to open the URI that is shown under a browser such as Edge, Chrome & Firefox. -3. If a page is shown, the server is working properly and if it doesn't, double check your port forwarding and also that you have entered the proper local IP under the Title hostname in core.yaml. - -## Unhandled command under AimeDB -1. Double check your AimeDB key under core.yaml, it is incorrect. - -## AttributeError: module 'collections' has no attribute 'Hashable' -1. This means the pyYAML module is obsolete, simply rerun pip with the -U (force update) flag, as shown below. - - Change your work path to the artemis-master (or artemis-develop) folder using 'cd' and run the following commands: - -```shell -pip install -r requirements.txt -U -``` +## Start ARTEMiS +- `python index.py` diff --git a/docs/config.md b/docs/config.md index 81fb43d..8a482e3 100644 --- a/docs/config.md +++ b/docs/config.md @@ -1,23 +1,24 @@ # ARTEMiS Configuration ## Server - `listen_address`: IP Address or hostname that the server will listen for connections on. Set to 127.0.0.1 for local only, or 0.0.0.0 for all interfaces. Default `127.0.0.1` +- `hostname`: Hostname that gets sent to clients to tell them where to connect. Games must be able to connect to your server via the hostname or IP you spcify here. Note that most games will reject `localhost` or `127.0.0.1`. Default `localhost` +- `port`: Port that the server will listen for connections on. Default `80` +- `ssl_key`: Location of the ssl server key for the secure title server. Ignored if you don't use SSL. Default `cert/title.key` +- `ssl_cert`: Location of the ssl server certificate for the secure title server. Must not be a self-signed SSL. Ignored if you don't use SSL. Default `cert/title.pem` - `allow_user_registration`: Allows users to register in-game via the AimeDB `register` function. Disable to be able to control who can use cards on your server. Default `True` - `allow_unregistered_serials`: Allows games that do not have registered keychips to connect and authenticate. Disable to restrict who can connect to your server. Recomended to disable for production setups. Default `True` - `name`: Name for the server, used by some games in their default MOTDs. Default `ARTEMiS` -- `is_develop`: Flags that the server is a development instance without a proxy standing in front of it. Setting to `False` tells the server not to listen for SSL, because the proxy should be handling all SSL-related things, among other things. Default `True` -- `threading`: Flags that `reactor.run` should be called via the `Thread` standard library. May provide a speed boost, but removes the ability to kill the server via `Ctrl + C`. Default: `False` -- `check_arcade_ip`: Checks IPs against the `arcade` table in the database, if one is defined. Default `False` -- `strict_ip_checking`: Rejects clients if there is no IP in the `arcade` table for the respective arcade +- `is_develop`: Flags that the server is a development instance, and enables some useful development features. Disable for production setups. Default `True`. +- `is_using_proxy`: Flags that you'll be using some other software, such as nginx, to proxy requests, and to send `proxy_port` or `proxy_port_ssl` to games instead of `port`. Default `False` +- `proxy_port`: Which port your front-facing proxy will be listening on. Ignored if `is_using_proxy` is `False` or if set to `0`. Default `0` +- `proxy_port`: Which port your front-facing proxy will be listening for ssl connections on. Ignored if `is_using_proxy` is `False` or if set to `0`. Default `0` - `log_dir`: Directory to store logs. Server MUST have read and write permissions to this directory or you will have issues. Default `logs` +- `check_arcade_ip`: Checks IPs against the `arcade` table in the database, if one is defined. Default `False` +- `strict_ip_checking`: Rejects clients if there is no IP in the `arcade` table for the respective arcade. Default `False` ## Title - `loglevel`: Logging level for the title server. Default `info` -- `hostname`: Hostname that gets sent to clients to tell them where to connect. Games must be able to connect to your server via the hostname or IP you spcify here. Note that most games will reject `localhost` or `127.0.0.1`. Default `localhost` -- `port`: Port that the title server will listen for connections on. Set to 0 to use the Allnet handler to reduce the port footprint. Default `8080` -- `port_ssl`: Port that the secure title server will listen for connections on. Set to 0 to use the Allnet handler to reduce the port footprint. Default `0` -- `ssl_key`: Location of the ssl server key for the secure title server. Ignored if `port_ssl` is set to `0` or `is_develop` set to `False`. Default `cert/title.key` -- `ssl_cert`: Location of the ssl server certificate for the secure title server. Must not be a self-signed SSL. Ignored if `port_ssl` is set to `0` or `is_develop` is set to `False`. Default `cert/title.pem` -- `reboot_start_time`: 24 hour JST time that clients will see as the start of maintenance period. Leave blank for no maintenance time. Default: "" -- `reboot_end_time`: 24 hour JST time that clients will see as the end of maintenance period. Leave blank for no maintenance time. Default: "" +- `reboot_start_time`: 24 hour JST time that clients will see as the start of maintenance period, ex `04:00`. A few games or early version will report errors if it is empty, ex maimai DX 1.00 +- `reboot_end_time`: 24 hour JST time that clients will see as the end of maintenance period, ex `07:00`. this must be set to 7:00 am for some game, please do not change it. ## Database - `host`: Host of the database. Default `localhost` - `username`: Username of the account the server should connect to the database with. Default `aime` @@ -25,24 +26,32 @@ - `name`: Name of the database the server should expect. Default `aime` - `port`: Port the database server is listening on. Default `3306` - `protocol`: Protocol used in the connection string, e.i `mysql` would result in `mysql://...`. Default `mysql` -- `sha2_password`: Weather or not the password in the connection string should be hashed via SHA2. Default `False` -- `loglevel`: Logging level for the database. Default `warn` -- `user_table_autoincrement_start`: What the `aime_user` table ID autoincrememnt should start with. Default `10000` +- `sha2_password`: Whether or not the password in the connection string should be hashed via SHA2. Default `False` +- `loglevel`: Logging level for the database. Default `info` - `memcached_host`: Host of the memcached server. Default `localhost` ## Frontend -- `enable`: Weather or not the frontend should be enabled. Default `False` -- `port`: Port the frontend should listen for connections on. Default `8090` +- `enable`: Whether or not the frontend servlet should run. Frontend can still be run via `python -m uvicorn core.frontend:app` even if this is set to `False`. Default `False` +- `port`: Port the frontend should listen on. Default `8080` - `loglevel`: Logging level for the frontend server. Default `info` +- `secret`: Base64-encoded JWT secret for session cookies, generated by you. Default `""` ## Allnet +- `standalone`: Whether allnet should launch it's own servlet on it's own port, or be part of the main servlet on the default port. Disable if you either have something proxying `naominet.jp` requests to port 80, or have port 80 set in `server` -> `port` +- `port`: Port the allnet server should listen for connections on if it's running standalone. Games are hardcoded to ask for port `80` so only change if you have a proxy redirecting properly. Ignored if `standalone` is `False`. Default `80` - `loglevel`: Logging level for the allnet server. Default `info` -- `port`: Port the allnet server should listen for connections on. Games are hardcoded to ask for port `80` so only change if you have a proxy redirecting properly. Default `80` - `allow_online_updates`: Allow allnet to distribute online updates via DownloadOrders. This system is currently non-functional, so leave it disabled. Default `False` +- `update_cfg_folder`: Folder where delivery INI files will be checked for. Ignored if `allow_online_updates` is `False`. Default `""` ## Billing -- `port`: Port the billing server should listen for connections on. Games are hardcoded to ask for port `8443` so only change if you have a proxy redirecting properly. Set to 0 to use the allnet handler to reduce the number of ports the server eats up. Default `8443` -- `ssl_key`: Location of the ssl server key for the billing server. Ignored if `port` is set to `0` or `is_develop` set to `False`. Default `cert/server.key` -- `ssl_cert`: Location of the ssl server certificate for the billing server. Must match the CA distributed to users or the billing server will not connect. Ignored if `port` is set to `0` or `is_develop` is set to `False`. Default `cert/server.pem` +- `standalone`: Whether the billing server should launch it's own servlet on it's own port, or be part of the main servlet on the default port. Setting this to `True` requires that you have `ssl_key` and `ssl_cert` set. Default `False` +- `loglevel`: Logging level for the billing server. Default `info` +- `port`: Port the billing server should listen for connections on. Games are hardcoded to ask for port `8443` so only change if you have a proxy redirecting properly. Ignored if `standalone` is `False`. Default `8443` +- `ssl_key`: Location of the ssl server key for the billing server. Ignored if `standalone` is `False`. Default `cert/server.key` +- `ssl_cert`: Location of the ssl server certificate for the billing server. Ignored if `standalone` is `False`. Must match the CA distributed to users or the billing server will not connect. Default `cert/server.pem` - `signing_key`: Location of the RSA Private key used to sign billing requests. Must match the public key distributed to users or the billing server will not connect. Default `cert/billing.key` ## Aimedb +- `enable`: Whether or not aimedb should run. Default `True` +- `listen_address`: IP Address or hostname that the aimedb server will listen for connections on. Leave this blank to use the listen address under `server`. Default `""` - `loglevel`: Logging level for the aimedb server. Default `info` - `port`: Port the aimedb server should listen for connections on. Games are hardcoded to ask for port `22345` so only change if you have a proxy redirecting properly. Default `22345` -- `key`: Key to encrypt/decrypt aimedb requests and responses. MUST be set or the server will not start. If set incorrectly, your server will not properly handle aimedb requests. Default `""` \ No newline at end of file +- `key`: Key to encrypt/decrypt aimedb requests and responses. MUST be set or the server will not start. If set incorrectly, your server will not properly handle aimedb requests. Default `""` +- `id_secret`: Base64-encoded JWT secret for Sega Auth IDs. Leaving this blank disables this feature. Default `""` +- `id_lifetime_seconds`: Number of secons a JWT generated should be valid for. Default `86400` (1 day) diff --git a/docs/game_specific_info.md b/docs/game_specific_info.md index a06f17f..33d5710 100644 --- a/docs/game_specific_info.md +++ b/docs/game_specific_info.md @@ -9,7 +9,15 @@ using the megaime database. Clean installations always create the latest databas To upgrade the core database and the database for every game, execute: ```shell -python dbutils.py autoupgrade +python dbutils.py upgrade +``` + +If you are using the old master branch that was not setup with alembic, make sure to do the following steps in order: +- Pull down latest master/develop +- Update core.yaml +- Back up your existing database +```shell +python dbutils.py migrate ``` # Table of content @@ -22,7 +30,7 @@ python dbutils.py autoupgrade - [Card Maker](#card-maker) - [WACCA](#wacca) - [Sword Art Online Arcade](#sao) - - [Initial D THE ARCADE](#initial-d-the-arcade) + - [Initial D THE ARCADE](#initial-d-the-arcade) # Supported Games @@ -55,6 +63,7 @@ Games listed below have been tested and confirmed working. | 12 | CHUNITHM NEW PLUS!! | | 13 | CHUNITHM SUN | | 14 | CHUNITHM SUN PLUS | +| 15 | CHUNITHM LUMINOUS | ### Importer @@ -79,13 +88,19 @@ Config file is located in `config/chuni.yaml`. | `crypto` | This option is used to enable the TLS Encryption | -**If you would like to use network encryption, the following will be required underneath but key, iv and hash are required:** +If you would like to use network encryption, add the keys to the `keys` section under `crypto`, where the key +is the version ID for Japanese (SDHD) versions and `"{versionID}_int"` for Export (SDGS) versions, and the value +is an array containing `[key, iv, salt, iter_count]` in order. + +`iter_count` is optional for all Japanese (SDHD) versions but may be required for some Export (SDGS) versions. +You will receive an error in the logs if it needs to be specified. ```yaml crypto: encrypted_only: False keys: 13: ["0000000000000000000000000000000000000000000000000000000000000000", "00000000000000000000000000000000", "0000000000000000"] + "13_int": ["0000000000000000000000000000000000000000000000000000000000000000", "00000000000000000000000000000000", "0000000000000000", 42] ``` ### Database upgrade @@ -93,7 +108,7 @@ crypto: Always make sure your database (tables) are up-to-date: ```shell -python dbutils.py --game SDBT upgrade +python dbutils.py upgrade ``` ### Online Battle @@ -167,6 +182,14 @@ Config file is located in `config/cxb.yaml`. ## maimai DX +### Presents +Presents are items given to the user when they login, with a little animation (for example, the KOP song was given to the finalists as a present). To add a present, you must insert it into the `mai2_item_present` table. In that table, a NULL version means any version, a NULL user means any user, a NULL start date means always open, and a NULL end date means it never expires. Below is a list of presents one might wish to add: + +| Game Version | Item ID | Item Kind | Item Description | Present Description | +|--------------|---------|-----------|-------------------------------------------------|------------------------------------------------| +| BUDDiES (21) | 409505 | Icon (3) | 旅行スタンプ(月面基地) (Travel Stamp - Moon Base) | Officially obtained on the webui with a serial | +| | | | | number, for project raputa | + ### Versions | Game Code | Version ID | Version Name | @@ -192,6 +215,7 @@ Config file is located in `config/cxb.yaml`. | SDEZ | 18 | maimai DX UNiVERSE PLUS | | SDEZ | 19 | maimai DX FESTiVAL | | SDEZ | 20 | maimai DX FESTiVAL PLUS | +| SDEZ | 21 | maimai DX BUDDiES | ### Importer @@ -215,7 +239,7 @@ The importer for maimai Pre-DX will import Events and Music. Not all games will Always make sure your database (tables) are up-to-date: ```shell -python dbutils.py --game SDEZ upgrade +python dbutils.py upgrade ``` Pre-Dx uses the same database as DX, so only upgrade using the SDEZ game code! @@ -245,10 +269,14 @@ the Shop, Modules and Customizations. Config file is located in `config/diva.yaml`. -| Option | Info | -| -------------------- | ----------------------------------------------------------------------------------------------- | -| `unlock_all_modules` | Unlocks all modules (costumes) by default, if set to `False` all modules need to be purchased | -| `unlock_all_items` | Unlocks all items (customizations) by default, if set to `False` all items need to be purchased | +| Option | Info | +| -------------------- | ------------------------------------------------------------------------------------------------ | +| `festa_enable` | Enable or disable the ingame festa | +| `festa_add_VP` | Set the extra VP you get when clearing a song, if festa is not enabled no extra VP will be given | +| `festa_multiply_VP` | Multiplier for festa add VP | +| `festa_end_time` | Set the date time for when festa will end and not show up in game anymore | +| `unlock_all_modules` | Unlocks all modules (costumes) by default, if set to `False` all modules need to be purchased | +| `unlock_all_items` | Unlocks all items (customizations) by default, if set to `False` all items need to be purchased | ### Custom PV Lists (databanks) @@ -259,7 +287,7 @@ In order to use custom PV Lists, simply drop in your .dat files inside of /title Always make sure your database (tables) are up-to-date: ```shell -python dbutils.py --game SBZV upgrade +python dbutils.py upgrade ``` ## O.N.G.E.K.I. @@ -315,7 +343,7 @@ crypto: Always make sure your database (tables) are up-to-date: ```shell -python dbutils.py --game SDDT upgrade +python dbutils.py upgrade ``` ### Controlling Events (Ranking Event, Technical Challenge Event, Mission Event) @@ -406,6 +434,7 @@ After that, on next login the present should be received (or whenever it suppose * UNiVERSE PLUS: Yes * FESTiVAL: Yes (added in A031) * FESTiVAL PLUS: Yes (added in A035) + * BUDDiES: Yes (added in A039) * O.N.G.E.K.I. bright MEMORY: Yes @@ -542,7 +571,7 @@ Config file is located in `config/wacca.yaml`. Always make sure your database (tables) are up-to-date: ```shell -python dbutils.py --game SDFE upgrade +python dbutils.py upgrade ``` ### VIP Rewards @@ -594,7 +623,7 @@ Below is a list of VIP rewards. Currently, VIP is not implemented, and thus thes In order to use the importer locate your game installation folder and execute: ```shell -python read.py --game SDEW --version --binfolder /path/to/game/extractedassets +python read.py --game SDEW --version 0 --binfolder /titles/sao/data/ ``` The importer for SAO will import all items, heroes, support skills and titles data. @@ -615,21 +644,23 @@ Config file is located in `config/sao.yaml`. Always make sure your database (tables) are up-to-date: ```shell -python dbutils.py --game SDEW upgrade +python dbutils.py upgrade ``` ### Notes - Defrag Match will crash at loading - Co-Op Online is not supported -- Shop is not functionnal +- Shop is displayed but cannot purchase heroes or items - Player title is currently static and cannot be changed in-game - QR Card Scanning currently only load a static hero - -**Network hashing in GssSite.dll must be disabled** +- Ex-quests progression not supported yet +- Daily Missions not implemented +- EX TOWER 1,2 & 3 are not yet supported +- Daily Yui coin not yet fixed ### Credits for SAO support: -- Midorica - Limited Network Support +- Midorica - Network Support - Dniel97 - Helping with network base - tungnotpunk - Source @@ -678,7 +709,7 @@ Config file is located in `config/idac.yaml`. Always make sure your database (tables) are up-to-date: ```shell -python dbutils.py --game SDGT upgrade +python dbutils.py upgrade ``` ### Notes diff --git a/docs/migrating.md b/docs/migrating.md new file mode 100644 index 0000000..3e2e056 --- /dev/null +++ b/docs/migrating.md @@ -0,0 +1,34 @@ +# Migrating from an older build of ARTEMiS +If you haven't updated artemis in a while, you may find that configuration options have moved, been renamed, or no longer exist. This document exists to help migrate from legacy versions of artemis to newer builds. + +## Dependancies +Make sure your dependiences are up to date with what's required to run artemis. A simple `pip install -r requirements.txt` will get you up to date. + +## Database +Database migration is required if you are using a version of artemis that still uses the old custom-rolled database versioning system (raw SQL scripts). Artemis now uses alembic to manage database versioning, and you will need to move to this new system. + +**BEFORE DOING ANY DATABASE WORK, ALWAYS MAKE SURE YOU HAVE FUNCTIONAL, UP-TO-DATE BACKUPS!!** + +For almost all situations, simply running `python dbutils.py migrate` will do the job. This will upgrade you to the latest version of the old system, move you over to alembic, then upgrade you to the newest alembic version. If you encounter any errors or data loss, you should report this as a bug to our issue tracker. + +## Configuration +Configuration management is the sewage cleaning of the sysadmin world. It sucks and nobody likes to do it, but it needs to be done or everyone ends up in deep shit. This section will walk through what configuration options have changed, and how to set them properly. + +### core.yaml +`title`->`hostname` is now `server`->`hostname`. This hostname is what gets sent to clients in response to auth requests, so it should be both accessable from whereever the client is, and point properly to the title server. + +With the move to starlette and uvicorn, different services now run as seperate USGI applications. `billing`->`standalone` and `allnet`->`standalone` are flags that determine weather the service runs as a stand-alone service, on it's own seperate port, or as a part of the whole application. For example, setting `billing`->`standalone` to `True` will cause a seperate instance of the billing server to spin up listening on 8443 with SSL using the certs listed in the config file. Setting it to `False` will just allow the main server to also serve `/request/` and assumes that something is standing in front of it proxying 8443 SSL to whatever `server`->`port` is set to. + +Beforehand, if `server`->`is_develop` was `False`, the server assumed that there was a proxy standing in front of it, proxying requests to proper channels. This was, in hindsight, a very dumb assumption. Now, `server`->`is_using_proxy` is what flags the server as having nginx or another proxy in front of it. The effects of setting this to true are somewhat game-dependant, but generally artemis will use the port listed in `server`->`proxy_port` (and `server`->`proxy_port_ssl` for SSL connections, as defined by the games) instead of `server`->`port`. If set to 0, `server`->`proxy_port` will default to what `server`->`port` (and `server`->`proxy_port_ssl` will default to 443) make sure to set them accordingly. Note that some title servers have their own needs and specify their own specific ports. Refer to [game_specific_info.md](docs/game_specific_info.md) for more infomation. (For example, pokken requires SSL using an older, weaker certificate, and always requires the port to be sent even if it's port 443) + +`index.py`'s args have changed. You can now override what port the title server listens on with `-p` and tell the server to use ssl with `-s`. + +Rather then having a `standalone` config variable, the frontend is a seperate wsgi app entirely. Having `enable` be `True` will launch it on the port specified in the config file. Otherwise, the fontend will not run. + +`title`->`reboot_start_time`/`reboot_end_time` allow you to specify when the games should be told network maintanence is happening. It's exact implementation depends on the game. Do note that many games will beave unexpectly if `reboot_end_time` is not `07:00`. + +If you wish to make use of aimedb's SegaAuthId system to better protect the few title servers that actually use it, set `aimedb`->`id_secret` to base64-encoded random bytes (32 is a good length) using something like `openssl rand -base64 64`. If you intend to use the frontend, the same thing must be done for `frontend`->`secret` or you won't be able to log in. + +`mucha`'s only option is now just log level. + +`aimedb` now has it's own `listen_address` field, in case you want to proxy everything but aimedb, so it can still listen on `0.0.0.0` instead of `127.0.0.1`. diff --git a/docs/prod.md b/docs/prod.md index de79b99..c398061 100644 --- a/docs/prod.md +++ b/docs/prod.md @@ -1,41 +1,34 @@ # ARTEMiS Production mode -Production mode is a configuration option that changes how the server listens to be more friendly to a production environment. This mode assumes that a proxy (for this guide, nginx) is standing in front of the server to handle port mapping and TLS. In order to activate production mode, simply change `is_develop` to `False` in `core.yaml`. Next time you start the server, you should see "Starting server in production mode". +ARTEMiS is designed to run in one of two ways. Developmen/local mode, which assumes you're just trying to set up something to save your scores and make the games work, and have patched your games to disable SSL and cert checks and encryption and the like, and production mode. In production mode, artemis assumes you have a proxy server, such as nginx or apache, standing in front of artemis doing HTTPS and port management. This document will cover how to properly set up a production instance of ARTEMiS. + +## ARTEMiS configuration +Step 1 is to edit your artemis configuration. Some recomended changes: +### `server` +- `listen_address` -> `127.0.0.1` +- `is_develop` -> `False` +- `is_using_proxy` -> `True` +- `port` -> The port nginx will send proxied requests to. If you're using the example config, set this to 8080. +- `proxy_port` -> The port your proxy will be accepting title server connections on. If you're using the example config, set this to 80. +- `proxy_port_ssl` -> The port your proxy will be accepting secure title server connections on. If you're using the example config, set this to 443. +- `allow_unregistered_serials` -> `False` +### `billing` +- `standalone` -> `False` +### `allnet` +- `standalone` -> `False` +### `frontend` +- `enable` -> `True` if you want the frontend +- `port` -> `8090` if you're using the default nginx config, otherwise whatever port your proxy will be sending requests to +### `aimedb` +- `listen_address` -> `0.0.0.0` unless you're proxying aimedb requests (not recomended at this time), in which case, leave this option unchanged + +If you plan to serve artemis behind a VPN, these additional settings are also recomended +- `check_arcade_ip` -> `True` +- `strict_ip_checking` -> `True` ## Nginx Configuration -### Port forwarding -Artemis requires that the following ports be forwarded to allow internet traffic to access the server. This will not change regardless of what you set in the config, as many of these ports are hard-coded in the games. -`tcp:80` all.net, non-ssl titles -`tcp:8443` billing -`tcp:22345` aimedb -`tcp:443` frontend, SSL titles +For most cases, the config in `example_config` will suffice. It makes the following assumptions +- ARTEMiS is running on port 8080 +- Billing is set to not be standalone +- You're not using cloudflare in front of your frontend -### A note about external proxy services (cloudflare, etc) -Due to the way that artemis functions, it is currently not possible to put the server behind something like Cloudflare. Cloudflare only proxies web traffic on the standard ports (80, 443) and, as shown above, this does not work with artemis. Server administrators should seek other means to protect their network (VPS hosting, VPN, etc) - -### SSL Certificates -You will need to generate SSL certificates for some games. The certificates vary in security and validity requirements. Please see the general guide below -- General Title: The certificate for the general title server should be valid, not self-signed and match the CN that the game will be reaching out to (e.i if your games are reaching out to titles.hostname.here, your ssl certificate should be valid for titles.hostname.here, or *.hostname.here) -- CXB: Same requires as the title server. It must not be self-signed, and CN must match. Recomended to get a wildcard cert if possible, and use it for both Title and CXB -- Pokken: Pokken can be self-signed, and the CN doesn't have to match, but it MUST use 2048-bit RSA. Due to the games age, andthing stronger then that will be rejected. - -### Port mappings -An example config is provided in the `config` folder called `nginx_example.conf`. It is set up for the following: -`naominet.jp:tcp:80` -> `localhost:tcp:8000` for allnet -`ib.naominet.jp:ssl:8443` -> `localhost:tcp:8444` for the billing server -`your.hostname.here:ssl:443` -> `localhost:tcp:8080` for the SSL title server -`your.hostname.here:tcp:80` -> `localhost:tcp:8080` for the non-SSL title server -`cxb.hostname.here:ssl:443` -> `localhost:tcp:8080` for crossbeats (appends /SDCA/104/ to the request) -`pokken.hostname.here:ssl:443` -> `localhost:tcp:8080` for pokken -`frontend.hostname.here:ssl:443` -> `localhost:tcp:8090` for the frontend, includes https redirection - -If you're using this as a guide, be sure to replace your.hostname.here with the hostname you specified in core.yaml under `titles->hostname`. Do *not* change naominet.jp, or allnet/billing will fail. Also remember to specifiy certificate paths correctly, as in the example they are simply placeholders. - -### Multi-service ports -It is possible to use nginx to redirect billing and title server requests to the same port that all.net uses. By setting `port` to 0 under billing and title server, you can change the nginx config to serve the following (entries not shown here should be the same) -`ib.naominet.jp:ssl:8443` -> `localhost:tcp:8000` for the billing server -`your.hostname.here:ssl:443` -> `localhost:tcp:8000` for the SSL title server -`your.hostname.here:tcp:80` -> `localhost:tcp:8000` for the non-SSL title server -`cxb.hostname.here:ssl:443` -> `localhost:tcp:8000` for crossbeats (appends /SDCA/104/ to the request) -`pokken.hostname.here:ssl:443` -> `localhost:tcp:8000` for pokken - -This will allow you to only use 3 ports locally, but you will still need to forward the same internet-facing ports as before. \ No newline at end of file +If this describes you, your only configuration needs are to edit the `server_name` and `certificate_*` directives. Otherwise, please see nginx configuration documentation to configure it to best suit your setup. diff --git a/example_config/chuni.yaml b/example_config/chuni.yaml index 53da186..4855fa1 100644 --- a/example_config/chuni.yaml +++ b/example_config/chuni.yaml @@ -22,6 +22,9 @@ version: 14: rom: 2.15.00 data: 2.15.00 + 15: + rom: 2.20.00 + data: 2.20.00 crypto: encrypted_only: False diff --git a/example_config/core.yaml b/example_config/core.yaml index 21b1a9d..daf18fc 100644 --- a/example_config/core.yaml +++ b/example_config/core.yaml @@ -1,25 +1,24 @@ server: - listen_address: "127.0.0.1" + listen_address: "127.0.0.1" + hostname: "localhost" + port: 80 + ssl_key: "cert/title.key" + ssl_cert: "cert/title.crt" allow_user_registration: True allow_unregistered_serials: True name: "ARTEMiS" is_develop: True is_using_proxy: False - threading: False + proxy_port: 0 + proxy_port_ssl: 0 log_dir: "logs" check_arcade_ip: False strict_ip_checking: False title: loglevel: "info" - hostname: "localhost" - port: 8080 - port_ssl: 0 - ssl_cert: "cert/title.crt" - ssl_key: "cert/title.key" reboot_start_time: "04:00" - reboot_end_time: "05:00" - + reboot_end_time: "07:00" # this must be set to 7:00 am for some game, please do not change it database: host: "localhost" @@ -29,30 +28,34 @@ database: port: 3306 protocol: "mysql" sha2_password: False - loglevel: "warn" - user_table_autoincrement_start: 10000 + loglevel: "info" enable_memcached: True memcached_host: "localhost" frontend: enable: False - port: 8090 + port: 8080 loglevel: "info" + secret: "" allnet: - loglevel: "info" + standalone: False port: 80 - ip_check: False + loglevel: "info" allow_online_updates: False update_cfg_folder: "" billing: + standalone: True + loglevel: "info" port: 8443 ssl_key: "cert/server.key" ssl_cert: "cert/server.pem" signing_key: "cert/billing.key" aimedb: + enable: True + listen_address: "" loglevel: "info" port: 22345 key: "" @@ -60,6 +63,4 @@ aimedb: id_lifetime_seconds: 86400 mucha: - enable: False - hostname: "localhost" loglevel: "info" diff --git a/example_config/cxb.yaml b/example_config/cxb.yaml index 7723ff4..628d49c 100644 --- a/example_config/cxb.yaml +++ b/example_config/cxb.yaml @@ -1,3 +1,4 @@ server: enable: True - loglevel: "info" \ No newline at end of file + loglevel: "info" + use_https: True \ No newline at end of file diff --git a/example_config/diva.yaml b/example_config/diva.yaml index ad1842a..7b8bdcb 100644 --- a/example_config/diva.yaml +++ b/example_config/diva.yaml @@ -1,6 +1,10 @@ server: enable: True loglevel: "info" + festa_enable: True + festa_add_VP: "20,5" + festa_multiply_VP: "1,2" + festa_end_time: "2029-01-01 00:00:00.0" mods: unlock_all_modules: True diff --git a/example_config/mai2.yaml b/example_config/mai2.yaml index 8557151..f0d7754 100644 --- a/example_config/mai2.yaml +++ b/example_config/mai2.yaml @@ -12,3 +12,6 @@ uploads: photos_dir: "" movies: False movies_dir: "" + +crypto: + encrypted_only: False \ No newline at end of file diff --git a/example_config/nginx_example.conf b/example_config/nginx_example.conf index ef3b7d4..b01a822 100644 --- a/example_config/nginx_example.conf +++ b/example_config/nginx_example.conf @@ -6,7 +6,7 @@ server { location / { proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_pass_request_headers on; - proxy_pass http://localhost:8000/; + proxy_pass http://127.0.0.1:8080/; } } @@ -18,7 +18,7 @@ server { location / { proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_pass_request_headers on; - proxy_pass http://localhost:8080/; + proxy_pass http://127.0.0.1:8080/; } } @@ -38,11 +38,13 @@ server { ssl_prefer_server_ciphers off; location / { - proxy_pass http://localhost:8080/; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_pass_request_headers on; + proxy_pass http://127.0.0.1:8080/; } } -# Billing +# Billing, comment this out if running billing standalone server { listen 8443 ssl; server_name ib.naominet.jp; @@ -57,30 +59,10 @@ server { ssl_ciphers "ALL:@SECLEVEL=0"; ssl_prefer_server_ciphers off; - location / { - proxy_pass http://localhost:8444/; - } -} - -# Pokken, comment this out if you don't plan on serving pokken. -server { - listen 443 ssl; - server_name pokken.hostname.here; - - ssl_certificate /path/to/cert/pokken.pem; - ssl_certificate_key /path/to/cert/pokken.key; - ssl_session_timeout 1d; - ssl_session_cache shared:MozSSL:10m; - ssl_session_tickets off; - - ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3; - ssl_ciphers "ALL:@SECLEVEL=0"; - ssl_prefer_server_ciphers off; - location / { proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_pass_request_headers on; - proxy_pass http://localhost:8080/; + proxy_pass http://127.0.0.1:8080/; } } @@ -91,12 +73,12 @@ server { location / { return 301 https://$host$request_uri; - # If you don't want https redirection, comment the line above and uncomment the line below - # proxy_pass http://localhost:8090/; + # If you don't want https redirection, or are using something like cloudflare to manage HTTPS, comment out the line above and uncomment the line below + # proxy_pass http://127.0.0.1:8090/; } } -# Frontend HTTPS. Comment out if you on't intend to use the frontend +# Frontend HTTPS. Comment out if you on't intend to use the frontend, or have cloudflare or something managing https for you. server { listen 443 ssl; server_name frontend.hostname.here; @@ -118,6 +100,6 @@ server { location / { proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_pass_request_headers on; - proxy_pass http://localhost:8090/; + proxy_pass http://127.0.0.1:8090/; } } diff --git a/example_config/sao.yaml b/example_config/sao.yaml index 0209ffe..d405461 100644 --- a/example_config/sao.yaml +++ b/example_config/sao.yaml @@ -2,12 +2,19 @@ server: enable: True loglevel: "info" auto_register: True + photon_app_id: "7df3a2f6-d69d-4073-aafe-810ee61e1cea" + data_version: 1 + game_version: 33 crypt: enable: False key: "" - iv: "" hash: verify_hash: False - hash_base: "" \ No newline at end of file + hash_base: "" + +card: + enable: True + crypt_password: "" + crypt_salt: "" diff --git a/index.py b/index.py index 798519c..40a1bbd 100644 --- a/index.py +++ b/index.py @@ -1,335 +1,117 @@ #!/usr/bin/env python3 import argparse -import logging, coloredlogs -from logging.handlers import TimedRotatingFileHandler -from typing import Dict import yaml -from os import path, mkdir, access, W_OK -from core import * +from os import path, environ +import uvicorn +import logging +import asyncio -from twisted.web import server, resource -from twisted.internet import reactor, endpoints -from twisted.web.http import Request -from routes import Mapper -from threading import Thread +from core import CoreConfig, AimedbServlette -class HttpDispatcher(resource.Resource): - def __init__(self, cfg: CoreConfig, config_dir: str): - super().__init__() - self.config = cfg - self.isLeaf = True - self.map_get = Mapper() - self.map_post = Mapper() - self.logger = logging.getLogger("core") - - self.title = TitleServlet(cfg, config_dir) - self.allnet = AllnetServlet(cfg, config_dir) - self.mucha = MuchaServlet(cfg, config_dir) - - self.map_get.connect( - "allnet_downloadorder_ini", - "/dl/ini/{file}", - controller="allnet", - action="handle_dlorder_ini", - conditions=dict(method=["GET"]), +async def launch_main(cfg: CoreConfig, ssl: bool) -> None: + if ssl: + server_cfg = uvicorn.Config( + "core.app:app", + host=cfg.server.listen_address, + port=cfg.server.port if args.port == 0 else args.port, + reload=cfg.server.is_develop, + log_level="info" if cfg.server.is_develop else "critical", + ssl_version=3, + ssl_certfile=cfg.server.ssl_cert, + ssl_keyfile=cfg.server.ssl_key + ) + else: + server_cfg = uvicorn.Config( + "core.app:app", + host=cfg.server.listen_address, + port=cfg.server.port if args.port == 0 else args.port, + reload=cfg.server.is_develop, + log_level="info" if cfg.server.is_develop else "critical" ) + server = uvicorn.Server(server_cfg) + await server.serve() - self.map_post.connect( - "allnet_downloadorder_report", - "/report-api/Report", - controller="allnet", - action="handle_dlorder_report", - conditions=dict(method=["POST"]), - ) +async def launch_billing(cfg: CoreConfig) -> None: + server_cfg = uvicorn.Config( + "core.allnet:app_billing", + host=cfg.server.listen_address, + port=cfg.billing.port, + reload=cfg.server.is_develop, + log_level="info" if cfg.server.is_develop else "critical", + ssl_version=3, + ssl_certfile=cfg.billing.ssl_cert, + ssl_keyfile=cfg.billing.ssl_key, + ssl_ciphers="DEFAULT:!aNULL:!eNULL:!MD5:!3DES:!DES:!RC4:!IDEA:!SEED:!aDSS:!SRP:!PSK", + ) + server = uvicorn.Server(server_cfg) + await server.serve() - self.map_get.connect( - "allnet_ping", - "/naomitest.html", - controller="allnet", - action="handle_naomitest", - conditions=dict(method=["GET"]), - ) - self.map_post.connect( - "allnet_poweron", - "/sys/servlet/PowerOn", - controller="allnet", - action="handle_poweron", - conditions=dict(method=["POST"]), - ) - self.map_post.connect( - "allnet_downloadorder", - "/sys/servlet/DownloadOrder", - controller="allnet", - action="handle_dlorder", - conditions=dict(method=["POST"]), - ) - self.map_post.connect( - "allnet_loaderstaterecorder", - "/sys/servlet/LoaderStateRecorder", - controller="allnet", - action="handle_loaderstaterecorder", - conditions=dict(method=["POST"]), - ) - self.map_post.connect( - "allnet_alive", - "/sys/servlet/Alive", - controller="allnet", - action="handle_alive", - conditions=dict(method=["POST"]), - ) - self.map_get.connect( - "allnet_alive", - "/sys/servlet/Alive", - controller="allnet", - action="handle_alive", - conditions=dict(method=["GET"]), - ) - self.map_post.connect( - "allnet_billing", - "/request", - controller="allnet", - action="handle_billing_request", - conditions=dict(method=["POST"]), - ) - self.map_post.connect( - "allnet_billing", - "/request/", - controller="allnet", - action="handle_billing_request", - conditions=dict(method=["POST"]), - ) +async def launch_frontend(cfg: CoreConfig) -> None: + server_cfg = uvicorn.Config( + "core.frontend:app", + host=cfg.server.listen_address, + port=cfg.frontend.port, + reload=cfg.server.is_develop, + log_level="info" if cfg.server.is_develop else "critical", + ) + server = uvicorn.Server(server_cfg) + await server.serve() - # Maintain compatability - self.map_post.connect( - "mucha_boardauth", - "/mucha/boardauth.do", - controller="mucha", - action="handle_boardauth", - conditions=dict(method=["POST"]), - ) - self.map_post.connect( - "mucha_updatacheck", - "/mucha/updatacheck.do", - controller="mucha", - action="handle_updatecheck", - conditions=dict(method=["POST"]), - ) - self.map_post.connect( - "mucha_dlstate", - "/mucha/downloadstate.do", - controller="mucha", - action="handle_dlstate", - conditions=dict(method=["POST"]), - ) +async def launch_allnet(cfg: CoreConfig) -> None: + server_cfg = uvicorn.Config( + "core.allnet:app_allnet", + host=cfg.server.listen_address, + port=cfg.allnet.port, + reload=cfg.server.is_develop, + log_level="info" if cfg.server.is_develop else "critical", + ) + server = uvicorn.Server(server_cfg) + await server.serve() - self.map_post.connect( - "mucha_boardauth", - "/mucha_front/boardauth.do", - controller="mucha", - action="handle_boardauth", - conditions=dict(method=["POST"]), - ) - self.map_post.connect( - "mucha_updatacheck", - "/mucha_front/updatacheck.do", - controller="mucha", - action="handle_updatecheck", - conditions=dict(method=["POST"]), - ) - self.map_post.connect( - "mucha_dlstate", - "/mucha_front/downloadstate.do", - controller="mucha", - action="handle_dlstate", - conditions=dict(method=["POST"]), - ) - - for code, game in self.title.title_registry.items(): - get_matchers, post_matchers = game.get_endpoint_matchers() - - for m in get_matchers: - self.map_get.connect( - "title_get", - m[1], - controller="title", - action="render_GET", - title=code, - subaction=m[0], - conditions=dict(method=["GET"]), - requirements=m[2], - ) - - for m in post_matchers: - self.map_post.connect( - "title_post", - m[1], - controller="title", - action="render_POST", - title=code, - subaction=m[0], - conditions=dict(method=["POST"]), - requirements=m[2], - ) - - def render_GET(self, request: Request) -> bytes: - test = self.map_get.match(request.uri.decode()) - client_ip = Utils.get_ip_addr(request) - - if test is None: - self.logger.debug( - f"Unknown GET endpoint {request.uri.decode()} from {client_ip} to port {request.getHost().port}" - ) - request.setResponseCode(404) - return b"Endpoint not found." - - return self.dispatch(test, request) - - def render_POST(self, request: Request) -> bytes: - test = self.map_post.match(request.uri.decode()) - client_ip = Utils.get_ip_addr(request) - - if test is None: - self.logger.debug( - f"Unknown POST endpoint {request.uri.decode()} from {client_ip} to port {request.getHost().port}" - ) - request.setResponseCode(404) - return b"Endpoint not found." - - return self.dispatch(test, request) - - def dispatch(self, matcher: Dict, request: Request) -> bytes: - controller = getattr(self, matcher["controller"], None) - if controller is None: - self.logger.error( - f"Controller {matcher['controller']} not found via endpoint {request.uri.decode()}" - ) - request.setResponseCode(404) - return b"Endpoint not found." - - handler = getattr(controller, matcher["action"], None) - if handler is None: - self.logger.error( - f"Action {matcher['action']} not found in controller {matcher['controller']} via endpoint {request.uri.decode()}" - ) - request.setResponseCode(404) - return b"Endpoint not found." - - url_vars = matcher - url_vars.pop("controller") - url_vars.pop("action") - ret = handler(request, url_vars) - - if type(ret) == str: - return ret.encode() - - elif type(ret) == bytes or type(ret) == tuple: # allow for bytes or tuple (data, response code) responses - return ret - - elif ret is None: - self.logger.warning(f"None returned by controller for {request.uri.decode()} endpoint") - return b"" - - else: - self.logger.warning(f"Unknown data type returned by controller for {request.uri.decode()} endpoint") - return b"" +async def launcher(cfg: CoreConfig, ssl: bool) -> None: + task_list = [asyncio.create_task(launch_main(cfg, ssl))] + + if cfg.billing.standalone: + task_list.append(asyncio.create_task(launch_billing(cfg))) + if cfg.frontend.enable: + task_list.append(asyncio.create_task(launch_frontend(cfg))) + if cfg.allnet.standalone: + task_list.append(asyncio.create_task(launch_allnet(cfg))) + if cfg.aimedb.enable: + AimedbServlette(cfg).start() + + done, pending = await asyncio.wait( + task_list, + return_when=asyncio.FIRST_COMPLETED, + ) + + logging.getLogger("core").info("Shutdown") + for pending_task in pending: + pending_task.cancel("Another service died, server is shutting down") if __name__ == "__main__": - parser = argparse.ArgumentParser(description="ARTEMiS main entry point") + parser = argparse.ArgumentParser(description="Artemis main entry point") parser.add_argument( "--config", "-c", type=str, default="config", help="Configuration folder" ) + parser.add_argument( + "--port", "-p", type=int, default=0, help="Port override" + ) + parser.add_argument( + "--ssl", "-s", type=bool, help="Launch with SSL" + ) args = parser.parse_args() if not path.exists(f"{args.config}/core.yaml"): print( - f"The config folder you specified ({args.config}) does not exist or does not contain core.yaml.\nDid you copy the example folder?" + f"The config folder you specified ({args.config}) does not exist or does not contain core.yaml. Defaults will be used.\nDid you copy the example folder?" ) - exit(1) - + cfg: CoreConfig = CoreConfig() if path.exists(f"{args.config}/core.yaml"): cfg.update(yaml.safe_load(open(f"{args.config}/core.yaml"))) - if not path.exists(cfg.server.log_dir): - mkdir(cfg.server.log_dir) + environ["ARTEMIS_CFG_DIR"] = args.config - if not access(cfg.server.log_dir, W_OK): - print( - f"Log directory {cfg.server.log_dir} NOT writable, please check permissions" - ) - exit(1) - - logger = logging.getLogger("core") - log_fmt_str = "[%(asctime)s] Core | %(levelname)s | %(message)s" - log_fmt = logging.Formatter(log_fmt_str) - - fileHandler = TimedRotatingFileHandler( - "{0}/{1}.log".format(cfg.server.log_dir, "core"), when="d", backupCount=10 - ) - fileHandler.setFormatter(log_fmt) - - consoleHandler = logging.StreamHandler() - consoleHandler.setFormatter(log_fmt) - - logger.addHandler(fileHandler) - logger.addHandler(consoleHandler) - - log_lv = logging.DEBUG if cfg.server.is_develop else logging.INFO - logger.setLevel(log_lv) - coloredlogs.install(level=log_lv, logger=logger, fmt=log_fmt_str) - - if not cfg.aimedb.key: - logger.error("!!AIMEDB KEY BLANK, SET KEY IN CORE.YAML!!") - exit(1) - - logger.info( - f"ARTEMiS starting in {'develop' if cfg.server.is_develop else 'production'} mode" - ) - - allnet_server_str = f"tcp:{cfg.allnet.port}:interface={cfg.server.listen_address}" - title_server_str = f"tcp:{cfg.title.port}:interface={cfg.server.listen_address}" - title_https_server_str = f"ssl:{cfg.title.port_ssl}:interface={cfg.server.listen_address}:privateKey={cfg.title.ssl_key}:certKey={cfg.title.ssl_cert}" - adb_server_str = f"tcp:{cfg.aimedb.port}:interface={cfg.server.listen_address}" - frontend_server_str = ( - f"tcp:{cfg.frontend.port}:interface={cfg.server.listen_address}" - ) - - billing_server_str = f"tcp:{cfg.billing.port}:interface={cfg.server.listen_address}" - if cfg.server.is_develop: - billing_server_str = ( - f"ssl:{cfg.billing.port}:interface={cfg.server.listen_address}" - f":privateKey={cfg.billing.ssl_key}:certKey={cfg.billing.ssl_cert}" - ) - - dispatcher = HttpDispatcher(cfg, args.config) - - endpoints.serverFromString(reactor, allnet_server_str).listen( - server.Site(dispatcher) - ) - endpoints.serverFromString(reactor, adb_server_str).listen(AimedbFactory(cfg)) - - if cfg.frontend.enable: - endpoints.serverFromString(reactor, frontend_server_str).listen( - server.Site(FrontendServlet(cfg, args.config)) - ) - - if cfg.billing.port > 0: - endpoints.serverFromString(reactor, billing_server_str).listen( - server.Site(dispatcher) - ) - - if cfg.title.port > 0: - endpoints.serverFromString(reactor, title_server_str).listen( - server.Site(dispatcher) - ) - - if cfg.title.port_ssl > 0: - endpoints.serverFromString(reactor, title_https_server_str).listen( - server.Site(dispatcher) - ) - - if cfg.server.threading: - Thread(target=reactor.run, args=(False,)).start() - else: - reactor.run() + asyncio.run(launcher(cfg, args.ssl)) diff --git a/read.py b/read.py index fa34314..8a0ae72 100644 --- a/read.py +++ b/read.py @@ -1,4 +1,4 @@ -# vim: set fileencoding=utf-8 +#!/usr/bin/env python3 import argparse import re import os @@ -6,6 +6,7 @@ import yaml from os import path import logging import coloredlogs +import asyncio from logging.handlers import TimedRotatingFileHandler from typing import List, Optional @@ -38,6 +39,9 @@ class BaseReader: ret.append(f"{root}/{dir}") return ret + + async def read(self) -> None: + pass if __name__ == "__main__": @@ -136,6 +140,8 @@ if __name__ == "__main__": for dir, mod in titles.items(): if args.game in mod.game_codes: handler = mod.reader(config, args.version, bin_arg, opt_arg, args.extra) - handler.read() + loop = asyncio.get_event_loop() + loop.run_until_complete(handler.read()) + logger.info("Done") diff --git a/readme.md b/readme.md index cbd89c9..505f241 100644 --- a/readme.md +++ b/readme.md @@ -4,37 +4,69 @@ A network service emulator for games running SEGA'S ALL.NET service, and similar # Supported games Games listed below have been tested and confirmed working. Only game versions older then the version currently active in arcades, or games versions that have not recieved a major update in over one year, are supported. -+ CHUNITHM - + All versions up to SUN PLUS - -+ crossbeats REV. - + All versions + omnimix - -+ maimai DX - + All versions up to FESTiVAL PLUS - -+ Hatsune Miku: Project DIVA Arcade - + All versions - + Card Maker + 1.30 + 1.35 -+ O.N.G.E.K.I. - + All versions up to bright MEMORY ++ CHUNITHM INTL + + SUPERSTAR + + SUPERSTAR PLUS + + NEW + + NEW PLUS + + SUN + + SUN PLUS -+ WACCA - + Lily R - + Reverse ++ CHUNITHM JP + + AIR + + AIR PLUS + + AMAZON + + AMAZON PLUS + + CRYSTAL + + CRYSTAL PLUS + + PARADISE + + PARADISE LOST + + NEW + + NEW PLUS + + SUN + + SUN PLUS + ++ crossbeats REV. + + Crossbeats REV. + + Crossbeats REV. SUNRiSE S1 + + Crossbeats REV. SUNRiSE S2 + omnimix + ++ Hatsune Miku: Project DIVA Arcade + + Future Tone Arcade - All versions + ++ Initial D THE ARCADE + + Season 2 + ++ maimai DX + + Splash + + Splash Plus + + UNiVERSE + + UNiVERSE PLUS + + FESTiVAL + + FESTiVAL PLUS + + BUDDiES + ++ O.N.G.E.K.I. + + SUMMER + + SUMMER PLUS + + R.E.D. + + R.E.D. PLUS + + bright + + bright MEMORY + POKKÉN TOURNAMENT + Final Online -+ Sword Art Online Arcade (partial support) - + Final ++ Sword Art Online Arcade + + Final (Single player only) -+ Initial D THE ARCADE - + Season 2 ++ WACCA + + Lily R + + Reverse ## Requirements - python 3 (tested working with 3.9 and 3.10, other versions YMMV) @@ -43,7 +75,7 @@ Games listed below have been tested and confirmed working. Only game versions ol - mysql/mariadb server ## Setup guides -Follow the platform-specific guides for [windows](docs/INSTALL_WINDOWS.md), [ubuntu](docs/INSTALL_UBUNTU.md) or [docker](docs/INSTALL_DOCKER.md) to setup and run the server. +Follow the platform-specific guides for [windows](docs/INSTALL_WINDOWS.md), [linux (Debian 12 or Rasperry Pi OS recomended, but anything works)](docs/INSTALL_LINUX.md) or [docker](docs/INSTALL_DOCKER.md) to setup and run the server. ## Game specific information Read [Games specific info](docs/game_specific_info.md) for all supported games, importer settings, configuration option and database upgrades. diff --git a/requirements.txt b/requirements.txt index c399e1f..fe5b4ef 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,21 +1,24 @@ -mypy -wheel -twisted -pytz -pyyaml -sqlalchemy==1.4.46 -mysqlclient -pyopenssl -service_identity -PyCryptodome -inflection -coloredlogs -pylibmc; platform_system != "Windows" -wacky -Routes -bcrypt -jinja2 -protobuf -autobahn -pillow -pyjwt +mypy +wheel +pytz +pyyaml +sqlalchemy==1.4.46 +mysqlclient +pyopenssl +service_identity +PyCryptodome +inflection +coloredlogs +pylibmc; platform_system != "Windows" +wacky +bcrypt +jinja2 +protobuf +pillow +pyjwt==2.8.0 +websockets +starlette +asyncio +uvicorn +alembic +python-multipart \ No newline at end of file diff --git a/titles/chuni/__init__.py b/titles/chuni/__init__.py index dc0e2f4..226594a 100644 --- a/titles/chuni/__init__.py +++ b/titles/chuni/__init__.py @@ -1,10 +1,11 @@ -from titles.chuni.index import ChuniServlet -from titles.chuni.const import ChuniConstants -from titles.chuni.database import ChuniData -from titles.chuni.read import ChuniReader +from .index import ChuniServlet +from .const import ChuniConstants +from .database import ChuniData +from .read import ChuniReader +from .frontend import ChuniFrontend index = ChuniServlet database = ChuniData reader = ChuniReader +frontend = ChuniFrontend game_codes = [ChuniConstants.GAME_CODE, ChuniConstants.GAME_CODE_NEW, ChuniConstants.GAME_CODE_INT] -current_schema_version = 5 \ No newline at end of file diff --git a/titles/chuni/air.py b/titles/chuni/air.py index b9bc1d3..094340c 100644 --- a/titles/chuni/air.py +++ b/titles/chuni/air.py @@ -11,7 +11,7 @@ class ChuniAir(ChuniBase): super().__init__(core_cfg, game_cfg) self.version = ChuniConstants.VER_CHUNITHM_AIR - def handle_get_game_setting_api_request(self, data: Dict) -> Dict: - ret = super().handle_get_game_setting_api_request(data) + async def handle_get_game_setting_api_request(self, data: Dict) -> Dict: + ret = await super().handle_get_game_setting_api_request(data) ret["gameSetting"]["dataVersion"] = "1.10.00" return ret diff --git a/titles/chuni/airplus.py b/titles/chuni/airplus.py index f0d8224..9b35c69 100644 --- a/titles/chuni/airplus.py +++ b/titles/chuni/airplus.py @@ -11,7 +11,7 @@ class ChuniAirPlus(ChuniBase): super().__init__(core_cfg, game_cfg) self.version = ChuniConstants.VER_CHUNITHM_AIR_PLUS - def handle_get_game_setting_api_request(self, data: Dict) -> Dict: - ret = super().handle_get_game_setting_api_request(data) + async def handle_get_game_setting_api_request(self, data: Dict) -> Dict: + ret = await super().handle_get_game_setting_api_request(data) ret["gameSetting"]["dataVersion"] = "1.15.00" return ret diff --git a/titles/chuni/amazon.py b/titles/chuni/amazon.py index b765c2f..84f5a12 100644 --- a/titles/chuni/amazon.py +++ b/titles/chuni/amazon.py @@ -13,7 +13,7 @@ class ChuniAmazon(ChuniBase): super().__init__(core_cfg, game_cfg) self.version = ChuniConstants.VER_CHUNITHM_AMAZON - def handle_get_game_setting_api_request(self, data: Dict) -> Dict: - ret = super().handle_get_game_setting_api_request(data) + async def handle_get_game_setting_api_request(self, data: Dict) -> Dict: + ret = await super().handle_get_game_setting_api_request(data) ret["gameSetting"]["dataVersion"] = "1.30.00" return ret diff --git a/titles/chuni/amazonplus.py b/titles/chuni/amazonplus.py index ea8d704..9ce13cf 100644 --- a/titles/chuni/amazonplus.py +++ b/titles/chuni/amazonplus.py @@ -13,7 +13,7 @@ class ChuniAmazonPlus(ChuniBase): super().__init__(core_cfg, game_cfg) self.version = ChuniConstants.VER_CHUNITHM_AMAZON_PLUS - def handle_get_game_setting_api_request(self, data: Dict) -> Dict: - ret = super().handle_get_game_setting_api_request(data) + async def handle_get_game_setting_api_request(self, data: Dict) -> Dict: + ret = await super().handle_get_game_setting_api_request(data) ret["gameSetting"]["dataVersion"] = "1.35.00" return ret diff --git a/titles/chuni/base.py b/titles/chuni/base.py index 8d22488..9be4bf7 100644 --- a/titles/chuni/base.py +++ b/titles/chuni/base.py @@ -22,7 +22,7 @@ class ChuniBase: self.game = ChuniConstants.GAME_CODE self.version = ChuniConstants.VER_CHUNITHM - def handle_game_login_api_request(self, data: Dict) -> Dict: + async def handle_game_login_api_request(self, data: Dict) -> Dict: """ Handles the login bonus logic, required for the game because getUserLoginBonus gets called after getUserItem and therefore the @@ -38,20 +38,20 @@ class ChuniBase: return {"returnCode": 1} user_id = data["userId"] - login_bonus_presets = self.data.static.get_login_bonus_presets(self.version) + login_bonus_presets = await self.data.static.get_login_bonus_presets(self.version) for preset in login_bonus_presets: # check if a user already has some pogress and if not add the # login bonus entry - user_login_bonus = self.data.item.get_login_bonus( + user_login_bonus = await self.data.item.get_login_bonus( user_id, self.version, preset["presetId"] ) if user_login_bonus is None: - self.data.item.put_login_bonus( + await self.data.item.put_login_bonus( user_id, self.version, preset["presetId"] ) # yeah i'm lazy - user_login_bonus = self.data.item.get_login_bonus( + user_login_bonus = await self.data.item.get_login_bonus( user_id, self.version, preset["presetId"] ) @@ -67,7 +67,7 @@ class ChuniBase: bonus_count = user_login_bonus["bonusCount"] + 1 last_update_date = datetime.now() - all_login_boni = self.data.static.get_login_bonus( + all_login_boni = await self.data.static.get_login_bonus( self.version, preset["presetId"] ) @@ -91,13 +91,13 @@ class ChuniBase: is_finished = True # grab the item for the corresponding day - login_item = self.data.static.get_login_bonus_by_required_days( + login_item = await self.data.static.get_login_bonus_by_required_days( self.version, preset["presetId"], bonus_count ) if login_item is not None: # now add the present to the database so the # handle_get_user_item_api_request can grab them - self.data.item.put_item( + await self.data.item.put_item( user_id, { "itemId": login_item["presentId"], @@ -107,7 +107,7 @@ class ChuniBase: }, ) - self.data.item.put_login_bonus( + await self.data.item.put_login_bonus( user_id, self.version, preset["presetId"], @@ -119,12 +119,12 @@ class ChuniBase: return {"returnCode": 1} - def handle_game_logout_api_request(self, data: Dict) -> Dict: + async def handle_game_logout_api_request(self, data: Dict) -> Dict: # self.data.base.log_event("chuni", "logout", logging.INFO, {"version": self.version, "user": data["userId"]}) return {"returnCode": 1} - def handle_get_game_charge_api_request(self, data: Dict) -> Dict: - game_charge_list = self.data.static.get_enabled_charges(self.version) + async def handle_get_game_charge_api_request(self, data: Dict) -> Dict: + game_charge_list = await self.data.static.get_enabled_charges(self.version) if game_charge_list is None or len(game_charge_list) == 0: return {"length": 0, "gameChargeList": []} @@ -145,8 +145,8 @@ class ChuniBase: ) return {"length": len(charges), "gameChargeList": charges} - def handle_get_game_event_api_request(self, data: Dict) -> Dict: - game_events = self.data.static.get_enabled_events(self.version) + async def handle_get_game_event_api_request(self, data: Dict) -> Dict: + game_events = await self.data.static.get_enabled_events(self.version) if game_events is None or len(game_events) == 0: self.logger.warning("No enabled events, did you run the reader?") @@ -177,10 +177,10 @@ class ChuniBase: "gameEventList": event_list, } - def handle_get_game_idlist_api_request(self, data: Dict) -> Dict: + async def handle_get_game_idlist_api_request(self, data: Dict) -> Dict: return {"type": data["type"], "length": 0, "gameIdlistList": []} - def handle_get_game_message_api_request(self, data: Dict) -> Dict: + async def handle_get_game_message_api_request(self, data: Dict) -> Dict: return { "type": data["type"], "length": 1, @@ -193,14 +193,14 @@ class ChuniBase: }] } - def handle_get_game_ranking_api_request(self, data: Dict) -> Dict: - rankings = self.data.score.get_rankings(self.version) + async def handle_get_game_ranking_api_request(self, data: Dict) -> Dict: + rankings = await self.data.score.get_rankings(self.version) return {"type": data["type"], "gameRankingList": rankings} - def handle_get_game_sale_api_request(self, data: Dict) -> Dict: + async def handle_get_game_sale_api_request(self, data: Dict) -> Dict: return {"type": data["type"], "length": 0, "gameSaleList": []} - def handle_get_game_setting_api_request(self, data: Dict) -> Dict: + async def handle_get_game_setting_api_request(self, data: Dict) -> Dict: # if reboot start/end time is not defined use the default behavior of being a few hours ago if self.core_cfg.title.reboot_start_time == "" or self.core_cfg.title.reboot_end_time == "": reboot_start = datetime.strftime( @@ -240,8 +240,8 @@ class ChuniBase: "isDumpUpload": "false", "isAou": "false", } - def handle_get_user_activity_api_request(self, data: Dict) -> Dict: - user_activity_list = self.data.profile.get_profile_activity( + async def handle_get_user_activity_api_request(self, data: Dict) -> Dict: + user_activity_list = await self.data.profile.get_profile_activity( data["userId"], data["kind"] ) @@ -261,8 +261,8 @@ class ChuniBase: "userActivityList": activity_list, } - def handle_get_user_character_api_request(self, data: Dict) -> Dict: - characters = self.data.item.get_characters(data["userId"]) + async def handle_get_user_character_api_request(self, data: Dict) -> Dict: + characters = await self.data.item.get_characters(data["userId"]) if characters is None: return { "userId": data["userId"], @@ -296,8 +296,8 @@ class ChuniBase: "userCharacterList": character_list, } - def handle_get_user_charge_api_request(self, data: Dict) -> Dict: - user_charge_list = self.data.profile.get_profile_charge(data["userId"]) + async def handle_get_user_charge_api_request(self, data: Dict) -> Dict: + user_charge_list = await self.data.profile.get_profile_charge(data["userId"]) charge_list = [] for charge in user_charge_list: @@ -312,15 +312,15 @@ class ChuniBase: "userChargeList": charge_list, } - def handle_get_user_recent_player_api_request(self, data: Dict) -> Dict: + async def handle_get_user_recent_player_api_request(self, data: Dict) -> Dict: return { "userId": data["userId"], "length": 0, "userRecentPlayerList": [], # playUserId, playUserName, playDate, friendPoint } - def handle_get_user_course_api_request(self, data: Dict) -> Dict: - user_course_list = self.data.score.get_courses(data["userId"]) + async def handle_get_user_course_api_request(self, data: Dict) -> Dict: + user_course_list = await self.data.score.get_courses(data["userId"]) if user_course_list is None: return { "userId": data["userId"], @@ -354,8 +354,8 @@ class ChuniBase: "userCourseList": course_list, } - def handle_get_user_data_api_request(self, data: Dict) -> Dict: - p = self.data.profile.get_profile_data(data["userId"], self.version) + async def handle_get_user_data_api_request(self, data: Dict) -> Dict: + p = await self.data.profile.get_profile_data(data["userId"], self.version) if p is None: return {} @@ -366,8 +366,8 @@ class ChuniBase: return {"userId": data["userId"], "userData": profile} - def handle_get_user_data_ex_api_request(self, data: Dict) -> Dict: - p = self.data.profile.get_profile_data_ex(data["userId"], self.version) + async def handle_get_user_data_ex_api_request(self, data: Dict) -> Dict: + p = await self.data.profile.get_profile_data_ex(data["userId"], self.version) if p is None: return {} @@ -378,8 +378,8 @@ class ChuniBase: return {"userId": data["userId"], "userDataEx": profile} - def handle_get_user_duel_api_request(self, data: Dict) -> Dict: - user_duel_list = self.data.item.get_duels(data["userId"]) + async def handle_get_user_duel_api_request(self, data: Dict) -> Dict: + user_duel_list = await self.data.item.get_duels(data["userId"]) if user_duel_list is None: return {} @@ -396,8 +396,8 @@ class ChuniBase: "userDuelList": duel_list, } - def handle_get_user_rival_data_api_request(self, data: Dict) -> Dict: - p = self.data.profile.get_rival(data["rivalId"]) + async def handle_get_user_rival_data_api_request(self, data: Dict) -> Dict: + p = await self.data.profile.get_rival(data["rivalId"]) if p is None: return {} userRivalData = { @@ -409,14 +409,14 @@ class ChuniBase: "userRivalData": userRivalData } - def handle_get_user_rival_music_api_request(self, data: Dict) -> Dict: + async def handle_get_user_rival_music_api_request(self, data: Dict) -> Dict: rival_id = data["rivalId"] next_index = int(data["nextIndex"]) max_count = int(data["maxCount"]) user_rival_music_list = [] # Fetch all the rival music entries for the user - all_entries = self.data.score.get_rival_music(rival_id) + all_entries = await self.data.score.get_rival_music(rival_id) # Process the entries based on max_count and nextIndex for music in all_entries: @@ -462,12 +462,12 @@ class ChuniBase: return result - def handle_get_user_favorite_item_api_request(self, data: Dict) -> Dict: + async def handle_get_user_favorite_item_api_request(self, data: Dict) -> Dict: user_fav_item_list = [] # still needs to be implemented on WebUI # 1: Music, 2: User, 3: Character - fav_list = self.data.item.get_all_favorites( + fav_list = await self.data.item.get_all_favorites( data["userId"], self.version, fav_kind=int(data["kind"]) ) if fav_list is not None: @@ -482,17 +482,17 @@ class ChuniBase: "userFavoriteItemList": user_fav_item_list, } - def handle_get_user_favorite_music_api_request(self, data: Dict) -> Dict: + async def handle_get_user_favorite_music_api_request(self, data: Dict) -> Dict: """ This is handled via the webui, which we don't have right now """ return {"userId": data["userId"], "length": 0, "userFavoriteMusicList": []} - def handle_get_user_item_api_request(self, data: Dict) -> Dict: + async def handle_get_user_item_api_request(self, data: Dict) -> Dict: kind = int(int(data["nextIndex"]) / 10000000000) next_idx = int(int(data["nextIndex"]) % 10000000000) - user_item_list = self.data.item.get_items(data["userId"], kind) + user_item_list = await self.data.item.get_items(data["userId"], kind) if user_item_list is None or len(user_item_list) == 0: return { @@ -526,9 +526,9 @@ class ChuniBase: "userItemList": items, } - def handle_get_user_login_bonus_api_request(self, data: Dict) -> Dict: + async def handle_get_user_login_bonus_api_request(self, data: Dict) -> Dict: user_id = data["userId"] - user_login_bonus = self.data.item.get_all_login_bonus(user_id, self.version) + user_login_bonus = await self.data.item.get_all_login_bonus(user_id, self.version) # ignore the loginBonus request if its disabled in config if user_login_bonus is None or not self.game_cfg.mods.use_login_bonus: return {"userId": user_id, "length": 0, "userLoginBonusList": []} @@ -552,8 +552,8 @@ class ChuniBase: "userLoginBonusList": user_login_list, } - def handle_get_user_map_api_request(self, data: Dict) -> Dict: - user_map_list = self.data.item.get_maps(data["userId"]) + async def handle_get_user_map_api_request(self, data: Dict) -> Dict: + user_map_list = await self.data.item.get_maps(data["userId"]) if user_map_list is None: return {} @@ -570,8 +570,8 @@ class ChuniBase: "userMapList": map_list, } - def handle_get_user_music_api_request(self, data: Dict) -> Dict: - music_detail = self.data.score.get_scores(data["userId"]) + async def handle_get_user_music_api_request(self, data: Dict) -> Dict: + music_detail = await self.data.score.get_scores(data["userId"]) if music_detail is None: return { "userId": data["userId"], @@ -629,8 +629,8 @@ class ChuniBase: "userMusicList": song_list, # 240 } - def handle_get_user_option_api_request(self, data: Dict) -> Dict: - p = self.data.profile.get_profile_option(data["userId"]) + async def handle_get_user_option_api_request(self, data: Dict) -> Dict: + p = await self.data.profile.get_profile_option(data["userId"]) option = p._asdict() option.pop("id") @@ -638,8 +638,8 @@ class ChuniBase: return {"userId": data["userId"], "userGameOption": option} - def handle_get_user_option_ex_api_request(self, data: Dict) -> Dict: - p = self.data.profile.get_profile_option_ex(data["userId"]) + async def handle_get_user_option_ex_api_request(self, data: Dict) -> Dict: + p = await self.data.profile.get_profile_option_ex(data["userId"]) option = p._asdict() option.pop("id") @@ -650,11 +650,11 @@ class ChuniBase: def read_wtf8(self, src): return bytes([ord(c) for c in src]).decode("utf-8") - def handle_get_user_preview_api_request(self, data: Dict) -> Dict: - profile = self.data.profile.get_profile_preview(data["userId"], self.version) + async def handle_get_user_preview_api_request(self, data: Dict) -> Dict: + profile = await self.data.profile.get_profile_preview(data["userId"], self.version) if profile is None: return None - profile_character = self.data.item.get_character( + profile_character = await self.data.item.get_character( data["userId"], profile["characterId"] ) @@ -692,8 +692,8 @@ class ChuniBase: "userNameEx": profile["userName"], } - def handle_get_user_recent_rating_api_request(self, data: Dict) -> Dict: - recent_rating_list = self.data.profile.get_profile_recent_rating(data["userId"]) + async def handle_get_user_recent_rating_api_request(self, data: Dict) -> Dict: + recent_rating_list = await self.data.profile.get_profile_recent_rating(data["userId"]) if recent_rating_list is None: return { "userId": data["userId"], @@ -707,7 +707,7 @@ class ChuniBase: "userRecentRatingList": recent_rating_list["recentRating"], } - def handle_get_user_region_api_request(self, data: Dict) -> Dict: + async def handle_get_user_region_api_request(self, data: Dict) -> Dict: # TODO: Region return { "userId": data["userId"], @@ -715,23 +715,33 @@ class ChuniBase: "userRegionList": [], } - def handle_get_user_team_api_request(self, data: Dict) -> Dict: + async def handle_get_user_team_api_request(self, data: Dict) -> Dict: # Default values team_id = 65535 team_name = self.game_cfg.team.team_name team_rank = 0 + team_user_point = 0 # Get user profile - profile = self.data.profile.get_profile_data(data["userId"], self.version) + profile = await self.data.profile.get_profile_data(data["userId"], self.version) + + if profile is None: + return {"userId": data["userId"], "teamId": 0} + if profile and profile["teamId"]: # Get team by id - team = self.data.profile.get_team_by_id(profile["teamId"]) + team = await self.data.profile.get_team_by_id(profile["teamId"]) if team: team_id = team["id"] team_name = team["teamName"] - team_rank = self.data.profile.get_team_rank(team["id"]) - + team_rank = await self.data.profile.get_team_rank(team["id"]) + team_point = team["teamPoint"] + if team["userTeamPoint"] is not None and team["userTeamPoint"] != "": + user_team_point_data = json.loads(team["userTeamPoint"]) + for user_point_data in user_team_point_data: + if user_point_data["user"] == data["userId"]: + team_user_point = int(user_point_data["userPoint"]) # Don't return anything if no team name has been defined for defaults and there is no team set for the player if not profile["teamId"] and team_name == "": return {"userId": data["userId"], "teamId": 0} @@ -741,16 +751,17 @@ class ChuniBase: "teamId": team_id, "teamRank": team_rank, "teamName": team_name, + "assaultTimeRate": 1, # TODO: Figure out assaultTime, which might be team point boost? "userTeamPoint": { "userId": data["userId"], "teamId": team_id, - "orderId": 1, - "teamPoint": 1, + "orderId": 0, + "teamPoint": team_user_point, "aggrDate": data["playDate"], }, } - def handle_get_team_course_setting_api_request(self, data: Dict) -> Dict: + async def handle_get_team_course_setting_api_request(self, data: Dict) -> Dict: return { "userId": data["userId"], "length": 0, @@ -758,7 +769,7 @@ class ChuniBase: "teamCourseSettingList": [], } - def handle_get_team_course_setting_api_request_proto(self, data: Dict) -> Dict: + async def handle_get_team_course_setting_api_request_proto(self, data: Dict) -> Dict: return { "userId": data["userId"], "length": 1, @@ -782,7 +793,7 @@ class ChuniBase: ], } - def handle_get_team_course_rule_api_request(self, data: Dict) -> Dict: + async def handle_get_team_course_rule_api_request(self, data: Dict) -> Dict: return { "userId": data["userId"], "length": 0, @@ -790,7 +801,7 @@ class ChuniBase: "teamCourseRuleList": [] } - def handle_get_team_course_rule_api_request_proto(self, data: Dict) -> Dict: + async def handle_get_team_course_rule_api_request_proto(self, data: Dict) -> Dict: return { "userId": data["userId"], "length": 1, @@ -807,10 +818,16 @@ class ChuniBase: ], } - def handle_upsert_user_all_api_request(self, data: Dict) -> Dict: + async def handle_upsert_user_all_api_request(self, data: Dict) -> Dict: upsert = data["upsertUserAll"] user_id = data["userId"] + if int(user_id) & 0x1000000000001 == 0x1000000000001: + place_id = int(user_id) & 0xFFFC00000000 + + self.logger.info("Guest play from place ID %d, ignoring.", place_id) + return {"returnCode": "1"} + if "userData" in upsert: try: upsert["userData"][0]["userName"] = self.read_wtf8( @@ -819,58 +836,58 @@ class ChuniBase: except Exception: pass - self.data.profile.put_profile_data( + await self.data.profile.put_profile_data( user_id, self.version, upsert["userData"][0] ) if "userDataEx" in upsert: - self.data.profile.put_profile_data_ex( + await self.data.profile.put_profile_data_ex( user_id, self.version, upsert["userDataEx"][0] ) if "userGameOption" in upsert: - self.data.profile.put_profile_option(user_id, upsert["userGameOption"][0]) + await self.data.profile.put_profile_option(user_id, upsert["userGameOption"][0]) if "userGameOptionEx" in upsert: - self.data.profile.put_profile_option_ex( + await self.data.profile.put_profile_option_ex( user_id, upsert["userGameOptionEx"][0] ) if "userRecentRatingList" in upsert: - self.data.profile.put_profile_recent_rating( + await self.data.profile.put_profile_recent_rating( user_id, upsert["userRecentRatingList"] ) if "userCharacterList" in upsert: for character in upsert["userCharacterList"]: - self.data.item.put_character(user_id, character) + await self.data.item.put_character(user_id, character) if "userMapList" in upsert: for map in upsert["userMapList"]: - self.data.item.put_map(user_id, map) + await self.data.item.put_map(user_id, map) if "userCourseList" in upsert: for course in upsert["userCourseList"]: - self.data.score.put_course(user_id, course) + await self.data.score.put_course(user_id, course) if "userDuelList" in upsert: for duel in upsert["userDuelList"]: - self.data.item.put_duel(user_id, duel) + await self.data.item.put_duel(user_id, duel) if "userItemList" in upsert: for item in upsert["userItemList"]: - self.data.item.put_item(user_id, item) + await self.data.item.put_item(user_id, item) if "userActivityList" in upsert: for activity in upsert["userActivityList"]: - self.data.profile.put_profile_activity(user_id, activity) + await self.data.profile.put_profile_activity(user_id, activity) if "userChargeList" in upsert: for charge in upsert["userChargeList"]: - self.data.profile.put_profile_charge(user_id, charge) + await self.data.profile.put_profile_charge(user_id, charge) if "userMusicDetailList" in upsert: for song in upsert["userMusicDetailList"]: - self.data.score.put_score(user_id, song) + await self.data.score.put_score(user_id, song) if "userPlaylogList" in upsert: for playlog in upsert["userPlaylogList"]: @@ -881,7 +898,7 @@ class ChuniBase: playlog["playedUserName2"] = self.read_wtf8(playlog["playedUserName2"]) if playlog["playedUserName3"] is not None: playlog["playedUserName3"] = self.read_wtf8(playlog["playedUserName3"]) - self.data.score.put_playlog(user_id, playlog, self.version) + await self.data.score.put_playlog(user_id, playlog, self.version) if "userTeamPoint" in upsert: team_points = upsert["userTeamPoint"] @@ -889,7 +906,7 @@ class ChuniBase: for tp in team_points: if tp["teamId"] != '65535': # Fetch the current team data - current_team = self.data.profile.get_team_by_id(tp["teamId"]) + current_team = await self.data.profile.get_team_by_id(tp["teamId"]) # Calculate the new teamPoint new_team_point = int(tp["teamPoint"]) + current_team["teamPoint"] @@ -900,24 +917,24 @@ class ChuniBase: } # Update the team data - self.data.profile.update_team(tp["teamId"], team_data) + await self.data.profile.update_team(tp["teamId"], team_data) except: pass # Probably a better way to catch if the team is not set yet (new profiles), but let's just pass if "userMapAreaList" in upsert: for map_area in upsert["userMapAreaList"]: - self.data.item.put_map_area(user_id, map_area) + await self.data.item.put_map_area(user_id, map_area) if "userOverPowerList" in upsert: for overpower in upsert["userOverPowerList"]: - self.data.profile.put_profile_overpower(user_id, overpower) + await self.data.profile.put_profile_overpower(user_id, overpower) if "userEmoneyList" in upsert: for emoney in upsert["userEmoneyList"]: - self.data.profile.put_profile_emoney(user_id, emoney) + await self.data.profile.put_profile_emoney(user_id, emoney) if "userLoginBonusList" in upsert: for login in upsert["userLoginBonusList"]: - self.data.item.put_login_bonus( + await self.data.item.put_login_bonus( user_id, self.version, login["presetId"], isWatched=True ) @@ -925,31 +942,67 @@ class ChuniBase: for rp in upsert["userRecentPlayerList"]: pass + for rating_type in {"userRatingBaseList", "userRatingBaseHotList", "userRatingBaseNextList"}: + if rating_type not in upsert: + continue + + await self.data.profile.put_profile_rating( + user_id, + self.version, + rating_type, + upsert[rating_type], + ) + + # added in LUMINOUS + if "userCMissionList" in upsert: + for cmission in upsert["userCMissionList"]: + mission_id = cmission["missionId"] + + await self.data.item.put_cmission( + user_id, + { + "missionId": mission_id, + "point": cmission["point"], + }, + ) + + for progress in cmission["userCMissionProgressList"]: + await self.data.item.put_cmission_progress(user_id, mission_id, progress) + + if "userNetBattleData" in upsert: + net_battle = upsert["userNetBattleData"][0] + + # fix the boolean + net_battle["isRankUpChallengeFailed"] = ( + False if net_battle["isRankUpChallengeFailed"] == "false" else True + ) + await self.data.profile.put_net_battle(user_id, net_battle) + return {"returnCode": "1"} - def handle_upsert_user_chargelog_api_request(self, data: Dict) -> Dict: + async def handle_upsert_user_chargelog_api_request(self, data: Dict) -> Dict: # add tickets after they got bought, this makes sure the tickets are # still valid after an unsuccessful logout - self.data.profile.put_profile_charge(data["userId"], data["userCharge"]) + await self.data.profile.put_profile_charge(data["userId"], data["userCharge"]) return {"returnCode": "1"} - def handle_upsert_client_bookkeeping_api_request(self, data: Dict) -> Dict: + async def handle_upsert_client_bookkeeping_api_request(self, data: Dict) -> Dict: return {"returnCode": "1"} - def handle_upsert_client_develop_api_request(self, data: Dict) -> Dict: + async def handle_upsert_client_develop_api_request(self, data: Dict) -> Dict: return {"returnCode": "1"} - def handle_upsert_client_error_api_request(self, data: Dict) -> Dict: + async def handle_upsert_client_error_api_request(self, data: Dict) -> Dict: return {"returnCode": "1"} - def handle_upsert_client_setting_api_request(self, data: Dict) -> Dict: + async def handle_upsert_client_setting_api_request(self, data: Dict) -> Dict: return {"returnCode": "1"} - def handle_upsert_client_testmode_api_request(self, data: Dict) -> Dict: + async def handle_upsert_client_testmode_api_request(self, data: Dict) -> Dict: return {"returnCode": "1"} - def handle_get_user_net_battle_data_api_request(self, data: Dict) -> Dict: + async def handle_get_user_net_battle_data_api_request(self, data: Dict) -> Dict: return { "userId": data["userId"], "userNetBattleData": {"recentNBSelectMusicList": []}, - } \ No newline at end of file + } diff --git a/titles/chuni/const.py b/titles/chuni/const.py index 3e83378..003c618 100644 --- a/titles/chuni/const.py +++ b/titles/chuni/const.py @@ -1,3 +1,6 @@ +from enum import Enum + + class ChuniConstants: GAME_CODE = "SDBT" GAME_CODE_NEW = "SDHD" @@ -16,10 +19,13 @@ class ChuniConstants: VER_CHUNITHM_CRYSTAL = 8 VER_CHUNITHM_CRYSTAL_PLUS = 9 VER_CHUNITHM_PARADISE = 10 + VER_CHUNITHM_NEW = 11 VER_CHUNITHM_NEW_PLUS = 12 VER_CHUNITHM_SUN = 13 VER_CHUNITHM_SUN_PLUS = 14 + VER_CHUNITHM_LUMINOUS = 15 + VERSION_NAMES = [ "CHUNITHM", "CHUNITHM PLUS", @@ -35,9 +41,53 @@ class ChuniConstants: "CHUNITHM NEW!!", "CHUNITHM NEW PLUS!!", "CHUNITHM SUN", - "CHUNITHM SUN PLUS" + "CHUNITHM SUN PLUS", + "CHUNITHM LUMINOUS", + ] + + SCORE_RANK_INTERVALS_OLD = [ + (1007500, "SSS"), + (1000000, "SS"), + ( 975000, "S"), + ( 950000, "AAA"), + ( 925000, "AA"), + ( 900000, "A"), + ( 800000, "BBB"), + ( 700000, "BB"), + ( 600000, "B"), + ( 500000, "C"), + ( 0, "D"), + ] + + SCORE_RANK_INTERVALS_NEW = [ + (1009000, "SSS+"), # New only + (1007500, "SSS"), + (1005000, "SS+"), # New only + (1000000, "SS"), + ( 990000, "S+"), # New only + ( 975000, "S"), + ( 950000, "AAA"), + ( 925000, "AA"), + ( 900000, "A"), + ( 800000, "BBB"), + ( 700000, "BB"), + ( 600000, "B"), + ( 500000, "C"), + ( 0, "D"), ] @classmethod def game_ver_to_string(cls, ver: int): - return cls.VERSION_NAMES[ver] \ No newline at end of file + return cls.VERSION_NAMES[ver] + + +class MapAreaConditionType(Enum): + UNLOCKED = 0 + MAP_CLEARED = 1 + MAP_AREA_CLEARED = 2 + TROPHY_OBTAINED = 3 + + +class MapAreaConditionLogicalOperator(Enum): + AND = 1 + OR = 2 diff --git a/titles/chuni/crystal.py b/titles/chuni/crystal.py index a727ac3..9c08fd7 100644 --- a/titles/chuni/crystal.py +++ b/titles/chuni/crystal.py @@ -13,7 +13,7 @@ class ChuniCrystal(ChuniBase): super().__init__(core_cfg, game_cfg) self.version = ChuniConstants.VER_CHUNITHM_CRYSTAL - def handle_get_game_setting_api_request(self, data: Dict) -> Dict: - ret = super().handle_get_game_setting_api_request(data) + async def handle_get_game_setting_api_request(self, data: Dict) -> Dict: + ret = await super().handle_get_game_setting_api_request(data) ret["gameSetting"]["dataVersion"] = "1.40.00" return ret diff --git a/titles/chuni/crystalplus.py b/titles/chuni/crystalplus.py index fbb3969..90a0479 100644 --- a/titles/chuni/crystalplus.py +++ b/titles/chuni/crystalplus.py @@ -13,7 +13,7 @@ class ChuniCrystalPlus(ChuniBase): super().__init__(core_cfg, game_cfg) self.version = ChuniConstants.VER_CHUNITHM_CRYSTAL_PLUS - def handle_get_game_setting_api_request(self, data: Dict) -> Dict: - ret = super().handle_get_game_setting_api_request(data) + async def handle_get_game_setting_api_request(self, data: Dict) -> Dict: + ret = await super().handle_get_game_setting_api_request(data) ret["gameSetting"]["dataVersion"] = "1.45.00" return ret diff --git a/titles/chuni/frontend.py b/titles/chuni/frontend.py new file mode 100644 index 0000000..74f7794 --- /dev/null +++ b/titles/chuni/frontend.py @@ -0,0 +1,299 @@ +from typing import List +from starlette.routing import Route, Mount +from starlette.requests import Request +from starlette.responses import Response, RedirectResponse +from os import path +import yaml +import jinja2 + +from core.frontend import FE_Base, UserSession +from core.config import CoreConfig +from .database import ChuniData +from .config import ChuniConfig +from .const import ChuniConstants + + +def pairwise(iterable): + # https://docs.python.org/3/library/itertools.html#itertools.pairwise + # but for Python < 3.10. pairwise('ABCDEFG') → AB BC CD DE EF FG + iterator = iter(iterable) + a = next(iterator, None) + for b in iterator: + yield a, b + a = b + + +def calculate_song_rank(score: int, game_version: int) -> str: + if game_version >= ChuniConstants.VER_CHUNITHM_NEW: + intervals = ChuniConstants.SCORE_RANK_INTERVALS_NEW + else: + intervals = ChuniConstants.SCORE_RANK_INTERVALS_OLD + + for (min_score, rank) in intervals: + if score >= min_score: + return rank + + return "D" + + +def calculate_song_rating(score: int, chart_constant: float, game_version: int) -> float: + is_new = game_version >= ChuniConstants.VER_CHUNITHM_NEW + + if is_new: # New and later + max_score = 1009000 + max_rating_modifier = 2.15 + else: # Up to Paradise Lost + max_score = 1007500 + max_rating_modifier = 2.0 + + if (score < 500000): + return 0.0 # D + elif (score >= max_score): + return chart_constant + max_rating_modifier # SSS/SSS+ + + # Okay, we're doing this the hard way. + # Rating goes up linearly between breakpoints listed below. + # Pick the score interval in which we are in, then calculate + # the position between possible ratings. + score_intervals = [ + ( 500000, 0.0), # C + ( 800000, max(0.0, (chart_constant - 5.0) / 2)), # BBB + ( 900000, max(0.0, (chart_constant - 5.0))), # A + ( 925000, max(0.0, (chart_constant - 3.0))), # AA + ( 975000, chart_constant), # S + (1000000, chart_constant + 1.0), # SS + (1005000, chart_constant + 1.5), # SS+ + (1007500, chart_constant + 2.0), # SSS + (1009000, chart_constant + max_rating_modifier), # SSS+! + ] + + for ((lo_score, lo_rating), (hi_score, hi_rating)) in pairwise(score_intervals): + if not (lo_score <= score < hi_score): + continue + + interval_pos = (score - lo_score) / (hi_score - lo_score) + return lo_rating + ((hi_rating - lo_rating) * interval_pos) + + + +class ChuniFrontend(FE_Base): + def __init__( + self, cfg: CoreConfig, environment: jinja2.Environment, cfg_dir: str + ) -> None: + super().__init__(cfg, environment) + self.data = ChuniData(cfg) + self.game_cfg = ChuniConfig() + if path.exists(f"{cfg_dir}/{ChuniConstants.CONFIG_NAME}"): + self.game_cfg.update( + yaml.safe_load(open(f"{cfg_dir}/{ChuniConstants.CONFIG_NAME}")) + ) + self.nav_name = "Chunithm" + + def get_routes(self) -> List[Route]: + return [ + Route("/", self.render_GET, methods=['GET']), + Route("/rating", self.render_GET_rating, methods=['GET']), + Mount("/playlog", routes=[ + Route("/", self.render_GET_playlog, methods=['GET']), + Route("/{index}", self.render_GET_playlog, methods=['GET']), + ]), + Route("/update.name", self.update_name, methods=['POST']), + Route("/version.change", self.version_change, methods=['POST']), + ] + + async def render_GET(self, request: Request) -> bytes: + template = self.environment.get_template( + "titles/chuni/templates/chuni_index.jinja" + ) + usr_sesh = self.validate_session(request) + if not usr_sesh: + usr_sesh = UserSession() + + if usr_sesh.user_id > 0: + versions = await self.data.profile.get_all_profile_versions(usr_sesh.user_id) + profile = [] + if versions: + # chunithm_version is -1 means it is not initialized yet, select a default version from existing. + if usr_sesh.chunithm_version < 0: + usr_sesh.chunithm_version = versions[0] + profile = await self.data.profile.get_profile_data(usr_sesh.user_id, usr_sesh.chunithm_version) + + resp = Response(template.render( + title=f"{self.core_config.server.name} | {self.nav_name}", + game_list=self.environment.globals["game_list"], + sesh=vars(usr_sesh), + user_id=usr_sesh.user_id, + profile=profile, + version_list=ChuniConstants.VERSION_NAMES, + versions=versions, + cur_version=usr_sesh.chunithm_version + ), media_type="text/html; charset=utf-8") + + if usr_sesh.chunithm_version >= 0: + encoded_sesh = self.encode_session(usr_sesh) + resp.set_cookie("ARTEMIS_SESH", encoded_sesh) + return resp + + else: + return RedirectResponse("/gate/", 303) + + async def render_GET_rating(self, request: Request) -> bytes: + template = self.environment.get_template( + "titles/chuni/templates/chuni_rating.jinja" + ) + usr_sesh = self.validate_session(request) + if not usr_sesh: + usr_sesh = UserSession() + + if usr_sesh.user_id > 0: + if usr_sesh.chunithm_version < 0: + return RedirectResponse("/game/chuni/", 303) + profile = await self.data.profile.get_profile_data(usr_sesh.user_id, usr_sesh.chunithm_version) + rating = await self.data.profile.get_profile_rating(usr_sesh.user_id, usr_sesh.chunithm_version) + hot_list=[] + base_list=[] + if profile and rating: + song_records = [] + + for song in rating: + music_chart = await self.data.static.get_music_chart(usr_sesh.chunithm_version, song.musicId, song.difficultId) + if not music_chart: + continue + + rank = calculate_song_rank(song.score, profile.version) + rating = calculate_song_rating(song.score, music_chart.level, profile.version) + + song_rating = int(rating * 10 ** 2) / 10 ** 2 + song_records.append({ + "difficultId": song.difficultId, + "musicId": song.musicId, + "title": music_chart.title, + "level": music_chart.level, + "score": song.score, + "type": song.type, + "rank": rank, + "song_rating": song_rating, + }) + + hot_list = [obj for obj in song_records if obj["type"] == "userRatingBaseHotList"] + base_list = [obj for obj in song_records if obj["type"] == "userRatingBaseList"] + return Response(template.render( + title=f"{self.core_config.server.name} | {self.nav_name}", + game_list=self.environment.globals["game_list"], + sesh=vars(usr_sesh), + profile=profile, + hot_list=hot_list, + base_list=base_list, + ), media_type="text/html; charset=utf-8") + else: + return RedirectResponse("/gate/", 303) + + async def render_GET_playlog(self, request: Request) -> bytes: + template = self.environment.get_template( + "titles/chuni/templates/chuni_playlog.jinja" + ) + usr_sesh = self.validate_session(request) + if not usr_sesh: + usr_sesh = UserSession() + + if usr_sesh.user_id > 0: + if usr_sesh.chunithm_version < 0: + return RedirectResponse("/game/chuni/", 303) + path_index = request.path_params.get('index') + if not path_index or int(path_index) < 1: + index = 0 + else: + index = int(path_index) - 1 # 0 and 1 are 1st page + user_id = usr_sesh.user_id + playlog_count = await self.data.score.get_user_playlogs_count(user_id) + if playlog_count < index * 20 : + return Response(template.render( + title=f"{self.core_config.server.name} | {self.nav_name}", + game_list=self.environment.globals["game_list"], + sesh=vars(usr_sesh), + playlog_count=0 + ), media_type="text/html; charset=utf-8") + playlog = await self.data.score.get_playlogs_limited(user_id, index, 20) + playlog_with_title = [] + for record in playlog: + music_chart = await self.data.static.get_music_chart(usr_sesh.chunithm_version, record.musicId, record.level) + if music_chart: + difficultyNum=music_chart.level + artist=music_chart.artist + title=music_chart.title + else: + difficultyNum=0 + artist="unknown" + title="musicid: " + str(record.musicId) + playlog_with_title.append({ + "raw": record, + "title": title, + "difficultyNum": difficultyNum, + "artist": artist, + }) + return Response(template.render( + title=f"{self.core_config.server.name} | {self.nav_name}", + game_list=self.environment.globals["game_list"], + sesh=vars(usr_sesh), + user_id=usr_sesh.user_id, + playlog=playlog_with_title, + playlog_count=playlog_count + ), media_type="text/html; charset=utf-8") + else: + return RedirectResponse("/gate/", 303) + + async def update_name(self, request: Request) -> bytes: + usr_sesh = self.validate_session(request) + if not usr_sesh: + return RedirectResponse("/gate/", 303) + + form_data = await request.form() + new_name: str = form_data.get("new_name") + new_name_full = "" + + if not new_name: + return RedirectResponse("/gate/?e=4", 303) + + if len(new_name) > 8: + return RedirectResponse("/gate/?e=8", 303) + + for x in new_name: # FIXME: This will let some invalid characters through atm + o = ord(x) + try: + if o == 0x20: + new_name_full += chr(0x3000) + elif o < 0x7F and o > 0x20: + new_name_full += chr(o + 0xFEE0) + elif o <= 0x7F: + self.logger.warn(f"Invalid ascii character {o:02X}") + return RedirectResponse("/gate/?e=4", 303) + else: + new_name_full += x + + except Exception as e: + self.logger.error(f"Something went wrong parsing character {o:04X} - {e}") + return RedirectResponse("/gate/?e=4", 303) + + if not await self.data.profile.update_name(usr_sesh.user_id, new_name_full): + return RedirectResponse("/gate/?e=999", 303) + + return RedirectResponse("/game/chuni/?s=1", 303) + + async def version_change(self, request: Request): + usr_sesh = self.validate_session(request) + if not usr_sesh: + usr_sesh = UserSession() + + if usr_sesh.user_id > 0: + form_data = await request.form() + chunithm_version = form_data.get("version") + self.logger.debug(f"version change to: {chunithm_version}") + if(chunithm_version.isdigit()): + usr_sesh.chunithm_version=int(chunithm_version) + encoded_sesh = self.encode_session(usr_sesh) + self.logger.debug(f"Created session with JWT {encoded_sesh}") + resp = RedirectResponse("/game/chuni/", 303) + resp.set_cookie("ARTEMIS_SESH", encoded_sesh) + return resp + else: + return RedirectResponse("/gate/", 303) diff --git a/titles/chuni/index.py b/titles/chuni/index.py index fa8a394..144f770 100644 --- a/titles/chuni/index.py +++ b/titles/chuni/index.py @@ -1,5 +1,8 @@ -from twisted.web.http import Request -import logging, coloredlogs +from starlette.requests import Request +from starlette.routing import Route +from starlette.responses import Response +import logging +import coloredlogs from logging.handlers import TimedRotatingFileHandler import zlib import yaml @@ -32,13 +35,13 @@ from .new import ChuniNew from .newplus import ChuniNewPlus from .sun import ChuniSun from .sunplus import ChuniSunPlus - +from .luminous import ChuniLuminous class ChuniServlet(BaseServlet): def __init__(self, core_cfg: CoreConfig, cfg_dir: str) -> None: super().__init__(core_cfg, cfg_dir) self.game_cfg = ChuniConfig() - self.hash_table: Dict[Dict[str, str]] = {} + self.hash_table: Dict[str, Dict[str, str]] = {} if path.exists(f"{cfg_dir}/{ChuniConstants.CONFIG_NAME}"): self.game_cfg.update( yaml.safe_load(open(f"{cfg_dir}/{ChuniConstants.CONFIG_NAME}")) @@ -60,6 +63,7 @@ class ChuniServlet(BaseServlet): ChuniNewPlus, ChuniSun, ChuniSunPlus, + ChuniLuminous, ] self.logger = logging.getLogger("chuni") @@ -88,30 +92,65 @@ class ChuniServlet(BaseServlet): ) self.logger.inited = True + known_iter_counts = { + ChuniConstants.VER_CHUNITHM_CRYSTAL_PLUS: 67, + f"{ChuniConstants.VER_CHUNITHM_CRYSTAL_PLUS}_int": 25, # SUPERSTAR + ChuniConstants.VER_CHUNITHM_PARADISE: 44, + f"{ChuniConstants.VER_CHUNITHM_PARADISE}_int": 51, # SUPERSTAR PLUS + ChuniConstants.VER_CHUNITHM_NEW: 54, + f"{ChuniConstants.VER_CHUNITHM_NEW}_int": 49, + ChuniConstants.VER_CHUNITHM_NEW_PLUS: 25, + f"{ChuniConstants.VER_CHUNITHM_NEW_PLUS}_int": 31, + ChuniConstants.VER_CHUNITHM_SUN: 70, + f"{ChuniConstants.VER_CHUNITHM_SUN}_int": 35, + ChuniConstants.VER_CHUNITHM_SUN_PLUS: 36, + f"{ChuniConstants.VER_CHUNITHM_SUN_PLUS}_int": 36, + ChuniConstants.VER_CHUNITHM_LUMINOUS: 8, + f"{ChuniConstants.VER_CHUNITHM_LUMINOUS}_int": 8, + } + for version, keys in self.game_cfg.crypto.keys.items(): if len(keys) < 3: continue - self.hash_table[version] = {} + if isinstance(version, int): + version_idx = version + else: + version_idx = int(version.split("_")[0]) + + salt = bytes.fromhex(keys[2]) + if len(keys) >= 4: + iter_count = keys[3] + elif (iter_count := known_iter_counts.get(version)) is None: + self.logger.error( + "Number of iteration rounds for version %s is not known, but it is not specified in the config", + version, + ) + continue + + self.hash_table[version] = {} method_list = [ method - for method in dir(self.versions[version]) + for method in dir(self.versions[version_idx]) if not method.startswith("__") ] + for method in method_list: method_fixed = inflection.camelize(method)[6:-7] - # number of iterations was changed to 70 in SUN and then to 36 - if version == ChuniConstants.VER_CHUNITHM_SUN_PLUS: - iter_count = 36 - elif version == ChuniConstants.VER_CHUNITHM_SUN: - iter_count = 70 - else: - iter_count = 44 + + # This only applies for CHUNITHM NEW International and later for some reason. + # CHUNITHM SUPERSTAR (PLUS) did not add "Exp" to the endpoint when hashing. + if ( + isinstance(version, str) + and version.endswith("_int") + and version_idx >= ChuniConstants.VER_CHUNITHM_NEW + ): + method_fixed += "C3Exp" hash = PBKDF2( method_fixed, - bytes.fromhex(keys[2]), + salt, 128, count=iter_count, hmac_hash_module=SHA1, @@ -121,18 +160,9 @@ class ChuniServlet(BaseServlet): self.hash_table[version][hashed_name] = method_fixed self.logger.debug( - f"Hashed v{version} method {method_fixed} with {bytes.fromhex(keys[2])} to get {hash.hex()}" + f"Hashed v{version} method {method_fixed} with {salt} to get {hashed_name}" ) - def get_endpoint_matchers(self) -> Tuple[List[Tuple[str, str, Dict]], List[Tuple[str, str, Dict]]]: - return ( - [], - [ - ("render_POST", "/{game}/{version}/ChuniServlet/{endpoint}", {}), - ("render_POST", "/{game}/{version}/ChuniServlet/MatchingServer/{endpoint}", {}) - ] - ) - @classmethod def is_game_enabled( cls, game_code: str, core_cfg: CoreConfig, cfg_dir: str @@ -150,19 +180,25 @@ class ChuniServlet(BaseServlet): def get_allnet_info(self, game_code: str, game_ver: int, keychip: str) -> Tuple[str, str]: if not self.core_cfg.server.is_using_proxy and Utils.get_title_port(self.core_cfg) != 80: - return (f"http://{self.core_cfg.title.hostname}:{Utils.get_title_port(self.core_cfg)}/{game_code}/{game_ver}/", self.core_cfg.title.hostname) + return (f"http://{self.core_cfg.server.hostname}:{Utils.get_title_port(self.core_cfg)}/{game_code}/{game_ver}/", self.core_cfg.server.hostname) - return (f"http://{self.core_cfg.title.hostname}/{game_code}/{game_ver}/", self.core_cfg.title.hostname) + return (f"http://{self.core_cfg.server.hostname}/{game_code}/{game_ver}/", self.core_cfg.server.hostname) - def render_POST(self, request: Request, game_code: str, matchers: Dict) -> bytes: - endpoint = matchers['endpoint'] - version = int(matchers['version']) - game_code = matchers['game'] + def get_routes(self) -> List[Route]: + return [ + Route("/{game:str}/{version:int}/ChuniServlet/{endpoint:str}", self.render_POST, methods=['POST']), + Route("/{game:str}/{version:int}/ChuniServlet/MatchingServer/{endpoint:str}", self.render_POST, methods=['POST']), + ] + + async def render_POST(self, request: Request) -> bytes: + endpoint: str = request.path_params.get('endpoint') + version: int = request.path_params.get('version') + game_code: str = request.path_params.get('game') if endpoint.lower() == "ping": - return zlib.compress(b'{"returnCode": "1"}') + return Response(zlib.compress(b'{"returnCode": "1"}')) - req_raw = request.content.getvalue() + req_raw = await request.body() encrtped = False internal_ver = 0 @@ -197,47 +233,61 @@ class ChuniServlet(BaseServlet): internal_ver = ChuniConstants.VER_CHUNITHM_NEW_PLUS elif version >= 210 and version < 215: # SUN internal_ver = ChuniConstants.VER_CHUNITHM_SUN - elif version >= 215: # SUN + elif version >= 215 and version < 220: # SUN PLUS internal_ver = ChuniConstants.VER_CHUNITHM_SUN_PLUS + elif version >= 220: # LUMINOUS + internal_ver = ChuniConstants.VER_CHUNITHM_LUMINOUS elif game_code == "SDGS": # Int - if version < 110: # SUPERSTAR - internal_ver = ChuniConstants.PARADISE + if version < 105: # SUPERSTAR + internal_ver = ChuniConstants.VER_CHUNITHM_CRYSTAL_PLUS + elif version >= 105 and version < 110: # SUPERSTAR PLUS *Cursed but needed due to different encryption key + internal_ver = ChuniConstants.VER_CHUNITHM_PARADISE elif version >= 110 and version < 115: # NEW internal_ver = ChuniConstants.VER_CHUNITHM_NEW elif version >= 115 and version < 120: # NEW PLUS!! internal_ver = ChuniConstants.VER_CHUNITHM_NEW_PLUS elif version >= 120 and version < 125: # SUN internal_ver = ChuniConstants.VER_CHUNITHM_SUN - elif version >= 125: # SUN PLUS + elif version >= 125 and version < 130: # SUN PLUS internal_ver = ChuniConstants.VER_CHUNITHM_SUN_PLUS + elif version >= 130: # LUMINOUS + internal_ver = ChuniConstants.VER_CHUNITHM_LUMINOUS if all(c in string.hexdigits for c in endpoint) and len(endpoint) == 32: # If we get a 32 character long hex string, it's a hash and we're - # doing encrypted. The likelyhood of false positives is low but + # doing encrypted. The likelihood of false positives is low but # technically not 0 + + if game_code == "SDGS": + crypto_cfg_key = f"{internal_ver}_int" + hash_table_key = f"{internal_ver}_int" + else: + crypto_cfg_key = internal_ver + hash_table_key = internal_ver + if internal_ver < ChuniConstants.VER_CHUNITHM_NEW: - endpoint = request.getHeader("User-Agent").split("#")[0] + endpoint = request.headers.get("User-Agent").split("#")[0] else: - if internal_ver not in self.hash_table: + if hash_table_key not in self.hash_table: self.logger.error( f"v{version} does not support encryption or no keys entered" ) - return zlib.compress(b'{"stat": "0"}') + return Response(zlib.compress(b'{"stat": "0"}')) - elif endpoint.lower() not in self.hash_table[internal_ver]: + elif endpoint.lower() not in self.hash_table[hash_table_key]: self.logger.error( f"No hash found for v{version} endpoint {endpoint}" ) - return zlib.compress(b'{"stat": "0"}') + return Response(zlib.compress(b'{"stat": "0"}')) - endpoint = self.hash_table[internal_ver][endpoint.lower()] + endpoint = self.hash_table[hash_table_key][endpoint.lower()] try: crypt = AES.new( - bytes.fromhex(self.game_cfg.crypto.keys[internal_ver][0]), + bytes.fromhex(self.game_cfg.crypto.keys[crypto_cfg_key][0]), AES.MODE_CBC, - bytes.fromhex(self.game_cfg.crypto.keys[internal_ver][1]), + bytes.fromhex(self.game_cfg.crypto.keys[crypto_cfg_key][1]), ) req_raw = crypt.decrypt(req_raw) @@ -246,7 +296,7 @@ class ChuniServlet(BaseServlet): self.logger.error( f"Failed to decrypt v{version} request to {endpoint} -> {e}" ) - return zlib.compress(b'{"stat": "0"}') + return Response(zlib.compress(b'{"stat": "0"}')) encrtped = True @@ -258,7 +308,7 @@ class ChuniServlet(BaseServlet): self.logger.error( f"Unencrypted v{version} {endpoint} request, but config is set to encrypted only: {req_raw}" ) - return zlib.compress(b'{"stat": "0"}') + return Response(zlib.compress(b'{"stat": "0"}')) try: unzip = zlib.decompress(req_raw) @@ -267,14 +317,20 @@ class ChuniServlet(BaseServlet): self.logger.error( f"Failed to decompress v{version} {endpoint} request -> {e}" ) - return b"" + return Response(zlib.compress(b'{"stat": "0"}')) req_data = json.loads(unzip) self.logger.info(f"v{version} {endpoint} request from {client_ip}") self.logger.debug(req_data) - endpoint = endpoint.replace("C3Exp", "") if game_code == "SDGS" else endpoint + if game_code == "SDGS" and version >= 110: + endpoint = endpoint.replace("C3Exp", "") + elif game_code == "SDGS" and version < 110: + endpoint = endpoint.replace("Exp", "") + else: + endpoint = endpoint + func_to_find = "handle_" + inflection.underscore(endpoint) + "_request" handler_cls = self.versions[internal_ver](self.core_cfg, self.game_cfg) @@ -285,13 +341,13 @@ class ChuniServlet(BaseServlet): else: try: handler = getattr(handler_cls, func_to_find) - resp = handler(req_data) + resp = await handler(req_data) except Exception as e: self.logger.error(f"Error handling v{version} method {endpoint} - {e}") - return zlib.compress(b'{"stat": "0"}') + return Response(zlib.compress(b'{"stat": "0"}')) - if resp == None: + if resp is None: resp = {"returnCode": 1} self.logger.debug(f"Response {resp}") @@ -299,14 +355,14 @@ class ChuniServlet(BaseServlet): zipped = zlib.compress(json.dumps(resp, ensure_ascii=False).encode("utf-8")) if not encrtped: - return zipped + return Response(zipped) padded = pad(zipped, 16) crypt = AES.new( - bytes.fromhex(self.game_cfg.crypto.keys[internal_ver][0]), + bytes.fromhex(self.game_cfg.crypto.keys[crypto_cfg_key][0]), AES.MODE_CBC, - bytes.fromhex(self.game_cfg.crypto.keys[internal_ver][1]), + bytes.fromhex(self.game_cfg.crypto.keys[crypto_cfg_key][1]), ) - return crypt.encrypt(padded) \ No newline at end of file + return Response(crypt.encrypt(padded)) diff --git a/titles/chuni/luminous.py b/titles/chuni/luminous.py new file mode 100644 index 0000000..8f02820 --- /dev/null +++ b/titles/chuni/luminous.py @@ -0,0 +1,298 @@ +from datetime import timedelta +from typing import Dict + +from core.config import CoreConfig +from titles.chuni.sunplus import ChuniSunPlus +from titles.chuni.const import ChuniConstants, MapAreaConditionLogicalOperator, MapAreaConditionType +from titles.chuni.config import ChuniConfig + + +class ChuniLuminous(ChuniSunPlus): + def __init__(self, core_cfg: CoreConfig, game_cfg: ChuniConfig) -> None: + super().__init__(core_cfg, game_cfg) + self.version = ChuniConstants.VER_CHUNITHM_LUMINOUS + + async def handle_cm_get_user_preview_api_request(self, data: Dict) -> Dict: + user_data = await super().handle_cm_get_user_preview_api_request(data) + + # Does CARD MAKER 1.35 work this far up? + user_data["lastDataVersion"] = "2.20.00" + return user_data + + async def handle_get_user_c_mission_api_request(self, data: Dict) -> Dict: + user_id = data["userId"] + mission_id = data["missionId"] + + progress_list = [] + point = 0 + + mission_data = await self.data.item.get_cmission(user_id, mission_id) + progress_data = await self.data.item.get_cmission_progress(user_id, mission_id) + + if mission_data and progress_data: + point = mission_data["point"] + + for progress in progress_data: + progress_list.append( + { + "order": progress["order"], + "stage": progress["stage"], + "progress": progress["progress"], + } + ) + + return { + "userId": user_id, + "missionId": mission_id, + "point": point, + "userCMissionProgressList": progress_list, + } + + async def handle_get_user_net_battle_ranking_info_api_request(self, data: Dict) -> Dict: + user_id = data["userId"] + + net_battle = {} + net_battle_data = await self.data.profile.get_net_battle(user_id) + + if net_battle_data: + net_battle = { + "isRankUpChallengeFailed": net_battle_data["isRankUpChallengeFailed"], + "highestBattleRankId": net_battle_data["highestBattleRankId"], + "battleIconId": net_battle_data["battleIconId"], + "battleIconNum": net_battle_data["battleIconNum"], + "avatarEffectPoint": net_battle_data["avatarEffectPoint"], + } + + return { + "userId": user_id, + "userNetBattleData": net_battle, + } + + async def handle_get_game_map_area_condition_api_request(self, data: Dict) -> Dict: + # There is no game data for this, everything is server side. + # However, we can selectively show/hide events as data is imported into the server. + events = await self.data.static.get_enabled_events(self.version) + event_by_id = {evt["eventId"]: evt for evt in events} + conditions = [] + + # The Mystic Rainbow of LUMINOUS map unlocks when any mainline LUMINOUS area + # (ep. I, ep. II, ep. III) are completed. + mystic_area_1_conditions = { + "mapAreaId": 3229301, # Mystic Rainbow of LUMINOUS Area 1 + "length": 0, + "mapAreaConditionList": [], + } + mystic_area_1_added = False + + # Secret AREA: MUSIC GAME + if 14029 in event_by_id: + start_date = event_by_id[14029]["startDate"].strftime(self.date_time_format) + mission_in_progress_end_date = "2099-12-31 00:00:00.0" + + # The "MISSION in progress" trophy required to trigger the secret area + # is only available in the first CHUNITHM mission. If the second mission + # (event ID 14214) was imported into ARTEMiS, we disable the requirement + # for this trophy. + if 14214 in event_by_id: + mission_in_progress_end_date = (event_by_id[14214]["startDate"] - timedelta(hours=2)).strftime(self.date_time_format) + + conditions.extend([ + { + "mapAreaId": 2206201, # BlythE ULTIMA + "length": 1, + # Obtain the trophy "MISSION in progress". + "mapAreaConditionList": [ + { + "type": MapAreaConditionType.TROPHY_OBTAINED.value, + "conditionId": 6832, + "logicalOpe": MapAreaConditionLogicalOperator.AND.value, + "startDate": start_date, + "endDate": mission_in_progress_end_date, + } + ], + }, + { + "mapAreaId": 2206202, # PRIVATE SERVICE ULTIMA + "length": 1, + # Obtain the trophy "MISSION in progress". + "mapAreaConditionList": [ + { + "type": MapAreaConditionType.TROPHY_OBTAINED.value, + "conditionId": 6832, + "logicalOpe": MapAreaConditionLogicalOperator.AND.value, + "startDate": start_date, + "endDate": mission_in_progress_end_date, + } + ], + }, + { + "mapAreaId": 2206203, # New York Back Raise + "length": 1, + # SS NightTheater's EXPERT chart and get the title + # "今宵、劇場に映し出される景色とは――――。" + "mapAreaConditionList": [ + { + "type": MapAreaConditionType.TROPHY_OBTAINED.value, + "conditionId": 6833, + "logicalOpe": MapAreaConditionLogicalOperator.AND.value, + "startDate": start_date, + "endDate": "2099-12-31 00:00:00.0", + }, + ], + }, + { + "mapAreaId": 2206204, # Spasmodic + "length": 2, + # - Get 1 miss on Random (any difficulty) and get the title "当たり待ち" + # - Get 1 miss on 花たちに希望を (any difficulty) and get the title "花たちに希望を" + "mapAreaConditionList": [ + { + "type": MapAreaConditionType.TROPHY_OBTAINED.value, + "conditionId": 6834, + "logicalOpe": MapAreaConditionLogicalOperator.AND.value, + "startDate": start_date, + "endDate": "2099-12-31 00:00:00.0", + }, + { + "type": MapAreaConditionType.TROPHY_OBTAINED.value, + "conditionId": 6835, + "logicalOpe": MapAreaConditionLogicalOperator.AND.value, + "startDate": start_date, + "endDate": "2099-12-31 00:00:00.0", + }, + ], + }, + { + "mapAreaId": 2206205, # ΩΩPARTS + "length": 2, + # - S Sage EXPERT to get the title "マターリ進行キボンヌ" + # - Equip this title and play cab-to-cab with another person with this title + # to get "マターリしようよ". Disabled because it is difficult to play cab2cab + # on data setups. A network operator may consider re-enabling it by uncommenting + # the second condition. + "mapAreaConditionList": [ + { + "type": MapAreaConditionType.TROPHY_OBTAINED.value, + "conditionId": 6836, + "logicalOpe": MapAreaConditionLogicalOperator.AND.value, + "startDate": start_date, + "endDate": "2099-12-31 00:00:00.0", + }, + # { + # "type": MapAreaConditionType.TROPHY_OBTAINED.value, + # "conditionId": 6837, + # "logicalOpe": MapAreaConditionLogicalOperator.AND.value, + # "startDate": start_date, + # "endDate": "2099-12-31 00:00:00.0", + # }, + ], + }, + { + "mapAreaId": 2206206, # Blow My Mind + "length": 1, + # SS on CHAOS EXPERT, Hydra EXPERT, Surive EXPERT and Jakarta PROGRESSION EXPERT + # to get the title "Can you hear me?" + "mapAreaConditionList": [ + { + "type": MapAreaConditionType.TROPHY_OBTAINED.value, + "conditionId": 6838, + "logicalOpe": MapAreaConditionLogicalOperator.AND.value, + "startDate": start_date, + "endDate": "2099-12-31 00:00:00.0", + }, + ], + }, + { + "mapAreaId": 2206207, # VALLIS-NERIA + "length": 6, + # Finish the 6 other areas + "mapAreaConditionList": [ + { + "type": MapAreaConditionType.MAP_AREA_CLEARED.value, + "conditionId": x, + "logicalOpe": MapAreaConditionLogicalOperator.AND.value, + "startDate": start_date, + "endDate": "2099-12-31 00:00:00.0", + } + for x in range(2206201, 2206207) + ], + }, + ]) + + # LUMINOUS ep. I + if 14005 in event_by_id: + start_date = event_by_id[14005]["startDate"].strftime(self.date_time_format) + + if not mystic_area_1_added: + conditions.append(mystic_area_1_conditions) + mystic_area_1_added = True + + mystic_area_1_conditions["length"] += 1 + mystic_area_1_conditions["mapAreaConditionList"].append( + { + "type": MapAreaConditionType.MAP_CLEARED.value, + "conditionId": 3020701, + "logicalOpe": MapAreaConditionLogicalOperator.OR.value, + "startDate": start_date, + "endDate": "2099-12-31 00:00:00.0", + } + ) + + conditions.append( + { + "mapAreaId": 3229302, # Mystic Rainbow of LUMINOUS Area 2, + "length": 1, + # Unlocks when LUMINOUS ep. I is completed. + "mapAreaConditionList": [ + { + "type": MapAreaConditionType.MAP_CLEARED.value, + "conditionId": 3020701, + "logicalOpe": MapAreaConditionLogicalOperator.AND.value, + "startDate": start_date, + "endDate": "2099-12-31 00:00:00.0", + }, + ], + } + ) + + # LUMINOUS ep. II + if 14251 in event_by_id: + start_date = event_by_id[14251]["startDate"].strftime(self.date_time_format) + + if not mystic_area_1_added: + conditions.append(mystic_area_1_conditions) + mystic_area_1_added = True + + mystic_area_1_conditions["length"] += 1 + mystic_area_1_conditions["mapAreaConditionList"].append( + { + "type": MapAreaConditionType.MAP_CLEARED.value, + "conditionId": 3020702, + "logicalOpe": MapAreaConditionLogicalOperator.OR.value, + "startDate": start_date, + "endDate": "2099-12-31 00:00:00.0", + } + ) + + conditions.append( + { + "mapAreaId": 3229303, # Mystic Rainbow of LUMINOUS Area 3, + "length": 1, + # Unlocks when LUMINOUS ep. II is completed. + "mapAreaConditionList": [ + { + "type": MapAreaConditionType.MAP_CLEARED.value, + "conditionId": 3020702, + "logicalOpe": MapAreaConditionLogicalOperator.AND.value, + "startDate": start_date, + "endDate": "2099-12-31 00:00:00.0", + }, + ], + } + ) + + + return { + "length": len(conditions), + "gameMapAreaConditionList": conditions, + } diff --git a/titles/chuni/new.py b/titles/chuni/new.py index d8a71b1..2275a6e 100644 --- a/titles/chuni/new.py +++ b/titles/chuni/new.py @@ -32,8 +32,10 @@ class ChuniNew(ChuniBase): return "210" if self.version == ChuniConstants.VER_CHUNITHM_SUN_PLUS: return "215" + if self.version == ChuniConstants.VER_CHUNITHM_LUMINOUS: + return "220" - def handle_get_game_setting_api_request(self, data: Dict) -> Dict: + async def handle_get_game_setting_api_request(self, data: Dict) -> Dict: # use UTC time and convert it to JST time by adding +9 # matching therefore starts one hour before and lasts for 8 hours match_start = datetime.strftime( @@ -82,27 +84,27 @@ class ChuniNew(ChuniBase): "matchErrorLimit": self.game_cfg.matching.match_error_limit, "romVersion": self.game_cfg.version.version(self.version)["rom"], "dataVersion": self.game_cfg.version.version(self.version)["data"], - "matchingUri": f"http://{self.core_cfg.title.hostname}:{t_port}/SDHD/{self._interal_ver_to_intver()}/ChuniServlet/", - "matchingUriX": f"http://{self.core_cfg.title.hostname}:{t_port}/SDHD/{self._interal_ver_to_intver()}/ChuniServlet/", + "matchingUri": f"http://{self.core_cfg.server.hostname}:{t_port}/SDHD/{self._interal_ver_to_intver()}/ChuniServlet/", + "matchingUriX": f"http://{self.core_cfg.server.hostname}:{t_port}/SDHD/{self._interal_ver_to_intver()}/ChuniServlet/", # might be really important for online battle to connect the cabs via UDP port 50201 - "udpHolePunchUri": f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDHD/{self._interal_ver_to_intver()}/ChuniServlet/", - "reflectorUri": f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDHD/{self._interal_ver_to_intver()}/ChuniServlet/", + "udpHolePunchUri": f"http://{self.core_cfg.server.hostname}:{self.core_cfg.server.port}/SDHD/{self._interal_ver_to_intver()}/ChuniServlet/", + "reflectorUri": f"http://{self.core_cfg.server.hostname}:{self.core_cfg.server.port}/SDHD/{self._interal_ver_to_intver()}/ChuniServlet/", }, "isDumpUpload": False, "isAou": False, } - def handle_remove_token_api_request(self, data: Dict) -> Dict: + async def handle_remove_token_api_request(self, data: Dict) -> Dict: return {"returnCode": "1"} - def handle_delete_token_api_request(self, data: Dict) -> Dict: + async def handle_delete_token_api_request(self, data: Dict) -> Dict: return {"returnCode": "1"} - def handle_create_token_api_request(self, data: Dict) -> Dict: + async def handle_create_token_api_request(self, data: Dict) -> Dict: return {"returnCode": "1"} - def handle_get_user_map_area_api_request(self, data: Dict) -> Dict: - user_map_areas = self.data.item.get_map_areas(data["userId"]) + async def handle_get_user_map_area_api_request(self, data: Dict) -> Dict: + user_map_areas = await self.data.item.get_map_areas(data["userId"]) map_areas = [] for map_area in user_map_areas: @@ -113,14 +115,14 @@ class ChuniNew(ChuniBase): return {"userId": data["userId"], "userMapAreaList": map_areas} - def handle_get_user_symbol_chat_setting_api_request(self, data: Dict) -> Dict: + async def handle_get_user_symbol_chat_setting_api_request(self, data: Dict) -> Dict: return {"userId": data["userId"], "symbolCharInfoList": []} - def handle_get_user_preview_api_request(self, data: Dict) -> Dict: - profile = self.data.profile.get_profile_preview(data["userId"], self.version) + async def handle_get_user_preview_api_request(self, data: Dict) -> Dict: + profile = await self.data.profile.get_profile_preview(data["userId"], self.version) if profile is None: return None - profile_character = self.data.item.get_character( + profile_character = await self.data.item.get_character( data["userId"], profile["characterId"] ) @@ -164,8 +166,8 @@ class ChuniNew(ChuniBase): } return data1 - def handle_cm_get_user_preview_api_request(self, data: Dict) -> Dict: - p = self.data.profile.get_profile_data(data["userId"], self.version) + async def handle_cm_get_user_preview_api_request(self, data: Dict) -> Dict: + p = await self.data.profile.get_profile_data(data["userId"], self.version) if p is None: return {} @@ -177,17 +179,17 @@ class ChuniNew(ChuniBase): "isLogin": False, } - def handle_printer_login_api_request(self, data: Dict) -> Dict: + async def handle_printer_login_api_request(self, data: Dict) -> Dict: return {"returnCode": 1} - def handle_printer_logout_api_request(self, data: Dict) -> Dict: + async def handle_printer_logout_api_request(self, data: Dict) -> Dict: return {"returnCode": 1} - def handle_get_game_gacha_api_request(self, data: Dict) -> Dict: + async def handle_get_game_gacha_api_request(self, data: Dict) -> Dict: """ returns all current active banners (gachas) """ - game_gachas = self.data.static.get_gachas(self.version) + game_gachas = await self.data.static.get_gachas(self.version) # clean the database rows game_gacha_list = [] @@ -213,11 +215,11 @@ class ChuniNew(ChuniBase): "registIdList": [], } - def handle_get_game_gacha_card_by_id_api_request(self, data: Dict) -> Dict: + async def handle_get_game_gacha_card_by_id_api_request(self, data: Dict) -> Dict: """ returns all valid cards for a given gachaId """ - game_gacha_cards = self.data.static.get_gacha_cards(data["gachaId"]) + game_gacha_cards = await self.data.static.get_gacha_cards(data["gachaId"]) game_gacha_card_list = [] for gacha_card in game_gacha_cards: @@ -237,8 +239,8 @@ class ChuniNew(ChuniBase): "ssrBookCalcList": [], } - def handle_cm_get_user_data_api_request(self, data: Dict) -> Dict: - p = self.data.profile.get_profile_data(data["userId"], self.version) + async def handle_cm_get_user_data_api_request(self, data: Dict) -> Dict: + p = await self.data.profile.get_profile_data(data["userId"], self.version) if p is None: return {} @@ -262,8 +264,8 @@ class ChuniNew(ChuniBase): ], } - def handle_get_user_gacha_api_request(self, data: Dict) -> Dict: - user_gachas = self.data.item.get_user_gachas(data["userId"]) + async def handle_get_user_gacha_api_request(self, data: Dict) -> Dict: + user_gachas = await self.data.item.get_user_gachas(data["userId"]) if user_gachas is None: return {"userId": data["userId"], "length": 0, "userGachaList": []} @@ -281,8 +283,8 @@ class ChuniNew(ChuniBase): "userGachaList": user_gacha_list, } - def handle_get_user_printed_card_api_request(self, data: Dict) -> Dict: - user_print_list = self.data.item.get_user_print_states( + async def handle_get_user_printed_card_api_request(self, data: Dict) -> Dict: + user_print_list = await self.data.item.get_user_print_states( data["userId"], has_completed=True ) if user_print_list is None: @@ -316,10 +318,10 @@ class ChuniNew(ChuniBase): "userPrintedCardList": print_list, } - def handle_get_user_card_print_error_api_request(self, data: Dict) -> Dict: + async def handle_get_user_card_print_error_api_request(self, data: Dict) -> Dict: user_id = data["userId"] - user_print_states = self.data.item.get_user_print_states( + user_print_states = await self.data.item.get_user_print_states( user_id, has_completed=False ) @@ -338,13 +340,13 @@ class ChuniNew(ChuniBase): "userCardPrintStateList": card_print_state_list, } - def handle_cm_get_user_character_api_request(self, data: Dict) -> Dict: - return super().handle_get_user_character_api_request(data) + async def handle_cm_get_user_character_api_request(self, data: Dict) -> Dict: + return await super().handle_get_user_character_api_request(data) - def handle_cm_get_user_item_api_request(self, data: Dict) -> Dict: - return super().handle_get_user_item_api_request(data) + async def handle_cm_get_user_item_api_request(self, data: Dict) -> Dict: + return await super().handle_get_user_item_api_request(data) - def handle_roll_gacha_api_request(self, data: Dict) -> Dict: + async def handle_roll_gacha_api_request(self, data: Dict) -> Dict: """ Handle a gacha roll API request, with: gachaId: the gachaId where the cards should be pulled from @@ -362,14 +364,14 @@ class ChuniNew(ChuniBase): # characterId should be returned if chara_id != -1: # get the - card = self.data.static.get_gacha_card_by_character(gacha_id, chara_id) + card = await self.data.static.get_gacha_card_by_character(gacha_id, chara_id) tmp = card._asdict() tmp.pop("id") rolled_cards.append(tmp) else: - gacha_cards = self.data.static.get_gacha_cards(gacha_id) + gacha_cards = await self.data.static.get_gacha_cards(gacha_id) # get the card id for each roll for _ in range(num_rolls): @@ -386,7 +388,7 @@ class ChuniNew(ChuniBase): return {"length": len(rolled_cards), "gameGachaCardList": rolled_cards} - def handle_cm_upsert_user_gacha_api_request(self, data: Dict) -> Dict: + async def handle_cm_upsert_user_gacha_api_request(self, data: Dict) -> Dict: upsert = data["cmUpsertUserGacha"] user_id = data["userId"] place_id = data["placeId"] @@ -396,7 +398,7 @@ class ChuniNew(ChuniBase): user_data.pop("rankUpChallengeResults") user_data.pop("userEmoney") - self.data.profile.put_profile_data(user_id, self.version, user_data) + await self.data.profile.put_profile_data(user_id, self.version, user_data) # save the user gacha user_gacha = upsert["userGacha"] @@ -404,16 +406,16 @@ class ChuniNew(ChuniBase): user_gacha.pop("gachaId") user_gacha.pop("dailyGachaDate") - self.data.item.put_user_gacha(user_id, gacha_id, user_gacha) + await self.data.item.put_user_gacha(user_id, gacha_id, user_gacha) # save all user items if "userItemList" in upsert: for item in upsert["userItemList"]: - self.data.item.put_item(user_id, item) + await self.data.item.put_item(user_id, item) # add every gamegachaCard to database for card in upsert["gameGachaCardList"]: - self.data.item.put_user_print_state( + await self.data.item.put_user_print_state( user_id, hasCompleted=False, placeId=place_id, @@ -423,7 +425,7 @@ class ChuniNew(ChuniBase): # retrieve every game gacha card which has been added in order to get # the orderId for the next request - user_print_states = self.data.item.get_user_print_states_by_gacha( + user_print_states = await self.data.item.get_user_print_states_by_gacha( user_id, gacha_id, has_completed=False ) card_print_state_list = [] @@ -441,7 +443,7 @@ class ChuniNew(ChuniBase): "userCardPrintStateList": card_print_state_list, } - def handle_cm_upsert_user_printlog_api_request(self, data: Dict) -> Dict: + async def handle_cm_upsert_user_printlog_api_request(self, data: Dict) -> Dict: return { "returnCode": 1, "orderId": 0, @@ -449,7 +451,7 @@ class ChuniNew(ChuniBase): "apiName": "CMUpsertUserPrintlogApi", } - def handle_cm_upsert_user_print_api_request(self, data: Dict) -> Dict: + async def handle_cm_upsert_user_print_api_request(self, data: Dict) -> Dict: user_print_detail = data["userPrintDetail"] user_id = data["userId"] @@ -465,7 +467,7 @@ class ChuniNew(ChuniBase): ) # add the entry to the user print table with the random serialId - self.data.item.put_user_print_detail(user_id, serial_id, user_print_detail) + await self.data.item.put_user_print_detail(user_id, serial_id, user_print_detail) return { "returnCode": 1, @@ -474,7 +476,7 @@ class ChuniNew(ChuniBase): "apiName": "CMUpsertUserPrintApi", } - def handle_cm_upsert_user_print_subtract_api_request(self, data: Dict) -> Dict: + async def handle_cm_upsert_user_print_subtract_api_request(self, data: Dict) -> Dict: upsert = data["userCardPrintState"] user_id = data["userId"] place_id = data["placeId"] @@ -482,37 +484,37 @@ class ChuniNew(ChuniBase): # save all user items if "userItemList" in data: for item in data["userItemList"]: - self.data.item.put_item(user_id, item) + await self.data.item.put_item(user_id, item) # set the card print state to success and use the orderId as the key - self.data.item.put_user_print_state( + await self.data.item.put_user_print_state( user_id, id=upsert["orderId"], hasCompleted=True ) return {"returnCode": "1", "apiName": "CMUpsertUserPrintSubtractApi"} - def handle_cm_upsert_user_print_cancel_api_request(self, data: Dict) -> Dict: + async def handle_cm_upsert_user_print_cancel_api_request(self, data: Dict) -> Dict: order_ids = data["orderIdList"] user_id = data["userId"] # set the card print state to success and use the orderId as the key for order_id in order_ids: - self.data.item.put_user_print_state(user_id, id=order_id, hasCompleted=True) + await self.data.item.put_user_print_state(user_id, id=order_id, hasCompleted=True) return {"returnCode": "1", "apiName": "CMUpsertUserPrintCancelApi"} - def handle_ping_request(self, data: Dict) -> Dict: + async def handle_ping_request(self, data: Dict) -> Dict: # matchmaking ping request return {"returnCode": "1"} - def handle_begin_matching_api_request(self, data: Dict) -> Dict: + async def handle_begin_matching_api_request(self, data: Dict) -> Dict: room_id = 1 # check if there is a free matching room - matching_room = self.data.item.get_oldest_free_matching(self.version) + matching_room = await self.data.item.get_oldest_free_matching(self.version) if matching_room is None: # grab the latest roomId and add 1 for the new room - newest_matching = self.data.item.get_newest_matching(self.version) + newest_matching = await self.data.item.get_newest_matching(self.version) if newest_matching is not None: room_id = newest_matching["roomId"] + 1 @@ -522,12 +524,12 @@ class ChuniNew(ChuniBase): # create the new room with room_id and the current user id (host) # user id is required for the countdown later on - self.data.item.put_matching( + await self.data.item.put_matching( self.version, room_id, [new_member], user_id=new_member["userId"] ) # get the newly created matching room - matching_room = self.data.item.get_matching(self.version, room_id) + matching_room = await self.data.item.get_matching(self.version, room_id) else: # a room already exists, so just add the new member to it matching_member_list = matching_room["matchingMemberInfoList"] @@ -537,7 +539,7 @@ class ChuniNew(ChuniBase): matching_member_list.append(new_member) # add the updated room to the database, make sure to set isFull correctly! - self.data.item.put_matching( + await self.data.item.put_matching( self.version, matching_room["roomId"], matching_member_list, @@ -554,8 +556,8 @@ class ChuniNew(ChuniBase): return {"roomId": 1, "matchingWaitState": matching_wait} - def handle_end_matching_api_request(self, data: Dict) -> Dict: - matching_room = self.data.item.get_matching(self.version, data["roomId"]) + async def handle_end_matching_api_request(self, data: Dict) -> Dict: + matching_room = await self.data.item.get_matching(self.version, data["roomId"]) members = matching_room["matchingMemberInfoList"] # only set the host user to role 1 every other to 0? @@ -564,7 +566,7 @@ class ChuniNew(ChuniBase): for m in members ] - self.data.item.put_matching( + await self.data.item.put_matching( self.version, matching_room["roomId"], members, @@ -579,13 +581,13 @@ class ChuniNew(ChuniBase): # no idea, maybe to differentiate between CPUs and real players? "matchingMemberRoleList": role_list, # TCP/UDP connection? - "reflectorUri": f"{self.core_cfg.title.hostname}", + "reflectorUri": f"{self.core_cfg.server.hostname}", } - def handle_remove_matching_member_api_request(self, data: Dict) -> Dict: + async def handle_remove_matching_member_api_request(self, data: Dict) -> Dict: # get all matching rooms, because Chuni only returns the userId # not the actual roomId - matching_rooms = self.data.item.get_all_matchings(self.version) + matching_rooms = await self.data.item.get_all_matchings(self.version) if matching_rooms is None: return {"returnCode": "1"} @@ -599,10 +601,10 @@ class ChuniNew(ChuniBase): # if the last user got removed, delete the matching room if len(new_members) <= 0: - self.data.item.delete_matching(self.version, room["roomId"]) + await self.data.item.delete_matching(self.version, room["roomId"]) else: # remove the user from the room - self.data.item.put_matching( + await self.data.item.put_matching( self.version, room["roomId"], new_members, @@ -612,10 +614,10 @@ class ChuniNew(ChuniBase): return {"returnCode": "1"} - def handle_get_matching_state_api_request(self, data: Dict) -> Dict: + async def handle_get_matching_state_api_request(self, data: Dict) -> Dict: polling_interval = 1 # get the current active room - matching_room = self.data.item.get_matching(self.version, data["roomId"]) + matching_room = await self.data.item.get_matching(self.version, data["roomId"]) members = matching_room["matchingMemberInfoList"] rest_sec = matching_room["restMSec"] @@ -638,7 +640,7 @@ class ChuniNew(ChuniBase): current_member["userName"] = self.read_wtf8(current_member["userName"]) members[i] = current_member - self.data.item.put_matching( + await self.data.item.put_matching( self.version, data["roomId"], members, diff --git a/titles/chuni/newplus.py b/titles/chuni/newplus.py index fa642d3..84467fb 100644 --- a/titles/chuni/newplus.py +++ b/titles/chuni/newplus.py @@ -11,8 +11,8 @@ class ChuniNewPlus(ChuniNew): super().__init__(core_cfg, game_cfg) self.version = ChuniConstants.VER_CHUNITHM_NEW_PLUS - def handle_cm_get_user_preview_api_request(self, data: Dict) -> Dict: - user_data = super().handle_cm_get_user_preview_api_request(data) + async def handle_cm_get_user_preview_api_request(self, data: Dict) -> Dict: + user_data = await super().handle_cm_get_user_preview_api_request(data) # hardcode lastDataVersion for CardMaker 1.35 A028 user_data["lastDataVersion"] = "2.05.00" diff --git a/titles/chuni/paradise.py b/titles/chuni/paradise.py index 19155d6..88ed91c 100644 --- a/titles/chuni/paradise.py +++ b/titles/chuni/paradise.py @@ -13,7 +13,7 @@ class ChuniParadise(ChuniBase): super().__init__(core_cfg, game_cfg) self.version = ChuniConstants.VER_CHUNITHM_PARADISE - def handle_get_game_setting_api_request(self, data: Dict) -> Dict: - ret = super().handle_get_game_setting_api_request(data) + async def handle_get_game_setting_api_request(self, data: Dict) -> Dict: + ret = await super().handle_get_game_setting_api_request(data) ret["gameSetting"]["dataVersion"] = "1.50.00" return ret diff --git a/titles/chuni/plus.py b/titles/chuni/plus.py index 62d9e0d..094be06 100644 --- a/titles/chuni/plus.py +++ b/titles/chuni/plus.py @@ -11,7 +11,7 @@ class ChuniPlus(ChuniBase): super().__init__(core_cfg, game_cfg) self.version = ChuniConstants.VER_CHUNITHM_PLUS - def handle_get_game_setting_api_request(self, data: Dict) -> Dict: - ret = super().handle_get_game_setting_api_request(data) + async def handle_get_game_setting_api_request(self, data: Dict) -> Dict: + ret = await super().handle_get_game_setting_api_request(data) ret["gameSetting"]["dataVersion"] = "1.05.00" return ret diff --git a/titles/chuni/read.py b/titles/chuni/read.py index 7100ae6..db7435c 100644 --- a/titles/chuni/read.py +++ b/titles/chuni/read.py @@ -28,7 +28,7 @@ class ChuniReader(BaseReader): self.logger.error(f"Invalid chunithm version {version}") exit(1) - def read(self) -> None: + async def read(self) -> None: data_dirs = [] if self.bin_dir is not None: data_dirs += self.get_data_directories(self.bin_dir) @@ -38,19 +38,18 @@ class ChuniReader(BaseReader): for dir in data_dirs: self.logger.info(f"Read from {dir}") - self.read_events(f"{dir}/event") - self.read_music(f"{dir}/music") - self.read_charges(f"{dir}/chargeItem") - self.read_avatar(f"{dir}/avatarAccessory") - self.read_login_bonus(f"{dir}/") + await self.read_events(f"{dir}/event") + await self.read_music(f"{dir}/music") + await self.read_charges(f"{dir}/chargeItem") + await self.read_avatar(f"{dir}/avatarAccessory") + await self.read_login_bonus(f"{dir}/") - def read_login_bonus(self, root_dir: str) -> None: + async def read_login_bonus(self, root_dir: str) -> None: for root, dirs, files in walk(f"{root_dir}loginBonusPreset"): for dir in dirs: if path.exists(f"{root}/{dir}/LoginBonusPreset.xml"): - with open(f"{root}/{dir}/LoginBonusPreset.xml", "rb") as fp: - bytedata = fp.read() - strdata = bytedata.decode("UTF-8") + with open(f"{root}/{dir}/LoginBonusPreset.xml", "r", encoding="utf-8") as fp: + strdata = fp.read() xml_root = ET.fromstring(strdata) for name in xml_root.findall("name"): @@ -60,7 +59,7 @@ class ChuniReader(BaseReader): True if xml_root.find("disableFlag").text == "false" else False ) - result = self.data.static.put_login_bonus_preset( + result = await self.data.static.put_login_bonus_preset( self.version, id, name, is_enabled ) @@ -98,7 +97,7 @@ class ChuniReader(BaseReader): bonus_root.find("loginBonusCategoryType").text ) - result = self.data.static.put_login_bonus( + result = await self.data.static.put_login_bonus( self.version, id, bonus_id, @@ -117,13 +116,12 @@ class ChuniReader(BaseReader): f"Failed to insert login bonus {bonus_id}" ) - def read_events(self, evt_dir: str) -> None: + async def read_events(self, evt_dir: str) -> None: for root, dirs, files in walk(evt_dir): for dir in dirs: if path.exists(f"{root}/{dir}/Event.xml"): - with open(f"{root}/{dir}/Event.xml", "rb") as fp: - bytedata = fp.read() - strdata = bytedata.decode("UTF-8") + with open(f"{root}/{dir}/Event.xml", "r", encoding="utf-8") as fp: + strdata = fp.read() xml_root = ET.fromstring(strdata) for name in xml_root.findall("name"): @@ -132,7 +130,7 @@ class ChuniReader(BaseReader): for substances in xml_root.findall("substances"): event_type = substances.find("type").text - result = self.data.static.put_event( + result = await self.data.static.put_event( self.version, id, event_type, name ) if result is not None: @@ -140,13 +138,12 @@ class ChuniReader(BaseReader): else: self.logger.warning(f"Failed to insert event {id}") - def read_music(self, music_dir: str) -> None: + async def read_music(self, music_dir: str) -> None: for root, dirs, files in walk(music_dir): for dir in dirs: if path.exists(f"{root}/{dir}/Music.xml"): - with open(f"{root}/{dir}/Music.xml", "rb") as fp: - bytedata = fp.read() - strdata = bytedata.decode("UTF-8") + with open(f"{root}/{dir}/Music.xml", "r", encoding='utf-8') as fp: + strdata = fp.read() xml_root = ET.fromstring(strdata) for name in xml_root.findall("name"): @@ -185,7 +182,7 @@ class ChuniReader(BaseReader): ) we_chara = None - result = self.data.static.put_music( + result = await self.data.static.put_music( self.version, song_id, chart_id, @@ -206,13 +203,12 @@ class ChuniReader(BaseReader): f"Failed to insert music {song_id} chart {chart_id}" ) - def read_charges(self, charge_dir: str) -> None: + async def read_charges(self, charge_dir: str) -> None: for root, dirs, files in walk(charge_dir): for dir in dirs: if path.exists(f"{root}/{dir}/ChargeItem.xml"): - with open(f"{root}/{dir}/ChargeItem.xml", "rb") as fp: - bytedata = fp.read() - strdata = bytedata.decode("UTF-8") + with open(f"{root}/{dir}/ChargeItem.xml", "r", encoding='utf-8') as fp: + strdata = fp.read() xml_root = ET.fromstring(strdata) for name in xml_root.findall("name"): @@ -222,7 +218,7 @@ class ChuniReader(BaseReader): consumeType = xml_root.find("consumeType").text sellingAppeal = bool(xml_root.find("sellingAppeal").text) - result = self.data.static.put_charge( + result = await self.data.static.put_charge( self.version, id, name, @@ -236,13 +232,12 @@ class ChuniReader(BaseReader): else: self.logger.warning(f"Failed to insert charge {id}") - def read_avatar(self, avatar_dir: str) -> None: + async def read_avatar(self, avatar_dir: str) -> None: for root, dirs, files in walk(avatar_dir): for dir in dirs: if path.exists(f"{root}/{dir}/AvatarAccessory.xml"): - with open(f"{root}/{dir}/AvatarAccessory.xml", "rb") as fp: - bytedata = fp.read() - strdata = bytedata.decode("UTF-8") + with open(f"{root}/{dir}/AvatarAccessory.xml", "r", encoding='utf-8') as fp: + strdata = fp.read() xml_root = ET.fromstring(strdata) for name in xml_root.findall("name"): @@ -254,7 +249,7 @@ class ChuniReader(BaseReader): for texture in xml_root.findall("texture"): texturePath = texture.find("path").text - result = self.data.static.put_avatar( + result = await self.data.static.put_avatar( self.version, id, name, category, iconPath, texturePath ) diff --git a/titles/chuni/schema/item.py b/titles/chuni/schema/item.py index dc2751d..30db4b8 100644 --- a/titles/chuni/schema/item.py +++ b/titles/chuni/schema/item.py @@ -243,9 +243,39 @@ matching = Table( mysql_charset="utf8mb4", ) +cmission = Table( + "chuni_item_cmission", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), + Column("missionId", Integer, nullable=False), + Column("point", Integer), + UniqueConstraint("user", "missionId", name="chuni_item_cmission_uk"), + mysql_charset="utf8mb4", +) + +cmission_progress = Table( + "chuni_item_cmission_progress", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False), + Column("missionId", Integer, nullable=False), + Column("order", Integer), + Column("stage", Integer), + Column("progress", Integer), + UniqueConstraint( + "user", "missionId", "order", name="chuni_item_cmission_progress_uk" + ), + mysql_charset="utf8mb4", +) + class ChuniItemData(BaseData): - def get_oldest_free_matching(self, version: int) -> Optional[Row]: + async def get_oldest_free_matching(self, version: int) -> Optional[Row]: sql = matching.select( and_( matching.c.version == version, @@ -253,46 +283,46 @@ class ChuniItemData(BaseData): ) ).order_by(matching.c.roomId.asc()) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchone() - def get_newest_matching(self, version: int) -> Optional[Row]: + async def get_newest_matching(self, version: int) -> Optional[Row]: sql = matching.select( and_( matching.c.version == version ) ).order_by(matching.c.roomId.desc()) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchone() - def get_all_matchings(self, version: int) -> Optional[List[Row]]: + async def get_all_matchings(self, version: int) -> Optional[List[Row]]: sql = matching.select( and_( matching.c.version == version ) ) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchall() - def get_matching(self, version: int, room_id: int) -> Optional[Row]: + async def get_matching(self, version: int, room_id: int) -> Optional[Row]: sql = matching.select( and_(matching.c.version == version, matching.c.roomId == room_id) ) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchone() - def put_matching( + async def put_matching( self, version: int, room_id: int, @@ -314,22 +344,22 @@ class ChuniItemData(BaseData): restMSec=rest_sec, matchingMemberInfoList=matching_member_info_list ) - result = self.execute(conflict) + result = await self.execute(conflict) if result is None: return None return result.lastrowid - def delete_matching(self, version: int, room_id: int): + async def delete_matching(self, version: int, room_id: int): sql = delete(matching).where( and_(matching.c.roomId == room_id, matching.c.version == version) ) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.lastrowid - def get_all_favorites( + async def get_all_favorites( self, user_id: int, version: int, fav_kind: int = 1 ) -> Optional[List[Row]]: sql = favorite.select( @@ -340,12 +370,12 @@ class ChuniItemData(BaseData): ) ) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchall() - def put_login_bonus( + async def put_login_bonus( self, user_id: int, version: int, preset_id: int, **login_bonus_data ) -> Optional[int]: sql = insert(login_bonus).values( @@ -354,12 +384,12 @@ class ChuniItemData(BaseData): conflict = sql.on_duplicate_key_update(presetId=preset_id, **login_bonus_data) - result = self.execute(conflict) + result = await self.execute(conflict) if result is None: return None return result.lastrowid - def get_all_login_bonus( + async def get_all_login_bonus( self, user_id: int, version: int, is_finished: bool = False ) -> Optional[List[Row]]: sql = login_bonus.select( @@ -370,12 +400,12 @@ class ChuniItemData(BaseData): ) ) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchall() - def get_login_bonus( + async def get_login_bonus( self, user_id: int, version: int, preset_id: int ) -> Optional[Row]: sql = login_bonus.select( @@ -386,12 +416,12 @@ class ChuniItemData(BaseData): ) ) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchone() - def put_character(self, user_id: int, character_data: Dict) -> Optional[int]: + async def put_character(self, user_id: int, character_data: Dict) -> Optional[int]: character_data["user"] = user_id character_data = self.fix_bools(character_data) @@ -399,30 +429,30 @@ class ChuniItemData(BaseData): sql = insert(character).values(**character_data) conflict = sql.on_duplicate_key_update(**character_data) - result = self.execute(conflict) + result = await self.execute(conflict) if result is None: return None return result.lastrowid - def get_character(self, user_id: int, character_id: int) -> Optional[Dict]: + async def get_character(self, user_id: int, character_id: int) -> Optional[Dict]: sql = select(character).where( and_(character.c.user == user_id, character.c.characterId == character_id) ) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchone() - def get_characters(self, user_id: int) -> Optional[List[Row]]: + async def get_characters(self, user_id: int) -> Optional[List[Row]]: sql = select(character).where(character.c.user == user_id) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchall() - def put_item(self, user_id: int, item_data: Dict) -> Optional[int]: + async def put_item(self, user_id: int, item_data: Dict) -> Optional[int]: item_data["user"] = user_id item_data = self.fix_bools(item_data) @@ -430,12 +460,12 @@ class ChuniItemData(BaseData): sql = insert(item).values(**item_data) conflict = sql.on_duplicate_key_update(**item_data) - result = self.execute(conflict) + result = await self.execute(conflict) if result is None: return None return result.lastrowid - def get_items(self, user_id: int, kind: int = None) -> Optional[List[Row]]: + async def get_items(self, user_id: int, kind: int = None) -> Optional[List[Row]]: if kind is None: sql = select(item).where(item.c.user == user_id) else: @@ -443,12 +473,12 @@ class ChuniItemData(BaseData): and_(item.c.user == user_id, item.c.itemKind == kind) ) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchall() - def put_duel(self, user_id: int, duel_data: Dict) -> Optional[int]: + async def put_duel(self, user_id: int, duel_data: Dict) -> Optional[int]: duel_data["user"] = user_id duel_data = self.fix_bools(duel_data) @@ -456,20 +486,20 @@ class ChuniItemData(BaseData): sql = insert(duel).values(**duel_data) conflict = sql.on_duplicate_key_update(**duel_data) - result = self.execute(conflict) + result = await self.execute(conflict) if result is None: return None return result.lastrowid - def get_duels(self, user_id: int) -> Optional[List[Row]]: + async def get_duels(self, user_id: int) -> Optional[List[Row]]: sql = select(duel).where(duel.c.user == user_id) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchall() - def put_map(self, user_id: int, map_data: Dict) -> Optional[int]: + async def put_map(self, user_id: int, map_data: Dict) -> Optional[int]: map_data["user"] = user_id map_data = self.fix_bools(map_data) @@ -477,20 +507,20 @@ class ChuniItemData(BaseData): sql = insert(map).values(**map_data) conflict = sql.on_duplicate_key_update(**map_data) - result = self.execute(conflict) + result = await self.execute(conflict) if result is None: return None return result.lastrowid - def get_maps(self, user_id: int) -> Optional[List[Row]]: + async def get_maps(self, user_id: int) -> Optional[List[Row]]: sql = select(map).where(map.c.user == user_id) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchall() - def put_map_area(self, user_id: int, map_area_data: Dict) -> Optional[int]: + async def put_map_area(self, user_id: int, map_area_data: Dict) -> Optional[int]: map_area_data["user"] = user_id map_area_data = self.fix_bools(map_area_data) @@ -498,28 +528,28 @@ class ChuniItemData(BaseData): sql = insert(map_area).values(**map_area_data) conflict = sql.on_duplicate_key_update(**map_area_data) - result = self.execute(conflict) + result = await self.execute(conflict) if result is None: return None return result.lastrowid - def get_map_areas(self, user_id: int) -> Optional[List[Row]]: + async def get_map_areas(self, user_id: int) -> Optional[List[Row]]: sql = select(map_area).where(map_area.c.user == user_id) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchall() - def get_user_gachas(self, aime_id: int) -> Optional[List[Row]]: + async def get_user_gachas(self, aime_id: int) -> Optional[List[Row]]: sql = gacha.select(gacha.c.user == aime_id) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchall() - def put_user_gacha( + async def put_user_gacha( self, aime_id: int, gacha_id: int, gacha_data: Dict ) -> Optional[int]: sql = insert(gacha).values(user=aime_id, gachaId=gacha_id, **gacha_data) @@ -527,14 +557,14 @@ class ChuniItemData(BaseData): conflict = sql.on_duplicate_key_update( user=aime_id, gachaId=gacha_id, **gacha_data ) - result = self.execute(conflict) + result = await self.execute(conflict) if result is None: self.logger.warning(f"put_user_gacha: Failed to insert! aime_id: {aime_id}") return None return result.lastrowid - def get_user_print_states( + async def get_user_print_states( self, aime_id: int, has_completed: bool = False ) -> Optional[List[Row]]: sql = print_state.select( @@ -544,12 +574,12 @@ class ChuniItemData(BaseData): ) ) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchall() - def get_user_print_states_by_gacha( + async def get_user_print_states_by_gacha( self, aime_id: int, gacha_id: int, has_completed: bool = False ) -> Optional[List[Row]]: sql = print_state.select( @@ -560,16 +590,16 @@ class ChuniItemData(BaseData): ) ) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchall() - def put_user_print_state(self, aime_id: int, **print_data) -> Optional[int]: + async def put_user_print_state(self, aime_id: int, **print_data) -> Optional[int]: sql = insert(print_state).values(user=aime_id, **print_data) conflict = sql.on_duplicate_key_update(user=aime_id, **print_data) - result = self.execute(conflict) + result = await self.execute(conflict) if result is None: self.logger.warning( @@ -578,7 +608,7 @@ class ChuniItemData(BaseData): return None return result.lastrowid - def put_user_print_detail( + async def put_user_print_detail( self, aime_id: int, serial_id: str, user_print_data: Dict ) -> Optional[int]: sql = insert(print_detail).values( @@ -586,7 +616,7 @@ class ChuniItemData(BaseData): ) conflict = sql.on_duplicate_key_update(user=aime_id, **user_print_data) - result = self.execute(conflict) + result = await self.execute(conflict) if result is None: self.logger.warning( @@ -594,3 +624,66 @@ class ChuniItemData(BaseData): ) return None return result.lastrowid + + async def put_cmission_progress( + self, user_id: int, mission_id: int, progress_data: Dict + ) -> Optional[int]: + progress_data["user"] = user_id + progress_data["missionId"] = mission_id + + sql = insert(cmission_progress).values(**progress_data) + conflict = sql.on_duplicate_key_update(**progress_data) + result = await self.execute(conflict) + + if result is None: + return None + + return result.lastrowid + + async def get_cmission_progress( + self, user_id: int, mission_id: int + ) -> Optional[List[Row]]: + sql = cmission_progress.select( + and_( + cmission_progress.c.user == user_id, + cmission_progress.c.missionId == mission_id, + ) + ).order_by(cmission_progress.c.order.asc()) + result = await self.execute(sql) + + if result is None: + return None + + return result.fetchall() + + async def get_cmission(self, user_id: int, mission_id: int) -> Optional[Row]: + sql = cmission.select( + and_(cmission.c.user == user_id, cmission.c.missionId == mission_id) + ) + result = await self.execute(sql) + + if result is None: + return None + + return result.fetchone() + + async def put_cmission(self, user_id: int, mission_data: Dict) -> Optional[int]: + mission_data["user"] = user_id + + sql = insert(cmission).values(**mission_data) + conflict = sql.on_duplicate_key_update(**mission_data) + result = await self.execute(conflict) + + if result is None: + return None + + return result.lastrowid + + async def get_cmissions(self, user_id: int) -> Optional[List[Row]]: + sql = cmission.select(cmission.c.user == user_id) + result = await self.execute(sql) + + if result is None: + return None + + return result.fetchall() diff --git a/titles/chuni/schema/profile.py b/titles/chuni/schema/profile.py index ea70583..f0b8c0f 100644 --- a/titles/chuni/schema/profile.py +++ b/titles/chuni/schema/profile.py @@ -1,10 +1,10 @@ +import json from typing import Dict, List, Optional -from sqlalchemy import Table, Column, UniqueConstraint, PrimaryKeyConstraint, and_ -from sqlalchemy.types import Integer, String, TIMESTAMP, Boolean, JSON, BigInteger -from sqlalchemy.engine.base import Connection +from sqlalchemy import Table, Column, UniqueConstraint, and_ +from sqlalchemy.types import Integer, String, Boolean, JSON, BigInteger from sqlalchemy.schema import ForeignKey from sqlalchemy.engine import Row -from sqlalchemy.sql import func, select +from sqlalchemy.sql import select, delete from sqlalchemy.dialects.mysql import insert from core.data.schema import BaseData, metadata @@ -390,12 +390,56 @@ team = Table( Column("id", Integer, primary_key=True, nullable=False), Column("teamName", String(255)), Column("teamPoint", Integer), + Column("userTeamPoint", JSON), mysql_charset="utf8mb4", ) +rating = Table( + "chuni_profile_rating", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), + Column("version", Integer, nullable=False), + Column("type", String(255), nullable=False), + Column("index", Integer, nullable=False), + Column("musicId", Integer), + Column("difficultId", Integer), + Column("romVersionCode", Integer), + Column("score", Integer), + UniqueConstraint("user", "version", "type", "index", name="chuni_profile_rating_best_uk"), + mysql_charset="utf8mb4", +) + +net_battle = Table( + "chuni_profile_net_battle", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column("user", Integer, ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False, unique=True), + Column("isRankUpChallengeFailed", Boolean), + Column("highestBattleRankId", Integer), + Column("battleIconId", Integer), + Column("battleIconNum", Integer), + Column("avatarEffectPoint", Integer), + mysql_charset="utf8mb4", +) class ChuniProfileData(BaseData): - def put_profile_data( + async def update_name(self, user_id: int, new_name: str) -> bool: + sql = profile.update(profile.c.user == user_id).values( + userName=new_name + ) + result = await self.execute(sql) + + if result is None: + self.logger.warning(f"Failed to set user {user_id} name to {new_name}") + return False + return True + + async def put_profile_data( self, aime_id: int, version: int, profile_data: Dict ) -> Optional[int]: profile_data["user"] = aime_id @@ -407,26 +451,26 @@ class ChuniProfileData(BaseData): sql = insert(profile).values(**profile_data) conflict = sql.on_duplicate_key_update(**profile_data) - result = self.execute(conflict) + result = await self.execute(conflict) if result is None: self.logger.warning(f"put_profile_data: Failed to update! aime_id: {aime_id}") return None return result.lastrowid - def get_profile_preview(self, aime_id: int, version: int) -> Optional[Row]: + async def get_profile_preview(self, aime_id: int, version: int) -> Optional[Row]: sql = ( select([profile, option]) .join(option, profile.c.user == option.c.user) .filter(and_(profile.c.user == aime_id, profile.c.version <= version)) ).order_by(profile.c.version.desc()) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchone() - def get_profile_data(self, aime_id: int, version: int) -> Optional[Row]: + async def get_profile_data(self, aime_id: int, version: int) -> Optional[Row]: sql = select(profile).where( and_( profile.c.user == aime_id, @@ -434,12 +478,12 @@ class ChuniProfileData(BaseData): ) ).order_by(profile.c.version.desc()) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchone() - def put_profile_data_ex( + async def put_profile_data_ex( self, aime_id: int, version: int, profile_ex_data: Dict ) -> Optional[int]: profile_ex_data["user"] = aime_id @@ -449,7 +493,7 @@ class ChuniProfileData(BaseData): sql = insert(profile_ex).values(**profile_ex_data) conflict = sql.on_duplicate_key_update(**profile_ex_data) - result = self.execute(conflict) + result = await self.execute(conflict) if result is None: self.logger.warning( @@ -458,7 +502,7 @@ class ChuniProfileData(BaseData): return None return result.lastrowid - def get_profile_data_ex(self, aime_id: int, version: int) -> Optional[Row]: + async def get_profile_data_ex(self, aime_id: int, version: int) -> Optional[Row]: sql = select(profile_ex).where( and_( profile_ex.c.user == aime_id, @@ -466,17 +510,17 @@ class ChuniProfileData(BaseData): ) ).order_by(profile_ex.c.version.desc()) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchone() - def put_profile_option(self, aime_id: int, option_data: Dict) -> Optional[int]: + async def put_profile_option(self, aime_id: int, option_data: Dict) -> Optional[int]: option_data["user"] = aime_id sql = insert(option).values(**option_data) conflict = sql.on_duplicate_key_update(**option_data) - result = self.execute(conflict) + result = await self.execute(conflict) if result is None: self.logger.warning( @@ -485,22 +529,22 @@ class ChuniProfileData(BaseData): return None return result.lastrowid - def get_profile_option(self, aime_id: int) -> Optional[Row]: + async def get_profile_option(self, aime_id: int) -> Optional[Row]: sql = select(option).where(option.c.user == aime_id) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchone() - def put_profile_option_ex( + async def put_profile_option_ex( self, aime_id: int, option_ex_data: Dict ) -> Optional[int]: option_ex_data["user"] = aime_id sql = insert(option_ex).values(**option_ex_data) conflict = sql.on_duplicate_key_update(**option_ex_data) - result = self.execute(conflict) + result = await self.execute(conflict) if result is None: self.logger.warning( @@ -509,15 +553,15 @@ class ChuniProfileData(BaseData): return None return result.lastrowid - def get_profile_option_ex(self, aime_id: int) -> Optional[Row]: + async def get_profile_option_ex(self, aime_id: int) -> Optional[Row]: sql = select(option_ex).where(option_ex.c.user == aime_id) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchone() - def put_profile_recent_rating( + async def put_profile_recent_rating( self, aime_id: int, recent_rating_data: List[Dict] ) -> Optional[int]: sql = insert(recent_rating).values( @@ -525,7 +569,7 @@ class ChuniProfileData(BaseData): ) conflict = sql.on_duplicate_key_update(recentRating=recent_rating_data) - result = self.execute(conflict) + result = await self.execute(conflict) if result is None: self.logger.warning( f"put_profile_recent_rating: Failed to update! aime_id: {aime_id}" @@ -533,15 +577,15 @@ class ChuniProfileData(BaseData): return None return result.lastrowid - def get_profile_recent_rating(self, aime_id: int) -> Optional[Row]: + async def get_profile_recent_rating(self, aime_id: int) -> Optional[Row]: sql = select(recent_rating).where(recent_rating.c.user == aime_id) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchone() - def put_profile_activity(self, aime_id: int, activity_data: Dict) -> Optional[int]: + async def put_profile_activity(self, aime_id: int, activity_data: Dict) -> Optional[int]: # The game just uses "id" but we need to distinguish that from the db column "id" activity_data["user"] = aime_id activity_data["activityId"] = activity_data["id"] @@ -549,7 +593,7 @@ class ChuniProfileData(BaseData): sql = insert(activity).values(**activity_data) conflict = sql.on_duplicate_key_update(**activity_data) - result = self.execute(conflict) + result = await self.execute(conflict) if result is None: self.logger.warning( @@ -558,24 +602,24 @@ class ChuniProfileData(BaseData): return None return result.lastrowid - def get_profile_activity(self, aime_id: int, kind: int) -> Optional[List[Row]]: + async def get_profile_activity(self, aime_id: int, kind: int) -> Optional[List[Row]]: sql = ( select(activity) .where(and_(activity.c.user == aime_id, activity.c.kind == kind)) .order_by(activity.c.sortNumber.desc()) # to get the last played track ) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchall() - def put_profile_charge(self, aime_id: int, charge_data: Dict) -> Optional[int]: + async def put_profile_charge(self, aime_id: int, charge_data: Dict) -> Optional[int]: charge_data["user"] = aime_id sql = insert(charge).values(**charge_data) conflict = sql.on_duplicate_key_update(**charge_data) - result = self.execute(conflict) + result = await self.execute(conflict) if result is None: self.logger.warning( @@ -584,40 +628,40 @@ class ChuniProfileData(BaseData): return None return result.lastrowid - def get_profile_charge(self, aime_id: int) -> Optional[List[Row]]: + async def get_profile_charge(self, aime_id: int) -> Optional[List[Row]]: sql = select(charge).where(charge.c.user == aime_id) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchall() - def add_profile_region(self, aime_id: int, region_id: int) -> Optional[int]: + async def add_profile_region(self, aime_id: int, region_id: int) -> Optional[int]: pass - def get_profile_regions(self, aime_id: int) -> Optional[List[Row]]: + async def get_profile_regions(self, aime_id: int) -> Optional[List[Row]]: pass - def put_profile_emoney(self, aime_id: int, emoney_data: Dict) -> Optional[int]: + async def put_profile_emoney(self, aime_id: int, emoney_data: Dict) -> Optional[int]: emoney_data["user"] = aime_id sql = insert(emoney).values(**emoney_data) conflict = sql.on_duplicate_key_update(**emoney_data) - result = self.execute(conflict) + result = await self.execute(conflict) if result is None: return None return result.lastrowid - def get_profile_emoney(self, aime_id: int) -> Optional[List[Row]]: + async def get_profile_emoney(self, aime_id: int) -> Optional[List[Row]]: sql = select(emoney).where(emoney.c.user == aime_id) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchall() - def put_profile_overpower( + async def put_profile_overpower( self, aime_id: int, overpower_data: Dict ) -> Optional[int]: overpower_data["user"] = aime_id @@ -625,31 +669,31 @@ class ChuniProfileData(BaseData): sql = insert(overpower).values(**overpower_data) conflict = sql.on_duplicate_key_update(**overpower_data) - result = self.execute(conflict) + result = await self.execute(conflict) if result is None: return None return result.lastrowid - def get_profile_overpower(self, aime_id: int) -> Optional[List[Row]]: + async def get_profile_overpower(self, aime_id: int) -> Optional[List[Row]]: sql = select(overpower).where(overpower.c.user == aime_id) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchall() - def get_team_by_id(self, team_id: int) -> Optional[Row]: + async def get_team_by_id(self, team_id: int) -> Optional[Row]: sql = select(team).where(team.c.id == team_id) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchone() - def get_team_rank(self, team_id: int) -> int: + async def get_team_rank(self, team_id: int) -> int: # Normal ranking system, likely the one used in the real servers # Query all teams sorted by 'teamPoint' - result = self.execute( + result = await self.execute( select(team.c.id).order_by(team.c.teamPoint.desc()) ) @@ -663,16 +707,40 @@ class ChuniProfileData(BaseData): # Return the rank if found, or a default rank otherwise return rank if rank is not None else 0 - # RIP scaled team ranking. Gone, but forgotten - # def get_team_rank_scaled(self, team_id: int) -> int: - - def update_team(self, team_id: int, team_data: Dict) -> bool: + async def update_team(self, team_id: int, team_data: Dict, user_id: str, user_point_delta: int) -> bool: + # Update the team data team_data["id"] = team_id + existing_team = self.get_team_by_id(team_id) + if existing_team is None or "userTeamPoint" not in existing_team: + self.logger.warn( + f"update_team: Failed to update team! team id: {team_id}. Existing team data not found." + ) + return False + user_team_point_data = [] + if existing_team["userTeamPoint"] is not None and existing_team["userTeamPoint"] != "": + user_team_point_data = json.loads(existing_team["userTeamPoint"]) + updated = False + + # Try to find the user in the existing data and update their points + for user_point_data in user_team_point_data: + if user_point_data["user"] == user_id: + user_point_data["userPoint"] = str(int(user_point_delta)) + updated = True + break + + # If the user was not found, add them to the data with the new points + if not updated: + user_team_point_data.append({"user": user_id, "userPoint": str(user_point_delta)}) + + # Update the team's userTeamPoint field in the team data + team_data["userTeamPoint"] = json.dumps(user_team_point_data) + + # Update the team in the database sql = insert(team).values(**team_data) conflict = sql.on_duplicate_key_update(**team_data) - result = self.execute(conflict) + result = await self.execute(conflict) if result is None: self.logger.warn( @@ -680,16 +748,17 @@ class ChuniProfileData(BaseData): ) return False return True - def get_rival(self, rival_id: int) -> Optional[Row]: + + async def get_rival(self, rival_id: int) -> Optional[Row]: sql = select(profile).where(profile.c.user == rival_id) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchone() - def get_overview(self) -> Dict: + async def get_overview(self) -> Dict: # Fetch and add up all the playcounts - playcount_sql = self.execute(select(profile.c.playCount)) + playcount_sql = await self.execute(select(profile.c.playCount)) if playcount_sql is None: self.logger.warn( @@ -697,9 +766,84 @@ class ChuniProfileData(BaseData): ) return 0 - total_play_count = 0; + total_play_count = 0 for row in playcount_sql: total_play_count += row[0] return { "total_play_count": total_play_count } + + async def put_profile_rating( + self, + aime_id: int, + version: int, + rating_type: str, + rating_data: List[Dict], + ): + inserted_values = [ + {"user": aime_id, "version": version, "type": rating_type, "index": i, **x} + for (i, x) in enumerate(rating_data) + ] + sql = insert(rating).values(inserted_values) + update_dict = {x.name: x for x in sql.inserted if x.name != "id"} + sql = sql.on_duplicate_key_update(**update_dict) + result = await self.execute(sql) + + if result is None: + self.logger.warn( + f"put_profile_rating: Could not insert {rating_type}, aime_id: {aime_id}", + ) + return + + return result.lastrowid + + async def get_profile_rating(self, aime_id: int, version: int) -> Optional[List[Row]]: + sql = select(rating).where(and_( + rating.c.user == aime_id, + rating.c.version <= version, + )) + + result = await self.execute(sql) + if result is None: + self.logger.warning(f"Rating of user {aime_id}, version {version} was None") + return None + return result.fetchall() + + async def get_all_profile_versions(self, aime_id: int) -> Optional[List[Row]]: + sql = select([profile.c.version]).where(profile.c.user == aime_id) + result = await self.execute(sql) + if result is None: + self.logger.warning(f"user {aime_id}, has no profile") + return None + else: + versions_raw = result.fetchall() + versions = [row[0] for row in versions_raw] + return sorted(versions, reverse=True) + + async def put_net_battle(self, aime_id: int, net_battle_data: Dict) -> Optional[int]: + sql = insert(net_battle).values( + user=aime_id, + isRankUpChallengeFailed=net_battle_data['isRankUpChallengeFailed'], + highestBattleRankId=net_battle_data['highestBattleRankId'], + battleIconId=net_battle_data['battleIconId'], + battleIconNum=net_battle_data['battleIconNum'], + avatarEffectPoint=net_battle_data['avatarEffectPoint'], + ) + + conflict = sql.on_duplicate_key_update( + isRankUpChallengeFailed=net_battle_data['isRankUpChallengeFailed'], + highestBattleRankId=net_battle_data['highestBattleRankId'], + battleIconId=net_battle_data['battleIconId'], + battleIconNum=net_battle_data['battleIconNum'], + avatarEffectPoint=net_battle_data['avatarEffectPoint'], + ) + + result = await self.execute(conflict) + if result: + return result.inserted_primary_key['id'] + self.logger.error(f"Failed to put net battle data for user {aime_id}") + + async def get_net_battle(self, aime_id: int) -> Optional[Row]: + result = await self.execute(net_battle.select(net_battle.c.user == aime_id)) + if result: + return result.fetchone() diff --git a/titles/chuni/schema/score.py b/titles/chuni/schema/score.py index 7e41b8f..766b4b9 100644 --- a/titles/chuni/schema/score.py +++ b/titles/chuni/schema/score.py @@ -142,55 +142,72 @@ playlog = Table( class ChuniScoreData(BaseData): - def get_courses(self, aime_id: int) -> Optional[Row]: + async def get_courses(self, aime_id: int) -> Optional[Row]: sql = select(course).where(course.c.user == aime_id) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchall() - def put_course(self, aime_id: int, course_data: Dict) -> Optional[int]: + async def put_course(self, aime_id: int, course_data: Dict) -> Optional[int]: course_data["user"] = aime_id course_data = self.fix_bools(course_data) sql = insert(course).values(**course_data) conflict = sql.on_duplicate_key_update(**course_data) - result = self.execute(conflict) + result = await self.execute(conflict) if result is None: return None return result.lastrowid - def get_scores(self, aime_id: int) -> Optional[Row]: + async def get_scores(self, aime_id: int) -> Optional[Row]: sql = select(best_score).where(best_score.c.user == aime_id) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchall() - def put_score(self, aime_id: int, score_data: Dict) -> Optional[int]: + async def put_score(self, aime_id: int, score_data: Dict) -> Optional[int]: score_data["user"] = aime_id score_data = self.fix_bools(score_data) sql = insert(best_score).values(**score_data) conflict = sql.on_duplicate_key_update(**score_data) - result = self.execute(conflict) + result = await self.execute(conflict) if result is None: return None return result.lastrowid - def get_playlogs(self, aime_id: int) -> Optional[Row]: + async def get_playlogs(self, aime_id: int) -> Optional[Row]: sql = select(playlog).where(playlog.c.user == aime_id) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchall() - def put_playlog(self, aime_id: int, playlog_data: Dict, version: int) -> Optional[int]: + async def get_playlogs_limited(self, aime_id: int, index: int, count: int) -> Optional[Row]: + sql = select(playlog).where(playlog.c.user == aime_id).order_by(playlog.c.id.desc()).limit(count).offset(index * count) + + result = await self.execute(sql) + if result is None: + self.logger.warning(f" aime_id {aime_id} has no playlog ") + return None + return result.fetchall() + + async def get_user_playlogs_count(self, aime_id: int) -> Optional[Row]: + sql = select(func.count()).where(playlog.c.user == aime_id) + result = await self.execute(sql) + if result is None: + self.logger.warning(f" aime_id {aime_id} has no playlog ") + return None + return result.scalar() + + async def put_playlog(self, aime_id: int, playlog_data: Dict, version: int) -> Optional[int]: # Calculate the ROM version that should be inserted into the DB, based on the version of the ggame being inserted # We only need from Version 10 (Plost) and back, as newer versions include romVersion in their upsert # This matters both for gameRankings, as well as a future DB update to keep version data separate @@ -216,15 +233,17 @@ class ChuniScoreData(BaseData): sql = insert(playlog).values(**playlog_data) conflict = sql.on_duplicate_key_update(**playlog_data) - result = self.execute(conflict) + result = await self.execute(conflict) if result is None: return None return result.lastrowid - def get_rankings(self, version: int) -> Optional[List[Dict]]: + async def get_rankings(self, version: int) -> Optional[List[Dict]]: # Calculates the ROM version that should be fetched for rankings, based on the game version being retrieved # This prevents tracks that are not accessible in your version from counting towards the 10 results romVer = { + 15: "2.20%", + 14: "2.15%", 13: "2.10%", 12: "2.05%", 11: "2.00%", @@ -241,7 +260,7 @@ class ChuniScoreData(BaseData): 0: "1.00%" } sql = select([playlog.c.musicId.label('id'), func.count(playlog.c.musicId).label('point')]).where((playlog.c.level != 4) & (playlog.c.romVersion.like(romVer.get(version, "%")))).group_by(playlog.c.musicId).order_by(func.count(playlog.c.musicId).desc()).limit(10) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None @@ -249,10 +268,10 @@ class ChuniScoreData(BaseData): rows = result.fetchall() return [dict(row) for row in rows] - def get_rival_music(self, rival_id: int) -> Optional[List[Dict]]: + async def get_rival_music(self, rival_id: int) -> Optional[List[Dict]]: sql = select(best_score).where(best_score.c.user == rival_id) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchall() diff --git a/titles/chuni/schema/static.py b/titles/chuni/schema/static.py index fe32d41..ed67b5d 100644 --- a/titles/chuni/schema/static.py +++ b/titles/chuni/schema/static.py @@ -175,7 +175,7 @@ login_bonus = Table( class ChuniStaticData(BaseData): - def put_login_bonus( + async def put_login_bonus( self, version: int, preset_id: int, @@ -207,12 +207,12 @@ class ChuniStaticData(BaseData): loginBonusCategoryType=login_bonus_category_type, ) - result = self.execute(conflict) + result = await self.execute(conflict) if result is None: return None return result.lastrowid - def get_login_bonus( + async def get_login_bonus( self, version: int, preset_id: int, @@ -224,12 +224,12 @@ class ChuniStaticData(BaseData): ) ).order_by(login_bonus.c.needLoginDayCount.desc()) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchall() - def get_login_bonus_by_required_days( + async def get_login_bonus_by_required_days( self, version: int, preset_id: int, need_login_day_count: int ) -> Optional[Row]: sql = login_bonus.select( @@ -240,12 +240,12 @@ class ChuniStaticData(BaseData): ) ) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchone() - def put_login_bonus_preset( + async def put_login_bonus_preset( self, version: int, preset_id: int, preset_name: str, is_enabled: bool ) -> Optional[int]: sql = insert(login_bonus_preset).values( @@ -259,12 +259,12 @@ class ChuniStaticData(BaseData): presetName=preset_name, isEnabled=is_enabled ) - result = self.execute(conflict) + result = await self.execute(conflict) if result is None: return None return result.lastrowid - def get_login_bonus_presets( + async def get_login_bonus_presets( self, version: int, is_enabled: bool = True ) -> Optional[List[Row]]: sql = login_bonus_preset.select( @@ -274,12 +274,12 @@ class ChuniStaticData(BaseData): ) ) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchall() - def put_event( + async def put_event( self, version: int, event_id: int, type: int, name: str ) -> Optional[int]: sql = insert(events).values( @@ -288,19 +288,19 @@ class ChuniStaticData(BaseData): conflict = sql.on_duplicate_key_update(name=name) - result = self.execute(conflict) + result = await self.execute(conflict) if result is None: return None return result.lastrowid - def update_event( + async def update_event( self, version: int, event_id: int, enabled: bool ) -> Optional[bool]: sql = events.update( and_(events.c.version == version, events.c.eventId == event_id) ).values(enabled=enabled) - result = self.execute(sql) + result = await self.execute(sql) if result is None: self.logger.warning( f"update_event: failed to update event! version: {version}, event_id: {event_id}, enabled: {enabled}" @@ -315,35 +315,35 @@ class ChuniStaticData(BaseData): return None return event["enabled"] - def get_event(self, version: int, event_id: int) -> Optional[Row]: + async def get_event(self, version: int, event_id: int) -> Optional[Row]: sql = select(events).where( and_(events.c.version == version, events.c.eventId == event_id) ) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchone() - def get_enabled_events(self, version: int) -> Optional[List[Row]]: + async def get_enabled_events(self, version: int) -> Optional[List[Row]]: sql = select(events).where( and_(events.c.version == version, events.c.enabled == True) ) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchall() - def get_events(self, version: int) -> Optional[List[Row]]: + async def get_events(self, version: int) -> Optional[List[Row]]: sql = select(events).where(events.c.version == version) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchall() - def put_music( + async def put_music( self, version: int, song_id: int, @@ -376,12 +376,12 @@ class ChuniStaticData(BaseData): worldsEndTag=we_tag, ) - result = self.execute(conflict) + result = await self.execute(conflict) if result is None: return None return result.lastrowid - def put_charge( + async def put_charge( self, version: int, charge_id: int, @@ -406,38 +406,38 @@ class ChuniStaticData(BaseData): sellingAppeal=selling_appeal, ) - result = self.execute(conflict) + result = await self.execute(conflict) if result is None: return None return result.lastrowid - def get_enabled_charges(self, version: int) -> Optional[List[Row]]: + async def get_enabled_charges(self, version: int) -> Optional[List[Row]]: sql = select(charge).where( and_(charge.c.version == version, charge.c.enabled == True) ) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchall() - def get_charges(self, version: int) -> Optional[List[Row]]: + async def get_charges(self, version: int) -> Optional[List[Row]]: sql = select(charge).where(charge.c.version == version) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchall() - def get_music(self, version: int) -> Optional[List[Row]]: + async def get_music(self, version: int) -> Optional[List[Row]]: sql = music.select(music.c.version <= version) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchall() - def get_music_chart( + async def get_music_chart( self, version: int, song_id: int, chart_id: int ) -> Optional[List[Row]]: sql = select(music).where( @@ -448,21 +448,21 @@ class ChuniStaticData(BaseData): ) ) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchone() - def get_song(self, music_id: int) -> Optional[Row]: + async def get_song(self, music_id: int) -> Optional[Row]: sql = music.select(music.c.id == music_id) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchone() - def put_avatar( + async def put_avatar( self, version: int, avatarAccessoryId: int, @@ -487,12 +487,12 @@ class ChuniStaticData(BaseData): texturePath=texturePath, ) - result = self.execute(conflict) + result = await self.execute(conflict) if result is None: return None return result.lastrowid - def put_gacha( + async def put_gacha( self, version: int, gacha_id: int, @@ -513,33 +513,33 @@ class ChuniStaticData(BaseData): **gacha_data, ) - result = self.execute(conflict) + result = await self.execute(conflict) if result is None: self.logger.warning(f"Failed to insert gacha! gacha_id {gacha_id}") return None return result.lastrowid - def get_gachas(self, version: int) -> Optional[List[Dict]]: + async def get_gachas(self, version: int) -> Optional[List[Dict]]: sql = gachas.select(gachas.c.version <= version).order_by( gachas.c.gachaId.asc() ) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchall() - def get_gacha(self, version: int, gacha_id: int) -> Optional[Dict]: + async def get_gacha(self, version: int, gacha_id: int) -> Optional[Dict]: sql = gachas.select( and_(gachas.c.version <= version, gachas.c.gachaId == gacha_id) ) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchone() - def put_gacha_card( + async def put_gacha_card( self, gacha_id: int, card_id: int, **gacha_card ) -> Optional[int]: sql = insert(gacha_cards).values(gachaId=gacha_id, cardId=card_id, **gacha_card) @@ -548,21 +548,21 @@ class ChuniStaticData(BaseData): gachaId=gacha_id, cardId=card_id, **gacha_card ) - result = self.execute(conflict) + result = await self.execute(conflict) if result is None: self.logger.warning(f"Failed to insert gacha card! gacha_id {gacha_id}") return None return result.lastrowid - def get_gacha_cards(self, gacha_id: int) -> Optional[List[Dict]]: + async def get_gacha_cards(self, gacha_id: int) -> Optional[List[Dict]]: sql = gacha_cards.select(gacha_cards.c.gachaId == gacha_id) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchall() - def get_gacha_card_by_character( + async def get_gacha_card_by_character( self, gacha_id: int, chara_id: int ) -> Optional[Dict]: sql_sub = ( @@ -574,26 +574,26 @@ class ChuniStaticData(BaseData): and_(gacha_cards.c.gachaId == gacha_id, gacha_cards.c.cardId == sql_sub) ) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchone() - def put_card(self, version: int, card_id: int, **card_data) -> Optional[int]: + async def put_card(self, version: int, card_id: int, **card_data) -> Optional[int]: sql = insert(cards).values(version=version, cardId=card_id, **card_data) conflict = sql.on_duplicate_key_update(**card_data) - result = self.execute(conflict) + result = await self.execute(conflict) if result is None: self.logger.warning(f"Failed to insert card! card_id {card_id}") return None return result.lastrowid - def get_card(self, version: int, card_id: int) -> Optional[Dict]: + async def get_card(self, version: int, card_id: int) -> Optional[Dict]: sql = cards.select(and_(cards.c.version <= version, cards.c.cardId == card_id)) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchone() \ No newline at end of file diff --git a/titles/chuni/star.py b/titles/chuni/star.py index 4c071e8..247934f 100644 --- a/titles/chuni/star.py +++ b/titles/chuni/star.py @@ -11,7 +11,7 @@ class ChuniStar(ChuniBase): super().__init__(core_cfg, game_cfg) self.version = ChuniConstants.VER_CHUNITHM_STAR - def handle_get_game_setting_api_request(self, data: Dict) -> Dict: - ret = super().handle_get_game_setting_api_request(data) + async def handle_get_game_setting_api_request(self, data: Dict) -> Dict: + ret = await super().handle_get_game_setting_api_request(data) ret["gameSetting"]["dataVersion"] = "1.20.00" return ret diff --git a/titles/chuni/starplus.py b/titles/chuni/starplus.py index 8c24cc8..616c3c6 100644 --- a/titles/chuni/starplus.py +++ b/titles/chuni/starplus.py @@ -11,7 +11,7 @@ class ChuniStarPlus(ChuniBase): super().__init__(core_cfg, game_cfg) self.version = ChuniConstants.VER_CHUNITHM_STAR_PLUS - def handle_get_game_setting_api_request(self, data: Dict) -> Dict: - ret = super().handle_get_game_setting_api_request(data) + async def handle_get_game_setting_api_request(self, data: Dict) -> Dict: + ret = await super().handle_get_game_setting_api_request(data) ret["gameSetting"]["dataVersion"] = "1.25.00" return ret diff --git a/titles/chuni/sun.py b/titles/chuni/sun.py index bfefd97..4957c4b 100644 --- a/titles/chuni/sun.py +++ b/titles/chuni/sun.py @@ -11,9 +11,19 @@ class ChuniSun(ChuniNewPlus): super().__init__(core_cfg, game_cfg) self.version = ChuniConstants.VER_CHUNITHM_SUN - def handle_cm_get_user_preview_api_request(self, data: Dict) -> Dict: - user_data = super().handle_cm_get_user_preview_api_request(data) + async def handle_cm_get_user_preview_api_request(self, data: Dict) -> Dict: + user_data = await super().handle_cm_get_user_preview_api_request(data) # hardcode lastDataVersion for CardMaker 1.35 A032 user_data["lastDataVersion"] = "2.10.00" - return user_data \ No newline at end of file + return user_data + + #SDGS Exclusive + async def handle_get_user_cto_c_play_api_request(self, data: Dict) -> Dict: + return { + "userId": data["userId"], + "orderBy": "0", + "count": "0", + #game request c2c play history while login but seem unused(?) + "userCtoCPlayList": [], + } \ No newline at end of file diff --git a/titles/chuni/sunplus.py b/titles/chuni/sunplus.py index fc86314..1f3f271 100644 --- a/titles/chuni/sunplus.py +++ b/titles/chuni/sunplus.py @@ -11,8 +11,8 @@ class ChuniSunPlus(ChuniSun): super().__init__(core_cfg, game_cfg) self.version = ChuniConstants.VER_CHUNITHM_SUN_PLUS - def handle_cm_get_user_preview_api_request(self, data: Dict) -> Dict: - user_data = super().handle_cm_get_user_preview_api_request(data) + async def handle_cm_get_user_preview_api_request(self, data: Dict) -> Dict: + user_data = await super().handle_cm_get_user_preview_api_request(data) # I don't know if lastDataVersion is going to matter, I don't think CardMaker 1.35 works this far up user_data["lastDataVersion"] = "2.15.00" diff --git a/titles/chuni/templates/chuni_header.jinja b/titles/chuni/templates/chuni_header.jinja new file mode 100644 index 0000000..76acdc5 --- /dev/null +++ b/titles/chuni/templates/chuni_header.jinja @@ -0,0 +1,24 @@ +
+

Chunithm

+ +
+ \ No newline at end of file diff --git a/titles/chuni/templates/chuni_index.jinja b/titles/chuni/templates/chuni_index.jinja new file mode 100644 index 0000000..1854a89 --- /dev/null +++ b/titles/chuni/templates/chuni_index.jinja @@ -0,0 +1,150 @@ +{% extends "core/templates/index.jinja" %} +{% block content %} + +
+ {% include 'titles/chuni/templates/chuni_header.jinja' %} + {% if profile is defined and profile is not none and profile|length > 0 %} +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
OVERVIEW
{{ profile.userName }} + +
version: + + {% if versions | length > 1 %} +

You have {{ versions | length }} versions.

+ {% endif %} +
Level:{{ profile.level }}
Rating: + + {{ profile.playerRating|float/100 }} + + + (highest: {{ profile.highestRating|float/100 }}) + +
Over Power:{{ profile.overPowerPoint|float/100 }}({{ profile.overPowerRate|float/100 }})
Current Point:{{ profile.point }}
Total Point:{{ profile.totalPoint }}
Play Counts:{{ profile.playCount }}
Last Play Date:{{ profile.lastPlayDate }}
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
SCORE
Total High Score:{{ profile.totalHiScore }}
Total Basic High Score:{{ profile.totalBasicHighScore }}
Total Advanced High Score:{{ profile.totalAdvancedHighScore }}
Total Expert High Score:{{ profile.totalExpertHighScore }}
Total Master High Score:{{ profile.totalMasterHighScore }}
Total Ultima High Score :{{ profile.totalUltimaHighScore }}
+
+
+
+ {% if error is defined %} + {% include "core/templates/widgets/err_banner.jinja" %} + {% endif %} + {% elif sesh is defined and sesh is not none and sesh.user_id > 0 %} + No profile information found for this account. + {% else %} + Login to view profile information. + {% endif %} +
+ + +{% endblock content %} \ No newline at end of file diff --git a/titles/chuni/templates/chuni_playlog.jinja b/titles/chuni/templates/chuni_playlog.jinja new file mode 100644 index 0000000..fd30746 --- /dev/null +++ b/titles/chuni/templates/chuni_playlog.jinja @@ -0,0 +1,184 @@ +{% extends "core/templates/index.jinja" %} +{% block content %} + +
+ {% include 'titles/chuni/templates/chuni_header.jinja' %} + {% if playlog is defined and playlog is not none %} +
+

Playlog counts: {{ playlog_count }}

+ {% set rankName = ['D', 'C', 'B', 'BB', 'BBB', 'A', 'AA', 'AAA', 'S', 'S+', 'SS', 'SS+', 'SSS', 'SSS+'] %} + {% set difficultyName = ['normal', 'hard', 'expert', 'master', 'ultimate'] %} + {% for record in playlog %} +
+
+
+
+
{{ record.title }}
+
+
{{ record.artist }}
+
+
+
{{ record.raw.userPlayDate }}
+
TRACK {{ record.raw.track }}
+
+
+
+
+

{{ record.raw.score }}

+

{{ rankName[record.raw.rank] }}

+
+ {{ difficultyName[record.raw.level] }}  {{ record.difficultyNum }} +
+
+
+ + + + + + + + + + + + + + + + + +
JUSTICE CRITIAL + {{ record.raw.judgeCritical + record.raw.judgeHeaven }} +
JUSTICE + {{ record.raw.judgeJustice }} +
ATTACK + {{ record.raw.judgeAttack }} +
MISS + {{ record.raw.judgeGuilty }} +
+
+
+ {%if record.raw.isFullCombo == 1 %} +
FULL COMBO
+ {% endif %} + {%if record.raw.isAllJustice == 1 %} +
ALL JUSTICE
+ {% endif %} + {%if record.raw.isNewRecord == 1 %} +
NEW RECORD
+ {% endif %} + {%if record.raw.fullChainKind > 0 %} +
FULL CHAIN
+ {% endif %} +
+
+
+
+ {% endfor %} +
+ {% set playlog_pages = playlog_count // 20 + 1 %} + {% elif sesh is defined and sesh is not none and sesh.user_id > 0 %} + No Playlog information found for this account. + {% else %} + Login to view profile information. + {% endif %} +
+ + + +{% endblock content %} \ No newline at end of file diff --git a/titles/chuni/templates/chuni_rating.jinja b/titles/chuni/templates/chuni_rating.jinja new file mode 100644 index 0000000..37ed6d0 --- /dev/null +++ b/titles/chuni/templates/chuni_rating.jinja @@ -0,0 +1,83 @@ +{% extends "core/templates/index.jinja" %} +{% block content %} + +
+ {% include 'titles/chuni/templates/chuni_header.jinja' %} + {% if profile is defined and profile is not none and profile.id > 0 %} +

Rating: {{ profile.playerRating|float/100 }}    Player Counts: {{ + profile.playCount }}

+
+ {% if hot_list %} +
+
+ + + + + + + + + + {% for row in hot_list %} + + + + + + + + {% endfor %} +
Recent 10
MusicDifficultyScoreRankRating
{{ row.title }} + {{ row.level }} + {{ row.score }}{{ row.rank }} + {{ row.song_rating }} +
+
+
+ {% else %} +

No r10 found

+ {% endif %} + {% if base_list %} +
+
+ + + + + + + + + + {% for row in base_list %} + + + + + + + + {% endfor %} +
Best 30
MusicDifficultyScoreRankRating
{{ row.title }} + {{ row.level }} + {{ row.score }}{{ row.rank }} + {{ row.song_rating }} +
+
+
+ {% else %} +

No b30 found

+ {% endif %} +
+ {% elif sesh is defined and sesh is not none and sesh.user_id > 0 %} + No profile information found for this account. + {% else %} + Login to view profile information. + {% endif %} +
+{% endblock content %} diff --git a/titles/chuni/templates/css/chuni_style.css b/titles/chuni/templates/css/chuni_style.css new file mode 100644 index 0000000..0900b9b --- /dev/null +++ b/titles/chuni/templates/css/chuni_style.css @@ -0,0 +1,195 @@ +.chuni-header { + text-align: center; +} + +ul.chuni-navi { + list-style-type: none; + padding: 0; + overflow: hidden; + background-color: #333; + text-align: center; + display: inline-block; +} + +ul.chuni-navi li { + display: inline-block; +} + +ul.chuni-navi li a { + display: block; + color: white; + text-align: center; + padding: 14px 16px; + text-decoration: none; +} + +ul.chuni-navi li a:hover:not(.active) { + background-color: #111; +} + +ul.chuni-navi li a.active { + background-color: #4CAF50; +} + +ul.chuni-navi li.right { + float: right; +} + +@media screen and (max-width: 600px) { + + ul.chuni-navi li.right, + ul.chuni-navi li { + float: none; + display: block; + text-align: center; + } +} + +table { + border-collapse: collapse; + border-spacing: 0; + border-collapse: separate; + overflow: hidden; + background-color: #555555; + +} + +th, td { + text-align: left; + border: none; + +} + +th { + color: white; +} + +.table-rowdistinct tr:nth-child(even) { + background-color: #303030; +} + +.table-rowdistinct tr:nth-child(odd) { + background-color: #555555; +} + +caption { + text-align: center; + color: white; + font-size: 18px; + font-weight: bold; +} + +.table-large { + margin: 16px; +} + +.table-large th, +.table-large td { + padding: 8px; +} + +.table-small { + width: 100%; + margin: 4px; +} + +.table-small th, +.table-small td { + padding: 2px; +} + +.bg-card { + background-color: #555555; +} + +.card-hover { + transition: all 0.2s ease-in-out; +} + +.card-hover:hover { + transform: scale(1.02); +} + +.normal { + color: #28a745; + font-weight: bold; +} + +.hard { + color: #ffc107; + + font-weight: bold; +} + +.expert { + color: #dc3545; + font-weight: bold; +} + +.master { + color: #dd09e8; + font-weight: bold; +} + +.ultimate { + color: #000000; + font-weight: bold; +} + +.score { + color: #ffffff; + font-weight: bold; +} + +.rainbow { + background: linear-gradient(to right, red, yellow, lime, aqua, blue, fuchsia) 0 / 5em; + background-clip: text; + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + font-weight: bold; +} + +.platinum { + color: #FFFF00; + font-weight: bold; +} + +.gold { + color: #FFFF00; + font-weight: bold; +} + +.scrolling-text { + overflow: hidden; +} + +.scrolling-text p { + white-space: nowrap; + display: inline-block; + +} + +.scrolling-text h6 { + white-space: nowrap; + display: inline-block; + +} + +.scrolling-text h5 { + white-space: nowrap; + display: inline-block; + +} + +.scrolling { + animation: scroll 10s linear infinite; +} + +@keyframes scroll { + 0% { + transform: translateX(100%); + } + 100% { + transform: translateX(-100%); + } +} \ No newline at end of file diff --git a/titles/cm/__init__.py b/titles/cm/__init__.py index 1115f96..a9dad07 100644 --- a/titles/cm/__init__.py +++ b/titles/cm/__init__.py @@ -6,7 +6,4 @@ from titles.cm.database import CardMakerData index = CardMakerServlet reader = CardMakerReader database = CardMakerData - game_codes = [CardMakerConstants.GAME_CODE] - -current_schema_version = 1 diff --git a/titles/cm/base.py b/titles/cm/base.py index b911983..4a49832 100644 --- a/titles/cm/base.py +++ b/titles/cm/base.py @@ -29,11 +29,11 @@ class CardMakerBase: def _parse_int_ver(version: str) -> str: return version.replace(".", "")[:3] - def handle_get_game_connect_api_request(self, data: Dict) -> Dict: + async def handle_get_game_connect_api_request(self, data: Dict) -> Dict: if not self.core_cfg.server.is_using_proxy and Utils.get_title_port(self.core_cfg) != 80: - uri = f"http://{self.core_cfg.title.hostname}:{Utils.get_title_port(self.core_cfg)}" + uri = f"http://{self.core_cfg.server.hostname}:{Utils.get_title_port(self.core_cfg)}" else: - uri = f"http://{self.core_cfg.title.hostname}" + uri = f"http://{self.core_cfg.server.hostname}" # grab the dict with all games version numbers from user config games_ver = self.game_cfg.version.version(self.version) @@ -51,7 +51,7 @@ class CardMakerBase: { "modelKind": 1, "type": 1, - "titleUri": f"{uri}/{self._parse_int_ver(games_ver['maimai'])}/Maimai2Servlet/", + "titleUri": f"{uri}/SDEZ/{self._parse_int_ver(games_ver['maimai'])}/Maimai2Servlet/", }, # ONGEKI { @@ -62,7 +62,7 @@ class CardMakerBase: ], } - def handle_get_game_setting_api_request(self, data: Dict) -> Dict: + async def handle_get_game_setting_api_request(self, data: Dict) -> Dict: # if reboot start/end time is not defined use the default behavior of being a few hours ago if self.core_cfg.title.reboot_start_time == "" or self.core_cfg.title.reboot_end_time == "": reboot_start = datetime.strftime( @@ -110,11 +110,11 @@ class CardMakerBase: "isAou": False, } - def handle_get_client_bookkeeping_api_request(self, data: Dict) -> Dict: + async def handle_get_client_bookkeeping_api_request(self, data: Dict) -> Dict: return {"placeId": data["placeId"], "length": 0, "clientBookkeepingList": []} - def handle_upsert_client_setting_api_request(self, data: Dict) -> Dict: + async def handle_upsert_client_setting_api_request(self, data: Dict) -> Dict: return {"returnCode": 1, "apiName": "UpsertClientSettingApi"} - def handle_upsert_client_bookkeeping_api_request(self, data: Dict) -> Dict: + async def handle_upsert_client_bookkeeping_api_request(self, data: Dict) -> Dict: return {"returnCode": 1, "apiName": "UpsertClientBookkeepingApi"} diff --git a/titles/cm/cm135.py b/titles/cm/cm135.py index e134974..5bc5460 100644 --- a/titles/cm/cm135.py +++ b/titles/cm/cm135.py @@ -12,7 +12,7 @@ class CardMaker135(CardMakerBase): super().__init__(core_cfg, game_cfg) self.version = CardMakerConstants.VER_CARD_MAKER_135 - def handle_get_game_setting_api_request(self, data: Dict) -> Dict: - ret = super().handle_get_game_setting_api_request(data) + async def handle_get_game_setting_api_request(self, data: Dict) -> Dict: + ret = await super().handle_get_game_setting_api_request(data) ret["gameSetting"]["dataVersion"] = "1.35.00" return ret diff --git a/titles/cm/index.py b/titles/cm/index.py index 489b846..c6d6746 100644 --- a/titles/cm/index.py +++ b/titles/cm/index.py @@ -5,10 +5,11 @@ import string import logging import coloredlogs import zlib - +from starlette.routing import Route +from starlette.responses import Response +from starlette.requests import Request from os import path -from typing import Tuple, List, Dict -from twisted.web.http import Request +from typing import List from logging.handlers import TimedRotatingFileHandler from core.config import CoreConfig @@ -19,7 +20,6 @@ from .const import CardMakerConstants from .base import CardMakerBase from .cm135 import CardMaker135 - class CardMakerServlet(BaseServlet): def __init__(self, core_cfg: CoreConfig, cfg_dir: str) -> None: super().__init__(core_cfg, cfg_dir) @@ -72,16 +72,15 @@ class CardMakerServlet(BaseServlet): return True - def get_endpoint_matchers(self) -> Tuple[List[Tuple[str, str, Dict]], List[Tuple[str, str, Dict]]]: - return ( - [], - [("render_POST", "/SDED/{version}/{endpoint}", {})] - ) - - def render_POST(self, request: Request, game_code: str, matchers: Dict) -> bytes: - version = int(matchers['version']) - endpoint = matchers['endpoint'] - req_raw = request.content.getvalue() + def get_routes(self) -> List[Route]: + return [ + Route("/SDED/{version:int}/{endpoint:str}", self.render_POST, methods=['POST']) + ] + + async def render_POST(self, request: Request) -> bytes: + version: int = request.path_params.get('version') + endpoint: str = request.path_params.get('endpoint') + req_raw = await request.body() internal_ver = 0 client_ip = Utils.get_ip_addr(request) @@ -103,7 +102,7 @@ class CardMakerServlet(BaseServlet): self.logger.error( f"Failed to decompress v{version} {endpoint} request -> {e}" ) - return zlib.compress(b'{"stat": "0"}') + return Response(zlib.compress(b'{"stat": "0"}')) req_data = json.loads(unzip) @@ -114,20 +113,20 @@ class CardMakerServlet(BaseServlet): if not hasattr(self.versions[internal_ver], func_to_find): self.logger.warning(f"Unhandled v{version} request {endpoint}") - return zlib.compress(b'{"returnCode": 1}') + return Response(zlib.compress(b'{"returnCode": 1}')) try: handler = getattr(self.versions[internal_ver], func_to_find) - resp = handler(req_data) + resp = await handler(req_data) except Exception as e: self.logger.error(f"Error handling v{version} method {endpoint} - {e}") raise - return zlib.compress(b'{"stat": "0"}') + return Response(zlib.compress(b'{"stat": "0"}')) if resp is None: resp = {"returnCode": 1} self.logger.debug(f"Response {resp}") - return zlib.compress(json.dumps(resp, ensure_ascii=False).encode("utf-8")) + return Response(zlib.compress(json.dumps(resp, ensure_ascii=False).encode("utf-8"))) diff --git a/titles/cm/read.py b/titles/cm/read.py index 376789d..2b5ec8a 100644 --- a/titles/cm/read.py +++ b/titles/cm/read.py @@ -50,7 +50,7 @@ class CardMakerReader(BaseReader): ): return f"{root}/{dir}" - def read(self) -> None: + async def read(self) -> None: static_datas = { "static_gachas.csv": "read_ongeki_gacha_csv", "static_gacha_cards.csv": "read_ongeki_gacha_card_csv", @@ -59,14 +59,14 @@ class CardMakerReader(BaseReader): if self.bin_dir is not None: data_dir = self._get_card_maker_directory(self.bin_dir) - self.read_chuni_card(f"{data_dir}/CHU/Data/A000/card") - self.read_chuni_gacha(f"{data_dir}/CHU/Data/A000/gacha") + await self.read_chuni_card(f"{data_dir}/CHU/Data/A000/card") + await self.read_chuni_gacha(f"{data_dir}/CHU/Data/A000/gacha") - self.read_mai2_card(f"{data_dir}/MAI/Data/A000/card") + await self.read_mai2_card(f"{data_dir}/MAI/Data/A000/card") for file, func in static_datas.items(): if os.path.exists(f"{self.bin_dir}/MU3/{file}"): read_csv = getattr(CardMakerReader, func) - read_csv(self, f"{self.bin_dir}/MU3/{file}") + await read_csv(self, f"{self.bin_dir}/MU3/{file}") else: self.logger.warning( f"Couldn't find {file} file in {self.bin_dir}, skipping" @@ -78,12 +78,12 @@ class CardMakerReader(BaseReader): # ONGEKI (MU3) cnnot easily access the bin data(A000.pac) # so only opt_dir will work for now for dir in data_dirs: - self.read_chuni_card(f"{dir}/CHU/card") - self.read_chuni_gacha(f"{dir}/CHU/gacha") - self.read_mai2_card(f"{dir}/MAI/card") - self.read_ongeki_gacha(f"{dir}/MU3/gacha") + await self.read_chuni_card(f"{dir}/CHU/card") + await self.read_chuni_gacha(f"{dir}/CHU/gacha") + await self.read_mai2_card(f"{dir}/MAI/card") + await self.read_ongeki_gacha(f"{dir}/MU3/gacha") - def read_chuni_card(self, base_dir: str) -> None: + async def read_chuni_card(self, base_dir: str) -> None: self.logger.info(f"Reading cards from {base_dir}...") version_ids = { @@ -114,7 +114,7 @@ class CardMakerReader(BaseReader): chain = int(troot.find("chain").text) skill_name = troot.find("skillName").text - self.chuni_data.static.put_card( + await self.chuni_data.static.put_card( version, card_id, charaName=chara_name, @@ -131,7 +131,7 @@ class CardMakerReader(BaseReader): self.logger.info(f"Added chuni card {card_id}") - def read_chuni_gacha(self, base_dir: str) -> None: + async def read_chuni_gacha(self, base_dir: str) -> None: self.logger.info(f"Reading gachas from {base_dir}...") version_ids = { @@ -158,7 +158,7 @@ class CardMakerReader(BaseReader): True if troot.find("ceilingType").text == "1" else False ) - self.chuni_data.static.put_gacha( + await self.chuni_data.static.put_gacha( version, gacha_id, name, @@ -181,7 +181,7 @@ class CardMakerReader(BaseReader): True if gacha_card.find("pickup").text == "1" else False ) - self.chuni_data.static.put_gacha_card( + await self.chuni_data.static.put_gacha_card( gacha_id, card_id, weight=weight, @@ -193,7 +193,7 @@ class CardMakerReader(BaseReader): f"Added chuni card {card_id} to gacha {gacha_id}" ) - def read_mai2_card(self, base_dir: str) -> None: + async def read_mai2_card(self, base_dir: str) -> None: self.logger.info(f"Reading cards from {base_dir}...") version_ids = { @@ -206,6 +206,7 @@ class CardMakerReader(BaseReader): "1.25": Mai2Constants.VER_MAIMAI_DX_UNIVERSE_PLUS, "1.30": Mai2Constants.VER_MAIMAI_DX_FESTIVAL, "1.35": Mai2Constants.VER_MAIMAI_DX_FESTIVAL_PLUS, + "1.40": Mai2Constants.VER_MAIMAI_DX_BUDDIES, } for root, dirs, files in os.walk(base_dir): @@ -225,24 +226,18 @@ class CardMakerReader(BaseReader): True if troot.find("disable").text == "false" else False ) - # check if a date is part of the name and disable the - # card if it is - enabled = ( - False if re.search(r"\d{2}/\d{2}/\d{2}", name) else enabled - ) - - self.mai2_data.static.put_card( + await self.mai2_data.static.put_card( version, card_id, name, enabled=enabled ) self.logger.info(f"Added mai2 card {card_id}") - def read_ongeki_gacha_csv(self, file_path: str) -> None: + async def read_ongeki_gacha_csv(self, file_path: str) -> None: self.logger.info(f"Reading gachas from {file_path}...") with open(file_path, encoding="utf-8") as f: reader = csv.DictReader(f) for row in reader: - self.ongeki_data.static.put_gacha( + await self.ongeki_data.static.put_gacha( row["version"], row["gachaId"], row["gachaName"], @@ -254,13 +249,13 @@ class CardMakerReader(BaseReader): self.logger.info(f"Added ongeki gacha {row['gachaId']}") - def read_ongeki_gacha_card_csv(self, file_path: str) -> None: + async def read_ongeki_gacha_card_csv(self, file_path: str) -> None: self.logger.info(f"Reading gacha cards from {file_path}...") with open(file_path, encoding="utf-8") as f: reader = csv.DictReader(f) for row in reader: - self.ongeki_data.static.put_gacha_card( + await self.ongeki_data.static.put_gacha_card( row["gachaId"], row["cardId"], rarity=row["rarity"], @@ -271,7 +266,7 @@ class CardMakerReader(BaseReader): self.logger.info(f"Added ongeki card {row['cardId']} to gacha") - def read_ongeki_gacha(self, base_dir: str) -> None: + async def read_ongeki_gacha(self, base_dir: str) -> None: self.logger.info(f"Reading gachas from {base_dir}...") # assuming some GachaKinds based on the GachaType @@ -294,7 +289,7 @@ class CardMakerReader(BaseReader): # skip already existing gachas if ( - self.ongeki_data.static.get_gacha( + await self.ongeki_data.static.get_gacha( OngekiConstants.VER_ONGEKI_BRIGHT_MEMORY, gacha_id ) is not None @@ -320,7 +315,7 @@ class CardMakerReader(BaseReader): is_ceiling = 1 max_select_point = 33 - self.ongeki_data.static.put_gacha( + await self.ongeki_data.static.put_gacha( version, gacha_id, name, diff --git a/titles/cxb/__init__.py b/titles/cxb/__init__.py index 37abdab..cfd4e91 100644 --- a/titles/cxb/__init__.py +++ b/titles/cxb/__init__.py @@ -7,4 +7,3 @@ index = CxbServlet database = CxbData reader = CxbReader game_codes = [CxbConstants.GAME_CODE] -current_schema_version = 1 diff --git a/titles/cxb/base.py b/titles/cxb/base.py index fe583e6..cc4e50d 100644 --- a/titles/cxb/base.py +++ b/titles/cxb/base.py @@ -28,14 +28,14 @@ class CxbBase: return [] - def handle_action_rpreq_request(self, data: Dict) -> Dict: + async def handle_action_rpreq_request(self, data: Dict) -> Dict: return {} - def handle_action_hitreq_request(self, data: Dict) -> Dict: + async def handle_action_hitreq_request(self, data: Dict) -> Dict: return {"data": []} - def handle_auth_usercheck_request(self, data: Dict) -> Dict: - profile = self.data.profile.get_profile_index( + async def handle_auth_usercheck_request(self, data: Dict) -> Dict: + profile = await self.data.profile.get_profile_index( 0, data["usercheck"]["authid"], self.version ) if profile is not None: @@ -45,12 +45,12 @@ class CxbBase: self.logger.info(f"No profile for aime id {data['usercheck']['authid']}") return {"exist": "false", "logout": "true"} - def handle_auth_entry_request(self, data: Dict) -> Dict: + async def handle_auth_entry_request(self, data: Dict) -> Dict: self.logger.info(f"New profile for {data['entry']['authid']}") return {"token": data["entry"]["authid"], "uid": data["entry"]["authid"]} - def handle_auth_login_request(self, data: Dict) -> Dict: - profile = self.data.profile.get_profile_index( + async def handle_auth_login_request(self, data: Dict) -> Dict: + profile = await self.data.profile.get_profile_index( 0, data["login"]["authid"], self.version ) @@ -198,14 +198,14 @@ class CxbBase: ).decode("utf-8") ) - def handle_action_loadrange_request(self, data: Dict) -> Dict: + async def handle_action_loadrange_request(self, data: Dict) -> Dict: range_start = data["loadrange"]["range"][0] range_end = data["loadrange"]["range"][1] uid = data["loadrange"]["uid"] self.logger.info(f"Load data for {uid}") - profile = self.data.profile.get_profile(uid, self.version) - songs = self.data.score.get_best_scores(uid) + profile = await self.data.profile.get_profile(uid, self.version) + songs = await self.data.score.get_best_scores(uid) data1 = [] index = [] @@ -271,7 +271,7 @@ class CxbBase: thread_ScoreData = Thread(target=CxbBase.task_generateScoreData(song, index, data1)) thread_ScoreData.start() - v_profile = self.data.profile.get_profile_index(0, uid, self.version) + v_profile = await self.data.profile.get_profile_index(0, uid, self.version) v_profile_data = v_profile["data"] for _, data in enumerate(profile): @@ -282,7 +282,7 @@ class CxbBase: return {"index": index, "data": data1, "version": versionindex} - def handle_action_saveindex_request(self, data: Dict) -> Dict: + async def handle_action_saveindex_request(self, data: Dict) -> Dict: save_data = data["saveindex"] try: @@ -300,11 +300,11 @@ class CxbBase: for value in data["saveindex"]["data"]: if "playedUserId" in value[1]: - self.data.profile.put_profile( + await self.data.profile.put_profile( data["saveindex"]["uid"], self.version, value[0], value[1] ) if "mcode" not in value[1]: - self.data.profile.put_profile( + await self.data.profile.put_profile( data["saveindex"]["uid"], self.version, value[0], value[1] ) if "shopId" in value: @@ -335,7 +335,7 @@ class CxbBase: "index": value[0], } ) - self.data.score.put_best_score( + await self.data.score.put_best_score( data["saveindex"]["uid"], song_json["mcode"], self.version, @@ -360,32 +360,32 @@ class CxbBase: for index, value in enumerate(data["saveindex"]["data"]): if int(data["saveindex"]["index"][index]) == 101: - self.data.profile.put_profile( + await self.data.profile.put_profile( aimeId, self.version, data["saveindex"]["index"][index], value ) if ( int(data["saveindex"]["index"][index]) >= 700000 and int(data["saveindex"]["index"][index]) <= 701000 ): - self.data.profile.put_profile( + await self.data.profile.put_profile( aimeId, self.version, data["saveindex"]["index"][index], value ) if ( int(data["saveindex"]["index"][index]) >= 500 and int(data["saveindex"]["index"][index]) <= 510 ): - self.data.profile.put_profile( + await self.data.profile.put_profile( aimeId, self.version, data["saveindex"]["index"][index], value ) if "playedUserId" in value: - self.data.profile.put_profile( + await self.data.profile.put_profile( aimeId, self.version, data["saveindex"]["index"][index], json.loads(value), ) if "mcode" not in value and "normalCR" not in value: - self.data.profile.put_profile( + await self.data.profile.put_profile( aimeId, self.version, data["saveindex"]["index"][index], @@ -437,16 +437,16 @@ class CxbBase: } ) - self.data.score.put_best_score( + await self.data.score.put_best_score( aimeId, data1["mcode"], self.version, indexSongList[i], songCode[0] ) i += 1 return {} - def handle_action_sprankreq_request(self, data: Dict) -> Dict: + async def handle_action_sprankreq_request(self, data: Dict) -> Dict: uid = data["sprankreq"]["uid"] self.logger.info(f"Get best rankings for {uid}") - p = self.data.score.get_best_rankings(uid) + p = await self.data.score.get_best_rankings(uid) rankList: List[Dict[str, Any]] = [] @@ -475,16 +475,16 @@ class CxbBase: "rankx": [1, 1, 1], } - def handle_action_getadv_request(self, data: Dict) -> Dict: + async def handle_action_getadv_request(self, data: Dict) -> Dict: return {"data": [{"r": "1", "i": "100300", "c": "20"}]} - def handle_action_getmsg_request(self, data: Dict) -> Dict: + async def handle_action_getmsg_request(self, data: Dict) -> Dict: return {"msgs": []} - def handle_auth_logout_request(self, data: Dict) -> Dict: + async def handle_auth_logout_request(self, data: Dict) -> Dict: return {"auth": True} - def handle_action_rankreg_request(self, data: Dict) -> Dict: + async def handle_action_rankreg_request(self, data: Dict) -> Dict: uid = data["rankreg"]["uid"] self.logger.info(f"Put {len(data['rankreg']['data'])} rankings for {uid}") @@ -492,7 +492,7 @@ class CxbBase: # REV S2 if "clear" in rid: try: - self.data.score.put_ranking( + await self.data.score.put_ranking( user_id=uid, rev_id=int(rid["rid"]), song_id=int(rid["sc"][1]), @@ -500,7 +500,7 @@ class CxbBase: clear=rid["clear"], ) except Exception: - self.data.score.put_ranking( + await self.data.score.put_ranking( user_id=uid, rev_id=int(rid["rid"]), song_id=0, @@ -510,7 +510,7 @@ class CxbBase: # REV else: try: - self.data.score.put_ranking( + await self.data.score.put_ranking( user_id=uid, rev_id=int(rid["rid"]), song_id=int(rid["sc"][1]), @@ -518,7 +518,7 @@ class CxbBase: clear=0, ) except Exception: - self.data.score.put_ranking( + await self.data.score.put_ranking( user_id=uid, rev_id=int(rid["rid"]), song_id=0, @@ -527,15 +527,15 @@ class CxbBase: ) return {} - def handle_action_addenergy_request(self, data: Dict) -> Dict: + async def handle_action_addenergy_request(self, data: Dict) -> Dict: uid = data["addenergy"]["uid"] self.logger.info(f"Add energy to user {uid}") - profile = self.data.profile.get_profile_index(0, uid, self.version) + profile = await self.data.profile.get_profile_index(0, uid, self.version) data1 = profile["data"] - p = self.data.item.get_energy(uid) + p = await self.data.item.get_energy(uid) if not p: - self.data.item.put_energy(uid, 5) + await self.data.item.put_energy(uid, 5) return { "class": data1["myClass"], @@ -548,7 +548,7 @@ class CxbBase: energy = p["energy"] newenergy = int(energy) + 5 - self.data.item.put_energy(uid, newenergy) + await self.data.item.put_energy(uid, newenergy) if int(energy) <= 995: array.append( @@ -570,10 +570,10 @@ class CxbBase: ) return array[0] - def handle_action_eventreq_request(self, data: Dict) -> Dict: + async def handle_action_eventreq_request(self, data: Dict) -> Dict: self.logger.info(data) return {"eventreq": ""} - def handle_action_stampreq_request(self, data: Dict) -> Dict: + async def handle_action_stampreq_request(self, data: Dict) -> Dict: self.logger.info(data) return {"stampreq": ""} \ No newline at end of file diff --git a/titles/cxb/config.py b/titles/cxb/config.py index fa5a6a3..49ab7c0 100644 --- a/titles/cxb/config.py +++ b/titles/cxb/config.py @@ -18,6 +18,12 @@ class CxbServerConfig: self.__config, "cxb", "server", "loglevel", default="info" ) ) + + @property + def use_https(self) -> bool: + return CoreConfig.get_config_field( + self.__config, "cxb", "server", "use_https", default=True + ) class CxbConfig(dict): diff --git a/titles/cxb/data/rss1/Shop/ShopList_Sale.csv b/titles/cxb/data/rss1/Shop/ShopList_Sale.csv deleted file mode 100644 index 5e9fb72..0000000 --- a/titles/cxb/data/rss1/Shop/ShopList_Sale.csv +++ /dev/null @@ -1,3 +0,0 @@ -saleID.,�J�n��,�I����,ShopID,Price, -0,1411696799,1443236400,0,7000, -1,1411783199,1443322800,1,7000, diff --git a/titles/cxb/data/rss1/Shop/ShopList_SkinBg.csv b/titles/cxb/data/rss1/Shop/ShopList_SkinBg.csv deleted file mode 100644 index 44c4843..0000000 --- a/titles/cxb/data/rss1/Shop/ShopList_SkinBg.csv +++ /dev/null @@ -1,4 +0,0 @@ -shopID.,pNo.,Ver.,otO,otOQID,,o,Ŏ,ItemCode,i,\^Cv,Text,Type,Value(op),Value,Ώۋ,Difficulty(op),Difficulty,Level(op),Level,Grade(Op),Grade,GaugeType(op),GaugeType,HS(op)i,HS,APP,DAP,F-V,F-H,FullCombo,Combo(op),Combo,ClearRate(op),ClearRate,vC,n, -3000,1,1.00.00,1,-1,-,1411697520.0288,1443233520.0288,skb0000,10,1,MASTERȏ2S+ȏtR{NAB,1,1,1,-,1,2,-1,-1,1,1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,,, -3001,2,1.00.00,1,-1,-,1411697520.0288,1443233520.0288,skb0001,10,1,Next Frontier (MasterjNA,0,-1,-1,bleeze,1,2,-1,-1,1,1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,,, -3002,3,1.00.00,1,-1,-,1412103600.0288,1443639598.992,skb0002,10,2,Masterȏ1ȂS+ȏŃNAB,1,1,1,-,1,2,-1,-1,1,1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,,1, diff --git a/titles/cxb/data/rss1/Shop/ShopList_SkinEffect.csv b/titles/cxb/data/rss1/Shop/ShopList_SkinEffect.csv deleted file mode 100644 index bb6486e..0000000 --- a/titles/cxb/data/rss1/Shop/ShopList_SkinEffect.csv +++ /dev/null @@ -1,11 +0,0 @@ -shopID.,�����pNo.,Ver.,�o���t���O,�o���t���O�Q��ID,����,�o������,���Ŏ���,ItemCode,���i,�\���^�C�v,Text,Type,Value(op),Value,�Ώۋ�,Difficulty(op),Difficulty,Level(op),Level,Grade(Op),Grade,GaugeType(op),GaugeType,HS(op)i,HS,APP,DAP,F-V,F-H,FullCombo,Combo(op),Combo,ClearRate(op),ClearRate,�v���C��,�n��, -5000,1,10000,1,-1,-,1411697520,1443233520,ske0000,10,1,MASTER�ȏ��2��S+�ȏ�t���R���{�N���A����B,1,1,1,-,1,2,-1,-1,1,1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,,, -5001,2,10000,1,-1,-,1411697520,1443233520,ske0001,10,1,Next Frontier (Master�j���N���A,0,-1,-1,megaro,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,,, -5002,3,10000,1,-1,-,1412103600,1443639598,ske0002,10,2,Master�ȏ��1�Ȃ�S+�ȏ�ŃN���A����B,1,1,1,-,1,2,-1,-1,1,1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,,1, -5003,4,10000,1,-1,-,1412103600,1443639598,ske0003,10,0,Master�ȏ��1�Ȃ�S+�ȏ�ŃN���A����B,1,1,1,-,1,2,-1,-1,1,1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,,, -5004,5,10000,1,-1,-,1412103600,1443639598,ske0004,10,2,2�ȃN���A,1,1,2,-,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,,, -5005,5,10000,1,-1,-,1412103600,1443639598,ske0005,10,2,3�ȃN���A,1,1,3,-,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,,, -5006,5,10000,1,-1,-,1412103600,1443639598,ske0006,10,2,4�ȃN���A,1,1,4,-,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,,, -5007,5,10000,1,-1,-,1412103600,1443639598,ske0007,10,2,5�ȃN���A,1,1,5,-,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,,, -5008,5,10000,1,-1,-,1412103600,1443639598,ske0008,10,2,6�ȃN���A,1,1,6,-,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,,, -5009,5,10000,1,-1,-,1412103600,1443639598,ske0009,10,2,7�ȃN���A,1,1,7,-,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,,, diff --git a/titles/cxb/data/rss1/Shop/ShopList_SkinNotes.csv b/titles/cxb/data/rss1/Shop/ShopList_SkinNotes.csv deleted file mode 100644 index 0b496f2..0000000 --- a/titles/cxb/data/rss1/Shop/ShopList_SkinNotes.csv +++ /dev/null @@ -1,6 +0,0 @@ -shopID.,�����pNo.,Ver.,�o���t���O,�o���t���O�Q��ID,����,�o������,���Ŏ���,ItemCode,���i,�\���^�C�v,Text,Type,Value(op),Value,�Ώۋ�,Difficulty(op),Difficulty,Level(op),Level,Grade(Op),Grade,GaugeType(op),GaugeType,HS(op)i,HS,APP,DAP,F-V,F-H,FullCombo,Combo(op),Combo,ClearRate(op),ClearRate,�v���C��,�n��, -4000,1,10000,1,-1,-,1411697520,4096483201,skt0000,10,1,MASTER�ȏ��2��S+�ȏ�t���R���{�N���A����B,1,1,1,-,1,2,-1,-1,1,1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,,, -4001,2,10000,1,-1,-,1411697520,4096483201,skt0001,10,1,Next Frontier (Master�j���N���A,0,-1,-1,megaro,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,,, -4002,3,10000,1,-1,-,1412103600,4096483201,skt0002,10,2,Master�ȏ��1�Ȃ�S+�ȏ�ŃN���A����B,1,1,1,-,1,2,-1,-1,1,1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,,1, -4003,4,10000,1,-1,-,1412103600,4096483201,skt0003,10,0,Master�ȏ��1�Ȃ�S+�ȏ�ŃN���A����B,1,1,1,-,1,2,-1,-1,1,1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,,, -4004,5,10000,1,-1,-,1412103600,4096483201,skt0004,10,2,aaaaaaaaaaaaaaaaa,1,1,20,-,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,,, diff --git a/titles/cxb/index.py b/titles/cxb/index.py index 04bbc92..513b813 100644 --- a/titles/cxb/index.py +++ b/titles/cxb/index.py @@ -1,4 +1,6 @@ -from twisted.web.http import Request +from starlette.requests import Request +from starlette.routing import Route +from starlette.responses import Response, JSONResponse import traceback import sys import yaml @@ -11,7 +13,7 @@ from typing import Dict, Tuple, List from os import path from core.config import CoreConfig -from core.title import BaseServlet +from core.title import BaseServlet, JSONResponseNoASCII from core.utils import Utils from .config import CxbConfig from .const import CxbConstants @@ -62,6 +64,14 @@ class CxbServlet(BaseServlet): CxbRevSunriseS2(core_cfg, self.game_cfg), ] + def get_routes(self) -> List[Route]: + return [ + Route("/data", self.handle_data, methods=['POST']), + Route("/action", self.handle_action, methods=['POST']), + Route("/v2/action", self.handle_action, methods=['POST']), + Route("/auth", self.handle_auth, methods=['POST']), + ] + @classmethod def is_game_enabled(cls, game_code: str, core_cfg: CoreConfig, cfg_dir: str) -> bool: game_cfg = CxbConfig() @@ -79,35 +89,22 @@ class CxbServlet(BaseServlet): title_port_int = Utils.get_title_port(self.core_cfg) title_port_ssl_int = Utils.get_title_port_ssl(self.core_cfg) - proto = "https" if title_port_ssl_int != 443 else "http" + proto = "https" if self.game_cfg.server.use_https else "http" if proto == "https": - t_port = f":{title_port_ssl_int}" if title_port_ssl_int and not self.core_cfg.server.is_using_proxy else "" + t_port = f":{title_port_ssl_int}" if title_port_ssl_int != 443 else "" else: - t_port = f":{title_port_int}" if title_port_int and not self.core_cfg.server.is_using_proxy else "" + t_port = f":{title_port_int}" if title_port_int != 80 else "" return ( - f"{proto}://{self.core_cfg.title.hostname}{t_port}", + f"{proto}://{self.core_cfg.server.hostname}{t_port}", "", ) - - def get_endpoint_matchers(self) -> Tuple[List[Tuple[str, str, Dict]], List[Tuple[str, str, Dict]]]: - return ( - [], - [ - ("handle_data", "/data", {}), - ("handle_action", "/action", {}), - ("handle_action", "/v2/action", {}), - ("handle_auth", "/auth", {}), - ] - ) - def preprocess(self, req: Request) -> Dict: - try: - req_bytes = req.content.getvalue() - except: - req_bytes = req.content.read() # Can we just use this one? + + async def preprocess(self, req: Request) -> Dict: + req_bytes = await req.body() try: req_json: Dict = json.loads(req_bytes) @@ -126,8 +123,8 @@ class CxbServlet(BaseServlet): return req_json - def handle_data(self, request: Request, game_code: str, matchers: Dict) -> bytes: - req_json = self.preprocess(request) + async def handle_data(self, request: Request) -> bytes: + req_json = await self.preprocess(request) func_to_find = "handle_data_" version_string = "Base" internal_ver = 0 @@ -135,7 +132,7 @@ class CxbServlet(BaseServlet): if req_json == {}: self.logger.warning(f"Empty json request to /data") - return b"" + return Response() subcmd = list(req_json.keys())[0] if subcmd == "dldate": @@ -145,14 +142,14 @@ class CxbServlet(BaseServlet): or "filetype" not in req_json["dldate"] ): self.logger.warning(f"Malformed dldate request: {req_json}") - return b"" + return Response() filetype = req_json["dldate"]["filetype"] filetype_split = filetype.split("/") if len(filetype_split) < 2 or not filetype_split[0].isnumeric(): self.logger.warning(f"Malformed dldate request: {req_json}") - return b"" + return Response() version = int(filetype_split[0]) filename = filetype_split[len(filetype_split) - 1] @@ -184,7 +181,7 @@ class CxbServlet(BaseServlet): if not hasattr(self.versions[internal_ver], func_to_find): self.logger.warn(f"{version_string} has no handler for filetype {filetype} / {func_to_find}") - return({"data":""}) + return JSONResponse({"data":""}) self.logger.info(f"{version_string} request for filetype {filetype}") self.logger.debug(req_json) @@ -192,7 +189,7 @@ class CxbServlet(BaseServlet): handler = getattr(self.versions[internal_ver], func_to_find) try: - resp = handler(req_json) + resp = await handler(req_json) except Exception as e: self.logger.error(f"Error handling request for file {filetype} - {e}") @@ -201,19 +198,19 @@ class CxbServlet(BaseServlet): traceback.print_exception(tp, val, tb, limit=1) with open("{0}/{1}.log".format(self.core_cfg.server.log_dir, "cxb"), "a") as f: traceback.print_exception(tp, val, tb, limit=1, file=f) - return "" + return Response() self.logger.debug(f"{version_string} Response {resp}") - return json.dumps(resp, ensure_ascii=False).encode("utf-8") + return JSONResponseNoASCII(resp) - def handle_action(self, request: Request, game_code: str, matchers: Dict) -> bytes: - req_json = self.preprocess(request) + async def handle_action(self, request: Request) -> bytes: + req_json = await self.preprocess(request) subcmd = list(req_json.keys())[0] func_to_find = f"handle_action_{subcmd}_request" if not hasattr(self.versions[0], func_to_find): self.logger.warn(f"No handler for action {subcmd} request") - return "" + return Response() self.logger.info(f"Action {subcmd} Request") self.logger.debug(req_json) @@ -221,7 +218,7 @@ class CxbServlet(BaseServlet): handler = getattr(self.versions[0], func_to_find) try: - resp = handler(req_json) + resp = await handler(req_json) except Exception as e: self.logger.error(f"Error handling action {subcmd} request - {e}") @@ -230,19 +227,19 @@ class CxbServlet(BaseServlet): traceback.print_exception(tp, val, tb, limit=1) with open("{0}/{1}.log".format(self.core_cfg.server.log_dir, "cxb"), "a") as f: traceback.print_exception(tp, val, tb, limit=1, file=f) - return "" + return Response() self.logger.debug(f"Response {resp}") - return json.dumps(resp, ensure_ascii=False).encode("utf-8") + return JSONResponseNoASCII(resp) - def handle_auth(self, request: Request, game_code: str, matchers: Dict) -> bytes: - req_json = self.preprocess(request) + async def handle_auth(self, request: Request) -> bytes: + req_json = await self.preprocess(request) subcmd = list(req_json.keys())[0] func_to_find = f"handle_auth_{subcmd}_request" if not hasattr(self.versions[0], func_to_find): self.logger.warn(f"No handler for auth {subcmd} request") - return "" + return Response() self.logger.info(f"Action {subcmd} Request") self.logger.debug(req_json) @@ -250,7 +247,7 @@ class CxbServlet(BaseServlet): handler = getattr(self.versions[0], func_to_find) try: - resp = handler(req_json) + resp = await handler(req_json) except Exception as e: self.logger.error(f"Error handling auth {subcmd} request - {e}") @@ -259,7 +256,7 @@ class CxbServlet(BaseServlet): traceback.print_exception(tp, val, tb, limit=1) with open("{0}/{1}.log".format(self.core_cfg.server.log_dir, "cxb"), "a") as f: traceback.print_exception(tp, val, tb, limit=1, file=f) - return "" + return Response() self.logger.debug(f"Response {resp}") - return json.dumps(resp, ensure_ascii=False).encode("utf-8") + return JSONResponseNoASCII(resp) diff --git a/titles/cxb/read.py b/titles/cxb/read.py index b71740d..9a2ae98 100644 --- a/titles/cxb/read.py +++ b/titles/cxb/read.py @@ -1,6 +1,5 @@ -from typing import Optional, Dict, List -from os import walk, path -import urllib +from typing import Optional +from os import path import csv from read import BaseReader @@ -8,7 +7,6 @@ from core.config import CoreConfig from titles.cxb.database import CxbData from titles.cxb.const import CxbConstants - class CxbReader(BaseReader): def __init__( self, @@ -29,17 +27,14 @@ class CxbReader(BaseReader): self.logger.error(f"Invalid project cxb version {version}") exit(1) - def read(self) -> None: - pull_bin_ram = True + async def read(self) -> None: + if path.exists(self.bin_dir): + await self.read_csv(self.bin_dir) + + else: + self.logger.warn(f"{self.bin_dir} does not exist, nothing to import") - if not path.exists(f"{self.bin_dir}"): - self.logger.warning(f"Couldn't find csv file in {self.bin_dir}, skipping") - pull_bin_ram = False - - if pull_bin_ram: - self.read_csv(f"{self.bin_dir}") - - def read_csv(self, bin_dir: str) -> None: + async def read_csv(self, bin_dir: str) -> None: self.logger.info(f"Read csv from {bin_dir}") try: @@ -55,7 +50,7 @@ class CxbReader(BaseReader): if not "N/A" in row["standard"]: self.logger.info(f"Added song {song_id} chart 0") - self.data.static.put_music( + await self.data.static.put_music( self.version, song_id, index, @@ -71,7 +66,7 @@ class CxbReader(BaseReader): ) if not "N/A" in row["hard"]: self.logger.info(f"Added song {song_id} chart 1") - self.data.static.put_music( + await self.data.static.put_music( self.version, song_id, index, @@ -83,7 +78,7 @@ class CxbReader(BaseReader): ) if not "N/A" in row["master"]: self.logger.info(f"Added song {song_id} chart 2") - self.data.static.put_music( + await self.data.static.put_music( self.version, song_id, index, @@ -97,7 +92,7 @@ class CxbReader(BaseReader): ) if not "N/A" in row["unlimited"]: self.logger.info(f"Added song {song_id} chart 3") - self.data.static.put_music( + await self.data.static.put_music( self.version, song_id, index, @@ -113,7 +108,7 @@ class CxbReader(BaseReader): ) if not "N/A" in row["easy"]: self.logger.info(f"Added song {song_id} chart 4") - self.data.static.put_music( + await self.data.static.put_music( self.version, song_id, index, diff --git a/titles/cxb/rev.py b/titles/cxb/rev.py index e311a1e..7e4f591 100644 --- a/titles/cxb/rev.py +++ b/titles/cxb/rev.py @@ -17,15 +17,15 @@ class CxbRev(CxbBase): super().__init__(cfg, game_cfg) self.version = CxbConstants.VER_CROSSBEATS_REV - def handle_data_path_list_request(self, data: Dict) -> Dict: + async def handle_data_path_list_request(self, data: Dict) -> Dict: return {"data": ""} - def handle_data_putlog_request(self, data: Dict) -> Dict: + async def handle_data_putlog_request(self, data: Dict) -> Dict: if data["putlog"]["type"] == "ResultLog": score_data = json.loads(data["putlog"]["data"]) userid = score_data["usid"] - self.data.score.put_playlog( + await self.data.score.put_playlog( userid, score_data["mcode"], score_data["difficulty"], @@ -40,14 +40,15 @@ class CxbRev(CxbBase): score_data["slow2"], score_data["fail"], score_data["combo"], + score_data["grade"], ) return {"data": True} return {"data": True} @cached(lifetime=86400) - def handle_data_music_list_request(self, data: Dict) -> Dict: + async def handle_data_music_list_request(self, data: Dict) -> Dict: ret_str = "" - with open(r"titles/cxb/data/rss/MusicArchiveList.csv") as music: + with open(r"titles/cxb/data/rev/MusicArchiveList.csv") as music: lines = music.readlines() for line in lines: line_split = line.split(",") @@ -56,10 +57,10 @@ class CxbRev(CxbBase): return {"data": ret_str} @cached(lifetime=86400) - def handle_data_item_list_icon_request(self, data: Dict) -> Dict: + async def handle_data_item_list_icon_request(self, data: Dict) -> Dict: ret_str = "\r\n#ItemListIcon\r\n" with open( - r"titles/cxb/data/rss/Item/ItemArchiveList_Icon.csv", encoding="utf-8" + r"titles/cxb/data/rev/Item/ItemArchiveList_Icon.csv", encoding="shift-jis" ) as item: lines = item.readlines() for line in lines: @@ -67,10 +68,10 @@ class CxbRev(CxbBase): return {"data": ret_str} @cached(lifetime=86400) - def handle_data_item_list_skin_notes_request(self, data: Dict) -> Dict: + async def handle_data_item_list_skin_notes_request(self, data: Dict) -> Dict: ret_str = "\r\n#ItemListSkinNotes\r\n" with open( - r"titles/cxb/data/rss/Item/ItemArchiveList_SkinNotes.csv", encoding="utf-8" + r"titles/cxb/data/rev/Item/ItemArchiveList_SkinNotes.csv", encoding="utf-8" ) as item: lines = item.readlines() for line in lines: @@ -78,10 +79,10 @@ class CxbRev(CxbBase): return {"data": ret_str} @cached(lifetime=86400) - def handle_data_item_list_skin_effect_request(self, data: Dict) -> Dict: + async def handle_data_item_list_skin_effect_request(self, data: Dict) -> Dict: ret_str = "\r\n#ItemListSkinEffect\r\n" with open( - r"titles/cxb/data/rss/Item/ItemArchiveList_SkinEffect.csv", encoding="utf-8" + r"titles/cxb/data/rev/Item/ItemArchiveList_SkinEffect.csv", encoding="utf-8" ) as item: lines = item.readlines() for line in lines: @@ -89,10 +90,10 @@ class CxbRev(CxbBase): return {"data": ret_str} @cached(lifetime=86400) - def handle_data_item_list_skin_bg_request(self, data: Dict) -> Dict: + async def handle_data_item_list_skin_bg_request(self, data: Dict) -> Dict: ret_str = "\r\n#ItemListSkinBg\r\n" with open( - r"titles/cxb/data/rss/Item/ItemArchiveList_SkinBg.csv", encoding="utf-8" + r"titles/cxb/data/rev/Item/ItemArchiveList_SkinBg.csv", encoding="utf-8" ) as item: lines = item.readlines() for line in lines: @@ -100,10 +101,10 @@ class CxbRev(CxbBase): return {"data": ret_str} @cached(lifetime=86400) - def handle_data_item_list_title_request(self, data: Dict) -> Dict: + async def handle_data_item_list_title_request(self, data: Dict) -> Dict: ret_str = "\r\n#ItemListTitle\r\n" with open( - r"titles/cxb/data/rss/Item/ItemList_Title.csv", encoding="shift-jis" + r"titles/cxb/data/rev/Item/ItemList_Title.csv", encoding="shift-jis" ) as item: lines = item.readlines() for line in lines: @@ -111,10 +112,10 @@ class CxbRev(CxbBase): return {"data": ret_str} @cached(lifetime=86400) - def handle_data_shop_list_music_request(self, data: Dict) -> Dict: + async def handle_data_shop_list_music_request(self, data: Dict) -> Dict: ret_str = "\r\n#ShopListMusic\r\n" with open( - r"titles/cxb/data/rss/Shop/ShopList_Music.csv", encoding="shift-jis" + r"titles/cxb/data/rev/Shop/ShopList_Music.csv", encoding="shift-jis" ) as shop: lines = shop.readlines() for line in lines: @@ -122,10 +123,10 @@ class CxbRev(CxbBase): return {"data": ret_str} @cached(lifetime=86400) - def handle_data_shop_list_icon_request(self, data: Dict) -> Dict: + async def handle_data_shop_list_icon_request(self, data: Dict) -> Dict: ret_str = "\r\n#ShopListIcon\r\n" with open( - r"titles/cxb/data/rss/Shop/ShopList_Icon.csv", encoding="shift-jis" + r"titles/cxb/data/rev/Shop/ShopList_Icon.csv", encoding="shift-jis" ) as shop: lines = shop.readlines() for line in lines: @@ -133,83 +134,88 @@ class CxbRev(CxbBase): return {"data": ret_str} @cached(lifetime=86400) - def handle_data_shop_list_title_request(self, data: Dict) -> Dict: + async def handle_data_shop_list_title_request(self, data: Dict) -> Dict: ret_str = "\r\n#ShopListTitle\r\n" with open( - r"titles/cxb/data/rss/Shop/ShopList_Title.csv", encoding="shift-jis" + r"titles/cxb/data/rev/Shop/ShopList_Title.csv", encoding="shift-jis" ) as shop: lines = shop.readlines() for line in lines: ret_str += f"{line[:-1]}\r\n" return {"data": ret_str} - def handle_data_shop_list_skin_hud_request(self, data: Dict) -> Dict: + async def handle_data_shop_list_skin_hud_request(self, data: Dict) -> Dict: return {"data": ""} - def handle_data_shop_list_skin_arrow_request(self, data: Dict) -> Dict: + async def handle_data_shop_list_skin_arrow_request(self, data: Dict) -> Dict: return {"data": ""} - def handle_data_shop_list_skin_hit_request(self, data: Dict) -> Dict: + async def handle_data_shop_list_skin_hit_request(self, data: Dict) -> Dict: return {"data": ""} @cached(lifetime=86400) - def handle_data_shop_list_sale_request(self, data: Dict) -> Dict: + async def handle_data_shop_list_sale_request(self, data: Dict) -> Dict: ret_str = "\r\n#ShopListSale\r\n" with open( - r"titles/cxb/data/rss/Shop/ShopList_Sale.csv", encoding="shift-jis" + r"titles/cxb/data/rev/Shop/ShopList_Sale.csv", encoding="shift-jis" ) as shop: lines = shop.readlines() for line in lines: ret_str += f"{line[:-1]}\r\n" return {"data": ret_str} - def handle_data_extra_stage_list_request(self, data: Dict) -> Dict: - return {"data": ""} + async def handle_data_extra_stage_list_request(self, data: Dict) -> Dict: + ret_str = "" + with open(r"titles/cxb/data/rev/ExtraStageList.csv", encoding="shift-jis") as stage: + lines = stage.readlines() + for line in lines: + ret_str += f"{line[:-1]}\r\n" + return {"data": ret_str} @cached(lifetime=86400) - def handle_data_exxxxx_request(self, data: Dict) -> Dict: + async def handle_data_exxxxx_request(self, data: Dict) -> Dict: extra_num = int(data["dldate"]["filetype"][-4:]) ret_str = "" with open( - rf"titles/cxb/data/rss/Ex000{extra_num}.csv", encoding="shift-jis" + rf"titles/cxb/data/rev/Ex000{extra_num}.csv", encoding="shift-jis" ) as stage: lines = stage.readlines() for line in lines: ret_str += f"{line[:-1]}\r\n" return {"data": ret_str} - def handle_data_bonus_list10100_request(self, data: Dict) -> Dict: + async def handle_data_bonus_list10100_request(self, data: Dict) -> Dict: return {"data": ""} - def handle_data_free_coupon_request(self, data: Dict) -> Dict: + async def handle_data_free_coupon_request(self, data: Dict) -> Dict: return {"data": ""} @cached(lifetime=86400) - def handle_data_news_list_request(self, data: Dict) -> Dict: + async def handle_data_news_list_request(self, data: Dict) -> Dict: ret_str = "" - with open(r"titles/cxb/data/rss/NewsList.csv", encoding="UTF-8") as news: + with open(r"titles/cxb/data/rev/NewsList.csv", encoding="UTF-8") as news: lines = news.readlines() for line in lines: ret_str += f"{line[:-1]}\r\n" return {"data": ret_str} - def handle_data_tips_request(self, data: Dict) -> Dict: + async def handle_data_tips_request(self, data: Dict) -> Dict: return {"data": ""} @cached(lifetime=86400) - def handle_data_license_request(self, data: Dict) -> Dict: + async def handle_data_license_request(self, data: Dict) -> Dict: ret_str = "" - with open(r"titles/cxb/data/rss/License_Offline.csv", encoding="UTF-8") as lic: + with open(r"titles/cxb/data/rev/License_Offline.csv", encoding="UTF-8") as lic: lines = lic.readlines() for line in lines: ret_str += f"{line[:-1]}\r\n" return {"data": ret_str} @cached(lifetime=86400) - def handle_data_course_list_request(self, data: Dict) -> Dict: + async def handle_data_course_list_request(self, data: Dict) -> Dict: ret_str = "" with open( - r"titles/cxb/data/rss/Course/CourseList.csv", encoding="UTF-8" + r"titles/cxb/data/rev/Course/CourseList.csv", encoding="UTF-8" ) as course: lines = course.readlines() for line in lines: @@ -217,12 +223,12 @@ class CxbRev(CxbBase): return {"data": ret_str} @cached(lifetime=86400) - def handle_data_csxxxx_request(self, data: Dict) -> Dict: + async def handle_data_csxxxx_request(self, data: Dict) -> Dict: # Removed the CSVs since the format isnt quite right extra_num = int(data["dldate"]["filetype"][-4:]) ret_str = "" with open( - rf"titles/cxb/data/rss/Course/Cs000{extra_num}.csv", encoding="shift-jis" + rf"titles/cxb/data/rev/Course/Cs000{extra_num}.csv", encoding="shift-jis" ) as course: lines = course.readlines() for line in lines: @@ -230,77 +236,77 @@ class CxbRev(CxbBase): return {"data": ret_str} @cached(lifetime=86400) - def handle_data_mission_list_request(self, data: Dict) -> Dict: + async def handle_data_mission_list_request(self, data: Dict) -> Dict: ret_str = "" with open( - r"titles/cxb/data/rss/MissionList.csv", encoding="shift-jis" + r"titles/cxb/data/rev/MissionList.csv", encoding="shift-jis" ) as mission: lines = mission.readlines() for line in lines: ret_str += f"{line[:-1]}\r\n" return {"data": ret_str} - def handle_data_mission_bonus_request(self, data: Dict) -> Dict: + async def handle_data_mission_bonus_request(self, data: Dict) -> Dict: return {"data": ""} - def handle_data_unlimited_mission_request(self, data: Dict) -> Dict: + async def handle_data_unlimited_mission_request(self, data: Dict) -> Dict: return {"data": ""} @cached(lifetime=86400) - def handle_data_event_list_request(self, data: Dict) -> Dict: + async def handle_data_event_list_request(self, data: Dict) -> Dict: ret_str = "" with open( - r"titles/cxb/data/rss/Event/EventArchiveList.csv", encoding="shift-jis" + r"titles/cxb/data/rev/Event/EventArchiveList.csv", encoding="shift-jis" ) as mission: lines = mission.readlines() for line in lines: ret_str += f"{line[:-1]}\r\n" return {"data": ret_str} - def handle_data_event_music_list_request(self, data: Dict) -> Dict: + async def handle_data_event_music_list_request(self, data: Dict) -> Dict: return {"data": ""} - def handle_data_event_mission_list_request(self, data: Dict) -> Dict: + async def handle_data_event_mission_list_request(self, data: Dict) -> Dict: return {"data": ""} - def handle_data_event_achievement_single_high_score_list_request( + async def handle_data_event_achievement_single_high_score_list_request( self, data: Dict ) -> Dict: return {"data": ""} - def handle_data_event_achievement_single_accumulation_request( + async def handle_data_event_achievement_single_accumulation_request( self, data: Dict ) -> Dict: return {"data": ""} - def handle_data_event_ranking_high_score_list_request(self, data: Dict) -> Dict: + async def handle_data_event_ranking_high_score_list_request(self, data: Dict) -> Dict: return {"data": ""} - def handle_data_event_ranking_accumulation_list_request(self, data: Dict) -> Dict: + async def handle_data_event_ranking_accumulation_list_request(self, data: Dict) -> Dict: return {"data": ""} - def handle_data_event_ranking_stamp_list_request(self, data: Dict) -> Dict: + async def handle_data_event_ranking_stamp_list_request(self, data: Dict) -> Dict: return {"data": ""} - def handle_data_event_ranking_store_list_request(self, data: Dict) -> Dict: + async def handle_data_event_ranking_store_list_request(self, data: Dict) -> Dict: return {"data": ""} - def handle_data_event_ranking_area_list_request(self, data: Dict) -> Dict: + async def handle_data_event_ranking_area_list_request(self, data: Dict) -> Dict: return {"data": ""} @cached(lifetime=86400) - def handle_data_event_stamp_list_request(self, data: Dict) -> Dict: + async def handle_data_event_stamp_list_request(self, data: Dict) -> Dict: ret_str = "" with open( - r"titles/cxb/data/rss/Event/EventStampList.csv", encoding="shift-jis" + r"titles/cxb/data/rev/Event/EventStampList.csv", encoding="shift-jis" ) as event: lines = event.readlines() for line in lines: ret_str += f"{line[:-1]}\r\n" return {"data": ret_str} - def handle_data_event_stamp_map_list_csxxxx_request(self, data: Dict) -> Dict: + async def handle_data_event_stamp_map_list_csxxxx_request(self, data: Dict) -> Dict: return {"data": "1,2,1,1,2,3,9,5,6,7,8,9,10,\r\n"} - def handle_data_server_state_request(self, data: Dict) -> Dict: + async def handle_data_server_state_request(self, data: Dict) -> Dict: return {"data": True} diff --git a/titles/cxb/rss1.py b/titles/cxb/rss1.py index fe43e42..4999cc5 100644 --- a/titles/cxb/rss1.py +++ b/titles/cxb/rss1.py @@ -17,11 +17,11 @@ class CxbRevSunriseS1(CxbBase): super().__init__(cfg, game_cfg) self.version = CxbConstants.VER_CROSSBEATS_REV_SUNRISE_S1 - def handle_data_path_list_request(self, data: Dict) -> Dict: + async def handle_data_path_list_request(self, data: Dict) -> Dict: return {"data": ""} @cached(lifetime=86400) - def handle_data_music_list_request(self, data: Dict) -> Dict: + async def handle_data_music_list_request(self, data: Dict) -> Dict: ret_str = "" with open(r"titles/cxb/data/rss1/MusicArchiveList.csv") as music: lines = music.readlines() @@ -32,7 +32,7 @@ class CxbRevSunriseS1(CxbBase): return {"data": ret_str} @cached(lifetime=86400) - def handle_data_item_list_detail_request(self, data: Dict) -> Dict: + async def handle_data_item_list_detail_request(self, data: Dict) -> Dict: # ItemListIcon load ret_str = "#ItemListIcon\r\n" with open( @@ -54,7 +54,7 @@ class CxbRevSunriseS1(CxbBase): return {"data": ret_str} @cached(lifetime=86400) - def handle_data_shop_list_detail_request(self, data: Dict) -> Dict: + async def handle_data_shop_list_detail_request(self, data: Dict) -> Dict: # ShopListIcon load ret_str = "#ShopListIcon\r\n" with open( @@ -73,42 +73,6 @@ class CxbRevSunriseS1(CxbBase): for line in lines: ret_str += f"{line[:-1]}\r\n" - # ShopListSale load - ret_str += "\r\n#ShopListSale\r\n" - with open( - r"titles/cxb/data/rss1/Shop/ShopList_Sale.csv", encoding="shift-jis" - ) as shop: - lines = shop.readlines() - for line in lines: - ret_str += f"{line[:-1]}\r\n" - - # ShopListSkinBg load - ret_str += "\r\n#ShopListSkinBg\r\n" - with open( - r"titles/cxb/data/rss1/Shop/ShopList_SkinBg.csv", encoding="shift-jis" - ) as shop: - lines = shop.readlines() - for line in lines: - ret_str += f"{line[:-1]}\r\n" - - # ShopListSkinEffect load - ret_str += "\r\n#ShopListSkinEffect\r\n" - with open( - r"titles/cxb/data/rss1/Shop/ShopList_SkinEffect.csv", encoding="shift-jis" - ) as shop: - lines = shop.readlines() - for line in lines: - ret_str += f"{line[:-1]}\r\n" - - # ShopListSkinNotes load - ret_str += "\r\n#ShopListSkinNotes\r\n" - with open( - r"titles/cxb/data/rss1/Shop/ShopList_SkinNotes.csv", encoding="shift-jis" - ) as shop: - lines = shop.readlines() - for line in lines: - ret_str += f"{line[:-1]}\r\n" - # ShopListTitle load ret_str += "\r\n#ShopListTitle\r\n" with open( @@ -119,26 +83,26 @@ class CxbRevSunriseS1(CxbBase): ret_str += f"{line[:-1]}\r\n" return {"data": ret_str} - def handle_data_extra_stage_list_request(self, data: Dict) -> Dict: + async def handle_data_extra_stage_list_request(self, data: Dict) -> Dict: return {"data": ""} - def handle_data_exxxxx_request(self, data: Dict) -> Dict: + async def handle_data_exxxxx_request(self, data: Dict) -> Dict: return {"data": ""} - def handle_data_one_more_extra_list_request(self, data: Dict) -> Dict: + async def handle_data_one_more_extra_list_request(self, data: Dict) -> Dict: return {"data": ""} - def handle_data_bonus_list10100_request(self, data: Dict) -> Dict: + async def handle_data_bonus_list10100_request(self, data: Dict) -> Dict: return {"data": ""} - def handle_data_oexxxx_request(self, data: Dict) -> Dict: + async def handle_data_oexxxx_request(self, data: Dict) -> Dict: return {"data": ""} - def handle_data_free_coupon_request(self, data: Dict) -> Dict: + async def handle_data_free_coupon_request(self, data: Dict) -> Dict: return {"data": ""} @cached(lifetime=86400) - def handle_data_news_list_request(self, data: Dict) -> Dict: + async def handle_data_news_list_request(self, data: Dict) -> Dict: ret_str = "" with open(r"titles/cxb/data/rss1/NewsList.csv", encoding="UTF-8") as news: lines = news.readlines() @@ -146,14 +110,14 @@ class CxbRevSunriseS1(CxbBase): ret_str += f"{line[:-1]}\r\n" return {"data": ret_str} - def handle_data_tips_request(self, data: Dict) -> Dict: + async def handle_data_tips_request(self, data: Dict) -> Dict: return {"data": ""} - def handle_data_release_info_list_request(self, data: Dict) -> Dict: + async def handle_data_release_info_list_request(self, data: Dict) -> Dict: return {"data": ""} @cached(lifetime=86400) - def handle_data_random_music_list_request(self, data: Dict) -> Dict: + async def handle_data_random_music_list_request(self, data: Dict) -> Dict: ret_str = "" with open(r"titles/cxb/data/rss1/MusicArchiveList.csv") as music: lines = music.readlines() @@ -167,7 +131,7 @@ class CxbRevSunriseS1(CxbBase): return {"data": ret_str} @cached(lifetime=86400) - def handle_data_license_request(self, data: Dict) -> Dict: + async def handle_data_license_request(self, data: Dict) -> Dict: ret_str = "" with open(r"titles/cxb/data/rss1/License.csv", encoding="UTF-8") as licenses: lines = licenses.readlines() @@ -176,7 +140,7 @@ class CxbRevSunriseS1(CxbBase): return {"data": ret_str} @cached(lifetime=86400) - def handle_data_course_list_request(self, data: Dict) -> Dict: + async def handle_data_course_list_request(self, data: Dict) -> Dict: ret_str = "" with open( r"titles/cxb/data/rss1/Course/CourseList.csv", encoding="UTF-8" @@ -187,7 +151,7 @@ class CxbRevSunriseS1(CxbBase): return {"data": ret_str} @cached(lifetime=86400) - def handle_data_csxxxx_request(self, data: Dict) -> Dict: + async def handle_data_csxxxx_request(self, data: Dict) -> Dict: extra_num = int(data["dldate"]["filetype"][-4:]) ret_str = "" with open( @@ -198,16 +162,16 @@ class CxbRevSunriseS1(CxbBase): ret_str += f"{line[:-1]}\r\n" return {"data": ret_str} - def handle_data_mission_list_request(self, data: Dict) -> Dict: + async def handle_data_mission_list_request(self, data: Dict) -> Dict: return {"data": ""} - def handle_data_mission_bonus_request(self, data: Dict) -> Dict: + async def handle_data_mission_bonus_request(self, data: Dict) -> Dict: return {"data": ""} - def handle_data_unlimited_mission_request(self, data: Dict) -> Dict: + async def handle_data_unlimited_mission_request(self, data: Dict) -> Dict: return {"data": ""} - def handle_data_partner_list_request(self, data: Dict) -> Dict: + async def handle_data_partner_list_request(self, data: Dict) -> Dict: ret_str = "" # Lord forgive me for the sins I am about to commit for i in range(0, 10): @@ -226,7 +190,7 @@ class CxbRevSunriseS1(CxbBase): return {"data": ret_str} @cached(lifetime=86400) - def handle_data_partnerxxxx_request(self, data: Dict) -> Dict: + async def handle_data_partnerxxxx_request(self, data: Dict) -> Dict: partner_num = int(data["dldate"]["filetype"][-4:]) ret_str = f"{partner_num},,{partner_num},1,10000,\r\n" with open(r"titles/cxb/data/rss1/Partner0000.csv") as partner: @@ -235,13 +199,13 @@ class CxbRevSunriseS1(CxbBase): ret_str += f"{line[:-1]}\r\n" return {"data": ret_str} - def handle_data_server_state_request(self, data: Dict) -> Dict: + async def handle_data_server_state_request(self, data: Dict) -> Dict: return {"data": True} - def handle_data_settings_request(self, data: Dict) -> Dict: + async def handle_data_settings_request(self, data: Dict) -> Dict: return {"data": "2,\r\n"} - def handle_data_story_list_request(self, data: Dict) -> Dict: + async def handle_data_story_list_request(self, data: Dict) -> Dict: # story id, story name, game version, start time, end time, course arc, unlock flag, song mcode for menu ret_str = "\r\n" ret_str += ( @@ -253,23 +217,23 @@ class CxbRevSunriseS1(CxbBase): ret_str += f"st0002,REMNANT,10104,1502127790,4096483201,Cs1000,-1,overcl,\r\n" return {"data": ret_str} - def handle_data_stxxxx_request(self, data: Dict) -> Dict: + async def handle_data_stxxxx_request(self, data: Dict) -> Dict: story_num = int(data["dldate"]["filetype"][-4:]) ret_str = "" for i in range(1, 11): ret_str += f"{i},st000{story_num}_{i-1},,,,,,,,,,,,,,,,1,,-1,1,\r\n" return {"data": ret_str} - def handle_data_event_stamp_list_request(self, data: Dict) -> Dict: + async def handle_data_event_stamp_list_request(self, data: Dict) -> Dict: return {"data": "Cs1032,1,1,1,1,1,1,1,1,1,1,\r\n"} - def handle_data_premium_list_request(self, data: Dict) -> Dict: + async def handle_data_premium_list_request(self, data: Dict) -> Dict: return {"data": "1,,,,10,,,,,99,,,,,,,,,100,,\r\n"} - def handle_data_event_list_request(self, data: Dict) -> Dict: + async def handle_data_event_list_request(self, data: Dict) -> Dict: return {"data": ""} - def handle_data_event_detail_list_request(self, data: Dict) -> Dict: + async def handle_data_event_detail_list_request(self, data: Dict) -> Dict: event_id = data["dldate"]["filetype"].split("/")[2] if "EventStampMapListCs1002" in event_id: return {"data": "1,2,1,1,2,3,9,5,6,7,8,9,10,\r\n"} @@ -278,7 +242,7 @@ class CxbRevSunriseS1(CxbBase): else: return {"data": ""} - def handle_data_event_stamp_map_list_csxxxx_request(self, data: Dict) -> Dict: + async def handle_data_event_stamp_map_list_csxxxx_request(self, data: Dict) -> Dict: event_id = data["dldate"]["filetype"].split("/")[2] if "EventStampMapListCs1002" in event_id: return {"data": "1,2,1,1,2,3,9,5,6,7,8,9,10,\r\n"} diff --git a/titles/cxb/rss2.py b/titles/cxb/rss2.py index b15deda..070d88b 100644 --- a/titles/cxb/rss2.py +++ b/titles/cxb/rss2.py @@ -17,11 +17,11 @@ class CxbRevSunriseS2(CxbBase): super().__init__(cfg, game_cfg) self.version = CxbConstants.VER_CROSSBEATS_REV_SUNRISE_S2_OMNI - def handle_data_path_list_request(self, data: Dict) -> Dict: + async def handle_data_path_list_request(self, data: Dict) -> Dict: return {"data": ""} @cached(lifetime=86400) - def handle_data_music_list_request(self, data: Dict) -> Dict: + async def handle_data_music_list_request(self, data: Dict) -> Dict: version = data["dldate"]["filetype"].split("/")[0] ret_str = "" @@ -41,7 +41,7 @@ class CxbRevSunriseS2(CxbBase): return {"data": ret_str} @cached(lifetime=86400) - def handle_data_item_list_detail_request(self, data: Dict) -> Dict: + async def handle_data_item_list_detail_request(self, data: Dict) -> Dict: # ItemListIcon load ret_str = "#ItemListIcon\r\n" with open( @@ -63,7 +63,7 @@ class CxbRevSunriseS2(CxbBase): return {"data": ret_str} @cached(lifetime=86400) - def handle_data_shop_list_detail_request(self, data: Dict) -> Dict: + async def handle_data_shop_list_detail_request(self, data: Dict) -> Dict: # ShopListIcon load ret_str = "#ShopListIcon\r\n" with open( @@ -128,7 +128,7 @@ class CxbRevSunriseS2(CxbBase): ret_str += f"{line[:-1]}\r\n" return {"data": ret_str} - def handle_data_extra_stage_list_request(self, data: Dict) -> Dict: + async def handle_data_extra_stage_list_request(self, data: Dict) -> Dict: ret_str="" with open(r"titles/cxb/data/rss2/ExtraStageList.csv") as extra: lines = extra.readlines() @@ -136,19 +136,19 @@ class CxbRevSunriseS2(CxbBase): ret_str += f"{line[:-1]}\r\n" return({"data":ret_str}) - def handle_data_exxxxx_request(self, data: Dict) -> Dict: + async def handle_data_exxxxx_request(self, data: Dict) -> Dict: return {"data": ""} - def handle_data_one_more_extra_list_request(self, data: Dict) -> Dict: + async def handle_data_one_more_extra_list_request(self, data: Dict) -> Dict: return {"data": ""} - def handle_data_bonus_list10100_request(self, data: Dict) -> Dict: + async def handle_data_bonus_list10100_request(self, data: Dict) -> Dict: return {"data": ""} - def handle_data_oexxxx_request(self, data: Dict) -> Dict: + async def handle_data_oexxxx_request(self, data: Dict) -> Dict: return {"data": ""} - def handle_data_free_coupon_request(self, data: Dict) -> Dict: + async def handle_data_free_coupon_request(self, data: Dict) -> Dict: ret_str="" with open(r"titles/cxb/data/rss2/FreeCoupon.csv") as coupon: lines = coupon.readlines() @@ -157,7 +157,7 @@ class CxbRevSunriseS2(CxbBase): return({"data":ret_str}) @cached(lifetime=86400) - def handle_data_news_list_request(self, data: Dict) -> Dict: + async def handle_data_news_list_request(self, data: Dict) -> Dict: ret_str = "" with open(r"titles/cxb/data/rss2/NewsList.csv", encoding="UTF-8") as news: lines = news.readlines() @@ -165,14 +165,14 @@ class CxbRevSunriseS2(CxbBase): ret_str += f"{line[:-1]}\r\n" return {"data": ret_str} - def handle_data_tips_request(self, data: Dict) -> Dict: + async def handle_data_tips_request(self, data: Dict) -> Dict: return {"data": ""} - def handle_data_release_info_list_request(self, data: Dict) -> Dict: + async def handle_data_release_info_list_request(self, data: Dict) -> Dict: return {"data": ""} @cached(lifetime=86400) - def handle_data_random_music_list_request(self, data: Dict) -> Dict: + async def handle_data_random_music_list_request(self, data: Dict) -> Dict: ret_str = "" with open(r"titles/cxb/data/rss2/MusicArchiveList.csv") as music: lines = music.readlines() @@ -186,7 +186,7 @@ class CxbRevSunriseS2(CxbBase): return {"data": ret_str} @cached(lifetime=86400) - def handle_data_license_request(self, data: Dict) -> Dict: + async def handle_data_license_request(self, data: Dict) -> Dict: ret_str = "" with open(r"titles/cxb/data/rss2/License.csv", encoding="UTF-8") as licenses: lines = licenses.readlines() @@ -195,7 +195,7 @@ class CxbRevSunriseS2(CxbBase): return {"data": ret_str} @cached(lifetime=86400) - def handle_data_course_list_request(self, data: Dict) -> Dict: + async def handle_data_course_list_request(self, data: Dict) -> Dict: ret_str = "" with open( r"titles/cxb/data/rss2/Course/CourseList.csv", encoding="UTF-8" @@ -206,7 +206,7 @@ class CxbRevSunriseS2(CxbBase): return {"data": ret_str} @cached(lifetime=86400) - def handle_data_csxxxx_request(self, data: Dict) -> Dict: + async def handle_data_csxxxx_request(self, data: Dict) -> Dict: extra_num = int(data["dldate"]["filetype"][-4:]) ret_str = "" with open( @@ -217,16 +217,16 @@ class CxbRevSunriseS2(CxbBase): ret_str += f"{line[:-1]}\r\n" return {"data": ret_str} - def handle_data_mission_list_request(self, data: Dict) -> Dict: + async def handle_data_mission_list_request(self, data: Dict) -> Dict: return {"data": ""} - def handle_data_mission_bonus_request(self, data: Dict) -> Dict: + async def handle_data_mission_bonus_request(self, data: Dict) -> Dict: return {"data": ""} - def handle_data_unlimited_mission_request(self, data: Dict) -> Dict: + async def handle_data_unlimited_mission_request(self, data: Dict) -> Dict: return {"data": ""} - def handle_data_partner_list_request(self, data: Dict) -> Dict: + async def handle_data_partner_list_request(self, data: Dict) -> Dict: ret_str = "" # Lord forgive me for the sins I am about to commit for i in range(0, 10): @@ -245,7 +245,7 @@ class CxbRevSunriseS2(CxbBase): return {"data": ret_str} @cached(lifetime=86400) - def handle_data_partnerxxxx_request(self, data: Dict) -> Dict: + async def handle_data_partnerxxxx_request(self, data: Dict) -> Dict: partner_num = int(data["dldate"]["filetype"][-4:]) ret_str = f"{partner_num},,{partner_num},1,10000,\r\n" with open(r"titles/cxb/data/rss2/Partner0000.csv") as partner: @@ -254,13 +254,13 @@ class CxbRevSunriseS2(CxbBase): ret_str += f"{line[:-1]}\r\n" return {"data": ret_str} - def handle_data_server_state_request(self, data: Dict) -> Dict: + async def handle_data_server_state_request(self, data: Dict) -> Dict: return {"data": True} - def handle_data_settings_request(self, data: Dict) -> Dict: + async def handle_data_settings_request(self, data: Dict) -> Dict: return {"data": "2,\r\n"} - def handle_data_story_list_request(self, data: Dict) -> Dict: + async def handle_data_story_list_request(self, data: Dict) -> Dict: # story id, story name, game version, start time, end time, course arc, unlock flag, song mcode for menu ret_str = "\r\n" ret_str += ( @@ -272,7 +272,7 @@ class CxbRevSunriseS2(CxbBase): ret_str += f"st0002,REMNANT,10104,1502127790,4096483201,Cs1000,-1,overcl,\r\n" return {"data": ret_str} - def handle_data_stxxxx_request(self, data: Dict) -> Dict: + async def handle_data_stxxxx_request(self, data: Dict) -> Dict: story_num = int(data["dldate"]["filetype"][-4:]) ret_str = "" # Each stories appears to have 10 pieces based on the wiki but as on how they are set.... no clue @@ -280,18 +280,18 @@ class CxbRevSunriseS2(CxbBase): ret_str += f"{i},st000{story_num}_{i-1},,,,,,,,,,,,,,,,1,,-1,1,\r\n" return {"data": ret_str} - def handle_data_event_stamp_list_request(self, data: Dict) -> Dict: + async def handle_data_event_stamp_list_request(self, data: Dict) -> Dict: return {"data": "Cs1002,1,1,1,1,1,1,1,1,1,1,\r\n"} - def handle_data_premium_list_request(self, data: Dict) -> Dict: + async def handle_data_premium_list_request(self, data: Dict) -> Dict: return {"data": "1,,,,10,,,,,99,,,,,,,,,100,,\r\n"} - def handle_data_event_list_request(self, data: Dict) -> Dict: + async def handle_data_event_list_request(self, data: Dict) -> Dict: return { "data": "Cs4001,0,10000,1601510400,1604188799,1,nv2006,1,\r\nCs4005,0,10000,1609459200,1615766399,1,nv2006,1,\r\n" } - def handle_data_event_detail_list_request(self, data: Dict) -> Dict: + async def handle_data_event_detail_list_request(self, data: Dict) -> Dict: event_id = data["dldate"]["filetype"].split("/")[2] if "Cs4001" in event_id: return { @@ -308,7 +308,7 @@ class CxbRevSunriseS2(CxbBase): else: return {"data": ""} - def handle_data_event_stamp_map_list_csxxxx_request(self, data: Dict) -> Dict: + async def handle_data_event_stamp_map_list_csxxxx_request(self, data: Dict) -> Dict: event_id = data["dldate"]["filetype"].split("/")[2] if "EventStampMapListCs1002" in event_id: return {"data": "1,2,1,1,2,3,9,5,6,7,8,9,10,\r\n"} diff --git a/titles/cxb/schema/item.py b/titles/cxb/schema/item.py index 022a036..9e6a904 100644 --- a/titles/cxb/schema/item.py +++ b/titles/cxb/schema/item.py @@ -19,12 +19,12 @@ energy = Table( class CxbItemData(BaseData): - def put_energy(self, user_id: int, rev_energy: int) -> Optional[int]: + async def put_energy(self, user_id: int, rev_energy: int) -> Optional[int]: sql = insert(energy).values(user=user_id, energy=rev_energy) conflict = sql.on_duplicate_key_update(energy=rev_energy) - result = self.execute(conflict) + result = await self.execute(conflict) if result is None: self.logger.error( f"{__name__} failed to insert item! user: {user_id}, energy: {rev_energy}" @@ -33,10 +33,10 @@ class CxbItemData(BaseData): return result.lastrowid - def get_energy(self, user_id: int) -> Optional[Dict]: + async def get_energy(self, user_id: int) -> Optional[Dict]: sql = energy.select(and_(energy.c.user == user_id)) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchone() diff --git a/titles/cxb/schema/profile.py b/titles/cxb/schema/profile.py index 5c62f76..a3e6039 100644 --- a/titles/cxb/schema/profile.py +++ b/titles/cxb/schema/profile.py @@ -21,7 +21,7 @@ profile = Table( class CxbProfileData(BaseData): - def put_profile( + async def put_profile( self, user_id: int, version: int, index: int, data: JSON ) -> Optional[int]: sql = insert(profile).values( @@ -30,7 +30,7 @@ class CxbProfileData(BaseData): conflict = sql.on_duplicate_key_update(index=index, data=data) - result = self.execute(conflict) + result = await self.execute(conflict) if result is None: self.logger.error( f"{__name__} failed to update! user: {user_id}, index: {index}, data: {data}" @@ -39,7 +39,7 @@ class CxbProfileData(BaseData): return result.lastrowid - def get_profile(self, aime_id: int, version: int) -> Optional[List[Dict]]: + async def get_profile(self, aime_id: int, version: int) -> Optional[List[Dict]]: """ Given a game version and either a profile or aime id, return the profile """ @@ -47,12 +47,12 @@ class CxbProfileData(BaseData): and_(profile.c.version == version, profile.c.user == aime_id) ) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchall() - def get_profile_index( + async def get_profile_index( self, index: int, aime_id: int = None, version: int = None ) -> Optional[Dict]: """ @@ -72,7 +72,7 @@ class CxbProfileData(BaseData): ) return None - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchone() diff --git a/titles/cxb/schema/score.py b/titles/cxb/schema/score.py index b6f4f16..5acc1ae 100644 --- a/titles/cxb/schema/score.py +++ b/titles/cxb/schema/score.py @@ -39,6 +39,7 @@ playlog = Table( Column("slow2", Integer), Column("fail", Integer), Column("combo", Integer), + Column("grade", Integer), Column("date_scored", TIMESTAMP, server_default=func.now()), mysql_charset="utf8mb4", ) @@ -58,7 +59,7 @@ ranking = Table( class CxbScoreData(BaseData): - def put_best_score( + async def put_best_score( self, user_id: int, song_mcode: str, @@ -79,7 +80,7 @@ class CxbScoreData(BaseData): conflict = sql.on_duplicate_key_update(data=sql.inserted.data) - result = self.execute(conflict) + result = await self.execute(conflict) if result is None: self.logger.error( f"{__name__} failed to insert best score! profile: {user_id}, song: {song_mcode}, data: {data}" @@ -88,7 +89,7 @@ class CxbScoreData(BaseData): return result.lastrowid - def put_playlog( + async def put_playlog( self, user_id: int, song_mcode: str, @@ -104,6 +105,7 @@ class CxbScoreData(BaseData): this_slow2: int, fail: int, combo: int, + grade: int, ) -> Optional[int]: """ Add an entry to the user's play log @@ -123,9 +125,10 @@ class CxbScoreData(BaseData): slow2=this_slow2, fail=fail, combo=combo, + grade=grade, ) - result = self.execute(sql) + result = await self.execute(sql) if result is None: self.logger.error( f"{__name__} failed to insert playlog! profile: {user_id}, song: {song_mcode}, chart: {chart_id}" @@ -134,7 +137,7 @@ class CxbScoreData(BaseData): return result.lastrowid - def put_ranking( + async def put_ranking( self, user_id: int, rev_id: int, song_id: int, score: int, clear: int ) -> Optional[int]: """ @@ -151,7 +154,7 @@ class CxbScoreData(BaseData): conflict = sql.on_duplicate_key_update(score=score) - result = self.execute(conflict) + result = await self.execute(conflict) if result is None: self.logger.error( f"{__name__} failed to insert ranking log! profile: {user_id}, score: {score}, clear: {clear}" @@ -160,28 +163,28 @@ class CxbScoreData(BaseData): return result.lastrowid - def get_best_score(self, user_id: int, song_mcode: int) -> Optional[Dict]: + async def get_best_score(self, user_id: int, song_mcode: int) -> Optional[Dict]: sql = score.select( and_(score.c.user == user_id, score.c.song_mcode == song_mcode) ) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchone() - def get_best_scores(self, user_id: int) -> Optional[Dict]: + async def get_best_scores(self, user_id: int) -> Optional[Dict]: sql = score.select(score.c.user == user_id) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchall() - def get_best_rankings(self, user_id: int) -> Optional[List[Dict]]: + async def get_best_rankings(self, user_id: int) -> Optional[List[Dict]]: sql = ranking.select(ranking.c.user == user_id) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchall() diff --git a/titles/cxb/schema/static.py b/titles/cxb/schema/static.py index 6459e99..b863ef9 100644 --- a/titles/cxb/schema/static.py +++ b/titles/cxb/schema/static.py @@ -29,7 +29,7 @@ music = Table( class CxbStaticData(BaseData): - def put_music( + async def put_music( self, version: int, mcode: str, @@ -55,12 +55,12 @@ class CxbStaticData(BaseData): title=title, artist=artist, category=category, level=level ) - result = self.execute(conflict) + result = await self.execute(conflict) if result is None: return None return result.lastrowid - def get_music( + async def get_music( self, version: int, song_id: Optional[int] = None ) -> Optional[List[Row]]: if song_id is None: @@ -73,12 +73,12 @@ class CxbStaticData(BaseData): ) ) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchall() - def get_music_chart( + async def get_music_chart( self, version: int, song_id: int, chart_id: int ) -> Optional[List[Row]]: sql = select(music).where( @@ -89,7 +89,7 @@ class CxbStaticData(BaseData): ) ) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchone() diff --git a/titles/diva/__init__.py b/titles/diva/__init__.py index 9a9e6ef..7bfa2cc 100644 --- a/titles/diva/__init__.py +++ b/titles/diva/__init__.py @@ -2,9 +2,10 @@ from titles.diva.index import DivaServlet from titles.diva.const import DivaConstants from titles.diva.database import DivaData from titles.diva.read import DivaReader +from .frontend import DivaFrontend index = DivaServlet database = DivaData reader = DivaReader +frontend = DivaFrontend game_codes = [DivaConstants.GAME_CODE] -current_schema_version = 6 diff --git a/titles/diva/base.py b/titles/diva/base.py index 6db0dbc..be7a241 100644 --- a/titles/diva/base.py +++ b/titles/diva/base.py @@ -1,8 +1,7 @@ import datetime -from typing import Any, List, Dict +from typing import Dict import logging -import json -import urllib +import urllib.parse from threading import Thread from core.config import CoreConfig @@ -24,13 +23,13 @@ class DivaBase: dt = datetime.datetime.now() self.time_lut = urllib.parse.quote(dt.strftime("%Y-%m-%d %H:%M:%S:16.0")) - def handle_test_request(self, data: Dict) -> Dict: + async def handle_test_request(self, data: Dict) -> Dict: return "" - def handle_game_init_request(self, data: Dict) -> Dict: + async def handle_game_init_request(self, data: Dict) -> Dict: return f"" - def handle_attend_request(self, data: Dict) -> Dict: + async def handle_attend_request(self, data: Dict) -> Dict: encoded = "&" params = { "atnd_prm1": "0,1,1,0,0,0,1,0,100,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1", @@ -44,7 +43,7 @@ class DivaBase: return encoded - def handle_ping_request(self, data: Dict) -> Dict: + async def handle_ping_request(self, data: Dict) -> Dict: encoded = "&" params = { "ping_b_msg": f"Welcome to {self.core_cfg.server.name} network!", @@ -89,7 +88,7 @@ class DivaBase: return encoded - def handle_pv_list_request(self, data: Dict) -> Dict: + async def handle_pv_list_request(self, data: Dict) -> Dict: pvlist = "" with open(r"titles/diva/data/PvList0.dat", encoding="utf-8") as shop: lines = shop.readlines() @@ -126,10 +125,10 @@ class DivaBase: return response - def handle_shop_catalog_request(self, data: Dict) -> Dict: + async def handle_shop_catalog_request(self, data: Dict) -> Dict: catalog = "" - shopList = self.data.static.get_enabled_shops(self.version) + shopList = await self.data.static.get_enabled_shops(self.version) if not shopList: with open(r"titles/diva/data/ShopCatalog.dat", encoding="utf-8") as shop: lines = shop.readlines() @@ -164,9 +163,9 @@ class DivaBase: return response - def handle_buy_module_request(self, data: Dict) -> Dict: - profile = self.data.profile.get_profile(data["pd_id"], self.version) - module = self.data.static.get_enabled_shop(self.version, int(data["mdl_id"])) + async def handle_buy_module_request(self, data: Dict) -> Dict: + profile = await self.data.profile.get_profile(data["pd_id"], self.version) + module = await self.data.static.get_enabled_shop(self.version, int(data["mdl_id"])) # make sure module is available to purchase if not module: @@ -178,11 +177,11 @@ class DivaBase: new_vcld_pts = profile["vcld_pts"] - int(data["mdl_price"]) - self.data.profile.update_profile(profile["user"], vcld_pts=new_vcld_pts) - self.data.module.put_module(data["pd_id"], self.version, data["mdl_id"]) + await self.data.profile.update_profile(profile["user"], vcld_pts=new_vcld_pts) + await self.data.module.put_module(data["pd_id"], self.version, data["mdl_id"]) # generate the mdl_have string - mdl_have = self.data.module.get_modules_have_string(data["pd_id"], self.version) + mdl_have = await self.data.module.get_modules_have_string(data["pd_id"], self.version) response = "&shp_rslt=1" response += f"&mdl_id={data['mdl_id']}" @@ -191,10 +190,10 @@ class DivaBase: return response - def handle_cstmz_itm_ctlg_request(self, data: Dict) -> Dict: + async def handle_cstmz_itm_ctlg_request(self, data: Dict) -> Dict: catalog = "" - itemList = self.data.static.get_enabled_items(self.version) + itemList = await self.data.static.get_enabled_items(self.version) if not itemList: with open(r"titles/diva/data/ItemCatalog.dat", encoding="utf-8") as item: lines = item.readlines() @@ -229,9 +228,9 @@ class DivaBase: return response - def handle_buy_cstmz_itm_request(self, data: Dict) -> Dict: - profile = self.data.profile.get_profile(data["pd_id"], self.version) - item = self.data.static.get_enabled_item( + async def handle_buy_cstmz_itm_request(self, data: Dict) -> Dict: + profile = await self.data.profile.get_profile(data["pd_id"], self.version) + item = await self.data.static.get_enabled_item( self.version, int(data["cstmz_itm_id"]) ) @@ -246,14 +245,14 @@ class DivaBase: new_vcld_pts = profile["vcld_pts"] - int(data["cstmz_itm_price"]) # save new Vocaloid Points balance - self.data.profile.update_profile(profile["user"], vcld_pts=new_vcld_pts) + await self.data.profile.update_profile(profile["user"], vcld_pts=new_vcld_pts) - self.data.customize.put_customize_item( + await self.data.customize.put_customize_item( data["pd_id"], self.version, data["cstmz_itm_id"] ) # generate the cstmz_itm_have string - cstmz_itm_have = self.data.customize.get_customize_items_have_string( + cstmz_itm_have = await self.data.customize.get_customize_items_have_string( data["pd_id"], self.version ) @@ -264,7 +263,12 @@ class DivaBase: return response - def handle_festa_info_request(self, data: Dict) -> Dict: + async def handle_festa_info_request(self, data: Dict) -> Dict: + if self.game_config.server.festa_enable: + festa_end_time = self.game_config.server.festa_end_time + else: + festa_end_time = (datetime.datetime.now() - datetime.timedelta(days=365)).strftime("%Y-%m-%d %H:%M:%S") + ".0" + encoded = "&" params = { "fi_id": "1,2", @@ -274,10 +278,10 @@ class DivaBase: "fi_difficulty": "-1,-1", "fi_pv_id_lst": "ALL,ALL", "fi_attr": "7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF,7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF", - "fi_add_vp": "20,5", - "fi_mul_vp": "1,2", + "fi_add_vp": f"{self.game_config.server.festa_add_VP}", + "fi_mul_vp": f"{self.game_config.server.festa_multiply_VP}", "fi_st": "2019-01-01 00:00:00.0,2019-01-01 00:00:00.0", - "fi_et": "2029-01-01 00:00:00.0,2029-01-01 00:00:00.0", + "fi_et": f"{festa_end_time},{festa_end_time}", "fi_lut": "{self.time_lut}", } @@ -287,7 +291,7 @@ class DivaBase: return encoded - def handle_contest_info_request(self, data: Dict) -> Dict: + async def handle_contest_info_request(self, data: Dict) -> Dict: response = "" response += f"&ci_lut={self.time_lut}" @@ -295,10 +299,10 @@ class DivaBase: return response - def handle_qst_inf_request(self, data: Dict) -> Dict: + async def handle_qst_inf_request(self, data: Dict) -> Dict: quest = "" - questList = self.data.static.get_enabled_quests(self.version) + questList = await self.data.static.get_enabled_quests(self.version) if not questList: with open(r"titles/diva/data/QuestInfo.dat", encoding="utf-8") as shop: lines = shop.readlines() @@ -345,45 +349,45 @@ class DivaBase: return response - def handle_nv_ranking_request(self, data: Dict) -> Dict: + async def handle_nv_ranking_request(self, data: Dict) -> Dict: return f"" - def handle_ps_ranking_request(self, data: Dict) -> Dict: + async def handle_ps_ranking_request(self, data: Dict) -> Dict: return f"" - def handle_ng_word_request(self, data: Dict) -> Dict: + async def handle_ng_word_request(self, data: Dict) -> Dict: return f"" - def handle_rmt_wp_list_request(self, data: Dict) -> Dict: + async def handle_rmt_wp_list_request(self, data: Dict) -> Dict: return f"" - def handle_pv_def_chr_list_request(self, data: Dict) -> Dict: + async def handle_pv_def_chr_list_request(self, data: Dict) -> Dict: return f"" - def handle_pv_ng_mdl_list_request(self, data: Dict) -> Dict: + async def handle_pv_ng_mdl_list_request(self, data: Dict) -> Dict: return f"" - def handle_cstmz_itm_ng_mdl_lst_request(self, data: Dict) -> Dict: + async def handle_cstmz_itm_ng_mdl_lst_request(self, data: Dict) -> Dict: return f"" - def handle_banner_info_request(self, data: Dict) -> Dict: + async def handle_banner_info_request(self, data: Dict) -> Dict: return f"" - def handle_banner_data_request(self, data: Dict) -> Dict: + async def handle_banner_data_request(self, data: Dict) -> Dict: return f"" - def handle_cm_ply_info_request(self, data: Dict) -> Dict: + async def handle_cm_ply_info_request(self, data: Dict) -> Dict: return f"" - def handle_pstd_h_ctrl_request(self, data: Dict) -> Dict: + async def handle_pstd_h_ctrl_request(self, data: Dict) -> Dict: return f"" - def handle_pstd_item_ng_lst_request(self, data: Dict) -> Dict: + async def handle_pstd_item_ng_lst_request(self, data: Dict) -> Dict: return f"" - def handle_pre_start_request(self, data: Dict) -> str: - profile = self.data.profile.get_profile(data["aime_id"], self.version) - profile_shop = self.data.item.get_shop(data["aime_id"], self.version) + async def handle_pre_start_request(self, data: Dict) -> str: + profile = await self.data.profile.get_profile(data["aime_id"], self.version) + profile_shop = await self.data.item.get_shop(data["aime_id"], self.version) if profile is None: return f"&ps_result=-3" @@ -422,29 +426,29 @@ class DivaBase: return response - def handle_registration_request(self, data: Dict) -> Dict: - self.data.profile.create_profile( + async def handle_registration_request(self, data: Dict) -> Dict: + await self.data.profile.create_profile( self.version, data["aime_id"], data["player_name"] ) return f"&cd_adm_result=1&pd_id={data['aime_id']}" - def handle_start_request(self, data: Dict) -> Dict: - profile = self.data.profile.get_profile(data["pd_id"], self.version) - profile_shop = self.data.item.get_shop(data["pd_id"], self.version) + async def handle_start_request(self, data: Dict) -> Dict: + profile = await self.data.profile.get_profile(data["pd_id"], self.version) + profile_shop = await self.data.item.get_shop(data["pd_id"], self.version) if profile is None: - return + return {} mdl_have = "F" * 250 # generate the mdl_have string if "unlock_all_modules" is disabled if not self.game_config.mods.unlock_all_modules: - mdl_have = self.data.module.get_modules_have_string( + mdl_have = await self.data.module.get_modules_have_string( data["pd_id"], self.version ) cstmz_itm_have = "F" * 250 # generate the cstmz_itm_have string if "unlock_all_items" is disabled if not self.game_config.mods.unlock_all_items: - cstmz_itm_have = self.data.customize.get_customize_items_have_string( + cstmz_itm_have = await self.data.customize.get_customize_items_have_string( data["pd_id"], self.version ) @@ -525,7 +529,7 @@ class DivaBase: } # get clear status from user scores - pv_records = self.data.score.get_best_scores(data["pd_id"]) + pv_records = await self.data.score.get_best_scores(data["pd_id"]) clear_status = "0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0" if pv_records is not None: @@ -583,11 +587,11 @@ class DivaBase: return response - def handle_pd_unlock_request(self, data: Dict) -> Dict: + async def handle_pd_unlock_request(self, data: Dict) -> Dict: return f"" - def handle_spend_credit_request(self, data: Dict) -> Dict: - profile = self.data.profile.get_profile(data["pd_id"], self.version) + async def handle_spend_credit_request(self, data: Dict) -> Dict: + profile = await self.data.profile.get_profile(data["pd_id"], self.version) if profile is None: return @@ -664,30 +668,30 @@ class DivaBase: return pv_result - def task_generateScoreData(self, data: Dict, pd_by_pv_id, song): + async def task_generateScoreData(self, data: Dict, pd_by_pv_id, song): if int(song) > 0: # the request do not send a edition so just perform a query best score and ranking for each edition. # 0=ORIGINAL, 1=EXTRA - pd_db_song_0 = self.data.score.get_best_user_score( + pd_db_song_0 = await self.data.score.get_best_user_score( data["pd_id"], int(song), data["difficulty"], edition=0 ) - pd_db_song_1 = self.data.score.get_best_user_score( + pd_db_song_1 = await self.data.score.get_best_user_score( data["pd_id"], int(song), data["difficulty"], edition=1 ) pd_db_ranking_0, pd_db_ranking_1 = None, None if pd_db_song_0: - pd_db_ranking_0 = self.data.score.get_global_ranking( + pd_db_ranking_0 = await self.data.score.get_global_ranking( data["pd_id"], int(song), data["difficulty"], edition=0 ) if pd_db_song_1: - pd_db_ranking_1 = self.data.score.get_global_ranking( + pd_db_ranking_1 = await self.data.score.get_global_ranking( data["pd_id"], int(song), data["difficulty"], edition=1 ) - pd_db_customize = self.data.pv_customize.get_pv_customize( + pd_db_customize = await self.data.pv_customize.get_pv_customize( data["pd_id"], int(song) ) @@ -705,7 +709,7 @@ class DivaBase: pd_by_pv_id.append(urllib.parse.quote(f"{song}***")) pd_by_pv_id.append(",") - def handle_get_pv_pd_request(self, data: Dict) -> Dict: + async def handle_get_pv_pd_request(self, data: Dict) -> Dict: song_id = data["pd_pv_id_lst"].split(",") pv = "" @@ -713,7 +717,7 @@ class DivaBase: pd_by_pv_id = [] for song in song_id: - thread_ScoreData = Thread(target=self.task_generateScoreData(data, pd_by_pv_id, song)) + thread_ScoreData = Thread(target=await self.task_generateScoreData(data, pd_by_pv_id, song)) threads.append(thread_ScoreData) for x in threads: @@ -732,11 +736,11 @@ class DivaBase: return response - def handle_stage_start_request(self, data: Dict) -> Dict: + async def handle_stage_start_request(self, data: Dict) -> Dict: return f"" - def handle_stage_result_request(self, data: Dict) -> Dict: - profile = self.data.profile.get_profile(data["pd_id"], self.version) + async def handle_stage_result_request(self, data: Dict) -> Dict: + profile = await self.data.profile.get_profile(data["pd_id"], self.version) pd_song_list = data["stg_ply_pv_id"].split(",") pd_song_difficulty = data["stg_difficulty"].split(",") @@ -754,14 +758,14 @@ class DivaBase: for index, value in enumerate(pd_song_list): if "-1" not in pd_song_list[index]: - profile_pd_db_song = self.data.score.get_best_user_score( + profile_pd_db_song = await self.data.score.get_best_user_score( data["pd_id"], pd_song_list[index], pd_song_difficulty[index], pd_song_edition[index], ) if profile_pd_db_song is None: - self.data.score.put_best_score( + await self.data.score.put_best_score( data["pd_id"], self.version, pd_song_list[index], @@ -778,7 +782,7 @@ class DivaBase: pd_song_worst_cnt[index], pd_song_max_combo[index], ) - self.data.score.put_playlog( + await self.data.score.put_playlog( data["pd_id"], self.version, pd_song_list[index], @@ -796,7 +800,7 @@ class DivaBase: pd_song_max_combo[index], ) elif int(pd_song_max_score[index]) >= int(profile_pd_db_song["score"]): - self.data.score.put_best_score( + await self.data.score.put_best_score( data["pd_id"], self.version, pd_song_list[index], @@ -813,7 +817,7 @@ class DivaBase: pd_song_worst_cnt[index], pd_song_max_combo[index], ) - self.data.score.put_playlog( + await self.data.score.put_playlog( data["pd_id"], self.version, pd_song_list[index], @@ -831,7 +835,7 @@ class DivaBase: pd_song_max_combo[index], ) elif int(pd_song_max_score[index]) != int(profile_pd_db_song["score"]): - self.data.score.put_playlog( + await self.data.score.put_playlog( data["pd_id"], self.version, pd_song_list[index], @@ -852,7 +856,7 @@ class DivaBase: # Profile saving based on registration list # Calculate new level - best_scores = self.data.score.get_best_scores(data["pd_id"]) + best_scores = await self.data.score.get_best_scores(data["pd_id"]) total_atn_pnt = 0 for best_score in best_scores: @@ -866,7 +870,7 @@ class DivaBase: response += f"&lv_pnt_old={int(profile['lv_pnt'])}" # update the profile and commit changes to the db - self.data.profile.update_profile( + await self.data.profile.update_profile( profile["user"], lv_num=new_level, lv_pnt=new_level_pnt, @@ -914,16 +918,16 @@ class DivaBase: return response - def handle_end_request(self, data: Dict) -> Dict: - profile = self.data.profile.get_profile(data["pd_id"], self.version) + async def handle_end_request(self, data: Dict) -> Dict: + profile = await self.data.profile.get_profile(data["pd_id"], self.version) - self.data.profile.update_profile( + await self.data.profile.update_profile( profile["user"], my_qst_id=data["my_qst_id"], my_qst_sts=data["my_qst_sts"] ) return f"" - def handle_shop_exit_request(self, data: Dict) -> Dict: - self.data.item.put_shop( + async def handle_shop_exit_request(self, data: Dict) -> Dict: + await self.data.item.put_shop( data["pd_id"], self.version, data["mdl_eqp_cmn_ary"], @@ -931,7 +935,7 @@ class DivaBase: data["ms_itm_flg_cmn_ary"], ) if int(data["use_pv_mdl_eqp"]) == 1: - self.data.pv_customize.put_pv_customize( + await self.data.pv_customize.put_pv_customize( data["pd_id"], self.version, data["ply_pv_id"], @@ -940,7 +944,7 @@ class DivaBase: data["ms_itm_flg_pv_ary"], ) else: - self.data.pv_customize.put_pv_customize( + await self.data.pv_customize.put_pv_customize( data["pd_id"], self.version, data["ply_pv_id"], @@ -952,8 +956,8 @@ class DivaBase: response = "&shp_rslt=1" return response - def handle_card_procedure_request(self, data: Dict) -> str: - profile = self.data.profile.get_profile(data["aime_id"], self.version) + async def handle_card_procedure_request(self, data: Dict) -> str: + profile = await self.data.profile.get_profile(data["aime_id"], self.version) if profile is None: return "&cd_adm_result=0" @@ -972,8 +976,8 @@ class DivaBase: return response - def handle_change_name_request(self, data: Dict) -> str: - profile = self.data.profile.get_profile(data["pd_id"], self.version) + async def handle_change_name_request(self, data: Dict) -> str: + profile = await self.data.profile.get_profile(data["pd_id"], self.version) # make sure user has enough Vocaloid Points if profile["vcld_pts"] < int(data["chg_name_price"]): @@ -981,7 +985,7 @@ class DivaBase: # update the vocaloid points and player name new_vcld_pts = profile["vcld_pts"] - int(data["chg_name_price"]) - self.data.profile.update_profile( + await self.data.profile.update_profile( profile["user"], player_name=data["player_name"], vcld_pts=new_vcld_pts ) @@ -992,15 +996,15 @@ class DivaBase: return response - def handle_change_passwd_request(self, data: Dict) -> str: - profile = self.data.profile.get_profile(data["pd_id"], self.version) + async def handle_change_passwd_request(self, data: Dict) -> str: + profile = await self.data.profile.get_profile(data["pd_id"], self.version) # TODO: return correct error number instead of 0 if data["passwd"] != profile["passwd"]: return "&cd_adm_result=0" # set password to true and update the saved password - self.data.profile.update_profile( + await self.data.profile.update_profile( profile["user"], passwd_stat=1, passwd=data["new_passwd"] ) diff --git a/titles/diva/config.py b/titles/diva/config.py index efa327e..48b5c0f 100644 --- a/titles/diva/config.py +++ b/titles/diva/config.py @@ -19,6 +19,29 @@ class DivaServerConfig: ) ) + @property + def festa_enable(self) -> bool: + return CoreConfig.get_config_field( + self.__config, "diva", "server", "festa_enable", default=True + ) + + @property + def festa_add_VP(self) -> str: + return CoreConfig.get_config_field( + self.__config, "diva", "server", "festa_add_VP", default="20,5" + ) + + @property + def festa_multiply_VP(self) -> str: + return CoreConfig.get_config_field( + self.__config, "diva", "server", "festa_multiply_VP", default="1,2" + ) + + @property + def festa_end_time(self) -> str: + return CoreConfig.get_config_field( + self.__config, "diva", "server", "festa_end_time", default="2029-01-01 00:00:00.0" + ) class DivaModsConfig: def __init__(self, parent_config: "DivaConfig") -> None: diff --git a/titles/diva/frontend.py b/titles/diva/frontend.py new file mode 100644 index 0000000..0b50dc4 --- /dev/null +++ b/titles/diva/frontend.py @@ -0,0 +1,182 @@ +from typing import List +from starlette.routing import Route, Mount +from starlette.requests import Request +from starlette.responses import Response, RedirectResponse +from os import path +import yaml +import jinja2 + +from core.frontend import FE_Base, UserSession +from core.config import CoreConfig +from .database import DivaData +from .config import DivaConfig +from .const import DivaConstants + +class DivaFrontend(FE_Base): + def __init__( + self, cfg: CoreConfig, environment: jinja2.Environment, cfg_dir: str + ) -> None: + super().__init__(cfg, environment) + self.data = DivaData(cfg) + self.game_cfg = DivaConfig() + if path.exists(f"{cfg_dir}/{DivaConstants.CONFIG_NAME}"): + self.game_cfg.update( + yaml.safe_load(open(f"{cfg_dir}/{DivaConstants.CONFIG_NAME}")) + ) + self.nav_name = "Project Diva" + + def get_routes(self) -> List[Route]: + return [ + Route("/", self.render_GET, methods=['GET']), + Mount("/playlog", routes=[ + Route("/", self.render_GET_playlog, methods=['GET']), + Route("/{index}", self.render_GET_playlog, methods=['GET']), + ]), + Route("/update.name", self.update_name, methods=['POST']), + Route("/update.lv", self.update_lv, methods=['POST']), + ] + + async def render_GET(self, request: Request) -> bytes: + template = self.environment.get_template( + "titles/diva/templates/diva_index.jinja" + ) + usr_sesh = self.validate_session(request) + if not usr_sesh: + usr_sesh = UserSession() + + if usr_sesh.user_id > 0: + profile = await self.data.profile.get_profile(usr_sesh.user_id, 1) + + resp = Response(template.render( + title=f"{self.core_config.server.name} | {self.nav_name}", + game_list=self.environment.globals["game_list"], + sesh=vars(usr_sesh), + user_id=usr_sesh.user_id, + profile=profile + ), media_type="text/html; charset=utf-8") + return resp + else: + return RedirectResponse("/gate") + + async def render_GET_playlog(self, request: Request) -> bytes: + template = self.environment.get_template( + "titles/diva/templates/diva_playlog.jinja" + ) + usr_sesh = self.validate_session(request) + if not usr_sesh: + usr_sesh = UserSession() + + if usr_sesh.user_id > 0: + path_index = request.path_params.get("index") + if not path_index or int(path_index) < 1: + index = 0 + else: + index = int(path_index) - 1 # 0 and 1 are 1st page + user_id = usr_sesh.user_id + playlog_count = await self.data.score.get_user_playlogs_count(user_id) + if playlog_count < index * 20 : + return Response(template.render( + title=f"{self.core_config.server.name} | {self.nav_name}", + game_list=self.environment.globals["game_list"], + sesh=vars(usr_sesh), + score_count=0 + ), media_type="text/html; charset=utf-8") + playlog = await self.data.score.get_playlogs(user_id, index, 20) #Maybe change to the playlog instead of direct scores + playlog_with_title = [] + for record in playlog: + song = await self.data.static.get_music_chart(record[2], record[3], record[4]) + if song: + title = song.title + vocaloid_arranger = song.vocaloid_arranger + else: + title = "Unknown" + vocaloid_arranger = "Unknown" + playlog_with_title.append({ + "raw": record, + "title": title, + "vocaloid_arranger": vocaloid_arranger + }) + return Response(template.render( + title=f"{self.core_config.server.name} | {self.nav_name}", + game_list=self.environment.globals["game_list"], + sesh=vars(usr_sesh), + user_id=usr_sesh.user_id, + playlog=playlog_with_title, + playlog_count=playlog_count + ), media_type="text/html; charset=utf-8") + else: + return RedirectResponse("/gate/", 300) + + async def update_name(self, request: Request) -> Response: + usr_sesh = self.validate_session(request) + if not usr_sesh: + return RedirectResponse("/gate") + + form_data = await request.form() + new_name: str = form_data.get("new_name") + new_name_full = "" + + if not new_name: + return RedirectResponse("/gate/?e=4", 303) + + if len(new_name) > 8: + return RedirectResponse("/gate/?e=8", 303) + + for x in new_name: # FIXME: This will let some invalid characters through atm + o = ord(x) + try: + if o == 0x20: + new_name_full += chr(0x3000) + elif o < 0x7F and o > 0x20: + new_name_full += chr(o + 0xFEE0) + elif o <= 0x7F: + self.logger.warn(f"Invalid ascii character {o:02X}") + return RedirectResponse("/gate/?e=4", 303) + else: + new_name_full += x + + except Exception as e: + self.logger.error(f"Something went wrong parsing character {o:04X} - {e}") + return RedirectResponse("/gate/?e=4", 303) + + if not await self.data.profile.update_profile(usr_sesh.user_id, player_name=new_name_full): + return RedirectResponse("/gate/?e=999", 303) + + return RedirectResponse("/game/diva", 303) + + async def update_lv(self, request: Request) -> Response: + usr_sesh = self.validate_session(request) + if not usr_sesh: + return RedirectResponse("/gate") + + form_data = await request.form() + new_lv: str = form_data.get("new_lv") + new_lv_full = "" + + if not new_lv: + return RedirectResponse("/gate/?e=4", 303) + + if len(new_lv) > 8: + return RedirectResponse("/gate/?e=8", 303) + + for x in new_lv: # FIXME: This will let some invalid characters through atm + o = ord(x) + try: + if o == 0x20: + new_lv_full += chr(0x3000) + elif o < 0x7F and o > 0x20: + new_lv_full += chr(o + 0xFEE0) + elif o <= 0x7F: + self.logger.warn(f"Invalid ascii character {o:02X}") + return RedirectResponse("/gate/?e=4", 303) + else: + new_lv_full += x + + except Exception as e: + self.logger.error(f"Something went wrong parsing character {o:04X} - {e}") + return RedirectResponse("/gate/?e=4", 303) + + if not await self.data.profile.update_profile(usr_sesh.user_id, lv_str=new_lv_full): + return RedirectResponse("/gate/?e=999", 303) + + return RedirectResponse("/game/diva", 303) diff --git a/titles/diva/index.py b/titles/diva/index.py index ac4114e..01e5eeb 100644 --- a/titles/diva/index.py +++ b/titles/diva/index.py @@ -1,4 +1,6 @@ -from twisted.web.http import Request +from starlette.requests import Request +from starlette.responses import PlainTextResponse +from starlette.routing import Route import yaml import logging, coloredlogs from logging.handlers import TimedRotatingFileHandler @@ -51,17 +53,16 @@ class DivaServlet(BaseServlet): level=self.game_cfg.server.loglevel, logger=self.logger, fmt=log_fmt_str ) - def get_endpoint_matchers(self) -> Tuple[List[Tuple[str, str, Dict]], List[Tuple[str, str, Dict]]]: - return ( - [], - [("render_POST", "/DivaServlet/", {})] - ) + def get_routes(self) -> List[Route]: + return [ + Route("/DivaServlet/", self.render_POST, methods=['POST']) + ] def get_allnet_info(self, game_code: str, game_ver: int, keychip: str) -> Tuple[str, str]: if not self.core_cfg.server.is_using_proxy and Utils.get_title_port(self.core_cfg) != 80: - return (f"http://{self.core_cfg.title.hostname}:{Utils.get_title_port(self.core_cfg)}/DivaServlet/", self.core_cfg.title.hostname) + return (f"http://{self.core_cfg.server.hostname}:{Utils.get_title_port(self.core_cfg)}/DivaServlet/", self.core_cfg.server.hostname) - return (f"http://{self.core_cfg.title.hostname}/DivaServlet/", self.core_cfg.title.hostname) + return (f"http://{self.core_cfg.server.hostname}/DivaServlet/", self.core_cfg.server.hostname) @classmethod def is_game_enabled( @@ -78,9 +79,9 @@ class DivaServlet(BaseServlet): return True - def render_POST(self, request: Request, game_code: str, matchers: Dict) -> bytes: - req_raw = request.content.getvalue() - url_header = request.getAllHeaders() + async def render_POST(self, request: Request) -> bytes: + req_raw = await request.body() + url_header = request.headers # Ping Dispatch if "THIS_STRING_SEPARATES" in str(url_header): @@ -97,15 +98,22 @@ class DivaServlet(BaseServlet): self.logger.info(f"Binary {bin_req_data['cmd']} Request") self.logger.debug(bin_req_data) - handler = getattr(self.base, f"handle_{bin_req_data['cmd']}_request") - resp = handler(bin_req_data) + try: + handler = getattr(self.base, f"handle_{bin_req_data['cmd']}_request") + resp = handler(bin_req_data) + + except AttributeError as e: + self.logger.warning(f"Unhandled {bin_req_data['cmd']} request {e}") + return PlainTextResponse(f"cmd={bin_req_data['cmd']}&req_id={bin_req_data['req_id']}&stat=ok") + + except Exception as e: + self.logger.error(f"Error handling method {e}") + return PlainTextResponse(f"cmd={bin_req_data['cmd']}&req_id={bin_req_data['req_id']}&stat=ok") self.logger.debug( f"Response cmd={bin_req_data['cmd']}&req_id={bin_req_data['req_id']}&stat=ok{resp}" ) - return f"cmd={bin_req_data['cmd']}&req_id={bin_req_data['req_id']}&stat=ok{resp}".encode( - "utf-8" - ) + return PlainTextResponse(f"cmd={bin_req_data['cmd']}&req_id={bin_req_data['req_id']}&stat=ok{resp}") # Main Dispatch json_string = json.dumps( @@ -122,7 +130,7 @@ class DivaServlet(BaseServlet): ) # Decompressing the gzip except zlib.error as e: self.logger.error(f"Failed to defalte! {e} -> {gz_string}") - return "stat=0" + return PlainTextResponse("stat=0") req_kvp = urllib.parse.unquote(url_data) req_data = {} @@ -141,27 +149,18 @@ class DivaServlet(BaseServlet): # Load the requests try: handler = getattr(self.base, func_to_find) - resp = handler(req_data) + resp = await handler(req_data) except AttributeError as e: self.logger.warning(f"Unhandled {req_data['cmd']} request {e}") - return f"cmd={req_data['cmd']}&req_id={req_data['req_id']}&stat=ok".encode( - "utf-8" - ) + return PlainTextResponse(f"cmd={req_data['cmd']}&req_id={req_data['req_id']}&stat=ok") except Exception as e: self.logger.error(f"Error handling method {func_to_find} {e}") - return f"cmd={req_data['cmd']}&req_id={req_data['req_id']}&stat=ok".encode( - "utf-8" - ) + return PlainTextResponse(f"cmd={req_data['cmd']}&req_id={req_data['req_id']}&stat=ok") - request.responseHeaders.addRawHeader(b"content-type", b"text/plain") self.logger.debug( f"Response cmd={req_data['cmd']}&req_id={req_data['req_id']}&stat=ok{resp}" ) - return ( - f"cmd={req_data['cmd']}&req_id={req_data['req_id']}&stat=ok{resp}".encode( - "utf-8" - ) - ) + return PlainTextResponse(f"cmd={req_data['cmd']}&req_id={req_data['req_id']}&stat=ok{resp}") diff --git a/titles/diva/read.py b/titles/diva/read.py index f143bb6..64603ca 100644 --- a/titles/diva/read.py +++ b/titles/diva/read.py @@ -28,7 +28,7 @@ class DivaReader(BaseReader): self.logger.error(f"Invalid project diva version {version}") exit(1) - def read(self) -> None: + async def read(self) -> None: pull_bin_ram = True pull_bin_rom = True pull_opt_rom = True @@ -48,14 +48,14 @@ class DivaReader(BaseReader): self.logger.warning("No option directory specified, skipping") if pull_bin_ram: - self.read_ram(f"{self.bin_dir}/ram") + await self.read_ram(f"{self.bin_dir}/ram") if pull_bin_rom: - self.read_rom(f"{self.bin_dir}/rom") + await self.read_rom(f"{self.bin_dir}/rom") if pull_opt_rom: for dir in opt_dirs: - self.read_rom(f"{dir}/rom") + await self.read_rom(f"{dir}/rom") - def read_ram(self, ram_root_dir: str) -> None: + async def read_ram(self, ram_root_dir: str) -> None: self.logger.info(f"Read RAM from {ram_root_dir}") if path.exists(f"{ram_root_dir}/databank"): @@ -91,7 +91,7 @@ class DivaReader(BaseReader): f"Added shop item {split[x+0]}" ) - self.data.static.put_shop( + await self.data.static.put_shop( self.version, split[x + 0], split[x + 2], @@ -109,7 +109,7 @@ class DivaReader(BaseReader): for x in range(0, len(split), 7): self.logger.info(f"Added item {split[x+0]}") - self.data.static.put_items( + await self.data.static.put_items( self.version, split[x + 0], split[x + 2], @@ -123,7 +123,7 @@ class DivaReader(BaseReader): elif file.startswith("QuestInfo") and len(split) >= 9: self.logger.info(f"Added quest {split[0]}") - self.data.static.put_quests( + await self.data.static.put_quests( self.version, split[0], split[6], @@ -141,7 +141,7 @@ class DivaReader(BaseReader): else: self.logger.warning(f"Databank folder not found in {ram_root_dir}, skipping") - def read_rom(self, rom_root_dir: str) -> None: + async def read_rom(self, rom_root_dir: str) -> None: self.logger.info(f"Read ROM from {rom_root_dir}") pv_list: Dict[str, Dict] = {} @@ -183,7 +183,11 @@ class DivaReader(BaseReader): pv_list[pv_id] = self.add_branch(pv_list[pv_id], key_args, val) for pv_id, pv_data in pv_list.items(): - song_id = int(pv_id.split("_")[1]) + try: + song_id = int(pv_id.split("_")[1]) + except ValueError: + self.logger.error(f"Invalid song ID format: {pv_id}") + continue if "songinfo" not in pv_data: continue if "illustrator" not in pv_data["songinfo"]: @@ -199,7 +203,7 @@ class DivaReader(BaseReader): diff = pv_data["difficulty"]["easy"]["0"]["level"].split("_") self.logger.info(f"Added song {song_id} chart 0") - self.data.static.put_music( + await self.data.static.put_music( self.version, song_id, 0, @@ -220,7 +224,7 @@ class DivaReader(BaseReader): diff = pv_data["difficulty"]["normal"]["0"]["level"].split("_") self.logger.info(f"Added song {song_id} chart 1") - self.data.static.put_music( + await self.data.static.put_music( self.version, song_id, 1, @@ -238,7 +242,7 @@ class DivaReader(BaseReader): diff = pv_data["difficulty"]["hard"]["0"]["level"].split("_") self.logger.info(f"Added song {song_id} chart 2") - self.data.static.put_music( + await self.data.static.put_music( self.version, song_id, 2, @@ -257,7 +261,7 @@ class DivaReader(BaseReader): diff = pv_data["difficulty"]["extreme"]["0"]["level"].split("_") self.logger.info(f"Added song {song_id} chart 3") - self.data.static.put_music( + await self.data.static.put_music( self.version, song_id, 3, @@ -275,7 +279,7 @@ class DivaReader(BaseReader): diff = pv_data["difficulty"]["extreme"]["1"]["level"].split("_") self.logger.info(f"Added song {song_id} chart 4") - self.data.static.put_music( + await self.data.static.put_music( self.version, song_id, 4, diff --git a/titles/diva/schema/customize.py b/titles/diva/schema/customize.py index 91480f5..21ccf7d 100644 --- a/titles/diva/schema/customize.py +++ b/titles/diva/schema/customize.py @@ -25,10 +25,10 @@ customize = Table( class DivaCustomizeItemData(BaseData): - def put_customize_item(self, aime_id: int, version: int, item_id: int) -> None: + async def put_customize_item(self, aime_id: int, version: int, item_id: int) -> None: sql = insert(customize).values(version=version, user=aime_id, item_id=item_id) - result = self.execute(sql) + result = await self.execute(sql) if result is None: self.logger.error( f"{__name__} Failed to insert diva profile customize item! aime id: {aime_id} item: {item_id}" @@ -36,7 +36,7 @@ class DivaCustomizeItemData(BaseData): return None return result.lastrowid - def get_customize_items(self, aime_id: int, version: int) -> Optional[List[Dict]]: + async def get_customize_items(self, aime_id: int, version: int) -> Optional[List[Dict]]: """ Given a game version and an aime id, return all the customize items, not used directly """ @@ -44,17 +44,17 @@ class DivaCustomizeItemData(BaseData): and_(customize.c.version == version, customize.c.user == aime_id) ) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchall() - def get_customize_items_have_string(self, aime_id: int, version: int) -> str: + async def get_customize_items_have_string(self, aime_id: int, version: int) -> str: """ Given a game version and an aime id, return the cstmz_itm_have hex string required for diva directly """ - items_list = self.get_customize_items(aime_id, version) + items_list = await self.get_customize_items(aime_id, version) if items_list is None: items_list = [] item_have = 0 diff --git a/titles/diva/schema/item.py b/titles/diva/schema/item.py index 4d484ae..c09896b 100644 --- a/titles/diva/schema/item.py +++ b/titles/diva/schema/item.py @@ -26,7 +26,7 @@ shop = Table( class DivaItemData(BaseData): - def put_shop( + async def put_shop( self, aime_id: int, version: int, @@ -48,7 +48,7 @@ class DivaItemData(BaseData): ms_itm_flg_ary=ms_itm_flg_ary, ) - result = self.execute(conflict) + result = await self.execute(conflict) if result is None: self.logger.error( f"{__name__} Failed to insert diva profile! aime id: {aime_id} array: {mdl_eqp_ary}" @@ -56,13 +56,13 @@ class DivaItemData(BaseData): return None return result.lastrowid - def get_shop(self, aime_id: int, version: int) -> Optional[List[Dict]]: + async def get_shop(self, aime_id: int, version: int) -> Optional[List[Dict]]: """ Given a game version and either a profile or aime id, return the profile """ sql = shop.select(and_(shop.c.version == version, shop.c.user == aime_id)) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchone() diff --git a/titles/diva/schema/module.py b/titles/diva/schema/module.py index 5872d68..de091b1 100644 --- a/titles/diva/schema/module.py +++ b/titles/diva/schema/module.py @@ -23,10 +23,10 @@ module = Table( class DivaModuleData(BaseData): - def put_module(self, aime_id: int, version: int, module_id: int) -> None: + async def put_module(self, aime_id: int, version: int, module_id: int) -> None: sql = insert(module).values(version=version, user=aime_id, module_id=module_id) - result = self.execute(sql) + result = await self.execute(sql) if result is None: self.logger.error( f"{__name__} Failed to insert diva profile module! aime id: {aime_id} module: {module_id}" @@ -34,23 +34,23 @@ class DivaModuleData(BaseData): return None return result.lastrowid - def get_modules(self, aime_id: int, version: int) -> Optional[List[Dict]]: + async def get_modules(self, aime_id: int, version: int) -> Optional[List[Dict]]: """ Given a game version and an aime id, return all the modules, not used directly """ sql = module.select(and_(module.c.version == version, module.c.user == aime_id)) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchall() - def get_modules_have_string(self, aime_id: int, version: int) -> str: + async def get_modules_have_string(self, aime_id: int, version: int) -> str: """ Given a game version and an aime id, return the mdl_have hex string required for diva directly """ - module_list = self.get_modules(aime_id, version) + module_list = await self.get_modules(aime_id, version) if module_list is None: module_list = [] module_have = 0 diff --git a/titles/diva/schema/profile.py b/titles/diva/schema/profile.py index 7bd6bf0..10d3b51 100644 --- a/titles/diva/schema/profile.py +++ b/titles/diva/schema/profile.py @@ -70,7 +70,7 @@ profile = Table( class DivaProfileData(BaseData): - def create_profile( + async def create_profile( self, version: int, aime_id: int, player_name: str ) -> Optional[int]: """ @@ -82,7 +82,7 @@ class DivaProfileData(BaseData): conflict = sql.on_duplicate_key_update(player_name=sql.inserted.player_name) - result = self.execute(conflict) + result = await self.execute(conflict) if result is None: self.logger.error( f"{__name__} Failed to insert diva profile! aime id: {aime_id} username: {player_name}" @@ -90,21 +90,23 @@ class DivaProfileData(BaseData): return None return result.lastrowid - def update_profile(self, aime_id: int, **profile_args) -> None: + async def update_profile(self, aime_id: int, **profile_args) -> bool: """ Given an aime_id update the profile corresponding to the arguments which are the diva_profile Columns """ sql = profile.update(profile.c.user == aime_id).values(**profile_args) - result = self.execute(sql) + result = await self.execute(sql) if result is None: self.logger.error( f"update_profile: failed to update profile! profile: {aime_id}" ) - return None + return False + + return True - def get_profile(self, aime_id: int, version: int) -> Optional[List[Dict]]: + async def get_profile(self, aime_id: int, version: int) -> Optional[List[Dict]]: """ Given a game version and either a profile or aime id, return the profile """ @@ -112,7 +114,7 @@ class DivaProfileData(BaseData): and_(profile.c.version == version, profile.c.user == aime_id) ) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchone() diff --git a/titles/diva/schema/pv_customize.py b/titles/diva/schema/pv_customize.py index 1ca8909..c56378d 100644 --- a/titles/diva/schema/pv_customize.py +++ b/titles/diva/schema/pv_customize.py @@ -39,7 +39,7 @@ pv_customize = Table( class DivaPvCustomizeData(BaseData): - def put_pv_customize( + async def put_pv_customize( self, aime_id: int, version: int, @@ -64,7 +64,7 @@ class DivaPvCustomizeData(BaseData): ms_itm_flg_ary=ms_itm_flg_ary, ) - result = self.execute(conflict) + result = await self.execute(conflict) if result is None: self.logger.error( f"{__name__} Failed to insert diva pv customize! aime id: {aime_id}" @@ -72,7 +72,7 @@ class DivaPvCustomizeData(BaseData): return None return result.lastrowid - def get_pv_customize(self, aime_id: int, pv_id: int) -> Optional[List[Dict]]: + async def get_pv_customize(self, aime_id: int, pv_id: int) -> Optional[List[Dict]]: """ Given either a profile or aime id, return a Pv Customize row """ @@ -80,7 +80,7 @@ class DivaPvCustomizeData(BaseData): and_(pv_customize.c.user == aime_id, pv_customize.c.pv_id == pv_id) ) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchone() diff --git a/titles/diva/schema/score.py b/titles/diva/schema/score.py index 2171659..ce89f74 100644 --- a/titles/diva/schema/score.py +++ b/titles/diva/schema/score.py @@ -57,7 +57,7 @@ playlog = Table( class DivaScoreData(BaseData): - def put_best_score( + async def put_best_score( self, user_id: int, game_version: int, @@ -109,7 +109,7 @@ class DivaScoreData(BaseData): max_combo=max_combo, ) - result = self.execute(conflict) + result = await self.execute(conflict) if result is None: self.logger.error( f"{__name__} failed to insert best score! profile: {user_id}, song: {song_id}" @@ -118,7 +118,7 @@ class DivaScoreData(BaseData): return result.lastrowid - def put_playlog( + async def put_playlog( self, user_id: int, game_version: int, @@ -157,7 +157,7 @@ class DivaScoreData(BaseData): max_combo=max_combo, ) - result = self.execute(sql) + result = await self.execute(sql) if result is None: self.logger.error( f"{__name__} failed to insert playlog! profile: {user_id}, song: {song_id}, chart: {difficulty}" @@ -166,7 +166,7 @@ class DivaScoreData(BaseData): return result.lastrowid - def get_best_user_score( + async def get_best_user_score( self, user_id: int, pv_id: int, difficulty: int, edition: int ) -> Optional[Row]: sql = score.select( @@ -178,12 +178,12 @@ class DivaScoreData(BaseData): ) ) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchone() - def get_top3_scores( + async def get_top3_scores( self, pv_id: int, difficulty: int, edition: int ) -> Optional[List[Row]]: sql = ( @@ -198,12 +198,12 @@ class DivaScoreData(BaseData): .limit(3) ) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchall() - def get_global_ranking( + async def get_global_ranking( self, user_id: int, pv_id: int, difficulty: int, edition: int ) -> Optional[List[Row]]: # get the subquery max score of a user with pv_id, difficulty and @@ -227,15 +227,35 @@ class DivaScoreData(BaseData): score.c.edition == edition, ) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchone() - def get_best_scores(self, user_id: int) -> Optional[List[Row]]: + async def get_best_scores(self, user_id: int) -> Optional[List[Row]]: sql = score.select(score.c.user == user_id) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchall() + + async def get_playlogs(self, aime_id: int, idx: int = 0, limit: int = 0) -> Optional[Row]: + sql = select(playlog).where(playlog.c.user == aime_id).order_by(playlog.c.date_scored.desc()) + + if limit: + sql = sql.limit(limit) + if idx: + sql = sql.offset(idx) + + result = await self.execute(sql) + if result: + return result.fetchall() + + async def get_user_playlogs_count(self, aime_id: int) -> Optional[int]: + sql = select(func.count()).where(playlog.c.user == aime_id) + result = await self.execute(sql) + if result is None: + self.logger.warning(f"aimu_id {aime_id} has no scores ") + return None + return result.scalar() diff --git a/titles/diva/schema/static.py b/titles/diva/schema/static.py index 02ee0ec..e6aa207 100644 --- a/titles/diva/schema/static.py +++ b/titles/diva/schema/static.py @@ -83,7 +83,7 @@ items = Table( class DivaStaticData(BaseData): - def put_quests( + async def put_quests( self, version: int, questId: int, @@ -111,22 +111,22 @@ class DivaStaticData(BaseData): conflict = sql.on_duplicate_key_update(name=name) - result = self.execute(conflict) + result = await self.execute(conflict) if result is None: return None return result.lastrowid - def get_enabled_quests(self, version: int) -> Optional[List[Row]]: + async def get_enabled_quests(self, version: int) -> Optional[List[Row]]: sql = select(quests).where( and_(quests.c.version == version, quests.c.quest_enable == True) ) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchall() - def put_shop( + async def put_shop( self, version: int, shopId: int, @@ -150,12 +150,12 @@ class DivaStaticData(BaseData): conflict = sql.on_duplicate_key_update(name=name) - result = self.execute(conflict) + result = await self.execute(conflict) if result is None: return None return result.lastrowid - def get_enabled_shop(self, version: int, shopId: int) -> Optional[Row]: + async def get_enabled_shop(self, version: int, shopId: int) -> Optional[Row]: sql = select(shop).where( and_( shop.c.version == version, @@ -164,22 +164,22 @@ class DivaStaticData(BaseData): ) ) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchone() - def get_enabled_shops(self, version: int) -> Optional[List[Row]]: + async def get_enabled_shops(self, version: int) -> Optional[List[Row]]: sql = select(shop).where( and_(shop.c.version == version, shop.c.enabled == True) ) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchall() - def put_items( + async def put_items( self, version: int, itemId: int, @@ -203,12 +203,12 @@ class DivaStaticData(BaseData): conflict = sql.on_duplicate_key_update(name=name) - result = self.execute(conflict) + result = await self.execute(conflict) if result is None: return None return result.lastrowid - def get_enabled_item(self, version: int, itemId: int) -> Optional[Row]: + async def get_enabled_item(self, version: int, itemId: int) -> Optional[Row]: sql = select(items).where( and_( items.c.version == version, @@ -217,22 +217,22 @@ class DivaStaticData(BaseData): ) ) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchone() - def get_enabled_items(self, version: int) -> Optional[List[Row]]: + async def get_enabled_items(self, version: int) -> Optional[List[Row]]: sql = select(items).where( and_(items.c.version == version, items.c.enabled == True) ) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchall() - def put_music( + async def put_music( self, version: int, song: int, @@ -271,12 +271,12 @@ class DivaStaticData(BaseData): date=date, ) - result = self.execute(conflict) + result = await self.execute(conflict) if result is None: return None return result.lastrowid - def get_music( + async def get_music( self, version: int, song_id: Optional[int] = None ) -> Optional[List[Row]]: if song_id is None: @@ -289,12 +289,12 @@ class DivaStaticData(BaseData): ) ) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchall() - def get_music_chart( + async def get_music_chart( self, version: int, song_id: int, chart_id: int ) -> Optional[List[Row]]: sql = select(music).where( @@ -305,7 +305,7 @@ class DivaStaticData(BaseData): ) ) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchone() diff --git a/titles/diva/templates/css/diva_style.css b/titles/diva/templates/css/diva_style.css new file mode 100644 index 0000000..672db0f --- /dev/null +++ b/titles/diva/templates/css/diva_style.css @@ -0,0 +1,195 @@ +.diva-header { + text-align: center; +} + +ul.diva-navi { + list-style-type: none; + padding: 0; + overflow: hidden; + background-color: #333; + text-align: center; + display: inline-block; +} + +ul.diva-navi li { + display: inline-block; +} + +ul.diva-navi li a { + display: block; + color: white; + text-align: center; + padding: 14px 16px; + text-decoration: none; +} + +ul.diva-navi li a:hover:not(.active) { + background-color: #111; +} + +ul.diva-navi li a.active { + background-color: #4CAF50; +} + +ul.diva-navi li.right { + float: right; +} + +@media screen and (max-width: 600px) { + + ul.diva-navi li.right, + ul.diva-navi li { + float: none; + display: block; + text-align: center; + } +} + +table { + border-collapse: collapse; + border-spacing: 0; + border-collapse: separate; + overflow: hidden; + background-color: #555555; + +} + +th, td { + text-align: left; + border: none; + +} + +th { + color: white; +} + +.table-rowdistinct tr:nth-child(even) { + background-color: #303030; +} + +.table-rowdistinct tr:nth-child(odd) { + background-color: #555555; +} + +caption { + text-align: center; + color: white; + font-size: 18px; + font-weight: bold; +} + +.table-large { + margin: 16px; +} + +.table-large th, +.table-large td { + padding: 8px; +} + +.table-small { + width: 100%; + margin: 4px; +} + +.table-small th, +.table-small td { + padding: 2px; +} + +.bg-card { + background-color: #555555; +} + +.card-hover { + transition: all 0.2s ease-in-out; +} + +.card-hover:hover { + transform: scale(1.02); +} + +.basic { + color: #28a745; + font-weight: bold; +} + +.hard { + color: #ffc107; + + font-weight: bold; +} + +.expert { + color: #dc3545; + font-weight: bold; +} + +.master { + color: #dd09e8; + font-weight: bold; +} + +.ultimate { + color: #000000; + font-weight: bold; +} + +.score { + color: #ffffff; + font-weight: bold; +} + +.rainbow { + background: linear-gradient(to right, red, yellow, lime, aqua, blue, fuchsia) 0 / 5em; + background-clip: text; + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + font-weight: bold; +} + +.platinum { + color: #FFFF00; + font-weight: bold; +} + +.gold { + color: #FFFF00; + font-weight: bold; +} + +.scrolling-text { + overflow: hidden; +} + +.scrolling-text p { + white-space: nowrap; + display: inline-block; + +} + +.scrolling-text h6 { + white-space: nowrap; + display: inline-block; + +} + +.scrolling-text h5 { + white-space: nowrap; + display: inline-block; + +} + +.scrolling { + animation: scroll 10s linear infinite; +} + +@keyframes scroll { + 0% { + transform: translateX(100%); + } + 100% { + transform: translateX(-100%); + } +} \ No newline at end of file diff --git a/titles/diva/templates/diva_header.jinja b/titles/diva/templates/diva_header.jinja new file mode 100644 index 0000000..b92379a --- /dev/null +++ b/titles/diva/templates/diva_header.jinja @@ -0,0 +1,17 @@ +
+

diva

+ +
+ \ No newline at end of file diff --git a/titles/diva/templates/diva_index.jinja b/titles/diva/templates/diva_index.jinja new file mode 100644 index 0000000..c2f0888 --- /dev/null +++ b/titles/diva/templates/diva_index.jinja @@ -0,0 +1,111 @@ +{% extends "core/templates/index.jinja" %} +{% block content %} + +
+ {% include 'titles/diva/templates/diva_header.jinja' %} + {% if profile is defined and profile is not none and profile|length > 0 %} +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
OVERVIEW
Player name:{{ profile[3] }} + + Level string:{{ profile[4] }} + +
Lvl:{{ profile[5] }}
Lvl points:{{ profile[6] }}
Vocaloid points:{{ profile[7] }}
+
+
+
+ {% if error is defined %} + {% include "core/templates/widgets/err_banner.jinja" %} + {% endif %} + {% elif sesh is defined and sesh is not none and sesh.user_id > 0 %} + No profile information found for this account. + {% else %} + Login to view profile information. + {% endif %} +
+ + +{% endblock %} \ No newline at end of file diff --git a/titles/diva/templates/diva_playlog.jinja b/titles/diva/templates/diva_playlog.jinja new file mode 100644 index 0000000..c5a5618 --- /dev/null +++ b/titles/diva/templates/diva_playlog.jinja @@ -0,0 +1,169 @@ +{% extends "core/templates/index.jinja" %} +{% block content %} + +
+ {% include 'titles/diva/templates/diva_header.jinja' %} + {% if playlog is defined and playlog is not none %} +
+

Score counts: {{ playlog_count }}

+ {% set difficultyName = ['easy', 'normal', 'hard', 'extreme', 'extra extreme'] %} + {% set clearState = ['MISSxTAKE', 'STANDARD', 'GREAT', 'EXELLENT', 'PERFECT'] %} + {% for record in playlog %} +
+
+
+
+
+
{{ record.title }}
+
+
{{ record.vocaloid_arranger }}
+
+
+
{{record.raw.date_scored}}
+
+
+
+
+

{{ record.raw.score }}

+

{{ record.raw.atn_pnt / 100 }}%

+
{{ difficultyName[record.raw.difficulty] }}
+
+
+ + + + + + + + + + + + + + + + + + + + + +
COOL{{ record.raw.cool }}
FINE{{ record.raw.fine }}
SAFE{{ record.raw.safe }}
SAD{{ record.raw.sad }}
WORST{{ record.raw.worst }}
+
+
+
{{ record.raw.max_combo }}
+ {% if record.raw.clr_kind == -1 %} +
{{ clearState[0] }}
+ {% elif record.raw.clr_kind == 2 %} +
{{ clearState[1] }}
+ {% elif record.raw.clr_kind == 3 %} +
{{ clearState[2] }}
+ {% elif record.raw.clr_kind == 4 %} +
{{ clearState[3] }}
+ {% elif record.raw.clr_kind == 5 %} +
{{ clearState[4] }}
+ {% endif %} + {% if record.raw.clr_kind == -1 %} +
NOT CLEAR
+ {% else %} +
CLEAR
+ {% endif %} +
+
+
+
+
+ {% endfor %} +
+ {% set playlog_pages = playlog_count // 20 + 1 %} + {% elif sesh is defined and sesh is not none and sesh.user_id > 0 %} + No Score information found for this account. + {% else %} + Login to view profile information. + {% endif %} +
+ + + +{% endblock content %} \ No newline at end of file diff --git a/titles/idac/__init__.py b/titles/idac/__init__.py index 0c632bd..d308d43 100644 --- a/titles/idac/__init__.py +++ b/titles/idac/__init__.py @@ -9,4 +9,3 @@ database = IDACData reader = IDACReader frontend = IDACFrontend game_codes = [IDACConstants.GAME_CODE] -current_schema_version = 1 diff --git a/titles/idac/echo.py b/titles/idac/echo.py index 08e5526..0458159 100644 --- a/titles/idac/echo.py +++ b/titles/idac/echo.py @@ -1,7 +1,6 @@ import logging import socket -from twisted.internet.protocol import DatagramProtocol from socketserver import BaseRequestHandler, TCPServer from typing import Tuple @@ -10,19 +9,14 @@ from titles.idac.config import IDACConfig from titles.idac.database import IDACData -class IDACEchoUDP(DatagramProtocol): - def __init__(self, cfg: CoreConfig, game_cfg: IDACConfig, port: int) -> None: - super().__init__() - self.port = port - self.core_config = cfg - self.game_config = game_cfg - self.logger = logging.getLogger("idac") +class IDACEchoUDP: + def connection_made(self, transport): + self.transport = transport + + def datagram_received(self, data, addr): + logging.getLogger('idz').debug(f'Received echo from {addr}') + self.transport.sendto(data, addr) - def datagramReceived(self, data, addr): - self.logger.info( - f"UDP Ping from from {addr[0]}:{addr[1]} -> {self.port} - {data.hex()}" - ) - self.transport.write(data, addr) class IDACEchoTCP(BaseRequestHandler): diff --git a/titles/idac/frontend.py b/titles/idac/frontend.py index 78abae8..28fa624 100644 --- a/titles/idac/frontend.py +++ b/titles/idac/frontend.py @@ -1,12 +1,13 @@ import json +from typing import List +from starlette.routing import Route +from starlette.responses import Response, RedirectResponse import yaml import jinja2 from os import path -from twisted.web.util import redirectTo -from twisted.web.http import Request -from twisted.web.server import Session +from starlette.requests import Request -from core.frontend import FE_Base, IUserSession +from core.frontend import FE_Base, UserSession from core.config import CoreConfig from titles.idac.database import IDACData from titles.idac.schema.profile import * @@ -26,7 +27,8 @@ class IDACFrontend(FE_Base): self.game_cfg.update( yaml.safe_load(open(f"{cfg_dir}/{IDACConstants.CONFIG_NAME}")) ) - self.nav_name = "頭文字D THE ARCADE" + #self.nav_name = "頭文字D THE ARCADE" + self.nav_name = "IDAC" # TODO: Add version list self.version = IDACConstants.VER_IDAC_SEASON_2 @@ -36,8 +38,13 @@ class IDACFrontend(FE_Base): 25: "full_tune_tickets", 34: "full_tune_fragments", } + + def get_routes(self) -> List[Route]: + return [ + Route("/", self.render_GET) + ] - def generate_all_tables_json(self, user_id: int): + async def generate_all_tables_json(self, user_id: int): json_export = {} idac_tables = { @@ -73,7 +80,7 @@ class IDACFrontend(FE_Base): sql = sql.where(table.c.version == self.version) # lol use the profile connection for items, dirty hack - result = self.data.profile.execute(sql) + result = await self.data.profile.execute(sql) data_list = result.fetchall() # add the list to the json export with the correct table name @@ -86,49 +93,47 @@ class IDACFrontend(FE_Base): return json.dumps(json_export, indent=4, default=str, ensure_ascii=False) - def render_GET(self, request: Request) -> bytes: - uri: str = request.uri.decode() + async def render_GET(self, request: Request) -> bytes: + uri: str = request.url.path template = self.environment.get_template( - "titles/idac/frontend/idac_index.jinja" + "titles/idac/templates/idac_index.jinja" ) - sesh: Session = request.getSession() - usr_sesh = IUserSession(sesh) - user_id = usr_sesh.userId + usr_sesh = self.validate_session(request) + if not usr_sesh: + usr_sesh = UserSession() + user_id = usr_sesh.user_id # user_id = usr_sesh.user_id # profile export if uri.startswith("/game/idac/export"): if user_id == 0: - return redirectTo(b"/game/idac", request) + return RedirectResponse(b"/game/idac", request) # set the file name, content type and size to download the json - content = self.generate_all_tables_json(user_id).encode("utf-8") - request.responseHeaders.addRawHeader( - b"content-type", b"application/octet-stream" - ) - request.responseHeaders.addRawHeader( - b"content-disposition", b"attachment; filename=idac_profile.json" - ) - request.responseHeaders.addRawHeader( - b"content-length", str(len(content)).encode("utf-8") - ) + content = await self.generate_all_tables_json(user_id).encode("utf-8") self.logger.info(f"User {user_id} exported their IDAC data") - return content + return Response( + content, + 200, + {'content-disposition': 'attachment; filename=idac_profile.json'}, + "application/octet-stream" + ) profile_data, tickets, rank = None, None, None if user_id > 0: - profile_data = self.data.profile.get_profile(user_id, self.version) - ticket_data = self.data.item.get_tickets(user_id) - rank = self.data.profile.get_profile_rank(user_id, self.version) + profile_data = await self.data.profile.get_profile(user_id, self.version) + ticket_data = await self.data.item.get_tickets(user_id) + rank = await self.data.profile.get_profile_rank(user_id, self.version) - tickets = { - self.ticket_names[ticket["ticket_id"]]: ticket["ticket_cnt"] - for ticket in ticket_data - } + if ticket_data: + tickets = { + self.ticket_names[ticket["ticket_id"]]: ticket["ticket_cnt"] + for ticket in ticket_data + } - return template.render( + return Response(template.render( title=f"{self.core_config.server.name} | {self.nav_name}", game_list=self.environment.globals["game_list"], profile=profile_data, @@ -136,7 +141,4 @@ class IDACFrontend(FE_Base): rank=rank, sesh=vars(usr_sesh), active_page="idac", - ).encode("utf-16") - - def render_POST(self, request: Request) -> bytes: - pass + ), media_type="text/html; charset=utf-8") diff --git a/titles/idac/index.py b/titles/idac/index.py index 7daedae..4e24491 100644 --- a/titles/idac/index.py +++ b/titles/idac/index.py @@ -1,28 +1,26 @@ import json import traceback -import inflection +from starlette.routing import Route +from starlette.requests import Request +from starlette.responses import JSONResponse import yaml import logging import coloredlogs - from os import path from typing import Dict, List, Tuple from logging.handlers import TimedRotatingFileHandler -from twisted.web import server -from twisted.web.http import Request -from twisted.internet import reactor, endpoints +import asyncio from core.config import CoreConfig +from core.title import BaseServlet, JSONResponseNoASCII from core.utils import Utils from titles.idac.base import IDACBase from titles.idac.season2 import IDACSeason2 from titles.idac.config import IDACConfig from titles.idac.const import IDACConstants from titles.idac.echo import IDACEchoUDP -from titles.idac.matching import IDACMatching - -class IDACServlet: +class IDACServlet(BaseServlet): def __init__(self, core_cfg: CoreConfig, cfg_dir: str) -> None: self.core_cfg = core_cfg self.game_cfg = IDACConfig() @@ -72,12 +70,12 @@ class IDACServlet: return False return True - - def get_endpoint_matchers(self) -> Tuple[List[Tuple[str, str, Dict]], List[Tuple[str, str, Dict]]]: - return ( - [], - [("render_POST", "/SDGT/{version}/initiald/{category}/{endpoint}", {})] - ) + + def get_routes(self) -> List[Route]: + return [ + Route("/{version:int}/initiald/{category:str}/{endpoint:str}", self.render_POST, methods=["POST"]), + Route("/{version:int}/initiald-matching/{endpoint:str}", self.render_matching, methods=["POST"]), + ] def get_allnet_info( self, game_code: str, game_ver: int, keychip: str @@ -88,15 +86,15 @@ class IDACServlet: return ( f"", # requires http or else it defaults to https - f"http://{self.core_cfg.title.hostname}{t_port}/{game_code}/{game_ver}/", + f"http://{self.core_cfg.server.hostname}{t_port}/{game_ver}/", ) - def render_POST(self, request: Request, game_code: int, matchers: Dict) -> bytes: - req_raw = request.content.getvalue() + async def render_POST(self, request: Request) -> bytes: + req_raw = await request.body() internal_ver = 0 - version = int(matchers['version']) - category = matchers['category'] - endpoint = matchers['endpoint'] + version: int = request.path_params.get('version') + category: str = request.path_params.get('category') + endpoint: str = request.path_params.get('endpoint') client_ip = Utils.get_ip_addr(request) if version >= 100 and version < 140: # IDAC Season 1 @@ -104,7 +102,7 @@ class IDACServlet: elif version >= 140 and version < 171: # IDAC Season 2 internal_ver = IDACConstants.VER_IDAC_SEASON_2 - header_application = self.decode_header(request.getAllHeaders()) + header_application = self.decode_header(request.headers.get("application", "")) req_data = json.loads(req_raw) @@ -119,27 +117,61 @@ class IDACServlet: if not hasattr(self.versions[internal_ver], func_to_find): self.logger.warning(f"Unhandled v{version} request {endpoint}") - return '{"status_code": "0"}'.encode("utf-8") + return JSONResponse('{"status_code": "0"}') resp = None try: handler = getattr(self.versions[internal_ver], func_to_find) - resp = handler(req_data, header_application) + resp = await handler(req_data, header_application) except Exception as e: traceback.print_exc() self.logger.error(f"Error handling v{version} method {endpoint} - {e}") - return '{"status_code": "0"}'.encode("utf-8") + return JSONResponse('{"status_code": "0"}') if resp is None: resp = {"status_code": "0"} self.logger.debug(f"Response {resp}") - return json.dumps(resp, ensure_ascii=False).encode("utf-8") + return JSONResponseNoASCII(resp) + async def render_matching(self, request: Request): + url: str = request.path_params.get("endpoint") + ver: int = request.path_params.get("version") + client_ip = Utils.get_ip_addr(request) + req_data = await request.json() + header_application = self.decode_header(request.headers.get('application', '')) + user_id = int(header_application["session"]) - def decode_header(self, data: Dict) -> Dict: - app: str = data[b"application"].decode() + # self.getMatchingStatus(user_id) + + self.logger.info( + f"IDAC Matching request from {client_ip}: {url} - {req_data}" + ) + + resp = {"status_code": "0"} + if url == "/regist": + self.queue = self.queue + 1 + elif url == "/status": + if req_data.get("cancel_flag"): + self.queue = self.queue - 1 + self.logger.info( + f"IDAC Matching endpoint {client_ip} had quited" + ) + + resp = { + "status_code": "0", + # Only IPv4 is supported + "host": self.game_config.server.matching_host, + "port": self.game_config.server.matching_p2p, + "room_name": "INDTA", + "state": 1, + } + + self.logger.debug(f"Response {resp}") + return JSONResponseNoASCII(resp) + + def decode_header(self, app: str) -> Dict: ret = {} for x in app.split(", "): @@ -150,18 +182,13 @@ class IDACServlet: def setup(self): if self.game_cfg.server.enable: - endpoints.serverFromString( - reactor, - f"tcp:{self.game_cfg.server.matching}:interface={self.core_cfg.server.listen_address}", - ).listen(server.Site(IDACMatching(self.core_cfg, self.game_cfg))) - - reactor.listenUDP( - self.game_cfg.server.echo1, - IDACEchoUDP(self.core_cfg, self.game_cfg, self.game_cfg.server.echo1), - ) - reactor.listenUDP( - self.game_cfg.server.echo2, - IDACEchoUDP(self.core_cfg, self.game_cfg, self.game_cfg.server.echo2), + loop = asyncio.get_running_loop() + asyncio.create_task( + loop.create_datagram_endpoint( + lambda: IDACEchoUDP(), + local_addr=(self.core_cfg.server.listen_address, self.game_cfg.server.echo1) + ) ) - self.logger.info(f"Matching listening on {self.game_cfg.server.matching} with echos on {self.game_cfg.server.echo1} and {self.game_cfg.server.echo2}") + self.logger.info(f"Matching listening on {self.game_cfg.server.matching} with echo on {self.game_cfg.server.echo1}") + diff --git a/titles/idac/matching.py b/titles/idac/matching.py deleted file mode 100644 index 396eec8..0000000 --- a/titles/idac/matching.py +++ /dev/null @@ -1,72 +0,0 @@ -import json -import logging - -from typing import Dict -from twisted.web import resource - -from core import CoreConfig -from titles.idac.season2 import IDACBase -from titles.idac.config import IDACConfig - - -class IDACMatching(resource.Resource): - isLeaf = True - - def __init__(self, cfg: CoreConfig, game_cfg: IDACConfig) -> None: - self.core_config = cfg - self.game_config = game_cfg - self.base = IDACBase(cfg, game_cfg) - self.logger = logging.getLogger("idac") - - self.queue = 0 - - def get_matching_state(self): - if self.queue >= 1: - self.queue -= 1 - return 0 - else: - return 1 - - def render_POST(self, req) -> bytes: - url = req.uri.decode() - req_data = json.loads(req.content.getvalue().decode()) - header_application = self.decode_header(req.getAllHeaders()) - user_id = int(header_application["session"]) - - # self.getMatchingStatus(user_id) - - self.logger.info( - f"IDAC Matching request from {req.getClientIP()}: {url} - {req_data}" - ) - - resp = {"status_code": "0"} - if url == "/regist": - self.queue = self.queue + 1 - elif url == "/status": - if req_data.get("cancel_flag"): - self.queue = self.queue - 1 - self.logger.info( - f"IDAC Matching endpoint {req.getClientIP()} had quited" - ) - - resp = { - "status_code": "0", - # Only IPv4 is supported - "host": self.game_config.server.matching_host, - "port": self.game_config.server.matching_p2p, - "room_name": "INDTA", - "state": 1, - } - - self.logger.debug(f"Response {resp}") - return json.dumps(resp, ensure_ascii=False).encode("utf-8") - - def decode_header(self, data: Dict) -> Dict: - app: str = data[b"application"].decode() - ret = {} - - for x in app.split(", "): - y = x.split("=") - ret[y[0]] = y[1].replace('"', "") - - return ret diff --git a/titles/idac/read.py b/titles/idac/read.py index 8798e9b..3b7e034 100644 --- a/titles/idac/read.py +++ b/titles/idac/read.py @@ -33,7 +33,7 @@ class IDACReader(BaseReader): self.logger.error(f"Invalid Initial D THE ARCADE version {version}") exit(1) - def read(self) -> None: + async def read(self) -> None: if self.bin_dir is None and self.opt_dir is None: self.logger.error( ( @@ -59,9 +59,9 @@ class IDACReader(BaseReader): ) exit(1) - self.read_idac_profile(self.opt_dir) + await self.read_idac_profile(self.opt_dir) - def read_idac_profile(self, file_path: str) -> None: + async def read_idac_profile(self, file_path: str) -> None: self.logger.info(f"Reading profile from {file_path}...") # read it as binary to avoid encoding issues @@ -88,14 +88,14 @@ class IDACReader(BaseReader): self.logger.info("Exiting...") exit(0) - user_id = self.data.user.create_user() + user_id = await self.data.user.create_user() if user_id is None: self.logger.error("Failed to register user!") user_id = -1 else: - card_id = self.data.card.create_card(user_id, access_code) + card_id = await self.data.card.create_card(user_id, access_code) if card_id is None: self.logger.error("Failed to register card!") @@ -150,7 +150,7 @@ class IDACReader(BaseReader): # lol use the profile connection for items, dirty hack conflict = sql.on_duplicate_key_update(**data) - result = self.data.profile.execute(conflict) + result = await self.data.profile.execute(conflict) if result is None: self.logger.error(f"Failed to insert data into table {name}") diff --git a/titles/idac/schema/item.py b/titles/idac/schema/item.py index 80ee7ba..d617cd9 100644 --- a/titles/idac/schema/item.py +++ b/titles/idac/schema/item.py @@ -297,7 +297,7 @@ timetrial_event = Table( class IDACItemData(BaseData): - def get_random_user_car(self, aime_id: int, version: int) -> Optional[List[Row]]: + async def get_random_user_car(self, aime_id: int, version: int) -> Optional[List[Row]]: sql = ( select(car) .where(and_(car.c.user == aime_id, car.c.version == version)) @@ -305,20 +305,20 @@ class IDACItemData(BaseData): .limit(1) ) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchone() - def get_random_car(self, version: int) -> Optional[List[Row]]: + async def get_random_car(self, version: int) -> Optional[List[Row]]: sql = select(car).where(car.c.version == version).order_by(func.rand()).limit(1) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchone() - def get_car( + async def get_car( self, aime_id: int, version: int, style_car_id: int ) -> Optional[List[Row]]: sql = select(car).where( @@ -329,12 +329,12 @@ class IDACItemData(BaseData): ) ) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchone() - def get_cars( + async def get_cars( self, version: int, aime_id: int, only_pickup: bool = False ) -> Optional[List[Row]]: if only_pickup: @@ -350,106 +350,106 @@ class IDACItemData(BaseData): and_(car.c.user == aime_id, car.c.version == version) ) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchall() - def get_ticket(self, aime_id: int, ticket_id: int) -> Optional[Row]: + async def get_ticket(self, aime_id: int, ticket_id: int) -> Optional[Row]: sql = select(ticket).where( ticket.c.user == aime_id, ticket.c.ticket_id == ticket_id ) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchone() - def get_tickets(self, aime_id: int) -> Optional[List[Row]]: + async def get_tickets(self, aime_id: int) -> Optional[List[Row]]: sql = select(ticket).where(ticket.c.user == aime_id) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchall() - def get_story(self, aime_id: int, chapter_id: int) -> Optional[Row]: + async def get_story(self, aime_id: int, chapter_id: int) -> Optional[Row]: sql = select(story).where( and_(story.c.user == aime_id, story.c.chapter == chapter_id) ) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchone() - def get_stories(self, aime_id: int) -> Optional[List[Row]]: + async def get_stories(self, aime_id: int) -> Optional[List[Row]]: sql = select(story).where(story.c.user == aime_id) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchall() - def get_story_episodes(self, aime_id: int, chapter_id: int) -> Optional[List[Row]]: + async def get_story_episodes(self, aime_id: int, chapter_id: int) -> Optional[List[Row]]: sql = select(episode).where( and_(episode.c.user == aime_id, episode.c.chapter == chapter_id) ) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchall() - def get_story_episode(self, aime_id: int, episode_id: int) -> Optional[Row]: + async def get_story_episode(self, aime_id: int, episode_id: int) -> Optional[Row]: sql = select(episode).where( and_(episode.c.user == aime_id, episode.c.episode == episode_id) ) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchone() - def get_story_episode_difficulties( + async def get_story_episode_difficulties( self, aime_id: int, episode_id: int ) -> Optional[List[Row]]: sql = select(difficulty).where( and_(difficulty.c.user == aime_id, difficulty.c.episode == episode_id) ) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchall() - def get_courses(self, aime_id: int) -> Optional[List[Row]]: + async def get_courses(self, aime_id: int) -> Optional[List[Row]]: sql = select(course).where(course.c.user == aime_id) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchall() - def get_course(self, aime_id: int, course_id: int) -> Optional[Row]: + async def get_course(self, aime_id: int, course_id: int) -> Optional[Row]: sql = select(course).where( and_(course.c.user == aime_id, course.c.course_id == course_id) ) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchone() - def get_time_trial_courses(self, version: int) -> Optional[List[Row]]: + async def get_time_trial_courses(self, version: int) -> Optional[List[Row]]: sql = select(trial.c.course_id).where(trial.c.version == version).distinct() - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchall() - def get_time_trial_user_best_time_by_course_car( + async def get_time_trial_user_best_time_by_course_car( self, version: int, aime_id: int, course_id: int, style_car_id: int ) -> Optional[Row]: sql = select(trial).where( @@ -461,12 +461,12 @@ class IDACItemData(BaseData): ) ) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchone() - def get_time_trial_user_best_courses( + async def get_time_trial_user_best_courses( self, version: int, aime_id: int ) -> Optional[List[Row]]: # get for a given aime_id the best time for each course @@ -491,12 +491,12 @@ class IDACItemData(BaseData): ) ) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchall() - def get_time_trial_best_cars_by_course( + async def get_time_trial_best_cars_by_course( self, version: int, course_id: int, aime_id: Optional[int] = None ) -> Optional[List[Row]]: subquery = ( @@ -527,12 +527,12 @@ class IDACItemData(BaseData): ) ) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchall() - def get_time_trial_ranking_by_course( + async def get_time_trial_ranking_by_course( self, version: int, course_id: int, @@ -568,12 +568,12 @@ class IDACItemData(BaseData): if limit is not None: sql = sql.limit(limit) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchall() - def get_time_trial_best_ranking_by_course( + async def get_time_trial_best_ranking_by_course( self, version: int, aime_id: int, course_id: int ) -> Optional[Row]: sql = ( @@ -589,12 +589,12 @@ class IDACItemData(BaseData): .limit(1) ) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchone() - def get_challenge( + async def get_challenge( self, aime_id: int, vs_type: int, play_difficulty: int ) -> Optional[Row]: sql = select(challenge).where( @@ -605,20 +605,20 @@ class IDACItemData(BaseData): ) ) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchone() - def get_challenges(self, aime_id: int) -> Optional[List[Row]]: + async def get_challenges(self, aime_id: int) -> Optional[List[Row]]: sql = select(challenge).where(challenge.c.user == aime_id) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchall() - def get_best_challenges_by_vs_type( + async def get_best_challenges_by_vs_type( self, aime_id: int, story_type: int = 4 ) -> Optional[List[Row]]: subquery = ( @@ -653,20 +653,20 @@ class IDACItemData(BaseData): .order_by(challenge.c.vs_type) ) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchall() - def get_theory_courses(self, aime_id: int) -> Optional[List[Row]]: + async def get_theory_courses(self, aime_id: int) -> Optional[List[Row]]: sql = select(theory_course).where(theory_course.c.user == aime_id) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchall() - def get_theory_course_by_powerhouse_lv( + async def get_theory_course_by_powerhouse_lv( self, aime_id: int, course_id: int, powerhouse_lv: int, count: int = 3 ) -> Optional[List[Row]]: sql = ( @@ -682,40 +682,40 @@ class IDACItemData(BaseData): .limit(count) ) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchall() - def get_theory_course(self, aime_id: int, course_id: int) -> Optional[List[Row]]: + async def get_theory_course(self, aime_id: int, course_id: int) -> Optional[List[Row]]: sql = select(theory_course).where( and_( theory_course.c.user == aime_id, theory_course.c.course_id == course_id ) ) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchone() - def get_theory_partners(self, aime_id: int) -> Optional[List[Row]]: + async def get_theory_partners(self, aime_id: int) -> Optional[List[Row]]: sql = select(theory_partner).where(theory_partner.c.user == aime_id) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchall() - def get_theory_running(self, aime_id: int) -> Optional[List[Row]]: + async def get_theory_running(self, aime_id: int) -> Optional[List[Row]]: sql = select(theory_running).where(theory_running.c.user == aime_id) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchall() - def get_theory_running_by_course( + async def get_theory_running_by_course( self, aime_id: int, course_id: int ) -> Optional[Row]: sql = select(theory_running).where( @@ -725,32 +725,32 @@ class IDACItemData(BaseData): ) ) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchone() - def get_vs_infos(self, aime_id: int) -> Optional[List[Row]]: + async def get_vs_infos(self, aime_id: int) -> Optional[List[Row]]: sql = select(vs_info).where(vs_info.c.user == aime_id) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchall() - def get_stamps(self, aime_id: int) -> Optional[List[Row]]: + async def get_stamps(self, aime_id: int) -> Optional[List[Row]]: sql = select(stamp).where( and_( stamp.c.user == aime_id, ) ) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchall() - def get_timetrial_event(self, aime_id: int, timetrial_event_id: int) -> Optional[Row]: + async def get_timetrial_event(self, aime_id: int, timetrial_event_id: int) -> Optional[Row]: sql = select(timetrial_event).where( and_( timetrial_event.c.user == aime_id, @@ -758,49 +758,49 @@ class IDACItemData(BaseData): ) ) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchone() - def put_car(self, aime_id: int, version: int, car_data: Dict) -> Optional[int]: + async def put_car(self, aime_id: int, version: int, car_data: Dict) -> Optional[int]: car_data["user"] = aime_id car_data["version"] = version sql = insert(car).values(**car_data) conflict = sql.on_duplicate_key_update(**car_data) - result = self.execute(conflict) + result = await self.execute(conflict) if result is None: self.logger.warn(f"put_car: Failed to update! aime_id: {aime_id}") return None return result.lastrowid - def put_ticket(self, aime_id: int, ticket_data: Dict) -> Optional[int]: + async def put_ticket(self, aime_id: int, ticket_data: Dict) -> Optional[int]: ticket_data["user"] = aime_id sql = insert(ticket).values(**ticket_data) conflict = sql.on_duplicate_key_update(**ticket_data) - result = self.execute(conflict) + result = await self.execute(conflict) if result is None: self.logger.warn(f"put_ticket: Failed to update! aime_id: {aime_id}") return None return result.lastrowid - def put_story(self, aime_id: int, story_data: Dict) -> Optional[int]: + async def put_story(self, aime_id: int, story_data: Dict) -> Optional[int]: story_data["user"] = aime_id sql = insert(story).values(**story_data) conflict = sql.on_duplicate_key_update(**story_data) - result = self.execute(conflict) + result = await self.execute(conflict) if result is None: self.logger.warn(f"put_story: Failed to update! aime_id: {aime_id}") return None return result.lastrowid - def put_story_episode_play_status( + async def put_story_episode_play_status( self, aime_id: int, chapter_id: int, play_status: int = 1 ) -> Optional[int]: sql = ( @@ -809,7 +809,7 @@ class IDACItemData(BaseData): .values(play_status=play_status) ) - result = self.execute(sql) + result = await self.execute(sql) if result is None: self.logger.warn( f"put_story_episode_play_status: Failed to update! aime_id: {aime_id}" @@ -817,7 +817,7 @@ class IDACItemData(BaseData): return None return result.lastrowid - def put_story_episode( + async def put_story_episode( self, aime_id: int, chapter_id: int, episode_data: Dict ) -> Optional[int]: episode_data["user"] = aime_id @@ -825,14 +825,14 @@ class IDACItemData(BaseData): sql = insert(episode).values(**episode_data) conflict = sql.on_duplicate_key_update(**episode_data) - result = self.execute(conflict) + result = await self.execute(conflict) if result is None: self.logger.warn(f"put_story_episode: Failed to update! aime_id: {aime_id}") return None return result.lastrowid - def put_story_episode_difficulty( + async def put_story_episode_difficulty( self, aime_id: int, episode_id: int, difficulty_data: Dict ) -> Optional[int]: difficulty_data["user"] = aime_id @@ -840,7 +840,7 @@ class IDACItemData(BaseData): sql = insert(difficulty).values(**difficulty_data) conflict = sql.on_duplicate_key_update(**difficulty_data) - result = self.execute(conflict) + result = await self.execute(conflict) if result is None: self.logger.warn( @@ -849,19 +849,19 @@ class IDACItemData(BaseData): return None return result.lastrowid - def put_course(self, aime_id: int, course_data: Dict) -> Optional[int]: + async def put_course(self, aime_id: int, course_data: Dict) -> Optional[int]: course_data["user"] = aime_id sql = insert(course).values(**course_data) conflict = sql.on_duplicate_key_update(**course_data) - result = self.execute(conflict) + result = await self.execute(conflict) if result is None: self.logger.warn(f"put_course: Failed to update! aime_id: {aime_id}") return None return result.lastrowid - def put_time_trial( + async def put_time_trial( self, version: int, aime_id: int, time_trial_data: Dict ) -> Optional[int]: time_trial_data["user"] = aime_id @@ -869,47 +869,47 @@ class IDACItemData(BaseData): sql = insert(trial).values(**time_trial_data) conflict = sql.on_duplicate_key_update(**time_trial_data) - result = self.execute(conflict) + result = await self.execute(conflict) if result is None: self.logger.warn(f"put_time_trial: Failed to update! aime_id: {aime_id}") return None return result.lastrowid - def put_challenge(self, aime_id: int, challenge_data: Dict) -> Optional[int]: + async def put_challenge(self, aime_id: int, challenge_data: Dict) -> Optional[int]: challenge_data["user"] = aime_id sql = insert(challenge).values(**challenge_data) conflict = sql.on_duplicate_key_update(**challenge_data) - result = self.execute(conflict) + result = await self.execute(conflict) if result is None: self.logger.warn(f"put_challenge: Failed to update! aime_id: {aime_id}") return None return result.lastrowid - def put_theory_course( + async def put_theory_course( self, aime_id: int, theory_course_data: Dict ) -> Optional[int]: theory_course_data["user"] = aime_id sql = insert(theory_course).values(**theory_course_data) conflict = sql.on_duplicate_key_update(**theory_course_data) - result = self.execute(conflict) + result = await self.execute(conflict) if result is None: self.logger.warn(f"put_theory_course: Failed to update! aime_id: {aime_id}") return None return result.lastrowid - def put_theory_partner( + async def put_theory_partner( self, aime_id: int, theory_partner_data: Dict ) -> Optional[int]: theory_partner_data["user"] = aime_id sql = insert(theory_partner).values(**theory_partner_data) conflict = sql.on_duplicate_key_update(**theory_partner_data) - result = self.execute(conflict) + result = await self.execute(conflict) if result is None: self.logger.warn( @@ -918,14 +918,14 @@ class IDACItemData(BaseData): return None return result.lastrowid - def put_theory_running( + async def put_theory_running( self, aime_id: int, theory_running_data: Dict ) -> Optional[int]: theory_running_data["user"] = aime_id sql = insert(theory_running).values(**theory_running_data) conflict = sql.on_duplicate_key_update(**theory_running_data) - result = self.execute(conflict) + result = await self.execute(conflict) if result is None: self.logger.warn( @@ -934,26 +934,26 @@ class IDACItemData(BaseData): return None return result.lastrowid - def put_vs_info(self, aime_id: int, vs_info_data: Dict) -> Optional[int]: + async def put_vs_info(self, aime_id: int, vs_info_data: Dict) -> Optional[int]: vs_info_data["user"] = aime_id sql = insert(vs_info).values(**vs_info_data) conflict = sql.on_duplicate_key_update(**vs_info_data) - result = self.execute(conflict) + result = await self.execute(conflict) if result is None: self.logger.warn(f"put_vs_info: Failed to update! aime_id: {aime_id}") return None return result.lastrowid - def put_stamp( + async def put_stamp( self, aime_id: int, stamp_data: Dict ) -> Optional[int]: stamp_data["user"] = aime_id sql = insert(stamp).values(**stamp_data) conflict = sql.on_duplicate_key_update(**stamp_data) - result = self.execute(conflict) + result = await self.execute(conflict) if result is None: self.logger.warn( @@ -962,7 +962,7 @@ class IDACItemData(BaseData): return None return result.lastrowid - def put_timetrial_event( + async def put_timetrial_event( self, aime_id: int, time_trial_event_id: int, point: int ) -> Optional[int]: timetrial_event_data = { @@ -973,7 +973,7 @@ class IDACItemData(BaseData): sql = insert(timetrial_event).values(**timetrial_event_data) conflict = sql.on_duplicate_key_update(**timetrial_event_data) - result = self.execute(conflict) + result = await self.execute(conflict) if result is None: self.logger.warn( diff --git a/titles/idac/schema/profile.py b/titles/idac/schema/profile.py index 5e363ca..bb6593b 100644 --- a/titles/idac/schema/profile.py +++ b/titles/idac/schema/profile.py @@ -253,7 +253,7 @@ class IDACProfileData(BaseData): ) self.date_time_format_short = "%Y-%m-%d" - def get_profile(self, aime_id: int, version: int) -> Optional[Row]: + async def get_profile(self, aime_id: int, version: int) -> Optional[Row]: sql = select(profile).where( and_( profile.c.user == aime_id, @@ -261,12 +261,12 @@ class IDACProfileData(BaseData): ) ) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchone() - def get_different_random_profiles( + async def get_different_random_profiles( self, aime_id: int, version: int, count: int = 9 ) -> Optional[Row]: sql = ( @@ -281,36 +281,36 @@ class IDACProfileData(BaseData): .limit(count) ) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchall() - def get_profile_config(self, aime_id: int) -> Optional[Row]: + async def get_profile_config(self, aime_id: int) -> Optional[Row]: sql = select(config).where( and_( config.c.user == aime_id, ) ) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchone() - def get_profile_avatar(self, aime_id: int) -> Optional[Row]: + async def get_profile_avatar(self, aime_id: int) -> Optional[Row]: sql = select(avatar).where( and_( avatar.c.user == aime_id, ) ) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchone() - def get_profile_rank(self, aime_id: int, version: int) -> Optional[Row]: + async def get_profile_rank(self, aime_id: int, version: int) -> Optional[Row]: sql = select(rank).where( and_( rank.c.user == aime_id, @@ -318,12 +318,12 @@ class IDACProfileData(BaseData): ) ) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchone() - def get_profile_stock(self, aime_id: int, version: int) -> Optional[Row]: + async def get_profile_stock(self, aime_id: int, version: int) -> Optional[Row]: sql = select(stock).where( and_( stock.c.user == aime_id, @@ -331,12 +331,12 @@ class IDACProfileData(BaseData): ) ) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchone() - def get_profile_theory(self, aime_id: int, version: int) -> Optional[Row]: + async def get_profile_theory(self, aime_id: int, version: int) -> Optional[Row]: sql = select(theory).where( and_( theory.c.user == aime_id, @@ -344,12 +344,12 @@ class IDACProfileData(BaseData): ) ) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchone() - def put_profile( + async def put_profile( self, aime_id: int, version: int, profile_data: Dict ) -> Optional[int]: profile_data["user"] = aime_id @@ -357,19 +357,19 @@ class IDACProfileData(BaseData): sql = insert(profile).values(**profile_data) conflict = sql.on_duplicate_key_update(**profile_data) - result = self.execute(conflict) + result = await self.execute(conflict) if result is None: self.logger.warn(f"put_profile: Failed to update! aime_id: {aime_id}") return None return result.lastrowid - def put_profile_config(self, aime_id: int, config_data: Dict) -> Optional[int]: + async def put_profile_config(self, aime_id: int, config_data: Dict) -> Optional[int]: config_data["user"] = aime_id sql = insert(config).values(**config_data) conflict = sql.on_duplicate_key_update(**config_data) - result = self.execute(conflict) + result = await self.execute(conflict) if result is None: self.logger.warn( @@ -378,12 +378,12 @@ class IDACProfileData(BaseData): return None return result.lastrowid - def put_profile_avatar(self, aime_id: int, avatar_data: Dict) -> Optional[int]: + async def put_profile_avatar(self, aime_id: int, avatar_data: Dict) -> Optional[int]: avatar_data["user"] = aime_id sql = insert(avatar).values(**avatar_data) conflict = sql.on_duplicate_key_update(**avatar_data) - result = self.execute(conflict) + result = await self.execute(conflict) if result is None: self.logger.warn( @@ -392,7 +392,7 @@ class IDACProfileData(BaseData): return None return result.lastrowid - def put_profile_rank( + async def put_profile_rank( self, aime_id: int, version: int, rank_data: Dict ) -> Optional[int]: rank_data["user"] = aime_id @@ -400,14 +400,14 @@ class IDACProfileData(BaseData): sql = insert(rank).values(**rank_data) conflict = sql.on_duplicate_key_update(**rank_data) - result = self.execute(conflict) + result = await self.execute(conflict) if result is None: self.logger.warn(f"put_profile_rank: Failed to update! aime_id: {aime_id}") return None return result.lastrowid - def put_profile_stock( + async def put_profile_stock( self, aime_id: int, version: int, stock_data: Dict ) -> Optional[int]: stock_data["user"] = aime_id @@ -415,14 +415,14 @@ class IDACProfileData(BaseData): sql = insert(stock).values(**stock_data) conflict = sql.on_duplicate_key_update(**stock_data) - result = self.execute(conflict) + result = await self.execute(conflict) if result is None: self.logger.warn(f"put_profile_stock: Failed to update! aime_id: {aime_id}") return None return result.lastrowid - def put_profile_theory( + async def put_profile_theory( self, aime_id: int, version: int, theory_data: Dict ) -> Optional[int]: theory_data["user"] = aime_id @@ -430,7 +430,7 @@ class IDACProfileData(BaseData): sql = insert(theory).values(**theory_data) conflict = sql.on_duplicate_key_update(**theory_data) - result = self.execute(conflict) + result = await self.execute(conflict) if result is None: self.logger.warn( diff --git a/titles/idac/season2.py b/titles/idac/season2.py index ca57392..72b15c3 100644 --- a/titles/idac/season2.py +++ b/titles/idac/season2.py @@ -6,6 +6,7 @@ import json import logging from core.config import CoreConfig +from core.utils import Utils from titles.idac.const import IDACConstants from titles.idac.config import IDACConfig from titles.idac.base import IDACBase @@ -52,7 +53,7 @@ class IDACSeason2(IDACBase): "timetrial_event_id" ) - def handle_alive_get_request(self, data: Dict, headers: Dict): + async def handle_alive_get_request(self, data: Dict, headers: Dict): return { "status_code": "0", # 1 = success, 0 = failed @@ -75,7 +76,7 @@ class IDACSeason2(IDACBase): output[key] = value return output - def handle_boot_getconfigdata_request(self, data: Dict, headers: Dict): + async def handle_boot_getconfigdata_request(self, data: Dict, headers: Dict): """ category: 1 = D Coin @@ -108,10 +109,10 @@ class IDACSeason2(IDACBase): version = headers["device_version"] ver_str = version.replace(".", "")[:3] - if self.core_cfg.server.is_develop: - domain_api_game = f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDGT/{ver_str}/" + if self.core_cfg.server.is_using_proxy: + domain_api_game = f"http://{self.core_cfg.server.hostname}/{ver_str}/" else: - domain_api_game = f"http://{self.core_cfg.title.hostname}/SDGT/{ver_str}/" + domain_api_game = f"http://{self.core_cfg.server.hostname}:{Utils.get_title_port(self.core_cfg)}/{ver_str}/" return { "status_code": "0", @@ -136,10 +137,10 @@ class IDACSeason2(IDACBase): "server_maintenance_end_hour": 0, "server_maintenance_end_minutes": 0, "domain_api_game": domain_api_game, - "domain_matching": f"http://{self.core_cfg.title.hostname}:{self.game_config.server.matching}", - "domain_echo1": f"{self.core_cfg.title.hostname}:{self.game_config.server.echo1}", - "domain_echo2": f"{self.core_cfg.title.hostname}:{self.game_config.server.echo2}", - "domain_ping": f"{self.core_cfg.title.hostname}", + "domain_matching": f"{domain_api_game}initiald-matching/", + "domain_echo1": f"{self.core_cfg.server.hostname}:{self.game_config.server.echo1}", + "domain_echo2": f"{self.core_cfg.server.hostname}:{self.game_config.server.echo1}", + "domain_ping": f"{self.core_cfg.server.hostname}", "battle_gift_event_master": [], "round_event": [ { @@ -296,10 +297,10 @@ class IDACSeason2(IDACBase): "timetrial_event_data": self.timetrial_event, } - def handle_boot_bookkeep_request(self, data: Dict, headers: Dict): + async def handle_boot_bookkeep_request(self, data: Dict, headers: Dict): pass - def handle_boot_getgachadata_request(self, data: Dict, headers: Dict): + async def handle_boot_getgachadata_request(self, data: Dict, headers: Dict): """ Reward category types: 9: Face @@ -349,7 +350,7 @@ class IDACSeason2(IDACBase): return avatar_gacha_data - def handle_boot_gettimereleasedata_request(self, data: Dict, headers: Dict): + async def handle_boot_gettimereleasedata_request(self, data: Dict, headers: Dict): """ timerelease chapter: 1 = Story: 1, 2, 3, 4, 5, 6, 7, 8, 9, 19 (Chapter 10), (29 Chapter 11 lol?) @@ -384,12 +385,12 @@ class IDACSeason2(IDACBase): return time_release_data - def handle_advertise_getrankingdata_request(self, data: Dict, headers: Dict): + async def handle_advertise_getrankingdata_request(self, data: Dict, headers: Dict): best_data = [] for last_update in data.get("last_update_date"): course_id = last_update.get("course_id") - ranking = self.data.item.get_time_trial_ranking_by_course( + ranking = await self.data.item.get_time_trial_ranking_by_course( self.version, course_id ) ranking_data = [] @@ -397,8 +398,8 @@ class IDACSeason2(IDACBase): user_id = rank["user"] # get the username, country and store from the profile - profile = self.data.profile.get_profile(user_id, self.version) - arcade = self.data.arcade.get_arcade(profile["store"]) + profile = await self.data.profile.get_profile(user_id, self.version) + arcade = await self.data.arcade.get_arcade(profile["store"]) if arcade is None: arcade = {} @@ -443,17 +444,17 @@ class IDACSeason2(IDACBase): "rank_management_flag": 0, } - def handle_login_checklock_request(self, data: Dict, headers: Dict): + async def handle_login_checklock_request(self, data: Dict, headers: Dict): user_id = data["id"] access_code = data["accesscode"] is_new_player = 0 # check that the user_id from access_code matches the user_id - if user_id == self.data.card.get_user_id_from_card(access_code): + if user_id == await self.data.card.get_user_id_from_card(access_code): lock_result = 1 # check if an IDAC profile already exists - p = self.data.profile.get_profile(user_id, self.version) + p = await self.data.profile.get_profile(user_id, self.version) is_new_player = 1 if p is None else 0 else: lock_result = 0 @@ -473,35 +474,35 @@ class IDACSeason2(IDACBase): "server_status": 1, } - def handle_login_unlock_request(self, data: Dict, headers: Dict): + async def handle_login_unlock_request(self, data: Dict, headers: Dict): return { "status_code": "0", "lock_result": 1, } - def handle_login_relock_request(self, data: Dict, headers: Dict): + async def handle_login_relock_request(self, data: Dict, headers: Dict): return { "status_code": "0", "lock_result": 1, "lock_date": int(datetime.now().timestamp()), } - def handle_login_guestplay_request(self, data: Dict, headers: Dict): + async def handle_login_guestplay_request(self, data: Dict, headers: Dict): # TODO pass - def _generate_story_data(self, user_id: int) -> Dict: - stories = self.data.item.get_stories(user_id) + async def _generate_story_data(self, user_id: int) -> Dict: + stories = await self.data.item.get_stories(user_id) story_data = [] for s in stories: chapter_id = s["chapter"] - episodes = self.data.item.get_story_episodes(user_id, chapter_id) + episodes = await self.data.item.get_story_episodes(user_id, chapter_id) episode_data = [] for e in episodes: episode_id = e["episode"] - difficulties = self.data.item.get_story_episode_difficulties( + difficulties = await self.data.item.get_story_episode_difficulties( user_id, episode_id ) @@ -536,9 +537,9 @@ class IDACSeason2(IDACBase): return story_data - def _generate_special_data(self, user_id: int) -> Dict: + async def _generate_special_data(self, user_id: int) -> Dict: # 4 = special mode - specials = self.data.item.get_best_challenges_by_vs_type(user_id, story_type=4) + specials = await self.data.item.get_best_challenges_by_vs_type(user_id, story_type=4) special_data = [] for s in specials: @@ -555,9 +556,9 @@ class IDACSeason2(IDACBase): return special_data - def _generate_challenge_data(self, user_id: int) -> Dict: + async def _generate_challenge_data(self, user_id: int) -> Dict: # challenge mode (Bunta challenge only right now) - challenges = self.data.item.get_best_challenges_by_vs_type( + challenges = await self.data.item.get_best_challenges_by_vs_type( user_id, story_type=3 ) @@ -577,24 +578,24 @@ class IDACSeason2(IDACBase): return challenge_data - def _save_stock_data(self, user_id: int, stock_data: Dict): + async def _save_stock_data(self, user_id: int, stock_data: Dict): updated_stock_data = {} for k, v in stock_data.items(): if v != "": updated_stock_data[k] = v if updated_stock_data: - self.data.profile.put_profile_stock( + await self.data.profile.put_profile_stock( user_id, self.version, updated_stock_data ) - def handle_user_getdata_request(self, data: Dict, headers: Dict): + async def handle_user_getdata_request(self, data: Dict, headers: Dict): user_id = int(headers["session"]) # get the user's profile, can never be None - p = self.data.profile.get_profile(user_id, self.version) + p = await self.data.profile.get_profile(user_id, self.version) user_data = p._asdict() - arcade = self.data.arcade.get_arcade(user_data["store"]) + arcade = await self.data.arcade.get_arcade(user_data["store"]) del user_data["id"] del user_data["user"] @@ -607,7 +608,7 @@ class IDACSeason2(IDACBase): user_data["create_date"] = int(user_data["create_date"].timestamp()) # get the user's rank - r = self.data.profile.get_profile_rank(user_id, self.version) + r = await self.data.profile.get_profile_rank(user_id, self.version) rank_data = r._asdict() del rank_data["id"] del rank_data["user"] @@ -617,27 +618,27 @@ class IDACSeason2(IDACBase): user_data["mode_rank_data"] = rank_data # get the user's avatar - a = self.data.profile.get_profile_avatar(user_id) + a = await self.data.profile.get_profile_avatar(user_id) avatar_data = a._asdict() del avatar_data["id"] del avatar_data["user"] # get the user's stock - s = self.data.profile.get_profile_stock(user_id, self.version) + s = await self.data.profile.get_profile_stock(user_id, self.version) stock_data = s._asdict() del stock_data["id"] del stock_data["user"] del stock_data["version"] # get the user's config - c = self.data.profile.get_profile_config(user_id) + c = await self.data.profile.get_profile_config(user_id) config_data = c._asdict() del config_data["id"] del config_data["user"] config_data["id"] = config_data.pop("config_id") # get the user's ticket - tickets: list = self.data.item.get_tickets(user_id) + tickets: list = await self.data.item.get_tickets(user_id) """ ticket_id: @@ -657,7 +658,7 @@ class IDACSeason2(IDACBase): ) # get the user's course, required for the "course proeficiency" - courses = self.data.item.get_courses(user_id) + courses = await self.data.item.get_courses(user_id) course_data = [] for course in courses: course_data.append( @@ -672,7 +673,7 @@ class IDACSeason2(IDACBase): # get the profile theory data theory_data = {} - theory = self.data.profile.get_profile_theory(user_id, self.version) + theory = await self.data.profile.get_profile_theory(user_id, self.version) if theory is not None: theory_data = theory._asdict() del theory_data["id"] @@ -681,7 +682,7 @@ class IDACSeason2(IDACBase): # get the users theory course data theory_course_data = [] - theory_courses = self.data.item.get_theory_courses(user_id) + theory_courses = await self.data.item.get_theory_courses(user_id) for course in theory_courses: tmp = course._asdict() del tmp["id"] @@ -692,7 +693,7 @@ class IDACSeason2(IDACBase): # get the users theory partner data theory_partner_data = [] - theory_partners = self.data.item.get_theory_partners(user_id) + theory_partners = await self.data.item.get_theory_partners(user_id) for partner in theory_partners: tmp = partner._asdict() del tmp["id"] @@ -702,7 +703,7 @@ class IDACSeason2(IDACBase): # get the users theory running pram data theory_running_pram_data = [] - theory_running = self.data.item.get_theory_running(user_id) + theory_running = await self.data.item.get_theory_running(user_id) for running in theory_running: tmp = running._asdict() del tmp["id"] @@ -712,7 +713,7 @@ class IDACSeason2(IDACBase): # get the users vs info data vs_info_data = [] - vs_info = self.data.item.get_vs_infos(user_id) + vs_info = await self.data.item.get_vs_infos(user_id) for vs in vs_info: vs_info_data.append( { @@ -736,7 +737,7 @@ class IDACSeason2(IDACBase): ) # get the user's car - cars = self.data.item.get_cars(self.version, user_id, only_pickup=True) + cars = await self.data.item.get_cars(self.version, user_id, only_pickup=True) fulltune_count = 0 total_car_parts_count = 0 car_data = [] @@ -760,7 +761,7 @@ class IDACSeason2(IDACBase): user_data["have_car_cnt"] = len(car_data) # get the user's play stamps - stamps = self.data.item.get_stamps(user_id) + stamps = await self.data.item.get_stamps(user_id) stamp_event_data = [] for stamp in stamps: tmp = stamp._asdict() @@ -789,7 +790,7 @@ class IDACSeason2(IDACBase): tmp["weekly_bonus"] = 0 # update the play stamp in the database - self.data.item.put_stamp(user_id, tmp) + await self.data.item.put_stamp(user_id, tmp) del tmp["create_date_daily"] del tmp["create_date_weekly"] @@ -797,7 +798,7 @@ class IDACSeason2(IDACBase): # get the user's timetrial event data timetrial_event_data = {} - timetrial = self.data.item.get_timetrial_event(user_id, self.timetrial_event_id) + timetrial = await self.data.item.get_timetrial_event(user_id, self.timetrial_event_id) if timetrial is not None: timetrial_event_data = { "timetrial_event_id": timetrial["timetrial_event_id"], @@ -809,7 +810,7 @@ class IDACSeason2(IDACBase): "user_base_data": user_data, "avatar_data": avatar_data, "pick_up_car_data": car_data, - "story_data": self._generate_story_data(user_id), + "story_data": await self._generate_story_data(user_id), "vsinfo_data": vs_info_data, "stock_data": stock_data, "mission_data": { @@ -877,21 +878,21 @@ class IDACSeason2(IDACBase): "theory_course_data": theory_course_data, "theory_partner_data": theory_partner_data, "theory_running_pram_data": theory_running_pram_data, - "special_mode_data": self._generate_special_data(user_id), - "challenge_mode_data": self._generate_challenge_data(user_id), + "special_mode_data": await self._generate_special_data(user_id), + "challenge_mode_data": await self._generate_challenge_data(user_id), "season_rewards_data": [], "timetrial_event_data": timetrial_event_data, "special_mode_hint_data": {"story_type": 0, "hint_display_flag": 0}, } - def handle_timetrial_getbestrecordpreta_request(self, data: Dict, headers: Dict): + async def handle_timetrial_getbestrecordpreta_request(self, data: Dict, headers: Dict): user_id = headers["session"] for car_id in data["car_ids"]: pass course_mybest_data = [] - courses = self.data.item.get_time_trial_user_best_courses(self.version, user_id) + courses = await self.data.item.get_time_trial_user_best_courses(self.version, user_id) for course in courses: course_mybest_data.append( { @@ -919,10 +920,10 @@ class IDACSeason2(IDACBase): ) course_pickup_car_best_data = [] - courses = self.data.item.get_time_trial_courses(self.version) + courses = await self.data.item.get_time_trial_courses(self.version) for course in courses: car_list = [] - best_cars = self.data.item.get_time_trial_best_cars_by_course( + best_cars = await self.data.item.get_time_trial_best_cars_by_course( self.version, course["course_id"], user_id ) @@ -959,7 +960,7 @@ class IDACSeason2(IDACBase): "course_pickup_car_best_data": course_pickup_car_best_data, } - def handle_timetrial_getbestrecordprerace_request(self, data: Dict, headers: Dict): + async def handle_timetrial_getbestrecordprerace_request(self, data: Dict, headers: Dict): user_id = headers["session"] course_id = data["course_id"] @@ -968,7 +969,7 @@ class IDACSeason2(IDACBase): style_car_id = car["style_car_id"] # Not sure if this is actually correct - ranking = self.data.item.get_time_trial_ranking_by_course( + ranking = await self.data.item.get_time_trial_ranking_by_course( self.version, course_id ) course_best_data = [] @@ -976,8 +977,8 @@ class IDACSeason2(IDACBase): car_user_id = rank["user"] # get the username, country and store from the profile - profile = self.data.profile.get_profile(car_user_id, self.version) - arcade = self.data.arcade.get_arcade(profile["store"]) + profile = await self.data.profile.get_profile(car_user_id, self.version) + arcade = await self.data.arcade.get_arcade(profile["store"]) if arcade is None: arcade = {} @@ -1006,7 +1007,7 @@ class IDACSeason2(IDACBase): } ) - best_cars = self.data.item.get_time_trial_best_cars_by_course( + best_cars = await self.data.item.get_time_trial_best_cars_by_course( self.version, course_id ) @@ -1014,8 +1015,8 @@ class IDACSeason2(IDACBase): for i, rank in enumerate(best_cars): car_user_id = rank["user"] # get the username, country and store from the profile - profile = self.data.profile.get_profile(car_user_id, self.version) - arcade = self.data.arcade.get_arcade(profile["store"]) + profile = await self.data.profile.get_profile(car_user_id, self.version) + arcade = await self.data.arcade.get_arcade(profile["store"]) if arcade is None: arcade = {} @@ -1050,7 +1051,7 @@ class IDACSeason2(IDACBase): "course_best_data": course_best_data, } - def handle_user_createaccount_request(self, data: Dict, headers: Dict): + async def handle_user_createaccount_request(self, data: Dict, headers: Dict): user_id = headers["session"] car_data: Dict = data.pop("car_obj") @@ -1069,45 +1070,45 @@ class IDACSeason2(IDACBase): data["store"] = headers.get("a_store", 0) data["country"] = headers.get("a_country", 0) data["asset_version"] = headers.get("asset_version", 1) - self.data.profile.put_profile(user_id, self.version, data) + await self.data.profile.put_profile(user_id, self.version, data) # save rank data in database - self.data.profile.put_profile_rank(user_id, self.version, rank_data) + await self.data.profile.put_profile_rank(user_id, self.version, rank_data) # save stock data in database - self._save_stock_data(user_id, stock_data) + await self._save_stock_data(user_id, stock_data) # save tickets in database for ticket in takeover_ticket_list: - self.data.item.put_ticket(user_id, ticket) + await self.data.item.put_ticket(user_id, ticket) config_data["config_id"] = config_data.pop("id") - self.data.profile.put_profile_config(user_id, config_data) - self.data.profile.put_profile_avatar(user_id, avatar_data) + await self.data.profile.put_profile_config(user_id, config_data) + await self.data.profile.put_profile_avatar(user_id, avatar_data) # save car data and car parts in database car_data["parts_list"] = parts_data - self.data.item.put_car(user_id, self.version, car_data) + await self.data.item.put_car(user_id, self.version, car_data) return {"status_code": "0"} - def handle_user_updatelogin_request(self, data: Dict, headers: Dict): + async def handle_user_updatelogin_request(self, data: Dict, headers: Dict): pass - def handle_timetrial_getcarbest_request(self, data: Dict, headers: Dict): + async def handle_timetrial_getcarbest_request(self, data: Dict, headers: Dict): pass - def handle_factory_avatargacharesult_request(self, data: Dict, headers: Dict): + async def handle_factory_avatargacharesult_request(self, data: Dict, headers: Dict): user_id = headers["session"] stock_data: Dict = data.pop("stock_obj") use_ticket_cnt = data["use_ticket_cnt"] # save stock data in database - self._save_stock_data(user_id, stock_data) + await self._save_stock_data(user_id, stock_data) # get the user's ticket - tickets: list = self.data.item.get_tickets(user_id) + tickets: list = await self.data.item.get_tickets(user_id) ticket_list = [] for ticket in tickets: # avatar tickets @@ -1118,7 +1119,7 @@ class IDACSeason2(IDACBase): } # update the ticket in the database - self.data.item.put_ticket(user_id, ticket_data) + await self.data.item.put_ticket(user_id, ticket_data) ticket_list.append(ticket_data) continue @@ -1132,15 +1133,15 @@ class IDACSeason2(IDACBase): return {"status_code": "0", "ticket_data": ticket_list} - def handle_factory_savefavoritecar_request(self, data: Dict, headers: Dict): + async def handle_factory_savefavoritecar_request(self, data: Dict, headers: Dict): user_id = headers["session"] # save favorite cars in database for car in data["pickup_on_car_ids"]: - self.data.item.put_car(user_id, self.version, car) + await self.data.item.put_car(user_id, self.version, car) for car in data["pickup_off_car_ids"]: - self.data.item.put_car( + await self.data.item.put_car( user_id, self.version, {"style_car_id": car["style_car_id"], "pickup_seq": 0}, @@ -1148,7 +1149,7 @@ class IDACSeason2(IDACBase): return {"status_code": "0"} - def handle_factory_updatemultiplecustomizeresult_request( + async def handle_factory_updatemultiplecustomizeresult_request( self, data: Dict, headers: Dict ): user_id = headers["session"] @@ -1161,15 +1162,15 @@ class IDACSeason2(IDACBase): # save tickets in database for ticket in ticket_data: - self.data.item.put_ticket(user_id, ticket) + await self.data.item.put_ticket(user_id, ticket) for car in car_list: # save car data and car parts in database - self.data.item.put_car(user_id, self.version, car) + await self.data.item.put_car(user_id, self.version, car) return {"status_code": "0"} - def handle_factory_updatecustomizeresult_request(self, data: Dict, headers: Dict): + async def handle_factory_updatecustomizeresult_request(self, data: Dict, headers: Dict): user_id = headers["session"] parts_data: List = data.pop("parts_list") @@ -1177,18 +1178,18 @@ class IDACSeason2(IDACBase): # save tickets in database for ticket in ticket_data: - self.data.item.put_ticket(user_id, ticket) + await self.data.item.put_ticket(user_id, ticket) # save car data in database data["parts_list"] = parts_data - self.data.item.put_car(user_id, self.version, data) + await self.data.item.put_car(user_id, self.version, data) return {"status_code": "0"} - def handle_factory_getcardata_request(self, data: Dict, headers: Dict): + async def handle_factory_getcardata_request(self, data: Dict, headers: Dict): user_id = headers["session"] - cars = self.data.item.get_cars(self.version, user_id) + cars = await self.data.item.get_cars(self.version, user_id) car_data = [] for car in cars: tmp = car._asdict() @@ -1203,10 +1204,10 @@ class IDACSeason2(IDACBase): "car_data": car_data, } - def handle_factory_renamebefore_request(self, data: Dict, headers: Dict): + async def handle_factory_renamebefore_request(self, data: Dict, headers: Dict): pass - def handle_factory_buycarresult_request(self, data: Dict, headers: Dict): + async def handle_factory_buycarresult_request(self, data: Dict, headers: Dict): user_id = headers["session"] parts_data: List = data.pop("parts_list") @@ -1223,7 +1224,7 @@ class IDACSeason2(IDACBase): if car["style_car_id"] == style_car_id: pickup_seq = car["pickup_seq"] else: - self.data.item.put_car(user_id, self.version, car) + await self.data.item.put_car(user_id, self.version, car) data["pickup_seq"] = pickup_seq @@ -1231,7 +1232,7 @@ class IDACSeason2(IDACBase): total_cash = data.pop("total_cash") # save the new cash in database - self.data.profile.put_profile( + await self.data.profile.put_profile( user_id, self.version, {"total_cash": total_cash, "cash": cash} ) @@ -1239,10 +1240,10 @@ class IDACSeason2(IDACBase): use_ticket = data.pop("use_ticket") if use_ticket: # get the user's tickets, full tune ticket id is 25 - ticket = self.data.item.get_ticket(user_id, ticket_id=25) + ticket = await self.data.item.get_ticket(user_id, ticket_id=25) # update the ticket in the database - self.data.item.put_ticket( + await self.data.item.put_ticket( user_id, { "ticket_id": ticket["ticket_id"], @@ -1255,17 +1256,17 @@ class IDACSeason2(IDACBase): # save car data and car parts in database data["parts_list"] = parts_data - self.data.item.put_car(user_id, self.version, data) + await self.data.item.put_car(user_id, self.version, data) for car in pickup_off_list: - self.data.item.put_car( + await self.data.item.put_car( user_id, self.version, {"style_car_id": car["style_car_id"], "pickup_seq": 0}, ) # get the user's car - cars = self.data.item.get_cars(self.version, user_id) + cars = await self.data.item.get_cars(self.version, user_id) fulltune_count = 0 total_car_parts_count = 0 for car in cars: @@ -1277,7 +1278,7 @@ class IDACSeason2(IDACBase): # total_car_parts_count += car["total_car_parts_count"] # get the user's ticket - tickets = self.data.item.get_tickets(user_id) + tickets = await self.data.item.get_tickets(user_id) ticket_data = [] for ticket in tickets: ticket_data.append( @@ -1296,54 +1297,54 @@ class IDACSeason2(IDACBase): "car_style_count": [], } - def handle_factory_renameresult_request(self, data: Dict, headers: Dict): + async def handle_factory_renameresult_request(self, data: Dict, headers: Dict): user_id = headers["session"] new_username = data.get("username") # save new username in database if new_username: - self.data.profile.put_profile(user_id, self.version, data) + await self.data.profile.put_profile(user_id, self.version, data) return {"status_code": "0"} - def handle_factory_updatecustomizeavatar_request(self, data: Dict, headers: Dict): + async def handle_factory_updatecustomizeavatar_request(self, data: Dict, headers: Dict): user_id = headers["session"] avatar_data: Dict = data.pop("avatar_obj") stock_data: Dict = data.pop("stock_obj") # update the stock data in database - self._save_stock_data(user_id, stock_data) + await self._save_stock_data(user_id, stock_data) # save avatar data and avatar parts in database - self.data.profile.put_profile_avatar(user_id, avatar_data) + await self.data.profile.put_profile_avatar(user_id, avatar_data) return {"status_code": "0"} - def handle_factory_updatecustomizeuser_request(self, data: Dict, headers: Dict): + async def handle_factory_updatecustomizeuser_request(self, data: Dict, headers: Dict): user_id = headers["session"] stock_data: Dict = data.pop("stock_obj") # update the stock data in database - self._save_stock_data(user_id, stock_data) + await self._save_stock_data(user_id, stock_data) # update profile data and config in database - self.data.profile.put_profile(user_id, self.version, data) + await self.data.profile.put_profile(user_id, self.version, data) return {"status_code": "0"} - def handle_user_updatestampinfo_request(self, data: Dict, headers: Dict): + async def handle_user_updatestampinfo_request(self, data: Dict, headers: Dict): user_id = headers["session"] stamp_event_data = data.pop("stamp_event_data") for stamp in stamp_event_data: - self.data.item.put_stamp(user_id, stamp) + await self.data.item.put_stamp(user_id, stamp) return {"status_code": "0"} - def handle_user_updatetimetrialresult_request(self, data: Dict, headers: Dict): + async def handle_user_updatetimetrialresult_request(self, data: Dict, headers: Dict): user_id = headers["session"] stock_data: Dict = data.pop("stock_obj") @@ -1356,22 +1357,22 @@ class IDACSeason2(IDACBase): event_point = data.pop("event_point") # save stock data in database - self._save_stock_data(user_id, stock_data) + await self._save_stock_data(user_id, stock_data) # save tickets in database for ticket in ticket_data: - self.data.item.put_ticket(user_id, ticket) + await self.data.item.put_ticket(user_id, ticket) # save mode rank data in database rank_data.update(reward_dist_data) - self.data.profile.put_profile_rank(user_id, self.version, rank_data) + await self.data.profile.put_profile_rank(user_id, self.version, rank_data) # get the profile data, update total_play and daily_play, and save it - profile = self.data.profile.get_profile(user_id, self.version) + profile = await self.data.profile.get_profile(user_id, self.version) total_play = profile["total_play"] + 1 # update profile - self.data.profile.put_profile( + await self.data.profile.put_profile( user_id, self.version, { @@ -1391,7 +1392,8 @@ class IDACSeason2(IDACBase): # get the use_count and story_use_count of the used car style_car_id = data.get("style_car_id") car_mileage = data.pop("car_mileage") - used_car = self.data.item.get_car(user_id, self.version, style_car_id)._asdict() + used_car = await self.data.item.get_car(user_id, self.version, style_car_id) + used_car = used_car._asdict() # increase the use_count and story_use_count of the used car used_car["use_count"] += 1 @@ -1399,7 +1401,7 @@ class IDACSeason2(IDACBase): used_car["car_mileage"] = car_mileage # save the used car in database - self.data.item.put_car(user_id, self.version, used_car) + await self.data.item.put_car(user_id, self.version, used_car) # skill_level_exp is the "course proeficiency" and is saved # in the course table @@ -1408,12 +1410,12 @@ class IDACSeason2(IDACBase): skill_level_exp = data.pop("skill_level_exp") # get the course data - course = self.data.item.get_course(user_id, course_id) + course = await self.data.item.get_course(user_id, course_id) if course: # update run_counts run_counts = course["run_counts"] + 1 - self.data.item.put_course( + await self.data.item.put_course( user_id, { "course_id": course_id, @@ -1425,12 +1427,12 @@ class IDACSeason2(IDACBase): goal_time = data.get("goal_time") # grab the ranking data and count the numbers of rows with a faster time # than the current goal_time - course_rank = self.data.item.get_time_trial_ranking_by_course( + course_rank = await self.data.item.get_time_trial_ranking_by_course( self.version, course_id, limit=None ) course_rank = len([r for r in course_rank if r["goal_time"] < goal_time]) + 1 - car_course_rank = self.data.item.get_time_trial_ranking_by_course( + car_course_rank = await self.data.item.get_time_trial_ranking_by_course( self.version, course_id, style_car_id, limit=None ) car_course_rank = ( @@ -1441,7 +1443,7 @@ class IDACSeason2(IDACBase): if data.get("goal_time") > 0: # get the current best goal time best_time_trial = ( - self.data.item.get_time_trial_user_best_time_by_course_car( + await self.data.item.get_time_trial_user_best_time_by_course_car( self.version, user_id, course_id, style_car_id ) ) @@ -1452,10 +1454,10 @@ class IDACSeason2(IDACBase): ): # now finally save the time trial with updated timestamp data["play_dt"] = datetime.now() - self.data.item.put_time_trial(self.version, user_id, data) + await self.data.item.put_time_trial(self.version, user_id, data) # update the timetrial event points - self.data.item.put_timetrial_event( + await self.data.item.put_timetrial_event( user_id, self.timetrial_event_id, event_point ) @@ -1472,7 +1474,7 @@ class IDACSeason2(IDACBase): }, } - def handle_user_updatestoryresult_request(self, data: Dict, headers: Dict): + async def handle_user_updatestoryresult_request(self, data: Dict, headers: Dict): user_id = headers["session"] stock_data: Dict = data.pop("stock_obj") @@ -1483,15 +1485,15 @@ class IDACSeason2(IDACBase): # stamp_event_data = data.pop("stamp_event_data") # save stock data in database - self._save_stock_data(user_id, stock_data) + await self._save_stock_data(user_id, stock_data) # save tickets in database for ticket in ticket_data: - self.data.item.put_ticket(user_id, ticket) + await self.data.item.put_ticket(user_id, ticket) # save mode rank data in database rank_data.update(reward_dist_data) - self.data.profile.put_profile_rank(user_id, self.version, rank_data) + await self.data.profile.put_profile_rank(user_id, self.version, rank_data) # save the current story progress in database max_loop = data.get("chapter_loop_max") @@ -1502,7 +1504,7 @@ class IDACSeason2(IDACBase): play_status = data.get("play_status") # get the current loop from the database - story_data = self.data.item.get_story(user_id, chapter_id) + story_data = await self.data.item.get_story(user_id, chapter_id) # 1 = active, 2+ = cleared? loop_count = 1 if story_data: @@ -1516,13 +1518,13 @@ class IDACSeason2(IDACBase): # if the episode has already been cleared, set the play_status to 2 # so it won't be set to unplayed (play_status = 1) - episode_data = self.data.item.get_story_episode(user_id, episode_id) + episode_data = await self.data.item.get_story_episode(user_id, episode_id) if episode_data: if play_status < episode_data["play_status"]: play_status = 2 # save the current episode progress in database - self.data.item.put_story_episode( + await self.data.item.put_story_episode( user_id, chapter_id, { @@ -1536,9 +1538,9 @@ class IDACSeason2(IDACBase): loop_count += 1 # for the current chapter set all episode play_status back to 1 - self.data.item.put_story_episode_play_status(user_id, chapter_id, 1) + await self.data.item.put_story_episode_play_status(user_id, chapter_id, 1) - self.data.item.put_story( + await self.data.item.put_story( user_id, { "story_type": data.get("story_type"), @@ -1548,7 +1550,7 @@ class IDACSeason2(IDACBase): ) # save the current episode difficulty progress in database - self.data.item.put_story_episode_difficulty( + await self.data.item.put_story_episode_difficulty( user_id, episode_id, { @@ -1563,7 +1565,8 @@ class IDACSeason2(IDACBase): # get the use_count and story_use_count of the used car style_car_id = data.get("style_car_id") car_mileage = data.get("car_mileage") - used_car = self.data.item.get_car(user_id, self.version, style_car_id)._asdict() + used_car = await self.data.item.get_car(user_id, self.version, style_car_id) + used_car = used_car._asdict() # increase the use_count and story_use_count of the used car used_car["use_count"] += 1 @@ -1571,14 +1574,14 @@ class IDACSeason2(IDACBase): used_car["car_mileage"] = car_mileage # save the used car in database - self.data.item.put_car(user_id, self.version, used_car) + await self.data.item.put_car(user_id, self.version, used_car) # get the profile data, update total_play and daily_play, and save it - profile = self.data.profile.get_profile(user_id, self.version) + profile = await self.data.profile.get_profile(user_id, self.version) total_play = profile["total_play"] + 1 # save user profile in database - self.data.profile.put_profile( + await self.data.profile.put_profile( user_id, self.version, { @@ -1597,12 +1600,12 @@ class IDACSeason2(IDACBase): return { "status_code": "0", - "story_data": self._generate_story_data(user_id), + "story_data": await self._generate_story_data(user_id), "car_use_count": [], "maker_use_count": [], } - def handle_user_updatespecialmoderesult_request(self, data: Dict, headers: Dict): + async def handle_user_updatespecialmoderesult_request(self, data: Dict, headers: Dict): user_id = headers["session"] stock_data: Dict = data.pop("stock_obj") @@ -1616,11 +1619,11 @@ class IDACSeason2(IDACBase): # get the vs use count from database and update it style_car_id = data.pop("style_car_id") - car_data = self.data.item.get_car(user_id, self.version, style_car_id) + car_data = await self.data.item.get_car(user_id, self.version, style_car_id) story_use_count = car_data["story_use_count"] + 1 # save car data in database - self.data.item.put_car( + await self.data.item.put_car( user_id, self.version, { @@ -1631,11 +1634,11 @@ class IDACSeason2(IDACBase): ) # get the profile data, update total_play and daily_play, and save it - profile = self.data.profile.get_profile(user_id, self.version) + profile = await self.data.profile.get_profile(user_id, self.version) total_play = profile["total_play"] + 1 # save user profile in database - self.data.profile.put_profile( + await self.data.profile.put_profile( user_id, self.version, { @@ -1653,27 +1656,27 @@ class IDACSeason2(IDACBase): ) # save stock data in database - self._save_stock_data(user_id, stock_data) + await self._save_stock_data(user_id, stock_data) # save ticket data in database for ticket in ticket_data: - self.data.item.put_ticket(user_id, ticket) + await self.data.item.put_ticket(user_id, ticket) # save mode_rank and reward_dist data in database rank_data.update(reward_dist_data) - self.data.profile.put_profile_rank(user_id, self.version, rank_data) + await self.data.profile.put_profile_rank(user_id, self.version, rank_data) # finally save the special mode with story_type=4 in database - self.data.item.put_challenge(user_id, data) + await self.data.item.put_challenge(user_id, data) return { "status_code": "0", - "special_mode_data": self._generate_special_data(user_id), + "special_mode_data": await self._generate_special_data(user_id), "car_use_count": [], "maker_use_count": [], } - def handle_user_updatechallengemoderesult_request(self, data: Dict, headers: Dict): + async def handle_user_updatechallengemoderesult_request(self, data: Dict, headers: Dict): user_id = headers["session"] stock_data: Dict = data.pop("stock_obj") @@ -1684,11 +1687,11 @@ class IDACSeason2(IDACBase): # get the vs use count from database and update it style_car_id = data.get("style_car_id") - car_data = self.data.item.get_car(user_id, self.version, style_car_id) + car_data = await self.data.item.get_car(user_id, self.version, style_car_id) story_use_count = car_data["story_use_count"] + 1 # save car data in database - self.data.item.put_car( + await self.data.item.put_car( user_id, self.version, { @@ -1699,11 +1702,11 @@ class IDACSeason2(IDACBase): ) # get the profile data, update total_play and daily_play, and save it - profile = self.data.profile.get_profile(user_id, self.version) + profile = await self.data.profile.get_profile(user_id, self.version) total_play = profile["total_play"] + 1 # save user profile in database - self.data.profile.put_profile( + await self.data.profile.put_profile( user_id, self.version, { @@ -1721,18 +1724,18 @@ class IDACSeason2(IDACBase): ) # save stock data in database - self._save_stock_data(user_id, stock_data) + await self._save_stock_data(user_id, stock_data) # save ticket data in database for ticket in ticket_data: - self.data.item.put_ticket(user_id, ticket) + await self.data.item.put_ticket(user_id, ticket) # save mode_rank and reward_dist data in database rank_data.update(reward_dist_data) - self.data.profile.put_profile_rank(user_id, self.version, rank_data) + await self.data.profile.put_profile_rank(user_id, self.version, rank_data) # get the challenge mode data from database - challenge_data = self.data.item.get_challenge( + challenge_data = await self.data.item.get_challenge( user_id, data.get("vs_type"), data.get("play_difficulty") ) @@ -1742,20 +1745,20 @@ class IDACSeason2(IDACBase): data["play_count"] = play_count # finally save the challenge mode with story_type=3 in database - self.data.item.put_challenge(user_id, data) + await self.data.item.put_challenge(user_id, data) return { "status_code": "0", - "challenge_mode_data": self._generate_challenge_data(user_id), + "challenge_mode_data": await self._generate_challenge_data(user_id), "car_use_count": [], "maker_use_count": [], } - def _generate_time_trial_data(self, season_id: int, user_id: int) -> List[Dict]: + async def _generate_time_trial_data(self, season_id: int, user_id: int) -> List[Dict]: # get the season time trial data from database timetrial_data = [] - courses = self.data.item.get_courses(user_id) + courses = await self.data.item.get_courses(user_id) if courses is None or len(courses) == 0: return {"status_code": "0", "timetrial_data": timetrial_data} @@ -1765,7 +1768,7 @@ class IDACSeason2(IDACBase): skill_level_exp = course["skill_level_exp"] # get the best time for the current course for the current user - best_trial = self.data.item.get_time_trial_best_ranking_by_course( + best_trial = await self.data.item.get_time_trial_best_ranking_by_course( season_id, user_id, course_id ) if not best_trial: @@ -1773,7 +1776,7 @@ class IDACSeason2(IDACBase): goal_time = best_trial["goal_time"] # get the rank for the current course - course_rank = self.data.item.get_time_trial_ranking_by_course( + course_rank = await self.data.item.get_time_trial_ranking_by_course( season_id, course_id, limit=None ) course_rank = ( @@ -1793,12 +1796,12 @@ class IDACSeason2(IDACBase): return timetrial_data - def handle_user_getpastseasontadata_request(self, data: Dict, headers: Dict): + async def handle_user_getpastseasontadata_request(self, data: Dict, headers: Dict): user_id = headers["session"] season_id = data.get("season_id") # so to get the season 1 data just subtract 1 from the season id - past_timetrial_data = self._generate_time_trial_data(season_id - 1, user_id) + past_timetrial_data = await self._generate_time_trial_data(season_id - 1, user_id) # TODO: get the current season timetrial data somehow, because after requesting # GetPastSeasonTAData the game will NOT request GetTAData?! @@ -1808,10 +1811,10 @@ class IDACSeason2(IDACBase): "past_season_timetrial_data": past_timetrial_data, } - def handle_user_gettadata_request(self, data: Dict, headers: Dict): + async def handle_user_gettadata_request(self, data: Dict, headers: Dict): user_id = headers["session"] - timetrial_data = self._generate_time_trial_data(self.version, user_id) + timetrial_data = await self._generate_time_trial_data(self.version, user_id) # TODO: get the past season timetrial data somehow, because after requesting # GetTAData the game will NOT request GetPastSeasonTAData?! @@ -1821,17 +1824,17 @@ class IDACSeason2(IDACBase): # "past_season_timetrial_data": timetrial_data, } - def handle_user_updatecartune_request(self, data: Dict, headers: Dict): + async def handle_user_updatecartune_request(self, data: Dict, headers: Dict): user_id = headers["session"] # full tune ticket use_ticket = data.pop("use_ticket") if use_ticket: # get the user's tickets, full tune ticket id is 25 - ticket = self.data.item.get_ticket(user_id, ticket_id=25) + ticket = await self.data.item.get_ticket(user_id, ticket_id=25) # update the ticket in the database - self.data.item.put_ticket( + await self.data.item.put_ticket( user_id, { "ticket_id": ticket["ticket_id"], @@ -1842,22 +1845,22 @@ class IDACSeason2(IDACBase): # also set the tune_level to 16 (fully tuned) data["tune_level"] = 16 - self.data.item.put_car(user_id, self.version, data) + await self.data.item.put_car(user_id, self.version, data) return { "status_code": "0", - "story_data": self._generate_story_data(user_id), + "story_data": await self._generate_story_data(user_id), "car_use_count": [], "maker_use_count": [], } - def handle_log_saveplaylog_request(self, data: Dict, headers: Dict): + async def handle_log_saveplaylog_request(self, data: Dict, headers: Dict): pass - def handle_log_saveendlog_request(self, data: Dict, headers: Dict): + async def handle_log_saveendlog_request(self, data: Dict, headers: Dict): pass - def handle_user_updatemoderesult_request(self, data: Dict, headers: Dict): + async def handle_user_updatemoderesult_request(self, data: Dict, headers: Dict): user_id = headers["session"] config_data: Dict = data.pop("config_obj") @@ -1871,30 +1874,30 @@ class IDACSeason2(IDACBase): tips_list = data.pop("tips_list") # save stock data in database - self._save_stock_data(user_id, stock_data) + await self._save_stock_data(user_id, stock_data) # save tickets in database for ticket in ticket_data: - self.data.item.put_ticket(user_id, ticket) + await self.data.item.put_ticket(user_id, ticket) # save rank dist data in database - self.data.profile.put_profile_rank(user_id, self.version, reward_dist_data) + await self.data.profile.put_profile_rank(user_id, self.version, reward_dist_data) # update profile data and config in database - self.data.profile.put_profile(user_id, self.version, data) + await self.data.profile.put_profile(user_id, self.version, data) config_data["config_id"] = config_data.pop("id") - self.data.profile.put_profile_config(user_id, config_data) + await self.data.profile.put_profile_config(user_id, config_data) return {"status_code": "0", "server_status": 1} - def _generate_theory_rival_data( + async def _generate_theory_rival_data( self, user_list: list, course_id: int, req_user_id: int ) -> list: rival_data = [] for user_id in user_list: # if not enough players are available just use the data from the req_user if user_id == -1: - profile = self.data.profile.get_profile(req_user_id, self.version) + profile = await self.data.profile.get_profile(req_user_id, self.version) profile = profile._asdict() # set the name to CPU profile["username"] = f"CPU" @@ -1907,9 +1910,9 @@ class IDACSeason2(IDACBase): profile["stamp_key_assign_3"] = 3 profile["mytitle_id"] = 0 else: - profile = self.data.profile.get_profile(user_id, self.version) + profile = await self.data.profile.get_profile(user_id, self.version) - rank = self.data.profile.get_profile_rank(profile["user"], self.version) + rank = await self.data.profile.get_profile_rank(profile["user"], self.version) avatars = [ { @@ -1977,21 +1980,21 @@ class IDACSeason2(IDACBase): if user_id == -1: # get a random avatar from the list and some random car from all users avatar = choice(avatars) - car = self.data.item.get_random_car(self.version) + car = await self.data.item.get_random_car(self.version) else: - avatar = self.data.profile.get_profile_avatar(profile["user"]) - car = self.data.item.get_random_user_car(profile["user"], self.version) + avatar = await self.data.profile.get_profile_avatar(profile["user"]) + car = await self.data.item.get_random_user_car(profile["user"], self.version) parts_list = [] for part in car["parts_list"]: parts_list.append(part["parts"]) - course = self.data.item.get_theory_course(profile["user"], course_id) + course = await self.data.item.get_theory_course(profile["user"], course_id) powerhose_lv = 0 if course: powerhose_lv = course["powerhouse_lv"] - theory_running = self.data.item.get_theory_running_by_course( + theory_running = await self.data.item.get_theory_running_by_course( profile["user"], course_id ) @@ -2010,13 +2013,13 @@ class IDACSeason2(IDACBase): # get the time trial ranking medal eval_id = 0 - time_trial = self.data.item.get_time_trial_best_ranking_by_course( + time_trial = await self.data.item.get_time_trial_best_ranking_by_course( self.version, profile["user"], course_id ) if time_trial: eval_id = time_trial["eval_id"] - arcade = self.data.arcade.get_arcade(profile["store"]) + arcade = await self.data.arcade.get_arcade(profile["store"]) if arcade is None: arcade = {} arcade["name"] = self.core_cfg.server.name @@ -2078,7 +2081,7 @@ class IDACSeason2(IDACBase): return rival_data - def handle_theory_matching_request(self, data: Dict, headers: Dict): + async def handle_theory_matching_request(self, data: Dict, headers: Dict): user_id = headers["session"] course_id = data.pop("course_id") @@ -2093,7 +2096,7 @@ class IDACSeason2(IDACBase): powerhose_lv = data.pop("powerhouse_lv") # get random profiles for auto match - profiles = self.data.profile.get_different_random_profiles( + profiles = await self.data.profile.get_different_random_profiles( user_id, self.version, count=count_auto_match ) @@ -2102,10 +2105,10 @@ class IDACSeason2(IDACBase): while len(user_list) < count_auto_match: user_list.append(-1) - auto_match = self._generate_theory_rival_data(user_list, course_id, user_id) + auto_match = await self._generate_theory_rival_data(user_list, course_id, user_id) # get profiles with the same powerhouse_lv for power match - theory_courses = self.data.item.get_theory_course_by_powerhouse_lv( + theory_courses = await self.data.item.get_theory_course_by_powerhouse_lv( user_id, course_id, powerhose_lv, count=count_power_match ) user_list = [course["user"] for course in theory_courses] @@ -2114,7 +2117,7 @@ class IDACSeason2(IDACBase): while len(user_list) < count_power_match: user_list.append(-1) - power_match = self._generate_theory_rival_data(user_list, course_id, user_id) + power_match = await self._generate_theory_rival_data(user_list, course_id, user_id) return { "status_code": "0", @@ -2125,7 +2128,7 @@ class IDACSeason2(IDACBase): }, } - def handle_user_updatetheoryresult_request(self, data: Dict, headers: Dict): + async def handle_user_updatetheoryresult_request(self, data: Dict, headers: Dict): user_id = headers["session"] stock_data: Dict = data.pop("stock_obj") @@ -2135,15 +2138,15 @@ class IDACSeason2(IDACBase): driver_debut_data: Dict = data.pop("driver_debut_obj") # save stock data in database - self._save_stock_data(user_id, stock_data) + await self._save_stock_data(user_id, stock_data) # save tickets in database for ticket in ticket_data: - self.data.item.put_ticket(user_id, ticket) + await self.data.item.put_ticket(user_id, ticket) # save rank dist data in database rank_data.update(reward_dist_data) - self.data.profile.put_profile_rank(user_id, self.version, rank_data) + await self.data.profile.put_profile_rank(user_id, self.version, rank_data) # save the profile theory data in database play_count = 1 @@ -2151,7 +2154,7 @@ class IDACSeason2(IDACBase): win_count = 0 win_count_multi = 0 - theory_data = self.data.profile.get_profile_theory(user_id, self.version) + theory_data = await self.data.profile.get_profile_theory(user_id, self.version) if theory_data: play_count = theory_data["play_count"] + 1 play_count_multi = theory_data["play_count_multi"] + 1 @@ -2169,7 +2172,7 @@ class IDACSeason2(IDACBase): win_count += 1 win_count_multi += 1 - self.data.profile.put_profile_theory( + await self.data.profile.put_profile_theory( user_id, self.version, { @@ -2189,7 +2192,7 @@ class IDACSeason2(IDACBase): ) # save theory course in database - self.data.item.put_theory_course( + await self.data.item.put_theory_course( user_id, { "course_id": data.get("course_id"), @@ -2204,7 +2207,7 @@ class IDACSeason2(IDACBase): ) # save the theory partner in database - self.data.item.put_theory_partner( + await self.data.item.put_theory_partner( user_id, { "partner_id": data.get("partner_id"), @@ -2214,7 +2217,7 @@ class IDACSeason2(IDACBase): ) # save the theory running in database? - self.data.item.put_theory_running( + await self.data.item.put_theory_running( user_id, { "course_id": data.get("course_id"), @@ -2229,7 +2232,8 @@ class IDACSeason2(IDACBase): # get the use_count and theory_use_count of the used car style_car_id = data.get("style_car_id") car_mileage = data.get("car_mileage") - used_car = self.data.item.get_car(user_id, self.version, style_car_id)._asdict() + used_car = await self.data.item.get_car(user_id, self.version, style_car_id) + used_car = used_car._asdict() # increase the use_count and theory_use_count of the used car used_car["use_count"] += 1 @@ -2237,14 +2241,14 @@ class IDACSeason2(IDACBase): used_car["car_mileage"] = car_mileage # save the used car in database - self.data.item.put_car(user_id, self.version, used_car) + await self.data.item.put_car(user_id, self.version, used_car) # get the profile data, update total_play and daily_play, and save it - profile = self.data.profile.get_profile(user_id, self.version) + profile = await self.data.profile.get_profile(user_id, self.version) total_play = profile["total_play"] + 1 # save the profile in database - self.data.profile.put_profile( + await self.data.profile.put_profile( user_id, self.version, { @@ -2272,16 +2276,16 @@ class IDACSeason2(IDACBase): "win_count_multi": win_count_multi, } - def handle_timetrial_getbestrecordprebattle_request( + async def handle_timetrial_getbestrecordprebattle_request( self, data: Dict, headers: Dict ): user_id = headers["session"] course_pickup_car_best_data = [] - courses = self.data.item.get_time_trial_courses(self.version) + courses = await self.data.item.get_time_trial_courses(self.version) for course in courses: car_list = [] - best_cars = self.data.item.get_time_trial_best_cars_by_course( + best_cars = await self.data.item.get_time_trial_best_cars_by_course( self.version, course["course_id"], user_id ) @@ -2317,36 +2321,37 @@ class IDACSeason2(IDACBase): "course_pickup_car_best_data": course_pickup_car_best_data, } - def handle_user_updateonlinebattle_request(self, data: Dict, headers: Dict): + async def handle_user_updateonlinebattle_request(self, data: Dict, headers: Dict): return { "status_code": "0", "bothwin_penalty": 1, } - def handle_user_updateonlinebattleresult_request(self, data: Dict, headers: Dict): + async def handle_user_updateonlinebattleresult_request(self, data: Dict, headers: Dict): user_id = headers["session"] stock_data: Dict = data.pop("stock_obj") # save stock data in database - self._save_stock_data(user_id, stock_data) + await self._save_stock_data(user_id, stock_data) ticket_data: List = data.pop("ticket_data") for ticket in ticket_data: - self.data.item.put_ticket(user_id, ticket) + await self.data.item.put_ticket(user_id, ticket) reward_dist_data: Dict = data.pop("reward_dist_obj") rank_data: Dict = data.pop("mode_rank_obj") # save rank dist data in database rank_data.update(reward_dist_data) - self.data.profile.put_profile_rank(user_id, self.version, rank_data) + await self.data.profile.put_profile_rank(user_id, self.version, rank_data) driver_debut_data = data.pop("driver_debut_obj") # get the use_count and net_vs_use_count of the used car style_car_id = data.get("style_car_id") car_mileage = data.pop("car_mileage") - used_car = self.data.item.get_car(user_id, self.version, style_car_id)._asdict() + used_car = await self.data.item.get_car(user_id, self.version, style_car_id) + used_car = used_car._asdict() # increase the use_count and net_vs_use_count of the used car used_car["use_count"] += 1 @@ -2354,14 +2359,14 @@ class IDACSeason2(IDACBase): used_car["car_mileage"] = car_mileage # save the used car in database - self.data.item.put_car(user_id, self.version, used_car) + await self.data.item.put_car(user_id, self.version, used_car) # get the profile data, update total_play and daily_play, and save it - profile = self.data.profile.get_profile(user_id, self.version) + profile = await self.data.profile.get_profile(user_id, self.version) total_play = profile["total_play"] + 1 # save the profile in database - self.data.profile.put_profile( + await self.data.profile.put_profile( user_id, self.version, { @@ -2378,7 +2383,7 @@ class IDACSeason2(IDACBase): }, ) - self.data.item.put_vs_info(user_id, data) + await self.data.item.put_vs_info(user_id, data) vs_info = { "battle_mode": 0, @@ -2415,7 +2420,7 @@ class IDACSeason2(IDACBase): "maker_use_count": [], } - def handle_user_updatestorebattleresult_request(self, data: Dict, headers: Dict): + async def handle_user_updatestorebattleresult_request(self, data: Dict, headers: Dict): user_id = headers["session"] stock_data: Dict = data.pop("stock_obj") @@ -2430,20 +2435,21 @@ class IDACSeason2(IDACBase): gift_id = data.pop("gift_id") # save stock data in database - self._save_stock_data(user_id, stock_data) + await self._save_stock_data(user_id, stock_data) # save tickets in database for ticket in ticket_data: - self.data.item.put_ticket(user_id, ticket) + await self.data.item.put_ticket(user_id, ticket) # save rank dist data in database rank_data.update(reward_dist_data) - self.data.profile.put_profile_rank(user_id, self.version, rank_data) + await self.data.profile.put_profile_rank(user_id, self.version, rank_data) # get the use_count and net_vs_use_count of the used car style_car_id = data.get("style_car_id") car_mileage = data.pop("car_mileage") - used_car = self.data.item.get_car(user_id, self.version, style_car_id)._asdict() + used_car = await self.data.item.get_car(user_id, self.version, style_car_id) + used_car = used_car._asdict() # increase the use_count and net_vs_use_count of the used car used_car["use_count"] += 1 @@ -2451,14 +2457,14 @@ class IDACSeason2(IDACBase): used_car["car_mileage"] = car_mileage # save the used car in database - self.data.item.put_car(user_id, self.version, used_car) + await self.data.item.put_car(user_id, self.version, used_car) # get the profile data, update total_play and daily_play, and save it - profile = self.data.profile.get_profile(user_id, self.version) + profile = await self.data.profile.get_profile(user_id, self.version) total_play = profile["total_play"] + 1 # save the profile in database - self.data.profile.put_profile( + await self.data.profile.put_profile( user_id, self.version, { @@ -2476,7 +2482,7 @@ class IDACSeason2(IDACBase): ) # save vs_info in database - self.data.item.put_vs_info(user_id, data) + await self.data.item.put_vs_info(user_id, data) vs_info = { "battle_mode": 0, diff --git a/titles/idac/frontend/idac_index.jinja b/titles/idac/templates/idac_index.jinja similarity index 95% rename from titles/idac/frontend/idac_index.jinja rename to titles/idac/templates/idac_index.jinja index eeecc65..caa7770 100644 --- a/titles/idac/frontend/idac_index.jinja +++ b/titles/idac/templates/idac_index.jinja @@ -1,12 +1,13 @@ -{% extends "core/frontend/index.jinja" %} +{% extends "core/templates/index.jinja" %} {% block content %}

頭文字D THE ARCADE

-{% if sesh is defined and sesh["userId"] > 0 %} +{% if sesh is defined and sesh["user_id"] > 0 %}
-
+
+ {% if profile is defined and profile is not none %}
-
+

{{ sesh["username"] }}'s Profile

@@ -18,7 +19,6 @@
- {% if profile is defined and profile is not none %}
@@ -128,7 +128,7 @@
{% endblock content %} \ No newline at end of file diff --git a/titles/idac/frontend/js/idac_scripts.js b/titles/idac/templates/js/idac_scripts.js similarity index 100% rename from titles/idac/frontend/js/idac_scripts.js rename to titles/idac/templates/js/idac_scripts.js diff --git a/titles/idz/__init__.py b/titles/idz/__init__.py index 958d08a..457b6a4 100644 --- a/titles/idz/__init__.py +++ b/titles/idz/__init__.py @@ -5,4 +5,3 @@ from titles.idz.database import IDZData index = IDZServlet database = IDZData game_codes = [IDZConstants.GAME_CODE] -current_schema_version = 1 diff --git a/titles/idz/echo.py b/titles/idz/echo.py index 979fd19..8141e45 100644 --- a/titles/idz/echo.py +++ b/titles/idz/echo.py @@ -1,19 +1,9 @@ -from twisted.internet.protocol import DatagramProtocol import logging -from core.config import CoreConfig -from .config import IDZConfig +class IDZEcho: + def connection_made(self, transport): + self.transport = transport - -class IDZEcho(DatagramProtocol): - def __init__(self, cfg: CoreConfig, game_cfg: IDZConfig) -> None: - super().__init__() - self.core_config = cfg - self.game_config = game_cfg - self.logger = logging.getLogger("idz") - - def datagramReceived(self, data, addr): - self.logger.debug( - f"Echo from from {addr[0]}:{addr[1]} -> {self.transport.getHost().port} - {data.hex()}" - ) - self.transport.write(data, addr) + def datagram_received(self, data, addr): + logging.getLogger('idz').debug(f'Received echo from {addr}') + self.transport.sendto(data, addr) diff --git a/titles/idz/handlers/load_server_info.py b/titles/idz/handlers/load_server_info.py index 4c60dd7..f0ace03 100644 --- a/titles/idz/handlers/load_server_info.py +++ b/titles/idz/handlers/load_server_info.py @@ -24,10 +24,10 @@ class IDZHandlerLoadServerInfo(IDZHandlerBase): t_port = Utils.get_title_port(self.core_config) - news_str = f"http://{self.core_config.title.hostname}:{t_port}/idz/news/news80**.txt" - err_str = f"http://{self.core_config.title.hostname}:{t_port}/idz/error" + news_str = f"http://{self.core_config.server.hostname}:{t_port}/idz/news/news80**.txt" + err_str = f"http://{self.core_config.server.hostname}:{t_port}/idz/error" - len_hostname = len(self.core_config.title.hostname) + len_hostname = len(self.core_config.server.hostname) len_news = len(news_str) len_error = len(err_str) @@ -36,7 +36,7 @@ class IDZHandlerLoadServerInfo(IDZHandlerBase): f"{len_hostname}s", ret, 0x4 + offset, - self.core_config.title.hostname.encode(), + self.core_config.server.hostname.encode(), ) struct.pack_into(" Tuple[List[Tuple[str, str, Dict]], List[Tuple[str, str, Dict]]]: - return[ - [("render_GET", "/idz/news/{endpoint:.*?}", {}), - ("render_GET", "/idz/error", {})], - [] + def get_routes(self) -> List[Route]: + return [ + Route("/idz/news/{endpoint:str}", self.render_GET), + Route("/idz/error", self.render_GET) ] def get_allnet_info(self, game_code: str, game_ver: int, keychip: str) -> Tuple[str, str]: hostname = ( - self.core_cfg.title.hostname + self.core_cfg.server.hostname if not self.game_cfg.server.hostname else self.game_cfg.server.hostname ) @@ -135,38 +135,35 @@ class IDZServlet(BaseServlet): except AttributeError as e: continue - - endpoints.serverFromString( - reactor, - f"tcp:{self.game_cfg.ports.userdb}:interface={self.core_cfg.server.listen_address}", - ).listen( - IDZUserDBFactory(self.core_cfg, self.game_cfg, self.rsa_keys, handler_map) + + loop = asyncio.get_running_loop() + IDZUserDB(self.core_cfg, self.game_cfg, self.rsa_keys, handler_map).start() + asyncio.create_task( + loop.create_datagram_endpoint( + lambda: IDZEcho(), + local_addr=(self.core_cfg.server.listen_address, self.game_cfg.ports.echo) + ) + ) + asyncio.create_task( + loop.create_datagram_endpoint( + lambda: IDZEcho(), + local_addr=(self.core_cfg.server.listen_address, self.game_cfg.ports.match) + ) + ) + asyncio.create_task( + loop.create_datagram_endpoint( + lambda: IDZEcho(), + local_addr=(self.core_cfg.server.listen_address, self.game_cfg.ports.userdb + 1) + ) ) - reactor.listenUDP( - self.game_cfg.ports.echo, IDZEcho(self.core_cfg, self.game_cfg) - ) - reactor.listenUDP( - self.game_cfg.ports.echo + 1, IDZEcho(self.core_cfg, self.game_cfg) - ) - reactor.listenUDP( - self.game_cfg.ports.match, IDZEcho(self.core_cfg, self.game_cfg) - ) - reactor.listenUDP( - self.game_cfg.ports.userdb + 1, IDZEcho(self.core_cfg, self.game_cfg) - ) - self.logger.info(f"UserDB Listening on port {self.game_cfg.ports.userdb}") + async def render_GET(self, request: Request) -> bytes: + url_path = request.path_params.get('endpoint', '') + if not url_path: + return Response() - def render_GET(self, request: Request, game_code: str, matchers: Dict) -> bytes: - url_path = matchers['endpoint'] self.logger.info(f"IDZ GET request: {url_path}") - request.responseHeaders.setRawHeaders( - "Content-Type", [b"text/plain; charset=utf-8"] - ) - request.responseHeaders.setRawHeaders( - "Last-Modified", [b"Sun, 23 Apr 2023 05:33:20 GMT"] - ) news = ( self.game_cfg.server.news @@ -176,4 +173,4 @@ class IDZServlet(BaseServlet): news += "\r\n" news = "1979/01/01 00:00:00 2099/12/31 23:59:59 " + news - return news.encode() + return PlainTextResponse(news, media_type="text/plain; charset=utf-8", headers={"Last-Modified": "Sun, 23 Apr 2023 05:33:20 GMT"}) diff --git a/titles/idz/userdb.py b/titles/idz/userdb.py index fd555a1..089778a 100644 --- a/titles/idz/userdb.py +++ b/titles/idz/userdb.py @@ -1,15 +1,9 @@ -from twisted.internet.protocol import Factory, Protocol -import logging, coloredlogs +import logging from Crypto.Cipher import AES import struct from typing import Dict, Optional, List, Type -from twisted.web import server, resource -from twisted.internet import reactor, endpoints -from twisted.web.http import Request -from routes import Mapper import random -from os import walk -import importlib +import asyncio from core.config import CoreConfig from .database import IDZData @@ -28,7 +22,7 @@ class IDZKey: self.hashN = hashN -class IDZUserDBProtocol(Protocol): +class IDZUserDB: def __init__( self, core_cfg: CoreConfig, @@ -45,6 +39,10 @@ class IDZUserDBProtocol(Protocol): self.version = None self.version_internal = None self.skip_next = False + + def start(self) -> None: + self.logger.info(f"Start on port {self.game_config.ports.userdb}") + asyncio.create_task(asyncio.start_server(self.connection_cb, self.core_config.server.listen_address, self.game_config.ports.userdb)) def append_padding(self, data: bytes): """Appends 0s to the end of the data until it's at the correct size""" @@ -52,43 +50,54 @@ class IDZUserDBProtocol(Protocol): padding_size = length[0] - len(data) data += bytes(padding_size) return data + + async def connection_cb(self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter): + self.logger.debug(f"Connection made from {writer.get_extra_info('peername')[0]}") + while True: + try: + base = 0 - def connectionMade(self) -> None: - self.logger.debug(f"{self.transport.getPeer().host} Connected") - base = 0 + for i in range(len(self.static_key) - 1): + shift = 8 * i + byte = self.static_key[i] - for i in range(len(self.static_key) - 1): - shift = 8 * i - byte = self.static_key[i] + base |= byte << shift - base |= byte << shift + rsa_key = random.choice(self.rsa_keys) + key_enc: int = pow(base, rsa_key.e, rsa_key.N) + result = ( + key_enc.to_bytes(0x40, "little") + + struct.pack(" None: - self.logger.debug( - f"{self.transport.getPeer().host} Disconnected - {reason.value}" - ) - - def dataReceived(self, data: bytes) -> None: + def dataReceived(self, data: bytes, reader: asyncio.StreamReader, writer: asyncio.StreamWriter) -> None: self.logger.debug(f"Receive data {data.hex()}") + client_ip = writer.get_extra_info('peername')[0] crypt = AES.new(self.static_key, AES.MODE_ECB) try: data_dec = crypt.decrypt(data) except Exception as e: - self.logger.error(f"Failed to decrypt UserDB request from {self.transport.getPeer().host} because {e} - {data.hex()}") + self.logger.error(f"Failed to decrypt UserDB request from {client_ip} because {e} - {data.hex()}") self.logger.debug(f"Decrypt data {data_dec.hex()}") @@ -99,7 +108,7 @@ class IDZUserDBProtocol(Protocol): self.logger.info(f"Userdb serverbox request {data_dec.hex()}") self.skip_next = True - self.transport.write(b"\x00") + writer.write(b"\x00") return elif magic == 0x01020304: @@ -119,21 +128,21 @@ class IDZUserDBProtocol(Protocol): self.version_internal = None self.logger.debug( - f"Userdb v{self.version} handshake response from {self.transport.getPeer().host}" + f"Userdb v{self.version} handshake response from {client_ip}" ) return elif self.skip_next: self.skip_next = False - self.transport.write(b"\x00") + writer.write(b"\x00") return elif self.version is None: # We didn't get a handshake before, and this isn't one now, so we're up the creek self.logger.info( - f"Bad UserDB request from from {self.transport.getPeer().host}" + f"Bad UserDB request from from {client_ip}" ) - self.transport.write(b"\x00") + writer.write(b"\x00") return cmd = struct.unpack_from(" None: - self.core_config = cfg - self.game_config = game_cfg - self.keys = keys - self.handlers = handlers - - def buildProtocol(self, addr): - return IDZUserDBProtocol( - self.core_config, self.game_config, self.keys, self.handlers - ) - - -class IDZUserDBWeb(resource.Resource): - def __init__(self, core_cfg: CoreConfig, game_cfg: IDZConfig): - super().__init__() - self.isLeaf = True - self.core_config = core_cfg - self.game_config = game_cfg - self.logger = logging.getLogger("idz") - - def render_POST(self, request: Request) -> bytes: - self.logger.info( - f"IDZUserDBWeb POST from {request.getClientAddress().host} to {request.uri} with data {request.content.getvalue()}" - ) - return b"" - - def render_GET(self, request: Request) -> bytes: - self.logger.info( - f"IDZUserDBWeb GET from {request.getClientAddress().host} to {request.uri}" - ) - return b"" + writer.write(response_enc) diff --git a/titles/mai2/__init__.py b/titles/mai2/__init__.py index 4857644..234e864 100644 --- a/titles/mai2/__init__.py +++ b/titles/mai2/__init__.py @@ -2,10 +2,12 @@ from titles.mai2.index import Mai2Servlet from titles.mai2.const import Mai2Constants from titles.mai2.database import Mai2Data from titles.mai2.read import Mai2Reader +from .frontend import Mai2Frontend index = Mai2Servlet database = Mai2Data reader = Mai2Reader +frontend = Mai2Frontend game_codes = [ Mai2Constants.GAME_CODE_DX, Mai2Constants.GAME_CODE_FINALE, @@ -15,5 +17,5 @@ game_codes = [ Mai2Constants.GAME_CODE_ORANGE, Mai2Constants.GAME_CODE_GREEN, Mai2Constants.GAME_CODE, + Mai2Constants.GAME_CODE_DX_INT, ] -current_schema_version = 8 diff --git a/titles/mai2/base.py b/titles/mai2/base.py index 2f40176..b3c6a1d 100644 --- a/titles/mai2/base.py +++ b/titles/mai2/base.py @@ -4,6 +4,7 @@ import logging from base64 import b64decode from os import path, stat, remove from PIL import ImageFile +from random import randint import pytz from core.config import CoreConfig @@ -26,12 +27,12 @@ class Mai2Base: self.date_time_format = "%Y-%m-%d %H:%M:%S" if not self.core_config.server.is_using_proxy and Utils.get_title_port(self.core_config) != 80: - self.old_server = f"http://{self.core_config.title.hostname}:{Utils.get_title_port(cfg)}/197/MaimaiServlet/" + self.old_server = f"http://{self.core_config.server.hostname}:{Utils.get_title_port(cfg)}/SDEY/197/MaimaiServlet/" else: - self.old_server = f"http://{self.core_config.title.hostname}/197/MaimaiServlet/" + self.old_server = f"http://{self.core_config.server.hostname}/SDEY/197/MaimaiServlet/" - def handle_get_game_setting_api_request(self, data: Dict): + async def handle_get_game_setting_api_request(self, data: Dict): # if reboot start/end time is not defined use the default behavior of being a few hours ago if self.core_config.title.reboot_start_time == "" or self.core_config.title.reboot_end_time == "": reboot_start = datetime.strftime( @@ -74,15 +75,15 @@ class Mai2Base: }, } - def handle_get_game_ranking_api_request(self, data: Dict) -> Dict: + async def handle_get_game_ranking_api_request(self, data: Dict) -> Dict: return {"length": 0, "gameRankingList": []} - def handle_get_game_tournament_info_api_request(self, data: Dict) -> Dict: + async def handle_get_game_tournament_info_api_request(self, data: Dict) -> Dict: # TODO: Tournament support return {"length": 0, "gameTournamentInfoList": []} - def handle_get_game_event_api_request(self, data: Dict) -> Dict: - events = self.data.static.get_enabled_events(self.version) + async def handle_get_game_event_api_request(self, data: Dict) -> Dict: + events = await self.data.static.get_enabled_events(self.version) events_lst = [] if events is None or not events: self.logger.warning("No enabled events, did you run the reader?") @@ -108,11 +109,11 @@ class Mai2Base: "gameEventList": events_lst, } - def handle_get_game_ng_music_id_api_request(self, data: Dict) -> Dict: + async def handle_get_game_ng_music_id_api_request(self, data: Dict) -> Dict: return {"length": 0, "musicIdList": []} - def handle_get_game_charge_api_request(self, data: Dict) -> Dict: - game_charge_list = self.data.static.get_enabled_tickets(self.version, 1) + async def handle_get_game_charge_api_request(self, data: Dict) -> Dict: + game_charge_list = await self.data.static.get_enabled_tickets(self.version, 1) if game_charge_list is None: return {"length": 0, "gameChargeList": []} @@ -130,21 +131,21 @@ class Mai2Base: return {"length": len(charge_list), "gameChargeList": charge_list} - def handle_upsert_client_setting_api_request(self, data: Dict) -> Dict: + async def handle_upsert_client_setting_api_request(self, data: Dict) -> Dict: return {"returnCode": 1, "apiName": "UpsertClientSettingApi"} - def handle_upsert_client_upload_api_request(self, data: Dict) -> Dict: + async def handle_upsert_client_upload_api_request(self, data: Dict) -> Dict: return {"returnCode": 1, "apiName": "UpsertClientUploadApi"} - def handle_upsert_client_bookkeeping_api_request(self, data: Dict) -> Dict: + async def handle_upsert_client_bookkeeping_api_request(self, data: Dict) -> Dict: return {"returnCode": 1, "apiName": "UpsertClientBookkeepingApi"} - def handle_upsert_client_testmode_api_request(self, data: Dict) -> Dict: + async def handle_upsert_client_testmode_api_request(self, data: Dict) -> Dict: return {"returnCode": 1, "apiName": "UpsertClientTestmodeApi"} - def handle_get_user_preview_api_request(self, data: Dict) -> Dict: - p = self.data.profile.get_profile_detail(data["userId"], self.version, False) - w = self.data.profile.get_web_option(data["userId"], self.version) + async def handle_get_user_preview_api_request(self, data: Dict) -> Dict: + p = await self.data.profile.get_profile_detail(data["userId"], self.version, False) + w = await self.data.profile.get_web_option(data["userId"], self.version) if p is None or w is None: return {} # Register profile = p._asdict() @@ -169,16 +170,16 @@ class Mai2Base: "totalLv": profile["totalLv"], } - def handle_user_login_api_request(self, data: Dict) -> Dict: - profile = self.data.profile.get_profile_detail(data["userId"], self.version) - consec = self.data.profile.get_consec_login(data["userId"], self.version) + async def handle_user_login_api_request(self, data: Dict) -> Dict: + profile = await self.data.profile.get_profile_detail(data["userId"], self.version) + consec = await self.data.profile.get_consec_login(data["userId"], self.version) if profile is not None: lastLoginDate = profile["lastLoginDate"] loginCt = profile["playCount"] if "regionId" in data: - self.data.profile.put_profile_region(data["userId"], data["regionId"]) + await self.data.profile.put_profile_region(data["userId"], data["regionId"]) else: loginCt = 0 lastLoginDate = "2017-12-05 07:00:00.0" @@ -193,11 +194,11 @@ class Mai2Base: if lastlogindate_ < today_midnight: consec_ct = consec['logins'] + 1 - self.data.profile.add_consec_login(data["userId"], self.version) + await self.data.profile.add_consec_login(data["userId"], self.version) elif lastlogindate_ < yesterday_midnight: consec_ct = 1 - self.data.profile.reset_consec_login(data["userId"], self.version) + await self.data.profile.reset_consec_login(data["userId"], self.version) else: consec_ct = consec['logins'] @@ -210,21 +211,21 @@ class Mai2Base: "consecutiveLoginCount": consec_ct, # Number of consecutive days we've logged in. } - def handle_upload_user_playlog_api_request(self, data: Dict) -> Dict: + async def handle_upload_user_playlog_api_request(self, data: Dict) -> Dict: user_id = data["userId"] playlog = data["userPlaylog"] - self.data.score.put_playlog(user_id, playlog) + await self.data.score.put_playlog(user_id, playlog) return {"returnCode": 1, "apiName": "UploadUserPlaylogApi"} - def handle_upsert_user_chargelog_api_request(self, data: Dict) -> Dict: + async def handle_upsert_user_chargelog_api_request(self, data: Dict) -> Dict: user_id = data["userId"] charge = data["userCharge"] # remove the ".0" from the date string, festival only? charge["purchaseDate"] = charge["purchaseDate"].replace(".0", "") - self.data.item.put_charge( + await self.data.item.put_charge( user_id, charge["chargeId"], charge["stock"], @@ -234,76 +235,78 @@ class Mai2Base: return {"returnCode": 1, "apiName": "UpsertUserChargelogApi"} - def handle_upsert_user_all_api_request(self, data: Dict) -> Dict: + async def handle_upsert_user_all_api_request(self, data: Dict) -> Dict: user_id = data["userId"] upsert = data["upsertUserAll"] - if int(user_id) & 1000000000001 == 1000000000001: - self.logger.info("Guest play, ignoring.") + if int(user_id) & 0x1000000000001 == 0x1000000000001: + place_id = int(user_id) & 0xFFFC00000000 + + self.logger.info("Guest play from place ID %d, ignoring.", place_id) return {"returnCode": 1, "apiName": "UpsertUserAllApi"} if "userData" in upsert and len(upsert["userData"]) > 0: upsert["userData"][0].pop("accessCode") upsert["userData"][0].pop("userId") - self.data.profile.put_profile_detail( + await self.data.profile.put_profile_detail( user_id, self.version, upsert["userData"][0], False ) if "userWebOption" in upsert and len(upsert["userWebOption"]) > 0: upsert["userWebOption"][0]["isNetMember"] = True - self.data.profile.put_web_option( + await self.data.profile.put_web_option( user_id, self.version, upsert["userWebOption"][0] ) if "userGradeStatusList" in upsert and len(upsert["userGradeStatusList"]) > 0: - self.data.profile.put_grade_status( + await self.data.profile.put_grade_status( user_id, upsert["userGradeStatusList"][0] ) if "userBossList" in upsert and len(upsert["userBossList"]) > 0: - self.data.profile.put_boss_list( + await self.data.profile.put_boss_list( user_id, upsert["userBossList"][0] ) if "userPlaylogList" in upsert and len(upsert["userPlaylogList"]) > 0: for playlog in upsert["userPlaylogList"]: - self.data.score.put_playlog( + await self.data.score.put_playlog( user_id, playlog, False ) if "userExtend" in upsert and len(upsert["userExtend"]) > 0: - self.data.profile.put_profile_extend( + await self.data.profile.put_profile_extend( user_id, self.version, upsert["userExtend"][0] ) if "userGhost" in upsert: for ghost in upsert["userGhost"]: - self.data.profile.put_profile_ghost(user_id, self.version, ghost) + await self.data.profile.put_profile_ghost(user_id, self.version, ghost) if "userRecentRatingList" in upsert: - self.data.profile.put_recent_rating(user_id, upsert["userRecentRatingList"]) + await self.data.profile.put_recent_rating(user_id, upsert["userRecentRatingList"]) if "userOption" in upsert and len(upsert["userOption"]) > 0: upsert["userOption"][0].pop("userId") - self.data.profile.put_profile_option( + await self.data.profile.put_profile_option( user_id, self.version, upsert["userOption"][0], False ) if "userRatingList" in upsert and len(upsert["userRatingList"]) > 0: - self.data.profile.put_profile_rating( + await self.data.profile.put_profile_rating( user_id, self.version, upsert["userRatingList"][0] ) if "userActivityList" in upsert and len(upsert["userActivityList"]) > 0: for act in upsert["userActivityList"]: - self.data.profile.put_profile_activity(user_id, act) + await self.data.profile.put_profile_activity(user_id, act) if "userChargeList" in upsert and len(upsert["userChargeList"]) > 0: for charge in upsert["userChargeList"]: # remove the ".0" from the date string, festival only? charge["purchaseDate"] = charge["purchaseDate"].replace(".0", "") - self.data.item.put_charge( + await self.data.item.put_charge( user_id, charge["chargeId"], charge["stock"], @@ -313,14 +316,14 @@ class Mai2Base: if "userCharacterList" in upsert and len(upsert["userCharacterList"]) > 0: for char in upsert["userCharacterList"]: - self.data.item.put_character_( + await self.data.item.put_character_( user_id, char ) if "userItemList" in upsert and len(upsert["userItemList"]) > 0: for item in upsert["userItemList"]: - self.data.item.put_item( + await self.data.item.put_item( user_id, int(item["itemKind"]), item["itemId"], @@ -330,7 +333,7 @@ class Mai2Base: if "userLoginBonusList" in upsert and len(upsert["userLoginBonusList"]) > 0: for login_bonus in upsert["userLoginBonusList"]: - self.data.item.put_login_bonus( + await self.data.item.put_login_bonus( user_id, login_bonus["bonusId"], login_bonus["point"], @@ -340,7 +343,7 @@ class Mai2Base: if "userMapList" in upsert and len(upsert["userMapList"]) > 0: for map in upsert["userMapList"]: - self.data.item.put_map( + await self.data.item.put_map( user_id, map["mapId"], map["distance"], @@ -351,15 +354,15 @@ class Mai2Base: if "userMusicDetailList" in upsert and len(upsert["userMusicDetailList"]) > 0: for music in upsert["userMusicDetailList"]: - self.data.score.put_best_score(user_id, music, False) + await self.data.score.put_best_score(user_id, music, False) if "userCourseList" in upsert and len(upsert["userCourseList"]) > 0: for course in upsert["userCourseList"]: - self.data.score.put_course(user_id, course) + await self.data.score.put_course(user_id, course) if "userFavoriteList" in upsert and len(upsert["userFavoriteList"]) > 0: for fav in upsert["userFavoriteList"]: - self.data.item.put_favorite(user_id, fav["kind"], fav["itemIdList"]) + await self.data.item.put_favorite(user_id, fav["kind"], fav["itemIdList"]) if ( "userFriendSeasonRankingList" in upsert @@ -371,15 +374,15 @@ class Mai2Base: fsr["recordDate"], f"{Mai2Constants.DATE_TIME_FORMAT}.0" ), ) - self.data.item.put_friend_season_ranking(user_id, fsr) + await self.data.item.put_friend_season_ranking(user_id, fsr) return {"returnCode": 1, "apiName": "UpsertUserAllApi"} - def handle_user_logout_api_request(self, data: Dict) -> Dict: + async def handle_user_logout_api_request(self, data: Dict) -> Dict: return {"returnCode": 1} - def handle_get_user_data_api_request(self, data: Dict) -> Dict: - profile = self.data.profile.get_profile_detail(data["userId"], self.version, False) + async def handle_get_user_data_api_request(self, data: Dict) -> Dict: + profile = await self.data.profile.get_profile_detail(data["userId"], self.version, False) if profile is None: return @@ -390,8 +393,8 @@ class Mai2Base: return {"userId": data["userId"], "userData": profile_dict} - def handle_get_user_extend_api_request(self, data: Dict) -> Dict: - extend = self.data.profile.get_profile_extend(data["userId"], self.version) + async def handle_get_user_extend_api_request(self, data: Dict) -> Dict: + extend = await self.data.profile.get_profile_extend(data["userId"], self.version) if extend is None: return @@ -402,8 +405,8 @@ class Mai2Base: return {"userId": data["userId"], "userExtend": extend_dict} - def handle_get_user_option_api_request(self, data: Dict) -> Dict: - options = self.data.profile.get_profile_option(data["userId"], self.version, False) + async def handle_get_user_option_api_request(self, data: Dict) -> Dict: + options = await self.data.profile.get_profile_option(data["userId"], self.version, False) if options is None: return @@ -414,8 +417,8 @@ class Mai2Base: return {"userId": data["userId"], "userOption": options_dict} - def handle_get_user_card_api_request(self, data: Dict) -> Dict: - user_cards = self.data.item.get_cards(data["userId"]) + async def handle_get_user_card_api_request(self, data: Dict) -> Dict: + user_cards = await self.data.item.get_cards(data["userId"]) if user_cards is None: return {"userId": data["userId"], "nextIndex": 0, "userCardList": []} @@ -448,8 +451,8 @@ class Mai2Base: "userCardList": card_list[start_idx:end_idx], } - def handle_get_user_charge_api_request(self, data: Dict) -> Dict: - user_charges = self.data.item.get_charges(data["userId"]) + async def handle_get_user_charge_api_request(self, data: Dict) -> Dict: + user_charges = await self.data.item.get_charges(data["userId"]) if user_charges is None: return {"userId": data["userId"], "length": 0, "userChargeList": []} @@ -467,17 +470,37 @@ class Mai2Base: "userChargeList": user_charge_list, } - def handle_get_user_present_api_request(self, data: Dict) -> Dict: - return { "userId": data.get("userId", 0), "length": 0, "userPresentList": []} + async def handle_get_user_present_api_request(self, data: Dict) -> Dict: + items: List[Dict[str, Any]] = [] + user_pres_list = await self.data.item.get_presents_by_version_user(self.version, data["userId"]) + if user_pres_list: + self.logger.debug(f"Found {len(user_pres_list)} possible presents") + for present in user_pres_list: + if (present['startDate'] and present['startDate'].timestamp() > datetime.now().timestamp()): + self.logger.debug(f"Present {present['id']} distribution hasn't started yet (begins {present['startDate']})") + continue # present period hasn't started yet, move onto the next one + + if (present['endDate'] and present['endDate'].timestamp() < datetime.now().timestamp()): + self.logger.warn(f"Present {present['id']} ended on {present['endDate']} and should be removed") + continue # present period ended, move onto the next one + + test = await self.data.item.get_item(data["userId"], present['itemKind'], present['itemId']) + if not test: # Don't send presents for items the user already has + pres_id = present['itemKind'] * 1000000 + pres_id += present['itemId'] + items.append({"itemId": pres_id, "itemKind": 4, "stock": present['stock'], "isValid": True}) + self.logger.info(f"Give user {data['userId']} {present['stock']}x item {present['itemId']} (kind {present['itemKind']}) as present") + + return { "userId": data.get("userId", 0), "length": len(items), "userPresentList": items} - def handle_get_transfer_friend_api_request(self, data: Dict) -> Dict: + async def handle_get_transfer_friend_api_request(self, data: Dict) -> Dict: return {} - def handle_get_user_present_event_api_request(self, data: Dict) -> Dict: + async def handle_get_user_present_event_api_request(self, data: Dict) -> Dict: return { "userId": data.get("userId", 0), "length": 0, "userPresentEventList": []} - def handle_get_user_boss_api_request(self, data: Dict) -> Dict: - b = self.data.profile.get_boss_list(data["userId"]) + async def handle_get_user_boss_api_request(self, data: Dict) -> Dict: + b = await self.data.profile.get_boss_list(data["userId"]) if b is None: return { "userId": data.get("userId", 0), "userBossData": {}} boss_lst = b._asdict() @@ -486,10 +509,10 @@ class Mai2Base: return { "userId": data.get("userId", 0), "userBossData": boss_lst} - def handle_get_user_item_api_request(self, data: Dict) -> Dict: + async def handle_get_user_item_api_request(self, data: Dict) -> Dict: kind = int(data["nextIndex"] / 10000000000) next_idx = int(data["nextIndex"] % 10000000000) - user_item_list = self.data.item.get_items(data["userId"], kind) + user_item_list = await self.data.item.get_items(data["userId"], kind) items: List[Dict[str, Any]] = [] for i in range(next_idx, len(user_item_list)): @@ -514,8 +537,8 @@ class Mai2Base: "userItemList": items, } - def handle_get_user_character_api_request(self, data: Dict) -> Dict: - characters = self.data.item.get_characters(data["userId"]) + async def handle_get_user_character_api_request(self, data: Dict) -> Dict: + characters = await self.data.item.get_characters(data["userId"]) chara_list = [] for chara in characters: @@ -528,8 +551,8 @@ class Mai2Base: return {"userId": data["userId"], "userCharacterList": chara_list} - def handle_get_user_favorite_api_request(self, data: Dict) -> Dict: - favorites = self.data.item.get_favorites(data["userId"], data["itemKind"]) + async def handle_get_user_favorite_api_request(self, data: Dict) -> Dict: + favorites = await self.data.item.get_favorites(data["userId"], data["itemKind"]) if favorites is None: return @@ -545,8 +568,8 @@ class Mai2Base: return {"userId": data["userId"], "userFavoriteData": userFavs} - def handle_get_user_ghost_api_request(self, data: Dict) -> Dict: - ghost = self.data.profile.get_profile_ghost(data["userId"], self.version) + async def handle_get_user_ghost_api_request(self, data: Dict) -> Dict: + ghost = await self.data.profile.get_profile_ghost(data["userId"], self.version) if ghost is None: return @@ -557,8 +580,8 @@ class Mai2Base: return {"userId": data["userId"], "userGhost": ghost_dict} - def handle_get_user_recent_rating_api_request(self, data: Dict) -> Dict: - rating = self.data.profile.get_recent_rating(data["userId"]) + async def handle_get_user_recent_rating_api_request(self, data: Dict) -> Dict: + rating = await self.data.profile.get_recent_rating(data["userId"]) if rating is None: return @@ -567,8 +590,8 @@ class Mai2Base: return {"userId": data["userId"], "length": len(lst), "userRecentRatingList": lst} - def handle_get_user_rating_api_request(self, data: Dict) -> Dict: - rating = self.data.profile.get_profile_rating(data["userId"], self.version) + async def handle_get_user_rating_api_request(self, data: Dict) -> Dict: + rating = await self.data.profile.get_profile_rating(data["userId"], self.version) if rating is None: return @@ -579,12 +602,12 @@ class Mai2Base: return {"userId": data["userId"], "userRating": rating_dict} - def handle_get_user_activity_api_request(self, data: Dict) -> Dict: + async def handle_get_user_activity_api_request(self, data: Dict) -> Dict: """ kind 1 is playlist, kind 2 is music list """ - playlist = self.data.profile.get_profile_activity(data["userId"], 1) - musiclist = self.data.profile.get_profile_activity(data["userId"], 2) + playlist = await self.data.profile.get_profile_activity(data["userId"], 1) + musiclist = await self.data.profile.get_profile_activity(data["userId"], 2) if playlist is None or musiclist is None: return @@ -607,8 +630,8 @@ class Mai2Base: return {"userActivity": {"playList": plst, "musicList": mlst}} - def handle_get_user_course_api_request(self, data: Dict) -> Dict: - user_courses = self.data.score.get_courses(data["userId"]) + async def handle_get_user_course_api_request(self, data: Dict) -> Dict: + user_courses = await self.data.score.get_courses(data["userId"]) if user_courses is None: return {"userId": data["userId"], "nextIndex": 0, "userCourseList": []} @@ -621,12 +644,12 @@ class Mai2Base: return {"userId": data["userId"], "nextIndex": 0, "userCourseList": course_list} - def handle_get_user_portrait_api_request(self, data: Dict) -> Dict: + async def handle_get_user_portrait_api_request(self, data: Dict) -> Dict: # No support for custom pfps return {"length": 0, "userPortraitList": []} - def handle_get_user_friend_season_ranking_api_request(self, data: Dict) -> Dict: - friend_season_ranking = self.data.item.get_friend_season_ranking(data["userId"]) + async def handle_get_user_friend_season_ranking_api_request(self, data: Dict) -> Dict: + friend_season_ranking = await self.data.item.get_friend_season_ranking(data["userId"]) if friend_season_ranking is None: return { "userId": data["userId"], @@ -661,8 +684,8 @@ class Mai2Base: "userFriendSeasonRankingList": friend_season_ranking_list, } - def handle_get_user_map_api_request(self, data: Dict) -> Dict: - maps = self.data.item.get_maps(data["userId"]) + async def handle_get_user_map_api_request(self, data: Dict) -> Dict: + maps = await self.data.item.get_maps(data["userId"]) if maps is None: return { "userId": data["userId"], @@ -694,8 +717,8 @@ class Mai2Base: "userMapList": map_list, } - def handle_get_user_login_bonus_api_request(self, data: Dict) -> Dict: - login_bonuses = self.data.item.get_login_bonuses(data["userId"]) + async def handle_get_user_login_bonus_api_request(self, data: Dict) -> Dict: + login_bonuses = await self.data.item.get_login_bonuses(data["userId"]) if login_bonuses is None: return { "userId": data["userId"], @@ -727,11 +750,11 @@ class Mai2Base: "userLoginBonusList": login_bonus_list, } - def handle_get_user_region_api_request(self, data: Dict) -> Dict: + async def handle_get_user_region_api_request(self, data: Dict) -> Dict: return {"userId": data["userId"], "length": 0, "userRegionList": []} - def handle_get_user_web_option_api_request(self, data: Dict) -> Dict: - w = self.data.profile.get_web_option(data["userId"], self.version) + async def handle_get_user_web_option_api_request(self, data: Dict) -> Dict: + w = await self.data.profile.get_web_option(data["userId"], self.version) if w is None: return {"userId": data["userId"], "userWebOption": {}} @@ -742,11 +765,11 @@ class Mai2Base: return {"userId": data["userId"], "userWebOption": web_opt} - def handle_get_user_survival_api_request(self, data: Dict) -> Dict: + async def handle_get_user_survival_api_request(self, data: Dict) -> Dict: return {"userId": data["userId"], "length": 0, "userSurvivalList": []} - def handle_get_user_grade_api_request(self, data: Dict) -> Dict: - g = self.data.profile.get_grade_status(data["userId"]) + async def handle_get_user_grade_api_request(self, data: Dict) -> Dict: + g = await self.data.profile.get_grade_status(data["userId"]) if g is None: return {"userId": data["userId"], "userGradeStatus": {}, "length": 0, "userGradeList": []} grade_stat = g._asdict() @@ -755,7 +778,7 @@ class Mai2Base: return {"userId": data["userId"], "userGradeStatus": grade_stat, "length": 0, "userGradeList": []} - def handle_get_user_music_api_request(self, data: Dict) -> Dict: + async def handle_get_user_music_api_request(self, data: Dict) -> Dict: user_id = data.get("userId", 0) next_index = data.get("nextIndex", 0) max_ct = data.get("maxCount", 50) @@ -766,7 +789,7 @@ class Mai2Base: self.logger.warning("handle_get_user_music_api_request: Could not find userid in data, or userId is 0") return {} - songs = self.data.score.get_best_scores(user_id, is_dx=False) + songs = await self.data.score.get_best_scores(user_id, is_dx=False) if songs is None: self.logger.debug("handle_get_user_music_api_request: get_best_scores returned None!") return { @@ -794,10 +817,10 @@ class Mai2Base: "userMusicList": [{"userMusicDetailList": music_detail_list}], } - def handle_upload_user_portrait_api_request(self, data: Dict) -> Dict: + async def handle_upload_user_portrait_api_request(self, data: Dict) -> Dict: self.logger.debug(data) - def handle_upload_user_photo_api_request(self, data: Dict) -> Dict: + async def handle_upload_user_photo_api_request(self, data: Dict) -> Dict: if not self.game_config.uploads.photos or not self.game_config.uploads.photos_dir: return {'returnCode': 0, 'apiName': 'UploadUserPhotoApi'} @@ -884,3 +907,45 @@ class Mai2Base: self.logger.error(f"Failed to delete {out_name}.bin, please remove it manually") return {'returnCode': ret_code, 'apiName': 'UploadUserPhotoApi'} + + async def handle_get_user_favorite_item_api_request(self, data: Dict) -> Dict: + user_id = data.get("userId", 0) + kind = data.get("kind", 0) # 1 is fav music, 2 is rival user IDs + next_index = data.get("nextIndex", 0) + max_ct = data.get("maxCount", 100) # always 100 + is_all = data.get("isAllFavoriteItem", False) # always false + id_list: List[Dict] = [] + + if user_id: + if kind == 1: + fav_music = await self.data.item.get_fav_music(user_id) + if fav_music: + for fav in fav_music: + id_list.append({"orderId": 0, "id": fav["musicId"]}) + if len(id_list) >= 100: # Lazy but whatever + break + + elif kind == 2: + rivals = await self.data.profile.get_rivals_game(user_id) + if rivals: + for rival in rivals: + id_list.append({"orderId": 0, "id": rival["rival"]}) + + return { + "userId": user_id, + "kind": kind, + "nextIndex": 0, + "userFavoriteItemList": id_list, + } + + async def handle_get_user_recommend_rate_music_api_request(self, data: Dict) -> Dict: + """ + userRecommendRateMusicIdList: list[int] + """ + return {"userId": data["userId"], "userRecommendRateMusicIdList": []} + + async def handle_get_user_recommend_select_music_api_request(self, data: Dict) -> Dict: + """ + userRecommendSelectionMusicIdList: list[int] + """ + return {"userId": data["userId"], "userRecommendSelectionMusicIdList": []} diff --git a/titles/mai2/buddies.py b/titles/mai2/buddies.py new file mode 100644 index 0000000..38049a1 --- /dev/null +++ b/titles/mai2/buddies.py @@ -0,0 +1,19 @@ +from typing import Dict + +from core.config import CoreConfig +from titles.mai2.festivalplus import Mai2FestivalPlus +from titles.mai2.const import Mai2Constants +from titles.mai2.config import Mai2Config + + +class Mai2Buddies(Mai2FestivalPlus): + def __init__(self, cfg: CoreConfig, game_cfg: Mai2Config) -> None: + super().__init__(cfg, game_cfg) + self.version = Mai2Constants.VER_MAIMAI_DX_BUDDIES + + async def handle_cm_get_user_preview_api_request(self, data: Dict) -> Dict: + user_data = await super().handle_cm_get_user_preview_api_request(data) + + # hardcode lastDataVersion for CardMaker + user_data["lastDataVersion"] = "1.40.00" + return user_data diff --git a/titles/mai2/config.py b/titles/mai2/config.py index 850e0c1..efd3ba5 100644 --- a/titles/mai2/config.py +++ b/titles/mai2/config.py @@ -1,3 +1,5 @@ +from typing import Dict + from core.config import CoreConfig @@ -70,8 +72,32 @@ class Mai2UploadsConfig: ) +class Mai2CryptoConfig: + def __init__(self, parent_config: "Mai2Config") -> None: + self.__config = parent_config + + @property + def keys(self) -> Dict[int, list[str]]: + """ + in the form of: + internal_version: [key, iv, salt] + key and iv are hex strings + salt is a normal UTF-8 string + """ + return CoreConfig.get_config_field( + self.__config, "mai2", "crypto", "keys", default={} + ) + + @property + def encrypted_only(self) -> bool: + return CoreConfig.get_config_field( + self.__config, "mai2", "crypto", "encrypted_only", default=False + ) + + class Mai2Config(dict): def __init__(self) -> None: self.server = Mai2ServerConfig(self) self.deliver = Mai2DeliverConfig(self) self.uploads = Mai2UploadsConfig(self) + self.crypto = Mai2CryptoConfig(self) \ No newline at end of file diff --git a/titles/mai2/const.py b/titles/mai2/const.py index a4c29db..4dc10ce 100644 --- a/titles/mai2/const.py +++ b/titles/mai2/const.py @@ -28,6 +28,7 @@ class Mai2Constants: GAME_CODE_MILK = "SDDZ" GAME_CODE_FINALE = "SDEY" GAME_CODE_DX = "SDEZ" + GAME_CODE_DX_INT = "SDGA" CONFIG_NAME = "mai2.yaml" @@ -53,6 +54,7 @@ class Mai2Constants: VER_MAIMAI_DX_UNIVERSE_PLUS = 18 VER_MAIMAI_DX_FESTIVAL = 19 VER_MAIMAI_DX_FESTIVAL_PLUS = 20 + VER_MAIMAI_DX_BUDDIES = 21 VERSION_STRING = ( "maimai", @@ -76,6 +78,7 @@ class Mai2Constants: "maimai DX UNiVERSE PLUS", "maimai DX FESTiVAL", "maimai DX FESTiVAL PLUS", + "maimai DX BUDDiES" ) @classmethod diff --git a/titles/mai2/dx.py b/titles/mai2/dx.py index 508cebb..7a067d7 100644 --- a/titles/mai2/dx.py +++ b/titles/mai2/dx.py @@ -5,6 +5,7 @@ import json from random import randint from core.config import CoreConfig +from core.utils import Utils from titles.mai2.base import Mai2Base from titles.mai2.config import Mai2Config from titles.mai2.const import Mai2Constants @@ -15,27 +16,60 @@ class Mai2DX(Mai2Base): super().__init__(cfg, game_cfg) self.version = Mai2Constants.VER_MAIMAI_DX - def handle_get_game_setting_api_request(self, data: Dict): + # DX earlier version need a efficient old server uri to work + # game will auto add MaimaiServlet endpoint behind return uri + # so do not add "MaimaiServlet" + if not self.core_config.server.is_using_proxy and Utils.get_title_port(self.core_config) != 80: + self.old_server = f"http://{self.core_config.server.hostname}:{Utils.get_title_port(cfg)}/SDEY/197/" + + else: + self.old_server = f"http://{self.core_config.server.hostname}/SDEY/197/" + + async def handle_get_game_setting_api_request(self, data: Dict): + # if reboot start/end time is not defined use the default behavior of being a few hours ago + if self.core_config.title.reboot_start_time == "" or self.core_config.title.reboot_end_time == "": + reboot_start = datetime.strftime( + datetime.utcnow() + timedelta(hours=6), self.date_time_format + ) + reboot_end = datetime.strftime( + datetime.utcnow() + timedelta(hours=7), self.date_time_format + ) + else: + # get current datetime in JST + current_jst = datetime.now(pytz.timezone('Asia/Tokyo')).date() + + # parse config start/end times into datetime + reboot_start_time = datetime.strptime(self.core_config.title.reboot_start_time, "%H:%M") + reboot_end_time = datetime.strptime(self.core_config.title.reboot_end_time, "%H:%M") + + # offset datetimes with current date/time + reboot_start_time = reboot_start_time.replace(year=current_jst.year, month=current_jst.month, day=current_jst.day, tzinfo=pytz.timezone('Asia/Tokyo')) + reboot_end_time = reboot_end_time.replace(year=current_jst.year, month=current_jst.month, day=current_jst.day, tzinfo=pytz.timezone('Asia/Tokyo')) + + # create strings for use in gameSetting + reboot_start = reboot_start_time.strftime(self.date_time_format) + reboot_end = reboot_end_time.strftime(self.date_time_format) + return { "gameSetting": { "isMaintenance": False, "requestInterval": 1800, - "rebootStartTime": "2020-01-01 07:00:00.0", - "rebootEndTime": "2020-01-01 07:59:59.0", + "rebootStartTime": reboot_start, + "rebootEndTime": reboot_end, "movieUploadLimit": 100, "movieStatus": 1, - "movieServerUri": self.old_server + "movie/", - "deliverServerUri": self.old_server + "deliver/" if self.can_deliver and self.game_config.deliver.enable else "", - "oldServerUri": self.old_server + "old", - "usbDlServerUri": self.old_server + "usbdl/" if self.can_deliver and self.game_config.deliver.udbdl_enable else "", + "movieServerUri": "", + "deliverServerUri": "", + "oldServerUri": self.old_server, + "usbDlServerUri": "", "rebootInterval": 0, }, "isAouAccession": False, } - def handle_get_user_preview_api_request(self, data: Dict) -> Dict: - p = self.data.profile.get_profile_detail(data["userId"], self.version) - o = self.data.profile.get_profile_option(data["userId"], self.version) + async def handle_get_user_preview_api_request(self, data: Dict) -> Dict: + p = await self.data.profile.get_profile_detail(data["userId"], self.version) + o = await self.data.profile.get_profile_option(data["userId"], self.version) if p is None or o is None: return {} # Register profile = p._asdict() @@ -69,21 +103,21 @@ class Mai2DX(Mai2Base): else 0, # New with uni+ } - def handle_upload_user_playlog_api_request(self, data: Dict) -> Dict: + async def handle_upload_user_playlog_api_request(self, data: Dict) -> Dict: user_id = data["userId"] playlog = data["userPlaylog"] - self.data.score.put_playlog(user_id, playlog) + await self.data.score.put_playlog(user_id, playlog) return {"returnCode": 1, "apiName": "UploadUserPlaylogApi"} - def handle_upsert_user_chargelog_api_request(self, data: Dict) -> Dict: + async def handle_upsert_user_chargelog_api_request(self, data: Dict) -> Dict: user_id = data["userId"] charge = data["userCharge"] # remove the ".0" from the date string, festival only? charge["purchaseDate"] = charge["purchaseDate"].replace(".0", "") - self.data.item.put_charge( + await self.data.item.put_charge( user_id, charge["chargeId"], charge["stock"], @@ -93,50 +127,52 @@ class Mai2DX(Mai2Base): return {"returnCode": 1, "apiName": "UpsertUserChargelogApi"} - def handle_upsert_user_all_api_request(self, data: Dict) -> Dict: + async def handle_upsert_user_all_api_request(self, data: Dict) -> Dict: user_id = data["userId"] upsert = data["upsertUserAll"] - if int(user_id) & 1000000000001 == 1000000000001: - self.logger.info("Guest play, ignoring.") + if int(user_id) & 0x1000000000001 == 0x1000000000001: + place_id = int(user_id) & 0xFFFC00000000 + + self.logger.info("Guest play from place ID %d, ignoring.", place_id) return {"returnCode": 1, "apiName": "UpsertUserAllApi"} if "userData" in upsert and len(upsert["userData"]) > 0: upsert["userData"][0]["isNetMember"] = 1 upsert["userData"][0].pop("accessCode") - self.data.profile.put_profile_detail( + await self.data.profile.put_profile_detail( user_id, self.version, upsert["userData"][0] ) if "userExtend" in upsert and len(upsert["userExtend"]) > 0: - self.data.profile.put_profile_extend( + await self.data.profile.put_profile_extend( user_id, self.version, upsert["userExtend"][0] ) if "userGhost" in upsert: for ghost in upsert["userGhost"]: - self.data.profile.put_profile_ghost(user_id, self.version, ghost) + await self.data.profile.put_profile_ghost(user_id, self.version, ghost) if "userOption" in upsert and len(upsert["userOption"]) > 0: - self.data.profile.put_profile_option( + await self.data.profile.put_profile_option( user_id, self.version, upsert["userOption"][0] ) if "userRatingList" in upsert and len(upsert["userRatingList"]) > 0: - self.data.profile.put_profile_rating( + await self.data.profile.put_profile_rating( user_id, self.version, upsert["userRatingList"][0] ) if "userActivityList" in upsert and len(upsert["userActivityList"]) > 0: for k, v in upsert["userActivityList"][0].items(): for act in v: - self.data.profile.put_profile_activity(user_id, act) + await self.data.profile.put_profile_activity(user_id, act) if "userChargeList" in upsert and len(upsert["userChargeList"]) > 0: for charge in upsert["userChargeList"]: # remove the ".0" from the date string, festival only? charge["purchaseDate"] = charge["purchaseDate"].replace(".0", "") - self.data.item.put_charge( + await self.data.item.put_charge( user_id, charge["chargeId"], charge["stock"], @@ -150,7 +186,7 @@ class Mai2DX(Mai2Base): if "userCharacterList" in upsert and len(upsert["userCharacterList"]) > 0: for char in upsert["userCharacterList"]: - self.data.item.put_character( + await self.data.item.put_character( user_id, char["characterId"], char["level"], @@ -160,17 +196,24 @@ class Mai2DX(Mai2Base): if "userItemList" in upsert and len(upsert["userItemList"]) > 0: for item in upsert["userItemList"]: - self.data.item.put_item( + if item["itemKind"] == 4: + item_id = item["itemId"] % 1000000 + item_kind = item["itemId"] // 1000000 + else: + item_id = item["itemId"] + item_kind = item["itemKind"] + + await self.data.item.put_item( user_id, - int(item["itemKind"]), - item["itemId"], + item_kind, + item_id, item["stock"], item["isValid"], ) if "userLoginBonusList" in upsert and len(upsert["userLoginBonusList"]) > 0: for login_bonus in upsert["userLoginBonusList"]: - self.data.item.put_login_bonus( + await self.data.item.put_login_bonus( user_id, login_bonus["bonusId"], login_bonus["point"], @@ -180,7 +223,7 @@ class Mai2DX(Mai2Base): if "userMapList" in upsert and len(upsert["userMapList"]) > 0: for map in upsert["userMapList"]: - self.data.item.put_map( + await self.data.item.put_map( user_id, map["mapId"], map["distance"], @@ -191,15 +234,15 @@ class Mai2DX(Mai2Base): if "userMusicDetailList" in upsert and len(upsert["userMusicDetailList"]) > 0: for music in upsert["userMusicDetailList"]: - self.data.score.put_best_score(user_id, music) + await self.data.score.put_best_score(user_id, music) if "userCourseList" in upsert and len(upsert["userCourseList"]) > 0: for course in upsert["userCourseList"]: - self.data.score.put_course(user_id, course) + await self.data.score.put_course(user_id, course) if "userFavoriteList" in upsert and len(upsert["userFavoriteList"]) > 0: for fav in upsert["userFavoriteList"]: - self.data.item.put_favorite(user_id, fav["kind"], fav["itemIdList"]) + await self.data.item.put_favorite(user_id, fav["kind"], fav["itemIdList"]) if ( "userFriendSeasonRankingList" in upsert @@ -211,12 +254,15 @@ class Mai2DX(Mai2Base): fsr["recordDate"], f"{Mai2Constants.DATE_TIME_FORMAT}.0" ), ) - self.data.item.put_friend_season_ranking(user_id, fsr) + await self.data.item.put_friend_season_ranking(user_id, fsr) + + if "user2pPlaylog" in upsert: + await self.data.score.put_playlog_2p(user_id, upsert["user2pPlaylog"]) return {"returnCode": 1, "apiName": "UpsertUserAllApi"} - def handle_get_user_data_api_request(self, data: Dict) -> Dict: - profile = self.data.profile.get_profile_detail(data["userId"], self.version) + async def handle_get_user_data_api_request(self, data: Dict) -> Dict: + profile = await self.data.profile.get_profile_detail(data["userId"], self.version) if profile is None: return @@ -227,8 +273,8 @@ class Mai2DX(Mai2Base): return {"userId": data["userId"], "userData": profile_dict} - def handle_get_user_extend_api_request(self, data: Dict) -> Dict: - extend = self.data.profile.get_profile_extend(data["userId"], self.version) + async def handle_get_user_extend_api_request(self, data: Dict) -> Dict: + extend = await self.data.profile.get_profile_extend(data["userId"], self.version) if extend is None: return @@ -239,8 +285,8 @@ class Mai2DX(Mai2Base): return {"userId": data["userId"], "userExtend": extend_dict} - def handle_get_user_option_api_request(self, data: Dict) -> Dict: - options = self.data.profile.get_profile_option(data["userId"], self.version) + async def handle_get_user_option_api_request(self, data: Dict) -> Dict: + options = await self.data.profile.get_profile_option(data["userId"], self.version) if options is None: return @@ -251,8 +297,8 @@ class Mai2DX(Mai2Base): return {"userId": data["userId"], "userOption": options_dict} - def handle_get_user_card_api_request(self, data: Dict) -> Dict: - user_cards = self.data.item.get_cards(data["userId"]) + async def handle_get_user_card_api_request(self, data: Dict) -> Dict: + user_cards = await self.data.item.get_cards(data["userId"]) if user_cards is None: return {"userId": data["userId"], "nextIndex": 0, "userCardList": []} @@ -285,44 +331,40 @@ class Mai2DX(Mai2Base): "userCardList": card_list[start_idx:end_idx], } - def handle_get_user_charge_api_request(self, data: Dict) -> Dict: - user_charges = self.data.item.get_charges(data["userId"]) - if user_charges is None: - return {"userId": data["userId"], "length": 0, "userChargeList": []} - - user_charge_list = [] - for charge in user_charges: - tmp = charge._asdict() - tmp.pop("id") - tmp.pop("user") - tmp["purchaseDate"] = datetime.strftime( - tmp["purchaseDate"], Mai2Constants.DATE_TIME_FORMAT - ) - tmp["validDate"] = datetime.strftime( - tmp["validDate"], Mai2Constants.DATE_TIME_FORMAT - ) - - user_charge_list.append(tmp) - - return { - "userId": data["userId"], - "length": len(user_charge_list), - "userChargeList": user_charge_list, - } - - def handle_get_user_item_api_request(self, data: Dict) -> Dict: - kind = int(data["nextIndex"] / 10000000000) - next_idx = int(data["nextIndex"] % 10000000000) - user_item_list = self.data.item.get_items(data["userId"], kind) - + async def handle_get_user_item_api_request(self, data: Dict) -> Dict: + kind = data["nextIndex"] // 10000000000 + next_idx = data["nextIndex"] % 10000000000 items: List[Dict[str, Any]] = [] - for i in range(next_idx, len(user_item_list)): - tmp = user_item_list[i]._asdict() - tmp.pop("user") - tmp.pop("id") - items.append(tmp) - if len(items) >= int(data["maxCount"]): - break + + if kind == 4: # presents + user_pres_list = await self.data.item.get_presents_by_version_user(self.version, data["userId"]) + if user_pres_list: + self.logger.debug(f"Found {len(user_pres_list)} possible presents") + for present in user_pres_list: + if (present['startDate'] and present['startDate'].timestamp() > datetime.now().timestamp()): + self.logger.debug(f"Present {present['id']} distribution hasn't started yet (begins {present['startDate']})") + continue # present period hasn't started yet, move onto the next one + + if (present['endDate'] and present['endDate'].timestamp() < datetime.now().timestamp()): + self.logger.warn(f"Present {present['id']} ended on {present['endDate']} and should be removed") + continue # present period ended, move onto the next one + + test = await self.data.item.get_item(data["userId"], present['itemKind'], present['itemId']) + if not test: # Don't send presents for items the user already has + pres_id = present['itemKind'] * 1000000 + pres_id += present['itemId'] + items.append({"itemId": pres_id, "itemKind": 4, "stock": present['stock'], "isValid": True}) + self.logger.info(f"Give user {data['userId']} {present['stock']}x item {present['itemId']} (kind {present['itemKind']}) as present") + + else: + user_item_list = await self.data.item.get_items(data["userId"], kind) + for i in range(next_idx, len(user_item_list)): + tmp = user_item_list[i]._asdict() + tmp.pop("user") + tmp.pop("id") + items.append(tmp) + if len(items) >= int(data["maxCount"]): + break xout = kind * 10000000000 + next_idx + len(items) @@ -338,8 +380,8 @@ class Mai2DX(Mai2Base): "userItemList": items, } - def handle_get_user_character_api_request(self, data: Dict) -> Dict: - characters = self.data.item.get_characters(data["userId"]) + async def handle_get_user_character_api_request(self, data: Dict) -> Dict: + characters = await self.data.item.get_characters(data["userId"]) chara_list = [] for chara in characters: @@ -350,8 +392,8 @@ class Mai2DX(Mai2Base): return {"userId": data["userId"], "userCharacterList": chara_list} - def handle_get_user_favorite_api_request(self, data: Dict) -> Dict: - favorites = self.data.item.get_favorites(data["userId"], data["itemKind"]) + async def handle_get_user_favorite_api_request(self, data: Dict) -> Dict: + favorites = await self.data.item.get_favorites(data["userId"], data["itemKind"]) if favorites is None: return @@ -367,8 +409,8 @@ class Mai2DX(Mai2Base): return {"userId": data["userId"], "userFavoriteData": userFavs} - def handle_get_user_ghost_api_request(self, data: Dict) -> Dict: - ghost = self.data.profile.get_profile_ghost(data["userId"], self.version) + async def handle_get_user_ghost_api_request(self, data: Dict) -> Dict: + ghost = await self.data.profile.get_profile_ghost(data["userId"], self.version) if ghost is None: return @@ -379,8 +421,8 @@ class Mai2DX(Mai2Base): return {"userId": data["userId"], "userGhost": ghost_dict} - def handle_get_user_rating_api_request(self, data: Dict) -> Dict: - rating = self.data.profile.get_profile_rating(data["userId"], self.version) + async def handle_get_user_rating_api_request(self, data: Dict) -> Dict: + rating = await self.data.profile.get_profile_rating(data["userId"], self.version) if rating is None: return @@ -391,12 +433,12 @@ class Mai2DX(Mai2Base): return {"userId": data["userId"], "userRating": rating_dict} - def handle_get_user_activity_api_request(self, data: Dict) -> Dict: + async def handle_get_user_activity_api_request(self, data: Dict) -> Dict: """ kind 1 is playlist, kind 2 is music list """ - playlist = self.data.profile.get_profile_activity(data["userId"], 1) - musiclist = self.data.profile.get_profile_activity(data["userId"], 2) + playlist = await self.data.profile.get_profile_activity(data["userId"], 1) + musiclist = await self.data.profile.get_profile_activity(data["userId"], 2) if playlist is None or musiclist is None: return @@ -419,8 +461,8 @@ class Mai2DX(Mai2Base): return {"userActivity": {"playList": plst, "musicList": mlst}} - def handle_get_user_course_api_request(self, data: Dict) -> Dict: - user_courses = self.data.score.get_courses(data["userId"]) + async def handle_get_user_course_api_request(self, data: Dict) -> Dict: + user_courses = await self.data.score.get_courses(data["userId"]) if user_courses is None: return {"userId": data["userId"], "nextIndex": 0, "userCourseList": []} @@ -433,12 +475,12 @@ class Mai2DX(Mai2Base): return {"userId": data["userId"], "nextIndex": 0, "userCourseList": course_list} - def handle_get_user_portrait_api_request(self, data: Dict) -> Dict: + async def handle_get_user_portrait_api_request(self, data: Dict) -> Dict: # No support for custom pfps return {"length": 0, "userPortraitList": []} - def handle_get_user_friend_season_ranking_api_request(self, data: Dict) -> Dict: - friend_season_ranking = self.data.item.get_friend_season_ranking(data["userId"]) + async def handle_get_user_friend_season_ranking_api_request(self, data: Dict) -> Dict: + friend_season_ranking = await self.data.item.get_friend_season_ranking(data["userId"]) if friend_season_ranking is None: return { "userId": data["userId"], @@ -473,8 +515,8 @@ class Mai2DX(Mai2Base): "userFriendSeasonRankingList": friend_season_ranking_list, } - def handle_get_user_map_api_request(self, data: Dict) -> Dict: - maps = self.data.item.get_maps(data["userId"]) + async def handle_get_user_map_api_request(self, data: Dict) -> Dict: + maps = await self.data.item.get_maps(data["userId"]) if maps is None: return { "userId": data["userId"], @@ -506,8 +548,8 @@ class Mai2DX(Mai2Base): "userMapList": map_list, } - def handle_get_user_login_bonus_api_request(self, data: Dict) -> Dict: - login_bonuses = self.data.item.get_login_bonuses(data["userId"]) + async def handle_get_user_login_bonus_api_request(self, data: Dict) -> Dict: + login_bonuses = await self.data.item.get_login_bonuses(data["userId"]) if login_bonuses is None: return { "userId": data["userId"], @@ -539,7 +581,7 @@ class Mai2DX(Mai2Base): "userLoginBonusList": login_bonus_list, } - def handle_get_user_region_api_request(self, data: Dict) -> Dict: + async def handle_get_user_region_api_request(self, data: Dict) -> Dict: """ class UserRegionList: regionId: int @@ -548,36 +590,79 @@ class Mai2DX(Mai2Base): """ return {"userId": data["userId"], "length": 0, "userRegionList": []} - def handle_get_user_rival_data_api_request(self, data: Dict) -> Dict: + async def handle_get_user_rival_data_api_request(self, data: Dict) -> Dict: + user_id = data.get("userId", 0) + rival_id = data.get("rivalId", 0) + + if not user_id or not rival_id: return {} + + rival_pf = await self.data.profile.get_profile_detail(rival_id, self.version) + if not rival_pf: return {} + + return { + "userId": user_id, + "userRivalData": { + "rivalId": rival_id, + "rivalName": rival_pf['userName'] + } + } + + async def handle_get_user_rival_music_api_request(self, data: Dict) -> Dict: + user_id = data.get("userId", 0) + rival_id = data.get("rivalId", 0) + next_index = data.get("nextIndex", 0) + max_ct = 100 + upper_lim = next_index + max_ct + rival_music_list: Dict[int, List] = {} + + songs = await self.data.score.get_best_scores(rival_id) + if songs is None: + self.logger.debug("handle_get_user_rival_music_api_request: get_best_scores returned None!") + return { + "userId": user_id, + "rivalId": rival_id, + "nextIndex": 0, + "userRivalMusicList": [] # musicId userRivalMusicDetailList -> level achievement deluxscoreMax + } + + num_user_songs = len(songs) + + for x in range(next_index, upper_lim): + if x >= num_user_songs: + break + + tmp = songs[x]._asdict() + if tmp['musicId'] in rival_music_list: + rival_music_list[tmp['musicId']].append([{"level": tmp['level'], 'achievement': tmp['achievement'], 'deluxscoreMax': tmp['deluxscoreMax']}]) + + else: + if len(rival_music_list) >= max_ct: + break + rival_music_list[tmp['musicId']] = [{"level": tmp['level'], 'achievement': tmp['achievement'], 'deluxscoreMax': tmp['deluxscoreMax']}] + + next_index = 0 if len(rival_music_list) < max_ct or num_user_songs == upper_lim else upper_lim + self.logger.info(f"Send rival {rival_id} songs {next_index}-{upper_lim} ({len(rival_music_list)}) out of {num_user_songs} for user {user_id} (next idx {next_index})") + + return { + "userId": user_id, + "rivalId": rival_id, + "nextIndex": next_index, + "userRivalMusicList": [{"musicId": x, "userRivalMusicDetailList": y} for x, y in rival_music_list.items()] + } + + async def handle_get_user_new_item_api_request(self, data: Dict) -> Dict: + # TODO: Added in 1.41, implement this? user_id = data["userId"] - rival_id = data["rivalId"] + version = data.get("version", 1041000) + user_playlog_list = data.get("userPlaylogList", []) + + return { + "userId": user_id, + "itemKind": -1, + "itemId": -1, + } - """ - class UserRivalData: - rivalId: int - rivalName: str - """ - return {"userId": user_id, "userRivalData": {}} - - def handle_get_user_rival_music_api_request(self, data: Dict) -> Dict: - user_id = data["userId"] - rival_id = data["rivalId"] - next_idx = data["nextIndex"] - rival_music_levels = data["userRivalMusicLevelList"] - - """ - class UserRivalMusicList: - class UserRivalMusicDetailList: - level: int - achievement: int - deluxscoreMax: int - - musicId: int - userRivalMusicDetailList: list[UserRivalMusicDetailList] - """ - return {"userId": user_id, "nextIndex": 0, "userRivalMusicList": []} - - def handle_get_user_music_api_request(self, data: Dict) -> Dict: + async def handle_get_user_music_api_request(self, data: Dict) -> Dict: user_id = data.get("userId", 0) next_index = data.get("nextIndex", 0) max_ct = data.get("maxCount", 50) @@ -588,7 +673,7 @@ class Mai2DX(Mai2Base): self.logger.warning("handle_get_user_music_api_request: Could not find userid in data, or userId is 0") return {} - songs = self.data.score.get_best_scores(user_id) + songs = await self.data.score.get_best_scores(user_id) if songs is None: self.logger.debug("handle_get_user_music_api_request: get_best_scores returned None!") return { @@ -616,9 +701,214 @@ class Mai2DX(Mai2Base): "userMusicList": [{"userMusicDetailList": music_detail_list}], } - def handle_user_login_api_request(self, data: Dict) -> Dict: - ret = super().handle_user_login_api_request(data) + async def handle_user_login_api_request(self, data: Dict) -> Dict: + ret = await super().handle_user_login_api_request(data) if ret is None or not ret: return ret ret['loginId'] = ret.get('loginCount', 0) return ret + + # CardMaker support added in Universe + async def handle_cm_get_user_preview_api_request(self, data: Dict) -> Dict: + p = await self.data.profile.get_profile_detail(data["userId"], self.version) + if p is None: + return {} + + return { + "userName": p["userName"], + "rating": p["playerRating"], + # hardcode lastDataVersion for CardMaker + "lastDataVersion": "1.20.00", # Future versiohs should replace this with the correct version + # checks if the user is still logged in + "isLogin": False, + "isExistSellingCard": True, + } + + async def handle_cm_get_user_data_api_request(self, data: Dict) -> Dict: + # user already exists, because the preview checks that already + p = await self.data.profile.get_profile_detail(data["userId"], self.version) + + cards = await self.data.card.get_user_cards(data["userId"]) + if cards is None or len(cards) == 0: + # This should never happen + self.logger.error( + f"handle_get_user_data_api_request: Internal error - No cards found for user id {data['userId']}" + ) + return {} + + # get the dict representation of the row so we can modify values + user_data = p._asdict() + + # remove the values the game doesn't want + user_data.pop("id") + user_data.pop("user") + user_data.pop("version") + + return {"userId": data["userId"], "userData": user_data} + + async def handle_cm_login_api_request(self, data: Dict) -> Dict: + return {"returnCode": 1} + + async def handle_cm_logout_api_request(self, data: Dict) -> Dict: + return {"returnCode": 1} + + async def handle_cm_get_selling_card_api_request(self, data: Dict) -> Dict: + selling_cards = await self.data.static.get_enabled_cards(self.version) + if selling_cards is None: + return {"length": 0, "sellingCardList": []} + + selling_card_list = [] + for card in selling_cards: + tmp = card._asdict() + tmp.pop("id") + tmp.pop("version") + tmp.pop("cardName") + tmp.pop("enabled") + + tmp["startDate"] = datetime.strftime( + tmp["startDate"], Mai2Constants.DATE_TIME_FORMAT + ) + tmp["endDate"] = datetime.strftime( + tmp["endDate"], Mai2Constants.DATE_TIME_FORMAT + ) + tmp["noticeStartDate"] = datetime.strftime( + tmp["noticeStartDate"], Mai2Constants.DATE_TIME_FORMAT + ) + tmp["noticeEndDate"] = datetime.strftime( + tmp["noticeEndDate"], Mai2Constants.DATE_TIME_FORMAT + ) + + selling_card_list.append(tmp) + + return {"length": len(selling_card_list), "sellingCardList": selling_card_list} + + async def handle_cm_get_user_card_api_request(self, data: Dict) -> Dict: + user_cards = await self.data.item.get_cards(data["userId"]) + if user_cards is None: + return {"returnCode": 1, "length": 0, "nextIndex": 0, "userCardList": []} + + max_ct = data["maxCount"] + next_idx = data["nextIndex"] + start_idx = next_idx + end_idx = max_ct + start_idx + + if len(user_cards[start_idx:]) > max_ct: + next_idx += max_ct + else: + next_idx = 0 + + card_list = [] + for card in user_cards: + tmp = card._asdict() + tmp.pop("id") + tmp.pop("user") + + tmp["startDate"] = datetime.strftime( + tmp["startDate"], Mai2Constants.DATE_TIME_FORMAT + ) + tmp["endDate"] = datetime.strftime( + tmp["endDate"], Mai2Constants.DATE_TIME_FORMAT + ) + card_list.append(tmp) + + return { + "returnCode": 1, + "length": len(card_list[start_idx:end_idx]), + "nextIndex": next_idx, + "userCardList": card_list[start_idx:end_idx], + } + + async def handle_cm_get_user_item_api_request(self, data: Dict) -> Dict: + await self.handle_get_user_item_api_request(data) + + async def handle_cm_get_user_character_api_request(self, data: Dict) -> Dict: + characters = await self.data.item.get_characters(data["userId"]) + + chara_list = [] + for chara in characters: + chara_list.append( + { + "characterId": chara["characterId"], + # no clue why those values are even needed + "point": 0, + "count": 0, + "level": chara["level"], + "nextAwake": 0, + "nextAwakePercent": 0, + "favorite": False, + "awakening": chara["awakening"], + "useCount": chara["useCount"], + } + ) + + return { + "returnCode": 1, + "length": len(chara_list), + "userCharacterList": chara_list, + } + + async def handle_cm_get_user_card_print_error_api_request(self, data: Dict) -> Dict: + return {"length": 0, "userPrintDetailList": []} + + async def handle_cm_upsert_user_print_api_request(self, data: Dict) -> Dict: + user_id = data["userId"] + upsert = data["userPrintDetail"] + + # set a random card serial number + serial_id = "".join([str(randint(0, 9)) for _ in range(20)]) + + # calculate start and end date of the card + start_date = datetime.utcnow() + end_date = datetime.utcnow() + timedelta(days=15) + + user_card = upsert["userCard"] + await self.data.item.put_card( + user_id, + user_card["cardId"], + user_card["cardTypeId"], + user_card["charaId"], + user_card["mapId"], + # add the correct start date and also the end date in 15 days + start_date, + end_date, + ) + + # get the profile extend to save the new bought card + extend = await self.data.profile.get_profile_extend(user_id, self.version) + if extend: + extend = extend._asdict() + # parse the selectedCardList + # 6 = Freedom Pass, 4 = Gold Pass (cardTypeId) + selected_cards: List = extend["selectedCardList"] + + # if no pass is already added, add the corresponding pass + if not user_card["cardTypeId"] in selected_cards: + selected_cards.insert(0, user_card["cardTypeId"]) + + extend["selectedCardList"] = selected_cards + await self.data.profile.put_profile_extend(user_id, self.version, extend) + + # properly format userPrintDetail for the database + upsert.pop("userCard") + upsert.pop("serialId") + upsert["printDate"] = datetime.strptime(upsert["printDate"], "%Y-%m-%d") + + await self.data.item.put_user_print_detail(user_id, serial_id, upsert) + + return { + "returnCode": 1, + "orderId": 0, + "serialId": serial_id, + "startDate": datetime.strftime(start_date, Mai2Constants.DATE_TIME_FORMAT), + "endDate": datetime.strftime(end_date, Mai2Constants.DATE_TIME_FORMAT), + } + + async def handle_cm_upsert_user_printlog_api_request(self, data: Dict) -> Dict: + return { + "returnCode": 1, + "orderId": 0, + "serialId": data["userPrintlog"]["serialId"], + } + + async def handle_cm_upsert_buy_card_api_request(self, data: Dict) -> Dict: + return {"returnCode": 1} diff --git a/titles/mai2/festival.py b/titles/mai2/festival.py index 145fa71..451f0ba 100644 --- a/titles/mai2/festival.py +++ b/titles/mai2/festival.py @@ -11,27 +11,15 @@ class Mai2Festival(Mai2UniversePlus): super().__init__(cfg, game_cfg) self.version = Mai2Constants.VER_MAIMAI_DX_FESTIVAL - def handle_cm_get_user_preview_api_request(self, data: Dict) -> Dict: - user_data = super().handle_cm_get_user_preview_api_request(data) + async def handle_cm_get_user_preview_api_request(self, data: Dict) -> Dict: + user_data = await super().handle_cm_get_user_preview_api_request(data) # hardcode lastDataVersion for CardMaker user_data["lastDataVersion"] = "1.30.00" return user_data - def handle_user_login_api_request(self, data: Dict) -> Dict: - user_login = super().handle_user_login_api_request(data) - # useless? + async def handle_user_login_api_request(self, data: Dict) -> Dict: + user_login = await super().handle_user_login_api_request(data) + # TODO: Make use of this user_login["Bearer"] = "ARTEMiSTOKEN" return user_login - - def handle_get_user_recommend_rate_music_api_request(self, data: Dict) -> Dict: - """ - userRecommendRateMusicIdList: list[int] - """ - return {"userId": data["userId"], "userRecommendRateMusicIdList": []} - - def handle_get_user_recommend_select_music_api_request(self, data: Dict) -> Dict: - """ - userRecommendSelectionMusicIdList: list[int] - """ - return {"userId": data["userId"], "userRecommendSelectionMusicIdList": []} diff --git a/titles/mai2/festivalplus.py b/titles/mai2/festivalplus.py index 7deeb98..3314e34 100644 --- a/titles/mai2/festivalplus.py +++ b/titles/mai2/festivalplus.py @@ -11,28 +11,9 @@ class Mai2FestivalPlus(Mai2Festival): super().__init__(cfg, game_cfg) self.version = Mai2Constants.VER_MAIMAI_DX_FESTIVAL_PLUS - def handle_cm_get_user_preview_api_request(self, data: Dict) -> Dict: - user_data = super().handle_cm_get_user_preview_api_request(data) + async def handle_cm_get_user_preview_api_request(self, data: Dict) -> Dict: + user_data = await super().handle_cm_get_user_preview_api_request(data) # hardcode lastDataVersion for CardMaker user_data["lastDataVersion"] = "1.35.00" return user_data - - def handle_get_user_favorite_item_api_request(self, data: Dict) -> Dict: - user_id = data.get("userId", 0) - kind = data.get("kind", 2) - next_index = data.get("nextIndex", 0) - max_ct = data.get("maxCount", 100) - is_all = data.get("isAllFavoriteItem", False) - - """ - class userFavoriteItemList: - orderId: int - id: int - """ - return { - "userId": user_id, - "kind": kind, - "nextIndex": 0, - "userFavoriteItemList": [], - } diff --git a/titles/mai2/frontend.py b/titles/mai2/frontend.py new file mode 100644 index 0000000..976e2c4 --- /dev/null +++ b/titles/mai2/frontend.py @@ -0,0 +1,301 @@ +from typing import List +from starlette.routing import Route, Mount +from starlette.requests import Request +from starlette.responses import Response, RedirectResponse +from os import path +import yaml +import jinja2 +from datetime import datetime + +from core.frontend import FE_Base, UserSession, PermissionOffset +from core.config import CoreConfig +from .database import Mai2Data +from .config import Mai2Config +from .const import Mai2Constants + +class Mai2Frontend(FE_Base): + def __init__( + self, cfg: CoreConfig, environment: jinja2.Environment, cfg_dir: str + ) -> None: + super().__init__(cfg, environment) + self.data = Mai2Data(cfg) + self.game_cfg = Mai2Config() + if path.exists(f"{cfg_dir}/{Mai2Constants.CONFIG_NAME}"): + self.game_cfg.update( + yaml.safe_load(open(f"{cfg_dir}/{Mai2Constants.CONFIG_NAME}")) + ) + self.nav_name = "maimai" + + def get_routes(self) -> List[Route]: + return [ + Route("/", self.render_GET, methods=['GET']), + Mount("/playlog", routes=[ + Route("/", self.render_GET_playlog, methods=['GET']), + Route("/{index}", self.render_GET_playlog, methods=['GET']), + ]), + Mount("/events", routes=[ + Route("/", self.render_events, methods=['GET']), + Route("/{event_id:int}", self.render_event_edit, methods=['GET']), + Route("/update", self.update_event, methods=['POST']), + Route("/version.change", self.version_change, methods=['POST']), + ]), + Route("/update.name", self.update_name, methods=['POST']), + Route("/version.change", self.version_change, methods=['POST']), + ] + + async def render_GET(self, request: Request) -> bytes: + template = self.environment.get_template( + "titles/mai2/templates/mai2_index.jinja" + ) + usr_sesh = self.validate_session(request) + if not usr_sesh: + usr_sesh = UserSession() + + incoming_ver = usr_sesh.maimai_version + + if usr_sesh.user_id > 0: + versions = await self.data.profile.get_all_profile_versions(usr_sesh.user_id) + profile = [] + if versions: + # maimai_version is -1 means it is not initialized yet, select a default version from existing. + if incoming_ver < 0: + usr_sesh.maimai_version = versions[0]['version'] + profile = await self.data.profile.get_profile_detail(usr_sesh.user_id, usr_sesh.maimai_version) + versions = [x['version'] for x in versions] + + resp = Response(template.render( + title=f"{self.core_config.server.name} | {self.nav_name}", + game_list=self.environment.globals["game_list"], + sesh=vars(usr_sesh), + user_id=usr_sesh.user_id, + profile=profile, + version_list=Mai2Constants.VERSION_STRING, + versions=versions, + cur_version=usr_sesh.maimai_version + ), media_type="text/html; charset=utf-8") + + if incoming_ver < 0: + encoded_sesh = self.encode_session(usr_sesh) + resp.delete_cookie("ARTEMIS_SESH") + resp.set_cookie("ARTEMIS_SESH", encoded_sesh) + return resp + + else: + return RedirectResponse("/gate/", 303) + + async def render_GET_playlog(self, request: Request) -> bytes: + template = self.environment.get_template( + "titles/mai2/templates/mai2_playlog.jinja" + ) + usr_sesh = self.validate_session(request) + if not usr_sesh: + usr_sesh = UserSession() + + if usr_sesh.user_id > 0: + if usr_sesh.maimai_version < 0: + return RedirectResponse("/game/mai2/", 303) + path_index = request.path_params.get('index') + if not path_index or int(path_index) < 1: + index = 0 + else: + index = int(path_index) - 1 # 0 and 1 are 1st page + user_id = usr_sesh.user_id + playlog_count = await self.data.score.get_user_playlogs_count(user_id) + if playlog_count < index * 20 : + return Response(template.render( + title=f"{self.core_config.server.name} | {self.nav_name}", + game_list=self.environment.globals["game_list"], + sesh=vars(usr_sesh), + playlog_count=0 + ), media_type="text/html; charset=utf-8") + playlog = await self.data.score.get_playlogs(user_id, index, 20) + playlog_with_title = [] + for record in playlog: + music_chart = await self.data.static.get_music_chart(usr_sesh.maimai_version, record.musicId, record.level) + if music_chart: + difficultyNum=music_chart.chartId + difficulty=music_chart.difficulty + artist=music_chart.artist + title=music_chart.title + else: + difficultyNum=0 + difficulty=0 + artist="unknown" + title="musicid: " + str(record.musicId) + playlog_with_title.append({ + "raw": record, + "title": title, + "difficultyNum": difficultyNum, + "difficulty": difficulty, + "artist": artist, + }) + return Response(template.render( + title=f"{self.core_config.server.name} | {self.nav_name}", + game_list=self.environment.globals["game_list"], + sesh=vars(usr_sesh), + user_id=usr_sesh.user_id, + playlog=playlog_with_title, + playlog_count=playlog_count + ), media_type="text/html; charset=utf-8") + else: + return RedirectResponse("/gate/", 303) + + async def update_name(self, request: Request) -> bytes: + usr_sesh = self.validate_session(request) + if not usr_sesh: + return RedirectResponse("/gate/", 303) + + form_data = await request.form() + new_name: str = form_data.get("new_name") + new_name_full = "" + + if not new_name: + return RedirectResponse("/gate/?e=4", 303) + + if len(new_name) > 8: + return RedirectResponse("/gate/?e=8", 303) + + for x in new_name: # FIXME: This will let some invalid characters through atm + o = ord(x) + try: + if o == 0x20: + new_name_full += chr(0x3000) + elif o < 0x7F and o > 0x20: + new_name_full += chr(o + 0xFEE0) + elif o <= 0x7F: + self.logger.warn(f"Invalid ascii character {o:02X}") + return RedirectResponse("/gate/?e=4", 303) + else: + new_name_full += x + + except Exception as e: + self.logger.error(f"Something went wrong parsing character {o:04X} - {e}") + return RedirectResponse("/gate/?e=4", 303) + + if not await self.data.profile.update_name(usr_sesh.user_id, new_name_full): + return RedirectResponse("/gate/?e=999", 303) + + return RedirectResponse("/game/mai2/?s=1", 303) + + async def version_change(self, request: Request): + usr_sesh = self.validate_session(request) + if not usr_sesh: + usr_sesh = UserSession() + + if "/events/" in request.url.path: + resp = RedirectResponse("/game/mai2/events/", 303) + else: + resp = RedirectResponse("/game/mai2/", 303) + + if usr_sesh.user_id > 0: + form_data = await request.form() + maimai_version = form_data.get("version") + self.logger.info(f"version change to: {maimai_version}") + if(maimai_version.isdigit()): + usr_sesh.maimai_version=int(maimai_version) + encoded_sesh = self.encode_session(usr_sesh) + self.logger.debug(f"Created session with JWT {encoded_sesh}") + resp.set_cookie("ARTEMIS_SESH", encoded_sesh) + return resp + else: + return RedirectResponse("/gate/", 303) + + async def render_events(self, request: Request) -> Response: + usr_sesh = self.validate_session(request) + if not usr_sesh: + return RedirectResponse("/gate/", 303) + + if not self.test_perm(usr_sesh.permissions, PermissionOffset.SYSADMIN): + return RedirectResponse("/game/mai2/", 303) + + template = self.environment.get_template( + "titles/mai2/templates/events/mai2_events.jinja" + ) + + incoming_ver = usr_sesh.maimai_version + evts = [] + + if incoming_ver < 0: + usr_sesh.maimai_version = Mai2Constants.VER_MAIMAI_DX + + event_list = await self.data.static.get_game_events(usr_sesh.maimai_version) + self.logger.info(f"Get events for v{usr_sesh.maimai_version}") + + for event in event_list: + evts.append({ + "id": event['id'], + "version": event['version'], + "eventId": event['eventId'], + "eventType": event['type'], + "name": event['name'], + "startDate": event['startDate'].strftime("%x %X"), + "enabled": "true" if event['enabled'] else "false", + }) + + resp = Response(template.render( + title=f"{self.core_config.server.name} | {self.nav_name} Events", + game_list=self.environment.globals["game_list"], + sesh=vars(usr_sesh), + version_list=Mai2Constants.VERSION_STRING, + events=evts + ), media_type="text/html; charset=utf-8") + + if incoming_ver < 0: + encoded_sesh = self.encode_session(usr_sesh) + resp.delete_cookie("ARTEMIS_SESH") + resp.set_cookie("ARTEMIS_SESH", encoded_sesh) + + return resp + + async def render_event_edit(self, request: Request) -> Response: + usr_sesh = self.validate_session(request) + if not usr_sesh: + return RedirectResponse("/gate/", 303) + + if not self.test_perm(usr_sesh.permissions, PermissionOffset.SYSADMIN): + return RedirectResponse("/game/mai2/", 303) + + template = self.environment.get_template( + "titles/mai2/templates/events/mai2_event_edit.jinja" + ) + + evt_id = request.path_params.get("event_id") + + event_id = await self.data.static.get_event_by_id(evt_id) + if not event_id: + return RedirectResponse("/game/mai2/events/", 303) + + return Response(template.render( + title=f"{self.core_config.server.name} | {self.nav_name} Edit Event {evt_id}", + game_list=self.environment.globals["game_list"], + sesh=vars(usr_sesh), + user_id=usr_sesh.user_id, + version_list=Mai2Constants.VERSION_STRING, + cur_version=usr_sesh.maimai_version, + event=event_id._asdict() + ), media_type="text/html; charset=utf-8") + + async def update_event(self, request: Request) -> RedirectResponse: + usr_sesh = self.validate_session(request) + if not usr_sesh: + return RedirectResponse("/gate/", 303) + + if not self.test_perm(usr_sesh.permissions, PermissionOffset.SYSADMIN): + return RedirectResponse("/game/mai2/", 303) + + form_data = await request.form() + print(form_data) + event_id: int = form_data.get("evtId", None) + new_enabled: bool = bool(form_data.get("evtEnabled", False)) + try: + new_start_date: datetime = datetime.strptime(form_data.get("evtStart", None), "%Y-%m-%dT%H:%M:%S") + except: + new_start_date = None + + print(f"{event_id} {new_enabled} {new_start_date}") + if event_id is None or new_start_date is None: + return RedirectResponse("/game/mai2/events/?e=4", 303) + + await self.data.static.update_event_by_id(int(event_id), new_enabled, new_start_date) + + return RedirectResponse("/game/mai2/events/?s=1", 303) diff --git a/titles/mai2/index.py b/titles/mai2/index.py index 793aaef..ad02648 100644 --- a/titles/mai2/index.py +++ b/titles/mai2/index.py @@ -1,14 +1,18 @@ -from twisted.web.http import Request -from twisted.web.server import NOT_DONE_YET +from starlette.requests import Request +from starlette.responses import Response, JSONResponse +from starlette.routing import Route import json import inflection import yaml -import string import logging, coloredlogs import zlib +import string from logging.handlers import TimedRotatingFileHandler from os import path, mkdir from typing import Tuple, List, Dict +from Crypto.Hash import MD5 +from Crypto.Cipher import AES +from Crypto.Util.Padding import pad from core.config import CoreConfig from core.utils import Utils @@ -25,12 +29,14 @@ from .universe import Mai2Universe from .universeplus import Mai2UniversePlus from .festival import Mai2Festival from .festivalplus import Mai2FestivalPlus +from .buddies import Mai2Buddies class Mai2Servlet(BaseServlet): def __init__(self, core_cfg: CoreConfig, cfg_dir: str) -> None: super().__init__(core_cfg, cfg_dir) self.game_cfg = Mai2Config() + self.hash_table: Dict[int, Dict[str, str]] = {} if path.exists(f"{cfg_dir}/{Mai2Constants.CONFIG_NAME}"): self.game_cfg.update( yaml.safe_load(open(f"{cfg_dir}/{Mai2Constants.CONFIG_NAME}")) @@ -58,6 +64,7 @@ class Mai2Servlet(BaseServlet): Mai2UniversePlus, Mai2Festival, Mai2FestivalPlus, + Mai2Buddies ] self.logger = logging.getLogger("mai2") @@ -84,6 +91,37 @@ class Mai2Servlet(BaseServlet): level=self.game_cfg.server.loglevel, logger=self.logger, fmt=log_fmt_str ) self.logger.initted = True + + for version, keys in self.game_cfg.crypto.keys.items(): + if version < Mai2Constants.VER_MAIMAI_DX: + continue + + if len(keys) < 3: + continue + + self.hash_table[version] = {} + method_list = [ + method + for method in dir(self.versions[version]) + if not method.startswith("__") + ] + + for method in method_list: + # handle_method_api_request -> HandleMethodApiRequest + # remove the first 6 chars and the final 7 chars to get the canonical + # endpoint name. + method_fixed = inflection.camelize(method)[6:-7] + hash = MD5.new((method_fixed + keys[2]).encode()) + + # truncate unused bytes like the game does + hashed_name = hash.hexdigest() + self.hash_table[version][hashed_name] = method_fixed + + self.logger.debug( + "Hashed v%s method %s with %s to get %s", + version, method_fixed, keys[2], hashed_name + ) + @classmethod def is_game_enabled( @@ -101,33 +139,29 @@ class Mai2Servlet(BaseServlet): return True - def get_endpoint_matchers(self) -> Tuple[List[Tuple[str, str, Dict]], List[Tuple[str, str, Dict]]]: - return ( - [ - ("handle_movie", "/{version}/MaimaiServlet/api/movie/{endpoint}", {}), - ("handle_old_srv", "/{version}/MaimaiServlet/old/{endpoint}", {}), - ("handle_old_srv_userdata", "/{version}/MaimaiServlet/old/{endpoint}/{placeid}/{keychip}/{userid}", {}), - ("handle_old_srv_userdata", "/{version}/MaimaiServlet/old/{endpoint}/{userid}", {}), - ("handle_usbdl", "/{version}/MaimaiServlet/usbdl/{endpoint}", {}), - ("handle_deliver", "/{version}/MaimaiServlet/deliver/{endpoint}", {}), - ], - [ - ("handle_movie", "/{version}/MaimaiServlet/api/movie/{endpoint}", {}), - ("handle_mai", "/{version}/MaimaiServlet/{endpoint}", {}), - ("handle_mai2", "/{version}/Maimai2Servlet/{endpoint}", {}), - ] - ) - - def get_allnet_info(self, game_code: str, game_ver: int, keychip: str) -> Tuple[str, str]: + def get_routes(self) -> List[Route]: + return [ + Route("/{game:str}/{version:int}/MaimaiServlet/api/movie/{endpoint:str}", self.handle_movie, methods=['GET', 'POST']), + Route("/{game:str}/{version:int}/MaimaiServlet/old/{endpoint:str}", self.handle_old_srv), + Route("/{game:str}/{version:int}/MaimaiServlet/old/{endpoint:str}/{placeid:str}/{keychip:str}/{userid:int}", self.handle_old_srv_userdata), + Route("/{game:str}/{version:int}/MaimaiServlet/old/{endpoint:str}/{userid:int}", self.handle_old_srv_userdata), + Route("/{game:str}/{version:int}/MaimaiServlet/old/{endpoint:str}/{userid:int}", self.handle_old_srv_userdata), + Route("/{game:str}/{version:int}/MaimaiServlet/usbdl/{endpoint:str}", self.handle_usbdl), + Route("/{game:str}/{version:int}/MaimaiServlet/deliver/{endpoint:str}", self.handle_deliver), + Route("/{game:str}/{version:int}/MaimaiServlet/{endpoint:str}", self.handle_mai, methods=['POST']), + Route("/{game:str}/{version:int}/Maimai2Servlet/{endpoint:str}", self.handle_mai2, methods=['POST']), + ] + + def get_allnet_info(self, game_code: str, game_ver: int, keychip: str) -> Tuple[str, str]: if not self.core_cfg.server.is_using_proxy and Utils.get_title_port(self.core_cfg) != 80: return ( - f"http://{self.core_cfg.title.hostname}:{Utils.get_title_port(self.core_cfg)}/{game_ver}/", - f"{self.core_cfg.title.hostname}", + f"http://{self.core_cfg.server.hostname}:{Utils.get_title_port(self.core_cfg)}/{game_code}/{game_ver}/", + f"{self.core_cfg.server.hostname}", ) return ( - f"http://{self.core_cfg.title.hostname}/{game_ver}/", - f"{self.core_cfg.title.hostname}", + f"http://{self.core_cfg.server.hostname}/{game_code}/{game_ver}/", + f"{self.core_cfg.server.hostname}", ) def setup(self): @@ -155,13 +189,22 @@ class Mai2Servlet(BaseServlet): f"Failed to make movie upload directory at {self.game_cfg.uploads.movies_dir}" ) - def handle_mai(self, request: Request, game_code: str, matchers: Dict) -> bytes: - endpoint = matchers['endpoint'] - version = int(matchers['version']) + async def handle_movie(self, request: Request): + return JSONResponse() + + async def handle_usbdl(self, request: Request): + return Response("OK") + + async def handle_deliver(self, request: Request): + return Response(status_code=404) + + async def handle_mai(self, request: Request) -> bytes: + endpoint: str = request.path_params.get('endpoint') + version: int = request.path_params.get('version') if endpoint.lower() == "ping": - return zlib.compress(b'{"returnCode": "1"}') + return Response(zlib.compress(b'{"returnCode": "1"}')) - req_raw = request.content.getvalue() + req_raw = await request.body() internal_ver = 0 client_ip = Utils.get_ip_addr(request) @@ -199,7 +242,7 @@ class Mai2Servlet(BaseServlet): self.logger.error( f"Failed to decompress v{version} {endpoint} request -> {e}" ) - return zlib.compress(b'{"stat": "0"}') + return Response(zlib.compress(b'{"stat": "0"}')) req_data = json.loads(unzip) @@ -216,58 +259,117 @@ class Mai2Servlet(BaseServlet): else: try: handler = getattr(handler_cls, func_to_find) - resp = handler(req_data) + resp = await handler(req_data) except Exception as e: self.logger.error(f"Error handling v{version} method {endpoint} - {e}") - return zlib.compress(b'{"returnCode": "0"}') + return Response(zlib.compress(b'{"returnCode": "0"}')) - if resp == None: + if resp is None: resp = {"returnCode": 1} self.logger.debug(f"Response {resp}") - return zlib.compress(json.dumps(resp, ensure_ascii=False).encode("utf-8")) + return Response(zlib.compress(json.dumps(resp, ensure_ascii=False).encode("utf-8"))) + + async def handle_mai2(self, request: Request) -> bytes: + endpoint: str = request.path_params.get('endpoint') + version: int = request.path_params.get('version') + game_code = request.path_params.get('game') - def handle_mai2(self, request: Request, game_code: str, matchers: Dict) -> bytes: - endpoint = matchers['endpoint'] - version = int(matchers['version']) if endpoint.lower() == "ping": - return zlib.compress(b'{"returnCode": "1"}') + return Response(zlib.compress(b'{"returnCode": "1"}')) - req_raw = request.content.getvalue() + req_raw = await request.body() internal_ver = 0 client_ip = Utils.get_ip_addr(request) - if version < 105: # 1.0 - internal_ver = Mai2Constants.VER_MAIMAI_DX - elif version >= 105 and version < 110: # PLUS - internal_ver = Mai2Constants.VER_MAIMAI_DX_PLUS - elif version >= 110 and version < 115: # Splash - internal_ver = Mai2Constants.VER_MAIMAI_DX_SPLASH - elif version >= 115 and version < 120: # Splash PLUS - internal_ver = Mai2Constants.VER_MAIMAI_DX_SPLASH_PLUS - elif version >= 120 and version < 125: # UNiVERSE - internal_ver = Mai2Constants.VER_MAIMAI_DX_UNIVERSE - elif version >= 125 and version < 130: # UNiVERSE PLUS - internal_ver = Mai2Constants.VER_MAIMAI_DX_UNIVERSE_PLUS - elif version >= 130 and version < 135: # FESTiVAL - internal_ver = Mai2Constants.VER_MAIMAI_DX_FESTIVAL - elif version >= 135: # FESTiVAL PLUS - internal_ver = Mai2Constants.VER_MAIMAI_DX_FESTIVAL_PLUS + encrypted = False + + if game_code == "SDEZ": # JP + if version < 110: # 1.0 + internal_ver = Mai2Constants.VER_MAIMAI_DX + elif version >= 110 and version < 114: # PLUS + internal_ver = Mai2Constants.VER_MAIMAI_DX_PLUS + elif version >= 114 and version < 117: # Splash + internal_ver = Mai2Constants.VER_MAIMAI_DX_SPLASH + elif version >= 117 and version < 120: # Splash PLUS + internal_ver = Mai2Constants.VER_MAIMAI_DX_SPLASH_PLUS + elif version >= 120 and version < 125: # UNiVERSE + internal_ver = Mai2Constants.VER_MAIMAI_DX_UNIVERSE + elif version >= 125 and version < 130: # UNiVERSE PLUS + internal_ver = Mai2Constants.VER_MAIMAI_DX_UNIVERSE_PLUS + elif version >= 130 and version < 135: # FESTiVAL + internal_ver = Mai2Constants.VER_MAIMAI_DX_FESTIVAL + elif version >= 135 and version < 140: # FESTiVAL PLUS + internal_ver = Mai2Constants.VER_MAIMAI_DX_FESTIVAL_PLUS + elif version >= 140: # BUDDiES + internal_ver = Mai2Constants.VER_MAIMAI_DX_BUDDIES + elif game_code == "SDGA": # Int + if version < 105: # 1.0 + internal_ver = Mai2Constants.VER_MAIMAI_DX + elif version >= 105 and version < 110: # PLUS + internal_ver = Mai2Constants.VER_MAIMAI_DX_PLUS + elif version >= 110 and version < 115: # Splash + internal_ver = Mai2Constants.VER_MAIMAI_DX_SPLASH + elif version >= 115 and version < 120: # Splash PLUS + internal_ver = Mai2Constants.VER_MAIMAI_DX_SPLASH_PLUS + elif version >= 120 and version < 125: # UNiVERSE + internal_ver = Mai2Constants.VER_MAIMAI_DX_UNIVERSE + elif version >= 125 and version < 130: # UNiVERSE PLUS + internal_ver = Mai2Constants.VER_MAIMAI_DX_UNIVERSE_PLUS + elif version >= 130 and version < 135: # FESTiVAL + internal_ver = Mai2Constants.VER_MAIMAI_DX_FESTIVAL + elif version >= 135 and version < 140: # FESTiVAL PLUS + internal_ver = Mai2Constants.VER_MAIMAI_DX_FESTIVAL_PLUS + if all(c in string.hexdigits for c in endpoint) and len(endpoint) == 32: + # If we get a 32 character long hex string, it's a hash and we're + # dealing with an encrypted request. False positives shouldn't happen + # as long as requests are suffixed with `Api`. + if internal_ver not in self.hash_table: + self.logger.error( + "v%s does not support encryption or no keys entered", + version, + ) + return Response(zlib.compress(b'{"stat": "0"}')) + elif endpoint.lower() not in self.hash_table[internal_ver]: + self.logger.error( + "No hash found for v%s endpoint %s", + version, endpoint + ) + return Response(zlib.compress(b'{"stat": "0"}')) + + endpoint = self.hash_table[internal_ver][endpoint.lower()] + + try: + crypt = AES.new( + bytes.fromhex(self.game_cfg.crypto.keys[internal_ver][0]), + AES.MODE_CBC, + bytes.fromhex(self.game_cfg.crypto.keys[internal_ver][1]), + ) + + req_raw = crypt.decrypt(req_raw) + + except Exception as e: + self.logger.error( + "Failed to decrypt v%s request to %s", + version, endpoint, + exc_info=e, + ) + return Response(zlib.compress(b'{"stat": "0"}')) + + encrypted = True + if ( - request.getHeader("Mai-Encoding") is not None - or request.getHeader("X-Mai-Encoding") is not None + not encrypted + and self.game_cfg.crypto.encrypted_only + and version >= 110 ): - # The has is some flavor of MD5 of the endpoint with a constant bolted onto the end of it. - # See cake.dll's Obfuscator function for details. Hopefully most DLL edits will remove - # these two(?) headers to not cause issues, but given the general quality of SEGA data... - enc_ver = request.getHeader("Mai-Encoding") - if enc_ver is None: - enc_ver = request.getHeader("X-Mai-Encoding") - self.logger.debug( - f"Encryption v{enc_ver} - User-Agent: {request.getHeader('User-Agent')}" + self.logger.error( + "Unencrypted v%s %s request, but config is set to encrypted only: %r", + version, endpoint, req_raw ) + return Response(zlib.compress(b'{"stat": "0"}')) try: unzip = zlib.decompress(req_raw) @@ -276,13 +378,18 @@ class Mai2Servlet(BaseServlet): self.logger.error( f"Failed to decompress v{version} {endpoint} request -> {e}" ) - return zlib.compress(b'{"stat": "0"}') + return Response(zlib.compress(b'{"stat": "0"}')) req_data = json.loads(unzip) self.logger.info(f"v{version} {endpoint} request from {client_ip}") self.logger.debug(req_data) + endpoint = ( + endpoint.replace("MaimaiExp", "") + if game_code == Mai2Constants.GAME_CODE_DX_INT + else endpoint + ) func_to_find = "handle_" + inflection.underscore(endpoint) + "_request" handler_cls = self.versions[internal_ver](self.core_cfg, self.game_cfg) @@ -293,80 +400,41 @@ class Mai2Servlet(BaseServlet): else: try: handler = getattr(handler_cls, func_to_find) - resp = handler(req_data) + resp = await handler(req_data) except Exception as e: self.logger.error(f"Error handling v{version} method {endpoint} - {e}") - return zlib.compress(b'{"stat": "0"}') + return Response(zlib.compress(b'{"stat": "0"}')) - if resp == None: + if resp is None: resp = {"returnCode": 1} self.logger.debug(f"Response {resp}") - return zlib.compress(json.dumps(resp, ensure_ascii=False).encode("utf-8")) + zipped = zlib.compress(json.dumps(resp, ensure_ascii=False).encode("utf-8")) - def handle_old_srv(self, request: Request, game_code: str, matchers: Dict) -> bytes: - endpoint = matchers['endpoint'] - version = matchers['version'] + if not encrypted or version < 110: + return Response(zipped) + + padded = pad(zipped, 16) + + crypt = AES.new( + bytes.fromhex(self.game_cfg.crypto.keys[internal_ver][0]), + AES.MODE_CBC, + bytes.fromhex(self.game_cfg.crypto.keys[internal_ver][1]), + ) + + return Response(crypt.encrypt(padded)) + + + async def handle_old_srv(self, request: Request) -> bytes: + endpoint = request.path_params.get('endpoint') + version = request.path_params.get('version') self.logger.info(f"v{version} old server {endpoint}") - return zlib.compress(b"ok") + return Response(zlib.compress(b"ok")) - def handle_old_srv_userdata(self, request: Request, game_code: str, matchers: Dict) -> bytes: - endpoint = matchers['endpoint'] - version = matchers['version'] + async def handle_old_srv_userdata(self, request: Request) -> bytes: + endpoint = request.path_params.get('endpoint') + version = request.path_params.get('version') self.logger.info(f"v{version} old server {endpoint}") - return zlib.compress(b"{}") - - def render_GET(self, request: Request, version: int, url_path: str) -> bytes: - self.logger.debug(f"v{version} GET {url_path}") - url_split = url_path.split("/") - - if (url_split[0] == "api" and url_split[1] == "movie") or url_split[ - 0 - ] == "movie": - if url_split[2] == "moviestart": - return json.dumps({"moviestart": {"status": "OK"}}).encode() - - else: - request.setResponseCode(404) - return b"" - - elif url_split[0] == "usbdl": - if url_split[1] == "CONNECTIONTEST": - self.logger.info(f"v{version} usbdl server test") - return b"" - - elif self.game_cfg.deliver.udbdl_enable and path.exists( - f"{self.game_cfg.deliver.content_folder}/usb/{url_split[-1]}" - ): - with open( - f"{self.game_cfg.deliver.content_folder}/usb/{url_split[-1]}", "rb" - ) as f: - return f.read() - - else: - request.setResponseCode(404) - return b"" - - elif url_split[0] == "deliver": - file = url_split[len(url_split) - 1] - self.logger.info(f"v{version} {file} deliver inquire") - self.logger.debug( - f"{self.game_cfg.deliver.content_folder}/net_deliver/{file}" - ) - - if self.game_cfg.deliver.enable and path.exists( - f"{self.game_cfg.deliver.content_folder}/net_deliver/{file}" - ): - with open( - f"{self.game_cfg.deliver.content_folder}/net_deliver/{file}", "rb" - ) as f: - return f.read() - - else: - request.setResponseCode(404) - return b"" - - else: - return zlib.compress(b"{}") + return Response(zlib.compress(b"{}")) diff --git a/titles/mai2/read.py b/titles/mai2/read.py index cc4f678..d9450ac 100644 --- a/titles/mai2/read.py +++ b/titles/mai2/read.py @@ -35,7 +35,7 @@ class Mai2Reader(BaseReader): self.logger.error(f"Invalid maimai DX version {version}") exit(1) - def read(self) -> None: + async def read(self) -> None: data_dirs = [] if self.version >= Mai2Constants.VER_MAIMAI_DX: if self.bin_dir is not None: @@ -46,10 +46,10 @@ class Mai2Reader(BaseReader): for dir in data_dirs: self.logger.info(f"Read from {dir}") - self.get_events(f"{dir}/event") - self.disable_events(f"{dir}/information", f"{dir}/scoreRanking") - self.read_music(f"{dir}/music") - self.read_tickets(f"{dir}/ticket") + await self.get_events(f"{dir}/event") + await self.disable_events(f"{dir}/information", f"{dir}/scoreRanking") + await self.read_music(f"{dir}/music") + await self.read_tickets(f"{dir}/ticket") else: if not os.path.exists(f"{self.bin_dir}/tables"): @@ -70,16 +70,16 @@ class Mai2Reader(BaseReader): txt_table = self.load_table_raw(f"{self.bin_dir}/tables", "mmtextout_jp.bin", key) score_table = self.load_table_raw(f"{self.bin_dir}/tables", "mmScore.bin", key) - self.read_old_events(evt_table) - self.read_old_music(score_table, txt_table) + await self.read_old_events(evt_table) + await self.read_old_music(score_table, txt_table) if self.opt_dir is not None: evt_table = self.load_table_raw(f"{self.opt_dir}/tables", "mmEvent.bin", key) txt_table = self.load_table_raw(f"{self.opt_dir}/tables", "mmtextout_jp.bin", key) score_table = self.load_table_raw(f"{self.opt_dir}/tables", "mmScore.bin", key) - self.read_old_events(evt_table) - self.read_old_music(score_table, txt_table) + await self.read_old_events(evt_table) + await self.read_old_music(score_table, txt_table) return @@ -179,7 +179,7 @@ class Mai2Reader(BaseReader): self.logger.warning("Failed load table content, skipping") return - def get_events(self, base_dir: str) -> None: + async def get_events(self, base_dir: str) -> None: self.logger.info(f"Reading events from {base_dir}...") for root, dirs, files in os.walk(base_dir): @@ -192,12 +192,12 @@ class Mai2Reader(BaseReader): id = int(troot.find("name").find("id").text) event_type = int(troot.find("infoType").text) - self.data.static.put_game_event( + await self.data.static.put_game_event( self.version, event_type, id, name ) self.logger.info(f"Added event {id}...") - def disable_events( + async def disable_events( self, base_information_dir: str, base_score_ranking_dir: str ) -> None: self.logger.info(f"Reading disabled events from {base_information_dir}...") @@ -210,7 +210,7 @@ class Mai2Reader(BaseReader): event_id = int(troot.find("name").find("id").text) - self.data.static.toggle_game_event( + await self.data.static.toggle_game_event( self.version, event_id, toggle=False ) self.logger.info(f"Disabled event {event_id}...") @@ -223,7 +223,7 @@ class Mai2Reader(BaseReader): event_id = int(troot.find("eventName").find("id").text) - self.data.static.toggle_game_event( + await self.data.static.toggle_game_event( self.version, event_id, toggle=False ) self.logger.info(f"Disabled event {event_id}...") @@ -252,10 +252,10 @@ class Mai2Reader(BaseReader): 22091518, 22091519, ]: - self.data.static.toggle_game_event(self.version, event_id, toggle=False) + await self.data.static.toggle_game_event(self.version, event_id, toggle=False) self.logger.info(f"Disabled event {event_id}...") - def read_music(self, base_dir: str) -> None: + async def read_music(self, base_dir: str) -> None: self.logger.info(f"Reading music from {base_dir}...") for root, dirs, files in os.walk(base_dir): @@ -285,7 +285,7 @@ class Mai2Reader(BaseReader): dif.find("notesDesigner").find("str").text ) - self.data.static.put_game_music( + await self.data.static.put_game_music( self.version, song_id, chart_id, @@ -302,7 +302,7 @@ class Mai2Reader(BaseReader): f"Added music id {song_id} chart {chart_id}" ) - def read_tickets(self, base_dir: str) -> None: + async def read_tickets(self, base_dir: str) -> None: self.logger.info(f"Reading tickets from {base_dir}...") for root, dirs, files in os.walk(base_dir): @@ -316,12 +316,12 @@ class Mai2Reader(BaseReader): ticket_type = int(troot.find("ticketKind").find("id").text) price = int(troot.find("creditNum").text) - self.data.static.put_game_ticket( + await self.data.static.put_game_ticket( self.version, id, ticket_type, price, name ) self.logger.info(f"Added ticket {id}...") - def read_old_events(self, events: Optional[List[Dict[str, str]]]) -> None: + async def read_old_events(self, events: Optional[List[Dict[str, str]]]) -> None: if events is None: return @@ -332,12 +332,12 @@ class Mai2Reader(BaseReader): is_aou = bool(int(event.get('AOU許可', '0'))) name = event.get('comment', f'evt_{evt_id}') - self.data.static.put_game_event(self.version, 0, evt_id, name) + await self.data.static.put_game_event(self.version, 0, evt_id, name) if not (is_exp or is_aou): - self.data.static.toggle_game_event(self.version, evt_id, False) + await self.data.static.toggle_game_event(self.version, evt_id, False) - def read_old_music(self, scores: Optional[List[Dict[str, str]]], text: Optional[List[Dict[str, str]]]) -> None: + async def read_old_music(self, scores: Optional[List[Dict[str, str]]], text: Optional[List[Dict[str, str]]]) -> None: if scores is None or text is None: return # TODO diff --git a/titles/mai2/schema/item.py b/titles/mai2/schema/item.py index a6ed876..d53ebbc 100644 --- a/titles/mai2/schema/item.py +++ b/titles/mai2/schema/item.py @@ -2,8 +2,8 @@ from core.data.schema import BaseData, metadata from datetime import datetime from typing import Optional, Dict, List -from sqlalchemy import Table, Column, UniqueConstraint, PrimaryKeyConstraint, and_ -from sqlalchemy.types import Integer, String, TIMESTAMP, Boolean, JSON +from sqlalchemy import Table, Column, UniqueConstraint, PrimaryKeyConstraint, and_, or_ +from sqlalchemy.types import Integer, String, TIMESTAMP, Boolean, JSON, BIGINT, INTEGER from sqlalchemy.schema import ForeignKey from sqlalchemy.sql import func, select from sqlalchemy.dialects.mysql import insert @@ -134,6 +134,20 @@ favorite = Table( mysql_charset="utf8mb4", ) +fav_music = Table( + "mai2_item_favorite_music", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), + Column("musicId", Integer, nullable=False), + UniqueConstraint("user", "musicId", name="mai2_item_favorite_music_uk"), + mysql_charset="utf8mb4", +) + charge = Table( "mai2_item_charge", metadata, @@ -184,9 +198,23 @@ print_detail = Table( mysql_charset="utf8mb4", ) +present = Table( + "mai2_item_present", + metadata, + Column('id', BIGINT, primary_key=True, nullable=False), + Column('version', INTEGER), + Column("user", Integer, ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade")), + Column("itemKind", INTEGER, nullable=False), + Column("itemId", INTEGER, nullable=False), + Column("stock", INTEGER, nullable=False, server_default="1"), + Column("startDate", TIMESTAMP), + Column("endDate", TIMESTAMP), + UniqueConstraint("version", "user", "itemKind", "itemId", name="mai2_item_present_uk"), + mysql_charset="utf8mb4", +) class Mai2ItemData(BaseData): - def put_item( + async def put_item( self, user_id: int, item_kind: int, item_id: int, stock: int, is_valid: bool ) -> None: sql = insert(item).values( @@ -202,7 +230,7 @@ class Mai2ItemData(BaseData): isValid=is_valid, ) - result = self.execute(conflict) + result = await self.execute(conflict) if result is None: self.logger.warning( f"put_item: failed to insert item! user_id: {user_id}, item_kind: {item_kind}, item_id: {item_id}" @@ -210,7 +238,7 @@ class Mai2ItemData(BaseData): return None return result.lastrowid - def get_items(self, user_id: int, item_kind: int = None) -> Optional[List[Row]]: + async def get_items(self, user_id: int, item_kind: int = None) -> Optional[List[Row]]: if item_kind is None: sql = item.select(item.c.user == user_id) else: @@ -218,12 +246,12 @@ class Mai2ItemData(BaseData): and_(item.c.user == user_id, item.c.itemKind == item_kind) ) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchall() - def get_item(self, user_id: int, item_kind: int, item_id: int) -> Optional[Row]: + async def get_item(self, user_id: int, item_kind: int, item_id: int) -> Optional[Row]: sql = item.select( and_( item.c.user == user_id, @@ -232,12 +260,12 @@ class Mai2ItemData(BaseData): ) ) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchone() - def put_login_bonus( + async def put_login_bonus( self, user_id: int, bonus_id: int, @@ -259,7 +287,7 @@ class Mai2ItemData(BaseData): isComplete=is_complete, ) - result = self.execute(conflict) + result = await self.execute(conflict) if result is None: self.logger.warning( f"put_login_bonus: failed to insert item! user_id: {user_id}, bonus_id: {bonus_id}, point: {point}" @@ -267,25 +295,25 @@ class Mai2ItemData(BaseData): return None return result.lastrowid - def get_login_bonuses(self, user_id: int) -> Optional[List[Row]]: + async def get_login_bonuses(self, user_id: int) -> Optional[List[Row]]: sql = login_bonus.select(login_bonus.c.user == user_id) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchall() - def get_login_bonus(self, user_id: int, bonus_id: int) -> Optional[Row]: + async def get_login_bonus(self, user_id: int, bonus_id: int) -> Optional[Row]: sql = login_bonus.select( and_(login_bonus.c.user == user_id, login_bonus.c.bonus_id == bonus_id) ) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchone() - def put_map( + async def put_map( self, user_id: int, map_id: int, @@ -310,7 +338,7 @@ class Mai2ItemData(BaseData): isComplete=is_complete, ) - result = self.execute(conflict) + result = await self.execute(conflict) if result is None: self.logger.warning( f"put_map: failed to insert item! user_id: {user_id}, map_id: {map_id}, distance: {distance}" @@ -318,28 +346,28 @@ class Mai2ItemData(BaseData): return None return result.lastrowid - def get_maps(self, user_id: int) -> Optional[List[Row]]: + async def get_maps(self, user_id: int) -> Optional[List[Row]]: sql = map.select(map.c.user == user_id) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchall() - def get_map(self, user_id: int, map_id: int) -> Optional[Row]: + async def get_map(self, user_id: int, map_id: int) -> Optional[Row]: sql = map.select(and_(map.c.user == user_id, map.c.mapId == map_id)) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchone() - def put_character_(self, user_id: int, char_data: Dict) -> Optional[int]: + async def put_character_(self, user_id: int, char_data: Dict) -> Optional[int]: char_data["user"] = user_id sql = insert(character).values(**char_data) conflict = sql.on_duplicate_key_update(**char_data) - result = self.execute(conflict) + result = await self.execute(conflict) if result is None: self.logger.warning( f"put_character_: failed to insert item! user_id: {user_id}" @@ -347,7 +375,7 @@ class Mai2ItemData(BaseData): return None return result.lastrowid - def put_character( + async def put_character( self, user_id: int, character_id: int, @@ -369,7 +397,7 @@ class Mai2ItemData(BaseData): useCount=use_count, ) - result = self.execute(conflict) + result = await self.execute(conflict) if result is None: self.logger.warning( f"put_character: failed to insert item! user_id: {user_id}, character_id: {character_id}, level: {level}" @@ -377,33 +405,33 @@ class Mai2ItemData(BaseData): return None return result.lastrowid - def get_characters(self, user_id: int) -> Optional[List[Row]]: + async def get_characters(self, user_id: int) -> Optional[List[Row]]: sql = character.select(character.c.user == user_id) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchall() - def get_character(self, user_id: int, character_id: int) -> Optional[Row]: + async def get_character(self, user_id: int, character_id: int) -> Optional[Row]: sql = character.select( and_(character.c.user == user_id, character.c.character_id == character_id) ) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchone() - def get_friend_season_ranking(self, user_id: int) -> Optional[Row]: + async def get_friend_season_ranking(self, user_id: int) -> Optional[Row]: sql = friend_season_ranking.select(friend_season_ranking.c.user == user_id) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchall() - def put_friend_season_ranking( + async def put_friend_season_ranking( self, aime_id: int, friend_season_ranking_data: Dict ) -> Optional[int]: sql = insert(friend_season_ranking).values( @@ -411,7 +439,7 @@ class Mai2ItemData(BaseData): ) conflict = sql.on_duplicate_key_update(**friend_season_ranking_data) - result = self.execute(conflict) + result = await self.execute(conflict) if result is None: self.logger.warning( @@ -421,7 +449,7 @@ class Mai2ItemData(BaseData): return None return result.lastrowid - def put_favorite( + async def put_favorite( self, user_id: int, kind: int, item_id_list: List[int] ) -> Optional[int]: sql = insert(favorite).values( @@ -430,7 +458,7 @@ class Mai2ItemData(BaseData): conflict = sql.on_duplicate_key_update(item_id_list=item_id_list) - result = self.execute(conflict) + result = await self.execute(conflict) if result is None: self.logger.warning( f"put_favorite: failed to insert item! user_id: {user_id}, kind: {kind}" @@ -438,7 +466,7 @@ class Mai2ItemData(BaseData): return None return result.lastrowid - def get_favorites(self, user_id: int, kind: int = None) -> Optional[Row]: + async def get_favorites(self, user_id: int, kind: int = None) -> Optional[Row]: if kind is None: sql = favorite.select(favorite.c.user == user_id) else: @@ -446,12 +474,36 @@ class Mai2ItemData(BaseData): and_(favorite.c.user == user_id, favorite.c.itemKind == kind) ) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchall() - def put_card( + async def get_fav_music(self, user_id: int) -> Optional[List[Row]]: + result = await self.execute(fav_music.select(fav_music.c.user == user_id)) + if result: + return result.fetchall() + + async def add_fav_music(self, user_id: int, music_id: int) -> Optional[int]: + sql = insert(fav_music).values( + user = user_id, + musicId = music_id + ) + + conflict = sql.on_duplicate_key_update(musicId = music_id) + + result = await self.execute(conflict) + if result: + return result.lastrowid + + self.logger.error(f"Failed to add music {music_id} as favorite for user {user_id}!") + + async def remove_fav_music(self, user_id: int, music_id: int) -> None: + result = await self.execute(fav_music.delete(and_(fav_music.c.user == user_id, fav_music.c.musicId == music_id))) + if not result: + self.logger.error(f"Failed to remove music {music_id} as favorite for user {user_id}!") + + async def put_card( self, user_id: int, card_type_id: int, @@ -475,7 +527,7 @@ class Mai2ItemData(BaseData): charaId=chara_id, mapId=map_id, startDate=start_date, endDate=end_date ) - result = self.execute(conflict) + result = await self.execute(conflict) if result is None: self.logger.warning( f"put_card: failed to insert card! user_id: {user_id}, kind: {card_kind}" @@ -483,7 +535,7 @@ class Mai2ItemData(BaseData): return None return result.lastrowid - def get_cards(self, user_id: int, kind: int = None) -> Optional[Row]: + async def get_cards(self, user_id: int, kind: int = None) -> Optional[Row]: if kind is None: sql = card.select(card.c.user == user_id) else: @@ -491,12 +543,12 @@ class Mai2ItemData(BaseData): sql = sql.order_by(card.c.startDate.desc()) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchall() - def put_charge( + async def put_charge( self, user_id: int, charge_id: int, @@ -516,7 +568,7 @@ class Mai2ItemData(BaseData): stock=stock, purchaseDate=purchase_date, validDate=valid_date ) - result = self.execute(conflict) + result = await self.execute(conflict) if result is None: self.logger.warning( f"put_card: failed to insert charge! user_id: {user_id}, chargeId: {charge_id}" @@ -524,15 +576,15 @@ class Mai2ItemData(BaseData): return None return result.lastrowid - def get_charges(self, user_id: int) -> Optional[Row]: + async def get_charges(self, user_id: int) -> Optional[Row]: sql = charge.select(charge.c.user == user_id) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchall() - def put_user_print_detail( + async def put_user_print_detail( self, aime_id: int, serial_id: str, user_print_data: Dict ) -> Optional[int]: sql = insert(print_detail).values( @@ -540,7 +592,7 @@ class Mai2ItemData(BaseData): ) conflict = sql.on_duplicate_key_update(**user_print_data) - result = self.execute(conflict) + result = await self.execute(conflict) if result is None: self.logger.warning( @@ -548,3 +600,49 @@ class Mai2ItemData(BaseData): ) return None return result.lastrowid + + async def put_present(self, item_kind: int, item_id: int, version: int = None, user_id: int = None, start_date: datetime = None, end_date: datetime = None) -> Optional[int]: + sql = insert(present).values( + version = version, + user = user_id, + itemKind = item_kind, + itemId = item_id, + startDate = start_date, + endDate = end_date + ) + + conflict = sql.on_duplicate_key_update( + startDate = start_date, + endDate = end_date + ) + + result = await self.execute(conflict) + if result: + return result.lastrowid + + self.logger.error(f"Failed to add present item {item_id}!") + + async def get_presents_by_user(self, user_id: int = None) -> Optional[List[Row]]: + result = await self.execute(present.select(or_(present.c.user == user_id, present.c.user is None))) + if result: + return result.fetchall() + + async def get_presents_by_version(self, ver: int = None) -> Optional[List[Row]]: + result = await self.execute(present.select(or_(present.c.version == ver, present.c.version is None))) + if result: + return result.fetchall() + + async def get_presents_by_version_user(self, ver: int = None, user_id: int = None) -> Optional[List[Row]]: + result = await self.execute(present.select( + and_( + or_(present.c.user == user_id, present.c.user == None), + or_(present.c.version == ver, present.c.version == None) + ) + )) + if result: + return result.fetchall() + + async def get_present_by_id(self, present_id: int) -> Optional[Row]: + result = await self.execute(present.select(present.c.id == present_id)) + if result: + return result.fetchone() diff --git a/titles/mai2/schema/profile.py b/titles/mai2/schema/profile.py index 211440d..c191a1a 100644 --- a/titles/mai2/schema/profile.py +++ b/titles/mai2/schema/profile.py @@ -40,6 +40,8 @@ detail = Table( Column("charaLockSlot", JSON), Column("contentBit", BigInteger), Column("playCount", Integer), + Column("currentPlayCount", Integer), # new with buddies + Column("renameCredit", Integer), # new with buddies Column("mapStock", Integer), # new with fes+ Column("eventWatchedDate", String(25)), Column("lastGameId", String(25)), @@ -489,9 +491,32 @@ consec_logins = Table( mysql_charset="utf8mb4", ) +rival = Table( + "mai2_user_rival", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), + Column( + "rival", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), + Column("show", Boolean, nullable=False, server_default="0"), + UniqueConstraint("user", "rival", name="mai2_user_rival_uk"), + mysql_charset="utf8mb4", +) class Mai2ProfileData(BaseData): - def put_profile_detail( + async def get_all_profile_versions(self, user_id: int) -> Optional[List[Row]]: + result = await self.execute(detail.select(detail.c.user == user_id)) + if result: + return result.fetchall() + + async def put_profile_detail( self, user_id: int, version: int, detail_data: Dict, is_dx: bool = True ) -> Optional[Row]: detail_data["user"] = user_id @@ -504,7 +529,7 @@ class Mai2ProfileData(BaseData): conflict = sql.on_duplicate_key_update(**detail_data) - result = self.execute(conflict) + result = await self.execute(conflict) if result is None: self.logger.warning( f"put_profile: Failed to create profile! user_id {user_id} is_dx {is_dx}" @@ -512,7 +537,7 @@ class Mai2ProfileData(BaseData): return None return result.lastrowid - def get_profile_detail( + async def get_profile_detail( self, user_id: int, version: int, is_dx: bool = True ) -> Optional[Row]: if is_dx: @@ -531,12 +556,12 @@ class Mai2ProfileData(BaseData): .order_by(detail_old.c.version.desc()) ) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchone() - def put_profile_ghost( + async def put_profile_ghost( self, user_id: int, version: int, ghost_data: Dict ) -> Optional[int]: ghost_data["user"] = user_id @@ -545,25 +570,25 @@ class Mai2ProfileData(BaseData): sql = insert(ghost).values(**ghost_data) conflict = sql.on_duplicate_key_update(**ghost_data) - result = self.execute(conflict) + result = await self.execute(conflict) if result is None: self.logger.warning(f"put_profile_ghost: failed to update! {user_id}") return None return result.lastrowid - def get_profile_ghost(self, user_id: int, version: int) -> Optional[Row]: + async def get_profile_ghost(self, user_id: int, version: int) -> Optional[Row]: sql = ( select(ghost) .where(and_(ghost.c.user == user_id, ghost.c.version_int <= version)) .order_by(ghost.c.version.desc()) ) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchone() - def put_profile_extend( + async def put_profile_extend( self, user_id: int, version: int, extend_data: Dict ) -> Optional[int]: extend_data["user"] = user_id @@ -572,25 +597,25 @@ class Mai2ProfileData(BaseData): sql = insert(extend).values(**extend_data) conflict = sql.on_duplicate_key_update(**extend_data) - result = self.execute(conflict) + result = await self.execute(conflict) if result is None: self.logger.warning(f"put_profile_extend: failed to update! {user_id}") return None return result.lastrowid - def get_profile_extend(self, user_id: int, version: int) -> Optional[Row]: + async def get_profile_extend(self, user_id: int, version: int) -> Optional[Row]: sql = ( select(extend) .where(and_(extend.c.user == user_id, extend.c.version <= version)) .order_by(extend.c.version.desc()) ) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchone() - def put_profile_option( + async def put_profile_option( self, user_id: int, version: int, option_data: Dict, is_dx: bool = True ) -> Optional[int]: option_data["user"] = user_id @@ -602,7 +627,7 @@ class Mai2ProfileData(BaseData): sql = insert(option_old).values(**option_data) conflict = sql.on_duplicate_key_update(**option_data) - result = self.execute(conflict) + result = await self.execute(conflict) if result is None: self.logger.warning( f"put_profile_option: failed to update! {user_id} is_dx {is_dx}" @@ -610,7 +635,7 @@ class Mai2ProfileData(BaseData): return None return result.lastrowid - def get_profile_option( + async def get_profile_option( self, user_id: int, version: int, is_dx: bool = True ) -> Optional[Row]: if is_dx: @@ -628,12 +653,12 @@ class Mai2ProfileData(BaseData): .order_by(option_old.c.version.desc()) ) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchone() - def put_profile_rating( + async def put_profile_rating( self, user_id: int, version: int, rating_data: Dict ) -> Optional[int]: rating_data["user"] = user_id @@ -642,25 +667,25 @@ class Mai2ProfileData(BaseData): sql = insert(rating).values(**rating_data) conflict = sql.on_duplicate_key_update(**rating_data) - result = self.execute(conflict) + result = await self.execute(conflict) if result is None: self.logger.warning(f"put_profile_rating: failed to update! {user_id}") return None return result.lastrowid - def get_profile_rating(self, user_id: int, version: int) -> Optional[Row]: + async def get_profile_rating(self, user_id: int, version: int) -> Optional[Row]: sql = ( select(rating) .where(and_(rating.c.user == user_id, rating.c.version <= version)) .order_by(rating.c.version.desc()) ) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchone() - def put_profile_region(self, user_id: int, region_id: int) -> Optional[int]: + async def put_profile_region(self, user_id: int, region_id: int) -> Optional[int]: sql = insert(region).values( user=user_id, regionId=region_id, @@ -669,21 +694,21 @@ class Mai2ProfileData(BaseData): conflict = sql.on_duplicate_key_update(playCount=region.c.playCount + 1) - result = self.execute(conflict) + result = await self.execute(conflict) if result is None: self.logger.warning(f"put_region: failed to update! {user_id}") return None return result.lastrowid - def get_regions(self, user_id: int) -> Optional[List[Dict]]: + async def get_regions(self, user_id: int) -> Optional[List[Dict]]: sql = select(region).where(region.c.user == user_id) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchall() - def put_profile_activity(self, user_id: int, activity_data: Dict) -> Optional[int]: + async def put_profile_activity(self, user_id: int, activity_data: Dict) -> Optional[int]: if "id" in activity_data: activity_data["activityId"] = activity_data["id"] activity_data.pop("id") @@ -694,7 +719,7 @@ class Mai2ProfileData(BaseData): conflict = sql.on_duplicate_key_update(**activity_data) - result = self.execute(conflict) + result = await self.execute(conflict) if result is None: self.logger.warning( f"put_profile_activity: failed to update! user_id: {user_id}" @@ -702,7 +727,7 @@ class Mai2ProfileData(BaseData): return None return result.lastrowid - def get_profile_activity( + async def get_profile_activity( self, user_id: int, kind: int = None ) -> Optional[List[Row]]: sql = activity.select( @@ -712,12 +737,12 @@ class Mai2ProfileData(BaseData): ) ) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchall() - def put_web_option( + async def put_web_option( self, user_id: int, version: int, web_opts: Dict ) -> Optional[int]: web_opts["user"] = user_id @@ -726,29 +751,29 @@ class Mai2ProfileData(BaseData): conflict = sql.on_duplicate_key_update(**web_opts) - result = self.execute(conflict) + result = await self.execute(conflict) if result is None: self.logger.warning(f"put_web_option: failed to update! user_id: {user_id}") return None return result.lastrowid - def get_web_option(self, user_id: int, version: int) -> Optional[Row]: + async def get_web_option(self, user_id: int, version: int) -> Optional[Row]: sql = web_opt.select( and_(web_opt.c.user == user_id, web_opt.c.version == version) ) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchone() - def put_grade_status(self, user_id: int, grade_stat: Dict) -> Optional[int]: + async def put_grade_status(self, user_id: int, grade_stat: Dict) -> Optional[int]: grade_stat["user"] = user_id sql = insert(grade_status).values(**grade_stat) conflict = sql.on_duplicate_key_update(**grade_stat) - result = self.execute(conflict) + result = await self.execute(conflict) if result is None: self.logger.warning( f"put_grade_status: failed to update! user_id: {user_id}" @@ -756,40 +781,40 @@ class Mai2ProfileData(BaseData): return None return result.lastrowid - def get_grade_status(self, user_id: int) -> Optional[Row]: + async def get_grade_status(self, user_id: int) -> Optional[Row]: sql = grade_status.select(grade_status.c.user == user_id) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchone() - def put_boss_list(self, user_id: int, boss_stat: Dict) -> Optional[int]: + async def put_boss_list(self, user_id: int, boss_stat: Dict) -> Optional[int]: boss_stat["user"] = user_id sql = insert(boss).values(**boss_stat) conflict = sql.on_duplicate_key_update(**boss_stat) - result = self.execute(conflict) + result = await self.execute(conflict) if result is None: self.logger.warning(f"put_boss_list: failed to update! user_id: {user_id}") return None return result.lastrowid - def get_boss_list(self, user_id: int) -> Optional[Row]: + async def get_boss_list(self, user_id: int) -> Optional[Row]: sql = boss.select(boss.c.user == user_id) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchone() - def put_recent_rating(self, user_id: int, rr: Dict) -> Optional[int]: + async def put_recent_rating(self, user_id: int, rr: Dict) -> Optional[int]: sql = insert(recent_rating).values(user=user_id, userRecentRatingList=rr) conflict = sql.on_duplicate_key_update({"userRecentRatingList": rr}) - result = self.execute(conflict) + result = await self.execute(conflict) if result is None: self.logger.warning( f"put_recent_rating: failed to update! user_id: {user_id}" @@ -797,26 +822,26 @@ class Mai2ProfileData(BaseData): return None return result.lastrowid - def get_recent_rating(self, user_id: int) -> Optional[Row]: + async def get_recent_rating(self, user_id: int) -> Optional[Row]: sql = recent_rating.select(recent_rating.c.user == user_id) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchone() - def add_consec_login(self, user_id: int, version: int) -> None: + async def add_consec_login(self, user_id: int, version: int) -> None: sql = insert(consec_logins).values(user=user_id, version=version, logins=1) conflict = sql.on_duplicate_key_update(logins=consec_logins.c.logins + 1) - result = self.execute(conflict) + result = await self.execute(conflict) if result is None: self.logger.error( f"Failed to update consecutive login count for user {user_id} version {version}" ) - def get_consec_login(self, user_id: int, version: int) -> Optional[Row]: + async def get_consec_login(self, user_id: int, version: int) -> Optional[Row]: sql = select(consec_logins).where( and_( consec_logins.c.user == user_id, @@ -824,12 +849,12 @@ class Mai2ProfileData(BaseData): ) ) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchone() - def reset_consec_login(self, user_id: int, version: int) -> Optional[Row]: + async def reset_consec_login(self, user_id: int, version: int) -> Optional[Row]: sql = consec_logins.update( and_( consec_logins.c.user == user_id, @@ -837,7 +862,56 @@ class Mai2ProfileData(BaseData): ) ).values(logins=1) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchone() + + async def get_rivals(self, user_id: int) -> Optional[List[Row]]: + result = await self.execute(rival.select(rival.c.user == user_id)) + if result: + return result.fetchall() + + async def get_rivals_game(self, user_id: int) -> Optional[List[Row]]: + result = await self.execute(rival.select(and_(rival.c.user == user_id, rival.c.show == True)).limit(3)) + if result: + return result.fetchall() + + async def set_rival_shown(self, user_id: int, rival_id: int, is_shown: bool) -> None: + sql = rival.update(and_(rival.c.user == user_id, rival.c.rival == rival_id)).values( + show = is_shown + ) + + result = await self.execute(sql) + if not result: + self.logger.error(f"Failed to set rival {rival_id} shown status to {is_shown} for user {user_id}") + + async def add_rival(self, user_id: int, rival_id: int) -> Optional[int]: + sql = insert(rival).values( + user = user_id, + rival = rival_id + ) + + conflict = sql.on_duplicate_key_update(rival = rival_id) + + result = await self.execute(conflict) + if result: + return result.lastrowid + + self.logger.error(f"Failed to add music {rival_id} as favorite for user {user_id}!") + + async def remove_rival(self, user_id: int, rival_id: int) -> None: + result = await self.execute(rival.delete(and_(rival.c.user == user_id, rival.c.rival == rival_id))) + if not result: + self.logger.error(f"Failed to remove rival {rival_id} for user {user_id}!") + + async def update_name(self, user_id: int, new_name: str) -> bool: + sql = detail.update(detail.c.user == user_id).values( + userName=new_name + ) + result = await self.execute(sql) + + if result is None: + self.logger.warning(f"Failed to set user {user_id} name to {new_name}") + return False + return True diff --git a/titles/mai2/schema/score.py b/titles/mai2/schema/score.py index 181a895..d4ea5b9 100644 --- a/titles/mai2/schema/score.py +++ b/titles/mai2/schema/score.py @@ -145,11 +145,34 @@ playlog = Table( Column("isNewFree", Boolean), Column("extNum1", Integer), Column("extNum2", Integer), - Column("extNum4", Integer, server_default="0"), + Column("extNum4", Integer), + Column("extBool1", Boolean), # new with buddies Column("trialPlayAchievement", Integer), mysql_charset="utf8mb4", ) +# new with buddies +playlog_2p = Table( + "mai2_playlog_2p", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), + # TODO: ForeignKey to aime_user? + Column("userId1", Integer), + Column("userId2", Integer), + # TODO: ForeignKey to mai2_profile_detail? + Column("userName1", String(25)), + Column("userName2", String(25)), + Column("regionId", Integer), + Column("placeId", Integer), + Column("user2pPlaylogDetailList", JSON), + mysql_charset="utf8mb4", +) + course = Table( "mai2_score_course", metadata, @@ -273,7 +296,7 @@ best_score_old = Table( ) class Mai2ScoreData(BaseData): - def put_best_score(self, user_id: int, score_data: Dict, is_dx: bool = True) -> Optional[int]: + async def put_best_score(self, user_id: int, score_data: Dict, is_dx: bool = True) -> Optional[int]: score_data["user"] = user_id if is_dx: @@ -282,7 +305,7 @@ class Mai2ScoreData(BaseData): sql = insert(best_score_old).values(**score_data) conflict = sql.on_duplicate_key_update(**score_data) - result = self.execute(conflict) + result = await self.execute(conflict) if result is None: self.logger.error( f"put_best_score: Failed to insert best score! user_id {user_id} is_dx {is_dx}" @@ -291,28 +314,28 @@ class Mai2ScoreData(BaseData): return result.lastrowid @cached(2) - def get_best_scores(self, user_id: int, song_id: int = None, is_dx: bool = True) -> Optional[List[Row]]: + async def get_best_scores(self, user_id: int, song_id: int = None, is_dx: bool = True) -> Optional[List[Row]]: if is_dx: sql = best_score.select( and_( best_score.c.user == user_id, - (best_score.c.song_id == song_id) if song_id is not None else True, + (best_score.c.musicId == song_id) if song_id is not None else True, ) - ) + ).order_by(best_score.c.musicId).order_by(best_score.c.level) else: sql = best_score_old.select( and_( best_score_old.c.user == user_id, - (best_score_old.c.song_id == song_id) if song_id is not None else True, + (best_score_old.c.musicId == song_id) if song_id is not None else True, ) - ) + ).order_by(best_score.c.musicId).order_by(best_score.c.level) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchall() - def get_best_score( + async def get_best_score( self, user_id: int, song_id: int, chart_id: int ) -> Optional[Row]: sql = best_score.select( @@ -323,12 +346,12 @@ class Mai2ScoreData(BaseData): ) ) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchone() - def put_playlog(self, user_id: int, playlog_data: Dict, is_dx: bool = True) -> Optional[int]: + async def put_playlog(self, user_id: int, playlog_data: Dict, is_dx: bool = True) -> Optional[int]: playlog_data["user"] = user_id if is_dx: @@ -338,28 +361,60 @@ class Mai2ScoreData(BaseData): conflict = sql.on_duplicate_key_update(**playlog_data) - result = self.execute(conflict) + result = await self.execute(conflict) if result is None: self.logger.error(f"put_playlog: Failed to insert! user_id {user_id} is_dx {is_dx}") return None return result.lastrowid + + async def put_playlog_2p(self, user_id: int, playlog_2p_data: Dict) -> Optional[int]: + playlog_2p_data["user"] = user_id + sql = insert(playlog_2p).values(**playlog_2p_data) - def put_course(self, user_id: int, course_data: Dict) -> Optional[int]: + conflict = sql.on_duplicate_key_update(**playlog_2p_data) + + result = await self.execute(conflict) + if result is None: + self.logger.error(f"put_playlog_2p: Failed to insert! user_id {user_id}") + return None + return result.lastrowid + + async def put_course(self, user_id: int, course_data: Dict) -> Optional[int]: course_data["user"] = user_id sql = insert(course).values(**course_data) conflict = sql.on_duplicate_key_update(**course_data) - result = self.execute(conflict) + result = await self.execute(conflict) if result is None: self.logger.error(f"put_course: Failed to insert! user_id {user_id}") return None return result.lastrowid - def get_courses(self, user_id: int) -> Optional[List[Row]]: + async def get_courses(self, user_id: int) -> Optional[List[Row]]: sql = course.select(course.c.user == user_id) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchall() + + async def get_playlogs(self, user_id: int, idx: int = 0, limit: int = 0) -> Optional[List[Row]]: + sql = playlog.select(playlog.c.user == user_id) + + if limit: + sql = sql.limit(limit) + if idx: + sql = sql.offset(idx * limit) + + result = await self.execute(sql) + if result: + return result.fetchall() + + async def get_user_playlogs_count(self, aime_id: int) -> Optional[Row]: + sql = select(func.count()).where(playlog.c.user == aime_id) + result = await self.execute(sql) + if result is None: + self.logger.warning(f"aime_id {aime_id} has no playlog ") + return None + return result.scalar() diff --git a/titles/mai2/schema/static.py b/titles/mai2/schema/static.py index 0f5bfad..ddba0f8 100644 --- a/titles/mai2/schema/static.py +++ b/titles/mai2/schema/static.py @@ -7,6 +7,7 @@ from sqlalchemy.schema import ForeignKey from sqlalchemy.sql import func, select from sqlalchemy.engine import Row from sqlalchemy.dialects.mysql import insert +from datetime import datetime event = Table( "mai2_static_event", @@ -72,7 +73,7 @@ cards = Table( class Mai2StaticData(BaseData): - def put_game_event( + async def put_game_event( self, version: int, type: int, event_id: int, name: str ) -> Optional[int]: sql = insert(event).values( @@ -84,46 +85,46 @@ class Mai2StaticData(BaseData): conflict = sql.on_duplicate_key_update(eventId=event_id) - result = self.execute(conflict) + result = await self.execute(conflict) if result is None: self.logger.warning( f"put_game_event: Failed to insert event! event_id {event_id} type {type} name {name}" ) return result.lastrowid - def get_game_events(self, version: int) -> Optional[List[Row]]: + async def get_game_events(self, version: int) -> Optional[List[Row]]: sql = event.select(event.c.version == version) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchall() - def get_enabled_events(self, version: int) -> Optional[List[Row]]: + async def get_enabled_events(self, version: int) -> Optional[List[Row]]: sql = select(event).where( and_(event.c.version == version, event.c.enabled == True) ) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchall() - def toggle_game_event( + async def toggle_game_event( self, version: int, event_id: int, toggle: bool ) -> Optional[List]: sql = event.update( and_(event.c.version == version, event.c.eventId == event_id) ).values(enabled=int(toggle)) - result = self.execute(sql) + result = await self.execute(sql) if result is None: self.logger.warning( f"toggle_game_event: Failed to update event! event_id {event_id} toggle {toggle}" ) return result.last_updated_params() - def put_game_music( + async def put_game_music( self, version: int, song_id: int, @@ -159,13 +160,13 @@ class Mai2StaticData(BaseData): noteDesigner=note_designer, ) - result = self.execute(conflict) + result = await self.execute(conflict) if result is None: self.logger.warning(f"Failed to insert song {song_id} chart {chart_id}") return None return result.lastrowid - def put_game_ticket( + async def put_game_ticket( self, version: int, ticket_id: int, @@ -185,13 +186,13 @@ class Mai2StaticData(BaseData): conflict = sql.on_duplicate_key_update(price=ticket_price) - result = self.execute(conflict) + result = await self.execute(conflict) if result is None: self.logger.warning(f"Failed to insert charge {ticket_id} type {ticket_type}") return None return result.lastrowid - def get_enabled_tickets( + async def get_enabled_tickets( self, version: int, kind: int = None ) -> Optional[List[Row]]: if kind is not None: @@ -207,12 +208,12 @@ class Mai2StaticData(BaseData): and_(ticket.c.version == version, ticket.c.enabled == True) ) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchall() - def get_music_chart( + async def get_music_chart( self, version: int, song_id: int, chart_id: int ) -> Optional[List[Row]]: sql = select(music).where( @@ -223,28 +224,43 @@ class Mai2StaticData(BaseData): ) ) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchone() - def put_card(self, version: int, card_id: int, card_name: str, **card_data) -> int: + async def put_card(self, version: int, card_id: int, card_name: str, **card_data) -> int: sql = insert(cards).values( version=version, cardId=card_id, cardName=card_name, **card_data ) conflict = sql.on_duplicate_key_update(**card_data) - result = self.execute(conflict) + result = await self.execute(conflict) if result is None: self.logger.warning(f"Failed to insert card {card_id}") return None return result.lastrowid - def get_enabled_cards(self, version: int) -> Optional[List[Row]]: + async def get_enabled_cards(self, version: int) -> Optional[List[Row]]: sql = cards.select(and_(cards.c.version == version, cards.c.enabled == True)) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchall() + + async def get_event_by_id(self, table_id: int) -> Optional[Row]: + result = await self.execute(event.select(event.c.id == table_id)) + if result: + return result.fetchone() + + async def get_events_by_event_id(self, event_id: int) -> Optional[List[Row]]: + result = await self.execute(event.select(event.c.eventId == event_id)) + if result: + return result.fetchall() + + async def update_event_by_id(self, table_id: int, is_enable: bool, start_date: datetime) -> None: + result = await self.execute(event.update(event.c.id == table_id).values(enabled=is_enable, startDate = start_date)) + if not result: + self.logger.error(f"Failed to update event {table_id} - {is_enable} {start_date}") diff --git a/titles/mai2/templates/css/mai2_style.css b/titles/mai2/templates/css/mai2_style.css new file mode 100644 index 0000000..4aceab8 --- /dev/null +++ b/titles/mai2/templates/css/mai2_style.css @@ -0,0 +1,195 @@ +.mai2-header { + text-align: center; +} + +ul.mai2-navi { + list-style-type: none; + padding: 0; + overflow: hidden; + background-color: #333; + text-align: center; + display: inline-block; +} + +ul.mai2-navi li { + display: inline-block; +} + +ul.mai2-navi li a { + display: block; + color: white; + text-align: center; + padding: 14px 16px; + text-decoration: none; +} + +ul.mai2-navi li a:hover:not(.active) { + background-color: #111; +} + +ul.mai2-navi li a.active { + background-color: #4CAF50; +} + +ul.mai2-navi li.right { + float: right; +} + +@media screen and (max-width: 600px) { + + ul.mai2-navi li.right, + ul.mai2-navi li { + float: none; + display: block; + text-align: center; + } +} + +table { + border-collapse: collapse; + border-spacing: 0; + border-collapse: separate; + overflow: hidden; + background-color: #555555; + +} + +th, td { + text-align: left; + border: none; + +} + +th { + color: white; +} + +.table-rowdistinct tr:nth-child(even) { + background-color: #303030; +} + +.table-rowdistinct tr:nth-child(odd) { + background-color: #555555; +} + +caption { + text-align: center; + color: white; + font-size: 18px; + font-weight: bold; +} + +.table-large { + margin: 16px; +} + +.table-large th, +.table-large td { + padding: 8px; +} + +.table-small { + width: 100%; + margin: 4px; +} + +.table-small th, +.table-small td { + padding: 2px; +} + +.bg-card { + background-color: #555555; +} + +.card-hover { + transition: all 0.2s ease-in-out; +} + +.card-hover:hover { + transform: scale(1.02); +} + +.basic { + color: #28a745; + font-weight: bold; +} + +.hard { + color: #ffc107; + + font-weight: bold; +} + +.expert { + color: #dc3545; + font-weight: bold; +} + +.master { + color: #dd09e8; + font-weight: bold; +} + +.ultimate { + color: #000000; + font-weight: bold; +} + +.score { + color: #ffffff; + font-weight: bold; +} + +.rainbow { + background: linear-gradient(to right, red, yellow, lime, aqua, blue, fuchsia) 0 / 5em; + background-clip: text; + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + font-weight: bold; +} + +.platinum { + color: #FFFF00; + font-weight: bold; +} + +.gold { + color: #FFFF00; + font-weight: bold; +} + +.scrolling-text { + overflow: hidden; +} + +.scrolling-text p { + white-space: nowrap; + display: inline-block; + +} + +.scrolling-text h6 { + white-space: nowrap; + display: inline-block; + +} + +.scrolling-text h5 { + white-space: nowrap; + display: inline-block; + +} + +.scrolling { + animation: scroll 10s linear infinite; +} + +@keyframes scroll { + 0% { + transform: translateX(100%); + } + 100% { + transform: translateX(-100%); + } +} \ No newline at end of file diff --git a/titles/mai2/templates/events/mai2_event_edit.jinja b/titles/mai2/templates/events/mai2_event_edit.jinja new file mode 100644 index 0000000..df061f5 --- /dev/null +++ b/titles/mai2/templates/events/mai2_event_edit.jinja @@ -0,0 +1,16 @@ +{% extends "core/templates/index.jinja" %} +{% block content %} + +
+

Event {{ event.eventId }} for {{ version_list[event.version] }}: {{ event.name }}

+ +
+

+ +
+

+ +   + +
+{% endblock content %} diff --git a/titles/mai2/templates/events/mai2_events.jinja b/titles/mai2/templates/events/mai2_events.jinja new file mode 100644 index 0000000..a2e9325 --- /dev/null +++ b/titles/mai2/templates/events/mai2_events.jinja @@ -0,0 +1,156 @@ +{% extends "core/templates/index.jinja" %} +{% block content %} +

Events

+
+ +
+ + + + + + + + + + + + + + + {% if events is not defined or events|length == 0 %} + + + + {% endif %} +
Viewing all events
IDVersionEvent IDEvent TypeNameStart DateEnabledActions
No Events
+
+ +  + + +
+ +{% endblock content %} \ No newline at end of file diff --git a/titles/mai2/templates/mai2_header.jinja b/titles/mai2/templates/mai2_header.jinja new file mode 100644 index 0000000..f226fbe --- /dev/null +++ b/titles/mai2/templates/mai2_header.jinja @@ -0,0 +1,22 @@ +
+

maimai

+
    +
  • PROFILE
  • +
  • RECORD
  • + {% if sesh is defined and sesh is not none and "{:08b}".format(sesh.permissions)[4] == "1" %} +
  • EVENTS
  • + {% endif %} +
+
+ \ No newline at end of file diff --git a/titles/mai2/templates/mai2_index.jinja b/titles/mai2/templates/mai2_index.jinja new file mode 100644 index 0000000..6490fdc --- /dev/null +++ b/titles/mai2/templates/mai2_index.jinja @@ -0,0 +1,134 @@ +{% extends "core/templates/index.jinja" %} +{% block content %} + +
+ {% include 'titles/mai2/templates/mai2_header.jinja' %} + {% if profile is defined and profile is not none and profile|length > 0 %} +
+
+
+ + + + + + + + + + + + + + + + + + + + + + +
OVERVIEW
{{ profile.userName }} + +
version: + + {% if versions | length > 1 %} +

You have {{ versions | length }} versions.

+ {% endif %} +
Rating: + + {{ profile.playerRating }} + + + (highest: {{ profile.highestRating }}) + +
Play Counts:{{ profile.playCount }}
Last Play Date:{{ profile.lastPlayDate }}
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
SCORE
Total Delux Score:{{ profile.totalDeluxscore }}
Total Basic Delux Score:{{ profile.totalBasicDeluxscore }}
Total Advanced Delux Score:{{ profile.totalAdvancedDeluxscore }}
Total Expert Delux Score:{{ profile.totalExpertDeluxscore }}
Total Master Delux Score:{{ profile.totalMasterDeluxscore }}
Total ReMaster Delux Score:{{ profile.totalReMasterDeluxscore }}
+
+
+
+ {% if error is defined %} + {% include "core/templates/widgets/err_banner.jinja" %} + {% endif %} + {% elif sesh is defined and sesh is not none and sesh.user_id > 0 %} + No profile information found for this account. + {% else %} + Login to view profile information. + {% endif %} +
+ + +{% endblock content %} \ No newline at end of file diff --git a/titles/mai2/templates/mai2_playlog.jinja b/titles/mai2/templates/mai2_playlog.jinja new file mode 100644 index 0000000..3e1d5fd --- /dev/null +++ b/titles/mai2/templates/mai2_playlog.jinja @@ -0,0 +1,225 @@ +{% extends "core/templates/index.jinja" %} +{% block content %} + +
+ {% include 'titles/mai2/templates/mai2_header.jinja' %} + {% if playlog is defined and playlog is not none %} +
+

Playlog counts: {{ playlog_count }}

+ {% set rankName = ['D', 'C', 'B', 'BB', 'BBB', 'A', 'AA', 'AAA', 'S', 'S+', 'SS', 'SS+', 'SSS', 'SSS+'] %} + {% set difficultyName = ['basic', 'hard', 'expert', 'master', 'ultimate'] %} + {% for record in playlog %} +
+
+
+
+
{{ record.title }}
+
+
{{ record.artist }}
+
+
+
{{ record.raw.userPlayDate }}
+
TRACK {{ record.raw.trackNo }}
+
+
+
+
+

{{ record.raw.deluxscore }}

+

{{ rankName[record.raw.rank] }}

+
+ {{ difficultyName[record.raw.level] }}  {{ record.difficulty }} +
+
+
+ + + + + + + + + + + + + + + + + + + + + +
CRITICAL PERFECT + Tap: {{ record.raw.tapCriticalPerfect }}
+ Hold: {{ record.raw.holdCriticalPerfect }}
+ Slide: {{ record.raw.slideCriticalPerfect }}
+ Touch: {{ record.raw.touchCriticalPerfect }}
+ Break: {{ record.raw.breakCriticalPerfect }} +
PERFECT + Tap: {{ record.raw.tapPerfect }}
+ Hold: {{ record.raw.holdPerfect }}
+ Slide: {{ record.raw.slidePerfect }}
+ Touch: {{ record.raw.touchPerfect }}
+ Break: {{ record.raw.breakPerfect }} +
GREAT + Tap: {{ record.raw.tapGreat }}
+ Hold: {{ record.raw.holdGreat }}
+ Slide: {{ record.raw.slideGreat }}
+ Touch: {{ record.raw.touchGreat }}
+ Break: {{ record.raw.breakGreat }} +
GOOD + Tap: {{ record.raw.tapGood }}
+ Hold: {{ record.raw.holdGood }}
+ Slide: {{ record.raw.slideGood }}
+ Touch: {{ record.raw.touchGood }}
+ Break: {{ record.raw.breakGood }} +
MISS + Tap: {{ record.raw.tapMiss }}
+ Hold: {{ record.raw.holdMiss }}
+ Slide: {{ record.raw.slideMiss }}
+ Touch: {{ record.raw.touchMiss }}
+ Break: {{ record.raw.breakMiss }} +
+
+
+ {%if record.raw.comboStatus == 1 %} +
FULL COMBO
+ {% endif %} + {%if record.raw.comboStatus == 2 %} +
FULL COMBO +
+ {% endif %} + {%if record.raw.comboStatus == 3 %} +
ALL PERFECT
+ {% endif %} + {%if record.raw.comboStatus == 4 %} +
ALL PERFECT +
+ {% endif %} + {%if record.raw.syncStatus == 1 %} +
FULL SYNC
+ {% endif %} + {%if record.raw.syncStatus == 2 %} +
FULL SYNC +
+ {% endif %} + {%if record.raw.syncStatus == 3 %} +
FULL SYNC DX
+ {% endif %} + {%if record.raw.syncStatus == 4 %} +
FULL SYNC DX +
+ {% endif %} + {%if record.raw.isAchieveNewRecord == 1 or record.raw.isDeluxscoreNewRecord == 1 %} +
NEW RECORD
+ {% endif %} +
+
+
+
+ {% endfor %} +
+ {% set playlog_pages = playlog_count // 20 + 1 %} + {% elif sesh is defined and sesh is not none and sesh.user_id > 0 %} + No Playlog information found for this account. + {% else %} + Login to view profile information. + {% endif %} +
+ + + +{% endblock content %} \ No newline at end of file diff --git a/titles/mai2/universe.py b/titles/mai2/universe.py index d25a295..8ed0d88 100644 --- a/titles/mai2/universe.py +++ b/titles/mai2/universe.py @@ -1,8 +1,6 @@ from typing import Any, List, Dict from random import randint from datetime import datetime, timedelta -import pytz -import json from core.config import CoreConfig from titles.mai2.splashplus import Mai2SplashPlus @@ -14,207 +12,3 @@ class Mai2Universe(Mai2SplashPlus): def __init__(self, cfg: CoreConfig, game_cfg: Mai2Config) -> None: super().__init__(cfg, game_cfg) self.version = Mai2Constants.VER_MAIMAI_DX_UNIVERSE - - def handle_cm_get_user_preview_api_request(self, data: Dict) -> Dict: - p = self.data.profile.get_profile_detail(data["userId"], self.version) - if p is None: - return {} - - return { - "userName": p["userName"], - "rating": p["playerRating"], - # hardcode lastDataVersion for CardMaker - "lastDataVersion": "1.20.00", - # checks if the user is still logged in - "isLogin": False, - "isExistSellingCard": True, - } - - def handle_cm_get_user_data_api_request(self, data: Dict) -> Dict: - # user already exists, because the preview checks that already - p = self.data.profile.get_profile_detail(data["userId"], self.version) - - cards = self.data.card.get_user_cards(data["userId"]) - if cards is None or len(cards) == 0: - # This should never happen - self.logger.error( - f"handle_get_user_data_api_request: Internal error - No cards found for user id {data['userId']}" - ) - return {} - - # get the dict representation of the row so we can modify values - user_data = p._asdict() - - # remove the values the game doesn't want - user_data.pop("id") - user_data.pop("user") - user_data.pop("version") - - return {"userId": data["userId"], "userData": user_data} - - def handle_cm_login_api_request(self, data: Dict) -> Dict: - return {"returnCode": 1} - - def handle_cm_logout_api_request(self, data: Dict) -> Dict: - return {"returnCode": 1} - - def handle_cm_get_selling_card_api_request(self, data: Dict) -> Dict: - selling_cards = self.data.static.get_enabled_cards(self.version) - if selling_cards is None: - return {"length": 0, "sellingCardList": []} - - selling_card_list = [] - for card in selling_cards: - tmp = card._asdict() - tmp.pop("id") - tmp.pop("version") - tmp.pop("cardName") - tmp.pop("enabled") - - tmp["startDate"] = datetime.strftime( - tmp["startDate"], Mai2Constants.DATE_TIME_FORMAT - ) - tmp["endDate"] = datetime.strftime( - tmp["endDate"], Mai2Constants.DATE_TIME_FORMAT - ) - tmp["noticeStartDate"] = datetime.strftime( - tmp["noticeStartDate"], Mai2Constants.DATE_TIME_FORMAT - ) - tmp["noticeEndDate"] = datetime.strftime( - tmp["noticeEndDate"], Mai2Constants.DATE_TIME_FORMAT - ) - - selling_card_list.append(tmp) - - return {"length": len(selling_card_list), "sellingCardList": selling_card_list} - - def handle_cm_get_user_card_api_request(self, data: Dict) -> Dict: - user_cards = self.data.item.get_cards(data["userId"]) - if user_cards is None: - return {"returnCode": 1, "length": 0, "nextIndex": 0, "userCardList": []} - - max_ct = data["maxCount"] - next_idx = data["nextIndex"] - start_idx = next_idx - end_idx = max_ct + start_idx - - if len(user_cards[start_idx:]) > max_ct: - next_idx += max_ct - else: - next_idx = 0 - - card_list = [] - for card in user_cards: - tmp = card._asdict() - tmp.pop("id") - tmp.pop("user") - - tmp["startDate"] = datetime.strftime( - tmp["startDate"], Mai2Constants.DATE_TIME_FORMAT - ) - tmp["endDate"] = datetime.strftime( - tmp["endDate"], Mai2Constants.DATE_TIME_FORMAT - ) - card_list.append(tmp) - - return { - "returnCode": 1, - "length": len(card_list[start_idx:end_idx]), - "nextIndex": next_idx, - "userCardList": card_list[start_idx:end_idx], - } - - def handle_cm_get_user_item_api_request(self, data: Dict) -> Dict: - super().handle_get_user_item_api_request(data) - - def handle_cm_get_user_character_api_request(self, data: Dict) -> Dict: - characters = self.data.item.get_characters(data["userId"]) - - chara_list = [] - for chara in characters: - chara_list.append( - { - "characterId": chara["characterId"], - # no clue why those values are even needed - "point": 0, - "count": 0, - "level": chara["level"], - "nextAwake": 0, - "nextAwakePercent": 0, - "favorite": False, - "awakening": chara["awakening"], - "useCount": chara["useCount"], - } - ) - - return { - "returnCode": 1, - "length": len(chara_list), - "userCharacterList": chara_list, - } - - def handle_cm_get_user_card_print_error_api_request(self, data: Dict) -> Dict: - return {"length": 0, "userPrintDetailList": []} - - def handle_cm_upsert_user_print_api_request(self, data: Dict) -> Dict: - user_id = data["userId"] - upsert = data["userPrintDetail"] - - # set a random card serial number - serial_id = "".join([str(randint(0, 9)) for _ in range(20)]) - - # calculate start and end date of the card - start_date = datetime.utcnow() - end_date = datetime.utcnow() + timedelta(days=15) - - user_card = upsert["userCard"] - self.data.item.put_card( - user_id, - user_card["cardId"], - user_card["cardTypeId"], - user_card["charaId"], - user_card["mapId"], - # add the correct start date and also the end date in 15 days - start_date, - end_date, - ) - - # get the profile extend to save the new bought card - extend = self.data.profile.get_profile_extend(user_id, self.version) - if extend: - extend = extend._asdict() - # parse the selectedCardList - # 6 = Freedom Pass, 4 = Gold Pass (cardTypeId) - selected_cards: List = extend["selectedCardList"] - - # if no pass is already added, add the corresponding pass - if not user_card["cardTypeId"] in selected_cards: - selected_cards.insert(0, user_card["cardTypeId"]) - - extend["selectedCardList"] = selected_cards - self.data.profile.put_profile_extend(user_id, self.version, extend) - - # properly format userPrintDetail for the database - upsert.pop("userCard") - upsert.pop("serialId") - upsert["printDate"] = datetime.strptime(upsert["printDate"], "%Y-%m-%d") - - self.data.item.put_user_print_detail(user_id, serial_id, upsert) - - return { - "returnCode": 1, - "orderId": 0, - "serialId": serial_id, - "startDate": datetime.strftime(start_date, Mai2Constants.DATE_TIME_FORMAT), - "endDate": datetime.strftime(end_date, Mai2Constants.DATE_TIME_FORMAT), - } - - def handle_cm_upsert_user_printlog_api_request(self, data: Dict) -> Dict: - return { - "returnCode": 1, - "orderId": 0, - "serialId": data["userPrintlog"]["serialId"], - } - - def handle_cm_upsert_buy_card_api_request(self, data: Dict) -> Dict: - return {"returnCode": 1} diff --git a/titles/mai2/universeplus.py b/titles/mai2/universeplus.py index e45c719..909300e 100644 --- a/titles/mai2/universeplus.py +++ b/titles/mai2/universeplus.py @@ -11,8 +11,8 @@ class Mai2UniversePlus(Mai2Universe): super().__init__(cfg, game_cfg) self.version = Mai2Constants.VER_MAIMAI_DX_UNIVERSE_PLUS - def handle_cm_get_user_preview_api_request(self, data: Dict) -> Dict: - user_data = super().handle_cm_get_user_preview_api_request(data) + async def handle_cm_get_user_preview_api_request(self, data: Dict) -> Dict: + user_data = await super().handle_cm_get_user_preview_api_request(data) # hardcode lastDataVersion for CardMaker 1.35 user_data["lastDataVersion"] = "1.25.00" diff --git a/titles/ongeki/__init__.py b/titles/ongeki/__init__.py index f12343d..587a154 100644 --- a/titles/ongeki/__init__.py +++ b/titles/ongeki/__init__.py @@ -9,4 +9,3 @@ database = OngekiData reader = OngekiReader frontend = OngekiFrontend game_codes = [OngekiConstants.GAME_CODE] -current_schema_version = 6 diff --git a/titles/ongeki/base.py b/titles/ongeki/base.py index 596fb22..e454dc3 100644 --- a/titles/ongeki/base.py +++ b/titles/ongeki/base.py @@ -103,7 +103,7 @@ class OngekiBase: self.game = OngekiConstants.GAME_CODE self.version = OngekiConstants.VER_ONGEKI - def handle_get_game_setting_api_request(self, data: Dict) -> Dict: + async def handle_get_game_setting_api_request(self, data: Dict) -> Dict: # if reboot start/end time is not defined use the default behavior of being a few hours ago if self.core_cfg.title.reboot_start_time == "" or self.core_cfg.title.reboot_end_time == "": reboot_start = datetime.strftime( @@ -148,7 +148,7 @@ class OngekiBase: "isAou": "true", } - def handle_get_game_idlist_api_request(self, data: Dict) -> Dict: + async def handle_get_game_idlist_api_request(self, data: Dict) -> Dict: """ Gets lists of song IDs, either disabled songs or recomended songs depending on type? """ @@ -156,8 +156,8 @@ class OngekiBase: # id - int return {"type": data["type"], "length": 0, "gameIdlistList": []} - def handle_get_game_ranking_api_request(self, data: Dict) -> Dict: - game_ranking_list = self.data.static.get_ranking_list(self.version) + async def handle_get_game_ranking_api_request(self, data: Dict) -> Dict: + game_ranking_list = await self.data.static.get_ranking_list(self.version) ranking_list = [] for music in game_ranking_list: @@ -171,14 +171,14 @@ class OngekiBase: "gameRankingList": ranking_list, } - def handle_get_game_point_api_request(self, data: Dict) -> Dict: - get_game_point = self.data.static.get_static_game_point() + async def handle_get_game_point_api_request(self, data: Dict) -> Dict: + get_game_point = await self.data.static.get_static_game_point() game_point = [] if not get_game_point: self.logger.info(f"GP table is empty, inserting defaults") - self.data.static.put_static_game_point_defaults() - get_game_point = self.data.static.get_static_game_point() + await self.data.static.put_static_game_point_defaults() + get_game_point = await self.data.static.get_static_game_point() for gp in get_game_point: tmp = gp._asdict() game_point.append(tmp) @@ -194,17 +194,17 @@ class OngekiBase: "gamePointList": game_point, } - def handle_game_login_api_request(self, data: Dict) -> Dict: + async def handle_game_login_api_request(self, data: Dict) -> Dict: return {"returnCode": 1, "apiName": "gameLogin"} - def handle_game_logout_api_request(self, data: Dict) -> Dict: + async def handle_game_logout_api_request(self, data: Dict) -> Dict: return {"returnCode": 1, "apiName": "gameLogout"} - def handle_extend_lock_time_api_request(self, data: Dict) -> Dict: + async def handle_extend_lock_time_api_request(self, data: Dict) -> Dict: return {"returnCode": 1, "apiName": "ExtendLockTimeApi"} - def handle_get_game_reward_api_request(self, data: Dict) -> Dict: - get_game_rewards = self.data.static.get_reward_list(self.version) + async def handle_get_game_reward_api_request(self, data: Dict) -> Dict: + get_game_rewards = await self.data.static.get_reward_list(self.version) reward_list = [] for reward in get_game_rewards: @@ -221,8 +221,8 @@ class OngekiBase: "gameRewardList": reward_list, } - def handle_get_game_present_api_request(self, data: Dict) -> Dict: - get_present = self.data.static.get_present_list(self.version) + async def handle_get_game_present_api_request(self, data: Dict) -> Dict: + get_present = await self.data.static.get_present_list(self.version) present_list = [] for present in get_present: @@ -238,14 +238,14 @@ class OngekiBase: "gamePresentList": present_list, } - def handle_get_game_message_api_request(self, data: Dict) -> Dict: + async def handle_get_game_message_api_request(self, data: Dict) -> Dict: return {"length": 0, "gameMessageList": []} - def handle_get_game_sale_api_request(self, data: Dict) -> Dict: + async def handle_get_game_sale_api_request(self, data: Dict) -> Dict: return {"length": 0, "gameSaleList": []} - def handle_get_game_tech_music_api_request(self, data: Dict) -> Dict: - music_list = self.data.static.get_tech_music(self.version) + async def handle_get_game_tech_music_api_request(self, data: Dict) -> Dict: + music_list = await self.data.static.get_tech_music(self.version) prep_music_list = [] for music in music_list: @@ -262,41 +262,43 @@ class OngekiBase: "gameTechMusicList": prep_music_list, } - def handle_upsert_client_setting_api_request(self, data: Dict) -> Dict: + async def handle_upsert_client_setting_api_request(self, data: Dict) -> Dict: if self.core_cfg.server.is_develop: return {"returnCode": 1, "apiName": "UpsertClientSettingApi"} client_id = data["clientId"] client_setting_data = data["clientSetting"] - cab = self.data.arcade.get_machine(client_id) + cab = await self.data.arcade.get_machine(client_id) if cab is not None: - self.data.static.put_client_setting_data(cab['id'], client_setting_data) + await self.data.static.put_client_setting_data(cab['id'], client_setting_data) return {"returnCode": 1, "apiName": "UpsertClientSettingApi"} - def handle_upsert_client_testmode_api_request(self, data: Dict) -> Dict: + async def handle_upsert_client_testmode_api_request(self, data: Dict) -> Dict: if self.core_cfg.server.is_develop: return {"returnCode": 1, "apiName": "UpsertClientTestmodeApi"} region_id = data["regionId"] client_testmode_data = data["clientTestmode"] - self.data.static.put_client_testmode_data(region_id, client_testmode_data) + await self.data.static.put_client_testmode_data(region_id, client_testmode_data) return {"returnCode": 1, "apiName": "UpsertClientTestmodeApi"} - def handle_upsert_client_bookkeeping_api_request(self, data: Dict) -> Dict: + async def handle_upsert_client_bookkeeping_api_request(self, data: Dict) -> Dict: return {"returnCode": 1, "apiName": "upsertClientBookkeeping"} - def handle_upsert_client_develop_api_request(self, data: Dict) -> Dict: + async def handle_upsert_client_develop_api_request(self, data: Dict) -> Dict: return {"returnCode": 1, "apiName": "upsertClientDevelop"} - def handle_upsert_client_error_api_request(self, data: Dict) -> Dict: + async def handle_upsert_client_error_api_request(self, data: Dict) -> Dict: return {"returnCode": 1, "apiName": "upsertClientError"} - def handle_upsert_user_gplog_api_request(self, data: Dict) -> Dict: + async def handle_upsert_user_gplog_api_request(self, data: Dict) -> Dict: user = data["userId"] - if user >= 200000000000000: # Account for guest play + + # If playing as guest, the user ID is or(0x1000000000001, (placeId & 65535) << 32) + if user & 0x1000000000001 == 0x1000000000001: user = None - self.data.log.put_gp_log( + await self.data.log.put_gp_log( user, data["usedCredit"], data["placeName"], @@ -309,11 +311,11 @@ class OngekiBase: return {"returnCode": 1, "apiName": "UpsertUserGplogApi"} - def handle_extend_lock_time_api_request(self, data: Dict) -> Dict: + async def handle_extend_lock_time_api_request(self, data: Dict) -> Dict: return {"returnCode": 1, "apiName": "ExtendLockTimeApi"} - def handle_get_game_event_api_request(self, data: Dict) -> Dict: - evts = self.data.static.get_enabled_events(self.version) + async def handle_get_game_event_api_request(self, data: Dict) -> Dict: + evts = await self.data.static.get_enabled_events(self.version) if evts is None: return { @@ -342,7 +344,7 @@ class OngekiBase: "gameEventList": evt_list, } - def handle_get_game_id_list_api_request(self, data: Dict) -> Dict: + async def handle_get_game_id_list_api_request(self, data: Dict) -> Dict: game_idlist: List[str, Any] = [] # 1 to 230 & 8000 to 8050 if data["type"] == 1: @@ -362,11 +364,11 @@ class OngekiBase: "gameIdlistList": game_idlist, } - def handle_get_user_region_api_request(self, data: Dict) -> Dict: + async def handle_get_user_region_api_request(self, data: Dict) -> Dict: return {"userId": data["userId"], "length": 0, "userRegionList": []} - def handle_get_user_preview_api_request(self, data: Dict) -> Dict: - profile = self.data.profile.get_profile_preview(data["userId"], self.version) + async def handle_get_user_preview_api_request(self, data: Dict) -> Dict: + profile = await self.data.profile.get_profile_preview(data["userId"], self.version) if profile is None: return { @@ -417,18 +419,19 @@ class OngekiBase: "isWarningConfirmed": True, } - def handle_get_user_tech_count_api_request(self, data: Dict) -> Dict: + async def handle_get_user_tech_count_api_request(self, data: Dict) -> Dict: """ Gets the number of AB and ABPs a player has per-difficulty (7, 7+, 8, etc) The game sends this in upsert so we don't have to calculate it all out thankfully """ - utcl = self.data.score.get_tech_count(data["userId"]) + utcl = await self.data.score.get_tech_count(data["userId"]) userTechCountList = [] for tc in utcl: - tc.pop("id") - tc.pop("user") - userTechCountList.append(tc) + tmp = tc._asdict() + tmp.pop("id") + tmp.pop("user") + userTechCountList.append(tmp) return { "userId": data["userId"], @@ -436,8 +439,8 @@ class OngekiBase: "userTechCountList": userTechCountList, } - def handle_get_user_tech_event_api_request(self, data: Dict) -> Dict: - user_tech_event_list = self.data.item.get_tech_event(self.version, data["userId"]) + async def handle_get_user_tech_event_api_request(self, data: Dict) -> Dict: + user_tech_event_list = await self.data.item.get_tech_event(self.version, data["userId"]) if user_tech_event_list is None: return {} @@ -455,8 +458,8 @@ class OngekiBase: "userTechEventList": tech_evt, } - def handle_get_user_tech_event_ranking_api_request(self, data: Dict) -> Dict: - user_tech_event_ranks = self.data.item.get_tech_event_ranking(self.version, data["userId"]) + async def handle_get_user_tech_event_ranking_api_request(self, data: Dict) -> Dict: + user_tech_event_ranks = await self.data.item.get_tech_event_ranking(self.version, data["userId"]) if user_tech_event_ranks is None: return { "userId": data["userId"], @@ -481,8 +484,8 @@ class OngekiBase: "userTechEventRankingList": evt_ranking, } - def handle_get_user_kop_api_request(self, data: Dict) -> Dict: - kop_list = self.data.profile.get_kop(data["userId"]) + async def handle_get_user_kop_api_request(self, data: Dict) -> Dict: + kop_list = await self.data.profile.get_kop(data["userId"]) if kop_list is None: return {} @@ -496,8 +499,8 @@ class OngekiBase: "userKopList": kop_list, } - def handle_get_user_music_api_request(self, data: Dict) -> Dict: - song_list = self.util_generate_music_list(data["userId"]) + async def handle_get_user_music_api_request(self, data: Dict) -> Dict: + song_list = await self.util_generate_music_list(data["userId"]) max_ct = data["maxCount"] next_idx = data["nextIndex"] start_idx = next_idx @@ -516,9 +519,9 @@ class OngekiBase: "userMusicList": song_list[start_idx:end_idx], } - def handle_get_user_item_api_request(self, data: Dict) -> Dict: + async def handle_get_user_item_api_request(self, data: Dict) -> Dict: kind = data["nextIndex"] / 10000000000 - p = self.data.item.get_items(data["userId"], kind) + p = await self.data.item.get_items(data["userId"], kind) if p is None: return { @@ -552,8 +555,8 @@ class OngekiBase: "userItemList": items, } - def handle_get_user_option_api_request(self, data: Dict) -> Dict: - o = self.data.profile.get_profile_options(data["userId"]) + async def handle_get_user_option_api_request(self, data: Dict) -> Dict: + o = await self.data.profile.get_profile_options(data["userId"]) if o is None: return {} @@ -566,12 +569,12 @@ class OngekiBase: return {"userId": data["userId"], "userOption": user_opts} - def handle_get_user_data_api_request(self, data: Dict) -> Dict: - p = self.data.profile.get_profile_data(data["userId"], self.version) + async def handle_get_user_data_api_request(self, data: Dict) -> Dict: + p = await self.data.profile.get_profile_data(data["userId"], self.version) if p is None: return {} - cards = self.data.card.get_user_cards(data["userId"]) + cards = await self.data.card.get_user_cards(data["userId"]) if cards is None or len(cards) == 0: # This should never happen self.logger.error( @@ -594,8 +597,8 @@ class OngekiBase: return {"userId": data["userId"], "userData": user_data} - def handle_get_user_event_ranking_api_request(self, data: Dict) -> Dict: - user_event_ranking_list = self.data.item.get_ranking_event_ranks(self.version, data["userId"]) + async def handle_get_user_event_ranking_api_request(self, data: Dict) -> Dict: + user_event_ranking_list = await self.data.item.get_ranking_event_ranks(self.version, data["userId"]) if user_event_ranking_list is None: return {} @@ -617,8 +620,8 @@ class OngekiBase: "userEventRankingList": prep_event_ranking, } - def handle_get_user_login_bonus_api_request(self, data: Dict) -> Dict: - user_login_bonus_list = self.data.item.get_login_bonuses(data["userId"]) + async def handle_get_user_login_bonus_api_request(self, data: Dict) -> Dict: + user_login_bonus_list = await self.data.item.get_login_bonuses(data["userId"]) if user_login_bonus_list is None: return {} @@ -635,8 +638,8 @@ class OngekiBase: "userLoginBonusList": login_bonuses, } - def handle_get_user_bp_base_request(self, data: Dict) -> Dict: - p = self.data.profile.get_profile( + async def handle_get_user_bp_base_request(self, data: Dict) -> Dict: + p = await self.data.profile.get_profile( self.game, self.version, user_id=data["userId"] ) if p is None: @@ -648,8 +651,8 @@ class OngekiBase: "userBpBaseList": profile["userBpBaseList"], } - def handle_get_user_recent_rating_api_request(self, data: Dict) -> Dict: - recent_rating = self.data.profile.get_profile_recent_rating(data["userId"]) + async def handle_get_user_recent_rating_api_request(self, data: Dict) -> Dict: + recent_rating = await self.data.profile.get_profile_recent_rating(data["userId"]) if recent_rating is None: return { "userId": data["userId"], @@ -665,8 +668,8 @@ class OngekiBase: "userRecentRatingList": userRecentRatingList, } - def handle_get_user_activity_api_request(self, data: Dict) -> Dict: - activity = self.data.profile.get_profile_activity(data["userId"], data["kind"]) + async def handle_get_user_activity_api_request(self, data: Dict) -> Dict: + activity = await self.data.profile.get_profile_activity(data["userId"], data["kind"]) if activity is None: return {} @@ -692,8 +695,8 @@ class OngekiBase: "userActivityList": user_activity, } - def handle_get_user_story_api_request(self, data: Dict) -> Dict: - user_stories = self.data.item.get_stories(data["userId"]) + async def handle_get_user_story_api_request(self, data: Dict) -> Dict: + user_stories = await self.data.item.get_stories(data["userId"]) if user_stories is None: return {} @@ -710,8 +713,8 @@ class OngekiBase: "userStoryList": story_list, } - def handle_get_user_chapter_api_request(self, data: Dict) -> Dict: - user_chapters = self.data.item.get_chapters(data["userId"]) + async def handle_get_user_chapter_api_request(self, data: Dict) -> Dict: + user_chapters = await self.data.item.get_chapters(data["userId"]) if user_chapters is None: return {} @@ -728,15 +731,15 @@ class OngekiBase: "userChapterList": chapter_list, } - def handle_get_user_training_room_by_key_api_request(self, data: Dict) -> Dict: + async def handle_get_user_training_room_by_key_api_request(self, data: Dict) -> Dict: return { "userId": data["userId"], "length": 0, "userTrainingRoomList": [], } - def handle_get_user_character_api_request(self, data: Dict) -> Dict: - user_characters = self.data.item.get_characters(data["userId"]) + async def handle_get_user_character_api_request(self, data: Dict) -> Dict: + user_characters = await self.data.item.get_characters(data["userId"]) if user_characters is None: return {} @@ -753,8 +756,8 @@ class OngekiBase: "userCharacterList": character_list, } - def handle_get_user_card_api_request(self, data: Dict) -> Dict: - user_cards = self.data.item.get_cards(data["userId"]) + async def handle_get_user_card_api_request(self, data: Dict) -> Dict: + user_cards = await self.data.item.get_cards(data["userId"]) if user_cards is None: return {} @@ -771,9 +774,9 @@ class OngekiBase: "userCardList": card_list, } - def handle_get_user_deck_by_key_api_request(self, data: Dict) -> Dict: + async def handle_get_user_deck_by_key_api_request(self, data: Dict) -> Dict: # Auth key doesn't matter, it just wants all the decks - decks = self.data.item.get_decks(data["userId"]) + decks = await self.data.item.get_decks(data["userId"]) if decks is None: return {} @@ -790,8 +793,8 @@ class OngekiBase: "userDeckList": deck_list, } - def handle_get_user_trade_item_api_request(self, data: Dict) -> Dict: - user_trade_items = self.data.item.get_trade_items(data["userId"]) + async def handle_get_user_trade_item_api_request(self, data: Dict) -> Dict: + user_trade_items = await self.data.item.get_trade_items(data["userId"]) if user_trade_items is None: return {} @@ -808,8 +811,8 @@ class OngekiBase: "userTradeItemList": trade_item_list, } - def handle_get_user_scenario_api_request(self, data: Dict) -> Dict: - user_scenerio = self.data.item.get_scenerios(data["userId"]) + async def handle_get_user_scenario_api_request(self, data: Dict) -> Dict: + user_scenerio = await self.data.item.get_scenerios(data["userId"]) if user_scenerio is None: return {} @@ -826,8 +829,8 @@ class OngekiBase: "userScenarioList": scenerio_list, } - def handle_get_user_ratinglog_api_request(self, data: Dict) -> Dict: - rating_log = self.data.profile.get_profile_rating_log(data["userId"]) + async def handle_get_user_ratinglog_api_request(self, data: Dict) -> Dict: + rating_log = await self.data.profile.get_profile_rating_log(data["userId"]) if rating_log is None: return {} @@ -844,8 +847,8 @@ class OngekiBase: "userRatinglogList": userRatinglogList, } - def handle_get_user_mission_point_api_request(self, data: Dict) -> Dict: - user_mission_point_list = self.data.item.get_mission_points(self.version, data["userId"]) + async def handle_get_user_mission_point_api_request(self, data: Dict) -> Dict: + user_mission_point_list = await self.data.item.get_mission_points(self.version, data["userId"]) if user_mission_point_list is None: return {} @@ -864,8 +867,8 @@ class OngekiBase: "userMissionPointList": mission_point_list, } - def handle_get_user_event_point_api_request(self, data: Dict) -> Dict: - user_event_point_list = self.data.item.get_event_points(data["userId"]) + async def handle_get_user_event_point_api_request(self, data: Dict) -> Dict: + user_event_point_list = await self.data.item.get_event_points(data["userId"]) if user_event_point_list is None: return {} @@ -886,8 +889,8 @@ class OngekiBase: "userEventPointList": event_point_list, } - def handle_get_user_music_item_api_request(self, data: Dict) -> Dict: - user_music_item_list = self.data.item.get_music_items(data["userId"]) + async def handle_get_user_music_item_api_request(self, data: Dict) -> Dict: + user_music_item_list = await self.data.item.get_music_items(data["userId"]) if user_music_item_list is None: return {} @@ -904,8 +907,8 @@ class OngekiBase: "userMusicItemList": music_item_list, } - def handle_get_user_event_music_api_request(self, data: Dict) -> Dict: - user_evt_music_list = self.data.item.get_event_music(data["userId"]) + async def handle_get_user_event_music_api_request(self, data: Dict) -> Dict: + user_evt_music_list = await self.data.item.get_event_music(data["userId"]) if user_evt_music_list is None: return {} @@ -922,8 +925,8 @@ class OngekiBase: "userEventMusicList": evt_music_list, } - def handle_get_user_boss_api_request(self, data: Dict) -> Dict: - p = self.data.item.get_bosses(data["userId"]) + async def handle_get_user_boss_api_request(self, data: Dict) -> Dict: + p = await self.data.item.get_bosses(data["userId"]) if p is None: return {} @@ -940,27 +943,33 @@ class OngekiBase: "userBossList": boss_list, } - def handle_upsert_user_all_api_request(self, data: Dict) -> Dict: + async def handle_upsert_user_all_api_request(self, data: Dict) -> Dict: upsert = data["upsertUserAll"] user_id = data["userId"] + if user_id & 0x1000000000001 == 0x1000000000001: + place_id = int(user_id) & 0xFFFC00000000 + + self.logger.info("Guest play from place ID %d, ignoring.", place_id) + return {"returnCode": 1, "apiName": "UpsertUserAllApi"} + # The isNew fields are new as of Red and up. We just won't use them for now. if "userData" in upsert and len(upsert["userData"]) > 0: - self.data.profile.put_profile_data( + await self.data.profile.put_profile_data( user_id, self.version, upsert["userData"][0] ) if "userOption" in upsert and len(upsert["userOption"]) > 0: - self.data.profile.put_profile_options(user_id, upsert["userOption"][0]) + await self.data.profile.put_profile_options(user_id, upsert["userOption"][0]) if "userPlaylogList" in upsert: for playlog in upsert["userPlaylogList"]: - self.data.score.put_playlog(user_id, playlog) + await self.data.score.put_playlog(user_id, playlog) if "userActivityList" in upsert: for act in upsert["userActivityList"]: - self.data.profile.put_profile_activity( + await self.data.profile.put_profile_activity( user_id, act["kind"], act["id"], @@ -972,111 +981,129 @@ class OngekiBase: ) if "userRecentRatingList" in upsert: - self.data.profile.put_profile_recent_rating( + await self.data.profile.put_profile_recent_rating( user_id, upsert["userRecentRatingList"] ) if "userBpBaseList" in upsert: - self.data.profile.put_profile_bp_list(user_id, upsert["userBpBaseList"]) + await self.data.profile.put_profile_bp_list(user_id, upsert["userBpBaseList"]) if "userMusicDetailList" in upsert: for x in upsert["userMusicDetailList"]: - self.data.score.put_best_score(user_id, x) + await self.data.score.put_best_score(user_id, x) if "userCharacterList" in upsert: for x in upsert["userCharacterList"]: - self.data.item.put_character(user_id, x) + await self.data.item.put_character(user_id, x) if "userCardList" in upsert: for x in upsert["userCardList"]: - self.data.item.put_card(user_id, x) + await self.data.item.put_card(user_id, x) if "userDeckList" in upsert: for x in upsert["userDeckList"]: - self.data.item.put_deck(user_id, x) + await self.data.item.put_deck(user_id, x) if "userTrainingRoomList" in upsert: for x in upsert["userTrainingRoomList"]: - self.data.profile.put_training_room(user_id, x) + await self.data.profile.put_training_room(user_id, x) if "userStoryList" in upsert: for x in upsert["userStoryList"]: - self.data.item.put_story(user_id, x) + await self.data.item.put_story(user_id, x) if "userChapterList" in upsert: for x in upsert["userChapterList"]: - self.data.item.put_chapter(user_id, x) + await self.data.item.put_chapter(user_id, x) if "userMemoryChapterList" in upsert: for x in upsert["userMemoryChapterList"]: - self.data.item.put_memorychapter(user_id, x) + await self.data.item.put_memorychapter(user_id, x) if "userItemList" in upsert: for x in upsert["userItemList"]: - self.data.item.put_item(user_id, x) + await self.data.item.put_item(user_id, x) if "userMusicItemList" in upsert: for x in upsert["userMusicItemList"]: - self.data.item.put_music_item(user_id, x) + await self.data.item.put_music_item(user_id, x) if "userLoginBonusList" in upsert: for x in upsert["userLoginBonusList"]: - self.data.item.put_login_bonus(user_id, x) + await self.data.item.put_login_bonus(user_id, x) if "userEventPointList" in upsert: for x in upsert["userEventPointList"]: - self.data.item.put_event_point(user_id, self.version, x) + await self.data.item.put_event_point(user_id, self.version, x) if "userMissionPointList" in upsert: for x in upsert["userMissionPointList"]: - self.data.item.put_mission_point(user_id, self.version, x) + await self.data.item.put_mission_point(user_id, self.version, x) if "userRatinglogList" in upsert: for x in upsert["userRatinglogList"]: - self.data.profile.put_profile_rating_log( + await self.data.profile.put_profile_rating_log( user_id, x["dataVersion"], x["highestRating"] ) if "userBossList" in upsert: for x in upsert["userBossList"]: - self.data.item.put_boss(user_id, x) + await self.data.item.put_boss(user_id, x) if "userTechCountList" in upsert: for x in upsert["userTechCountList"]: - self.data.score.put_tech_count(user_id, x) + await self.data.score.put_tech_count(user_id, x) if "userScenerioList" in upsert: for x in upsert["userScenerioList"]: - self.data.item.put_scenerio(user_id, x) + await self.data.item.put_scenerio(user_id, x) if "userTradeItemList" in upsert: for x in upsert["userTradeItemList"]: - self.data.item.put_trade_item(user_id, x) + await self.data.item.put_trade_item(user_id, x) if "userEventMusicList" in upsert: for x in upsert["userEventMusicList"]: - self.data.item.put_event_music(user_id, x) + await self.data.item.put_event_music(user_id, x) if "userTechEventList" in upsert: for x in upsert["userTechEventList"]: - self.data.item.put_tech_event(user_id, self.version, x) + await self.data.item.put_tech_event(user_id, self.version, x) # This should be updated once a day in maintenance window, but for time being we will push the update on each upsert - self.data.item.put_tech_event_ranking(user_id, self.version, x) + await self.data.item.put_tech_event_ranking(user_id, self.version, x) if "userKopList" in upsert: for x in upsert["userKopList"]: - self.data.profile.put_kop(user_id, x) + await self.data.profile.put_kop(user_id, x) + + for rating_type in { + "userRatingBaseBestList", + "userRatingBaseBestNewList", + "userRatingBaseHotList", + "userRatingBaseNextList", + "userRatingBaseNextNewList", + "userRatingBaseHotNextList", + }: + if rating_type not in upsert: + continue + + await self.data.profile.put_profile_rating( + user_id, + self.version, + rating_type, + upsert[rating_type], + ) return {"returnCode": 1, "apiName": "upsertUserAll"} - def handle_get_user_rival_api_request(self, data: Dict) -> Dict: + async def handle_get_user_rival_api_request(self, data: Dict) -> Dict: """ Added in Bright """ rival_list = [] - user_rivals = self.data.profile.get_rivals(data["userId"]) + user_rivals = await self.data.profile.get_rivals(data["userId"]) for rival in user_rivals: tmp = {} tmp["rivalUserId"] = rival[0] @@ -1094,13 +1121,13 @@ class OngekiBase: "userRivalList": rival_list, } - def handle_get_user_rival_data_api_request(self, data: Dict) -> Dict: + async def handle_get_user_rival_data_api_request(self, data: Dict) -> Dict: """ Added in Bright """ rivals = [] for rival in data["userRivalList"]: - name = self.data.profile.get_profile_name( + name = await self.data.profile.get_profile_name( rival["rivalUserId"], self.version ) if name is None: @@ -1112,7 +1139,7 @@ class OngekiBase: "userRivalDataList": rivals, } - def handle_get_user_rival_music_api_request(self, data: Dict) -> Dict: + async def handle_get_user_rival_music_api_request(self, data: Dict) -> Dict: """ Added in Bright """ @@ -1135,8 +1162,8 @@ class OngekiBase: } @cached(2) - def util_generate_music_list(self, user_id: int) -> List: - music_detail = self.data.score.get_best_scores(user_id) + async def util_generate_music_list(self, user_id: int) -> List: + music_detail = await self.data.score.get_best_scores(user_id) song_list = [] for md in music_detail: diff --git a/titles/ongeki/bright.py b/titles/ongeki/bright.py index 49d6216..690a118 100644 --- a/titles/ongeki/bright.py +++ b/titles/ongeki/bright.py @@ -15,19 +15,19 @@ class OngekiBright(OngekiBase): super().__init__(core_cfg, game_cfg) self.version = OngekiConstants.VER_ONGEKI_BRIGHT - def handle_get_game_setting_api_request(self, data: Dict) -> Dict: - ret = super().handle_get_game_setting_api_request(data) + async def handle_get_game_setting_api_request(self, data: Dict) -> Dict: + ret = await super().handle_get_game_setting_api_request(data) ret["gameSetting"]["dataVersion"] = "1.30.00" ret["gameSetting"]["onlineDataVersion"] = "1.30.00" return ret - def handle_cm_get_user_data_api_request(self, data: Dict) -> Dict: + async def handle_cm_get_user_data_api_request(self, data: Dict) -> Dict: # check for a bright profile - p = self.data.profile.get_profile_data(data["userId"], self.version) + p = await self.data.profile.get_profile_data(data["userId"], self.version) if p is None: return {} - cards = self.data.card.get_user_cards(data["userId"]) + cards = await self.data.card.get_user_cards(data["userId"]) if cards is None or len(cards) == 0: # This should never happen self.logger.error( @@ -55,14 +55,14 @@ class OngekiBright(OngekiBase): return {"userId": data["userId"], "userData": user_data} - def handle_printer_login_api_request(self, data: Dict): + async def handle_printer_login_api_request(self, data: Dict): return {"returnCode": 1} - def handle_printer_logout_api_request(self, data: Dict): + async def handle_printer_logout_api_request(self, data: Dict): return {"returnCode": 1} - def handle_cm_get_user_card_api_request(self, data: Dict) -> Dict: - user_cards = self.data.item.get_cards(data["userId"]) + async def handle_cm_get_user_card_api_request(self, data: Dict) -> Dict: + user_cards = await self.data.item.get_cards(data["userId"]) if user_cards is None: return {} @@ -90,8 +90,8 @@ class OngekiBright(OngekiBase): "userCardList": card_list[start_idx:end_idx], } - def handle_cm_get_user_character_api_request(self, data: Dict) -> Dict: - user_characters = self.data.item.get_characters(data["userId"]) + async def handle_cm_get_user_character_api_request(self, data: Dict) -> Dict: + user_characters = await self.data.item.get_characters(data["userId"]) if user_characters is None: return { "userId": data["userId"], @@ -124,8 +124,8 @@ class OngekiBright(OngekiBase): "userCharacterList": character_list[start_idx:end_idx], } - def handle_get_user_gacha_api_request(self, data: Dict) -> Dict: - user_gachas = self.data.item.get_user_gachas(data["userId"]) + async def handle_get_user_gacha_api_request(self, data: Dict) -> Dict: + user_gachas = await self.data.item.get_user_gachas(data["userId"]) if user_gachas is None: return {"userId": data["userId"], "length": 0, "userGachaList": []} @@ -143,12 +143,12 @@ class OngekiBright(OngekiBase): "userGachaList": user_gacha_list, } - def handle_cm_get_user_item_api_request(self, data: Dict) -> Dict: - return self.handle_get_user_item_api_request(data) + async def handle_cm_get_user_item_api_request(self, data: Dict) -> Dict: + return await self.handle_get_user_item_api_request(data) - def handle_cm_get_user_gacha_supply_api_request(self, data: Dict) -> Dict: + async def handle_cm_get_user_gacha_supply_api_request(self, data: Dict) -> Dict: # not used for now? not sure what it even does - user_gacha_supplies = self.data.item.get_user_gacha_supplies(data["userId"]) + user_gacha_supplies = await self.data.item.get_user_gacha_supplies(data["userId"]) if user_gacha_supplies is None: return {"supplyId": 1, "length": 0, "supplyCardList": []} @@ -160,7 +160,7 @@ class OngekiBright(OngekiBase): "supplyCardList": supply_list, } - def handle_get_game_gacha_api_request(self, data: Dict) -> Dict: + async def handle_get_game_gacha_api_request(self, data: Dict) -> Dict: """ returns all current active banners (gachas) "Select Gacha" requires maxSelectPoint set and isCeiling set to 1 @@ -168,7 +168,7 @@ class OngekiBright(OngekiBase): game_gachas = [] # for every gacha_id in the OngekiConfig, grab the banner from the db for gacha_id in self.game_cfg.gachas.enabled_gachas: - game_gacha = self.data.static.get_gacha(self.version, gacha_id) + game_gacha = await self.data.static.get_gacha(self.version, gacha_id) if game_gacha: game_gachas.append(game_gacha) @@ -207,7 +207,7 @@ class OngekiBright(OngekiBase): "registIdList": [], } - def handle_roll_gacha_api_request(self, data: Dict) -> Dict: + async def handle_roll_gacha_api_request(self, data: Dict) -> Dict: """ Handle a gacha roll API request """ @@ -265,26 +265,26 @@ class OngekiBright(OngekiBase): return self.handle_roll_gacha_api_request(data) # get a list of cards for each rarity - cards_r = self.data.static.get_cards_by_rarity(self.version, 1) + cards_r = await self.data.static.get_cards_by_rarity(self.version, 1) cards_sr, cards_ssr = [], [] # free gachas are only allowed to get their specific cards! (R irrelevant) if gacha_id in {1011, 1012}: - gacha_cards = self.data.static.get_gacha_cards(gacha_id) + gacha_cards = await self.data.static.get_gacha_cards(gacha_id) for card in gacha_cards: if card["rarity"] == 3: cards_sr.append({"cardId": card["cardId"], "rarity": 2}) elif card["rarity"] == 4: cards_ssr.append({"cardId": card["cardId"], "rarity": 3}) else: - cards_sr = self.data.static.get_cards_by_rarity(self.version, 2) - cards_ssr = self.data.static.get_cards_by_rarity(self.version, 3) + cards_sr = await self.data.static.get_cards_by_rarity(self.version, 2) + cards_ssr = await self.data.static.get_cards_by_rarity(self.version, 3) # get the promoted cards for that gacha and add them multiple # times to increase chances by factor chances chances = 10 - gacha_cards = self.data.static.get_gacha_cards(gacha_id) + gacha_cards = await self.data.static.get_gacha_cards(gacha_id) for card in gacha_cards: # make sure to add the cards to the corresponding rarity if card["rarity"] == 2: @@ -323,7 +323,7 @@ class OngekiBright(OngekiBase): "gameGachaCardList": game_gacha_card_list, } - def handle_cm_upsert_user_gacha_api_request(self, data: Dict): + async def handle_cm_upsert_user_gacha_api_request(self, data: Dict): upsert = data["cmUpsertUserGacha"] user_id = data["userId"] @@ -339,7 +339,7 @@ class OngekiBright(OngekiBase): daily_gacha_date = datetime.strptime("2000-01-01", "%Y-%m-%d") # check if the user previously rolled the exact same gacha - user_gacha = self.data.item.get_user_gacha(user_id, gacha_id) + user_gacha = await self.data.item.get_user_gacha(user_id, gacha_id) if user_gacha: total_gacha_count = user_gacha["totalGachaCnt"] ceiling_gacha_count = user_gacha["ceilingGachaCnt"] @@ -358,7 +358,7 @@ class OngekiBright(OngekiBase): daily_gacha_date = play_date daily_gacha_cnt = 0 - self.data.item.put_user_gacha( + await self.data.item.put_user_gacha( user_id, gacha_id, totalGachaCnt=total_gacha_count + gacha_count, @@ -375,29 +375,29 @@ class OngekiBright(OngekiBase): if "userData" in upsert and len(upsert["userData"]) > 0: # check if the profile is a bright memory profile - p = self.data.profile.get_profile_data(data["userId"], self.version) + p = await self.data.profile.get_profile_data(data["userId"], self.version) if p is not None: # save the bright memory profile - self.data.profile.put_profile_data( + await self.data.profile.put_profile_data( user_id, self.version, upsert["userData"][0] ) else: # save the bright profile - self.data.profile.put_profile_data( + await self.data.profile.put_profile_data( user_id, self.version, upsert["userData"][0] ) if "userCharacterList" in upsert: for x in upsert["userCharacterList"]: - self.data.item.put_character(user_id, x) + await self.data.item.put_character(user_id, x) if "userItemList" in upsert: for x in upsert["userItemList"]: - self.data.item.put_item(user_id, x) + await self.data.item.put_item(user_id, x) if "userCardList" in upsert: for x in upsert["userCardList"]: - self.data.item.put_card(user_id, x) + await self.data.item.put_card(user_id, x) # TODO? # if "gameGachaCardList" in upsert: @@ -405,35 +405,35 @@ class OngekiBright(OngekiBase): return {"returnCode": 1, "apiName": "CMUpsertUserGachaApi"} - def handle_cm_upsert_user_select_gacha_api_request(self, data: Dict) -> Dict: + async def handle_cm_upsert_user_select_gacha_api_request(self, data: Dict) -> Dict: upsert = data["cmUpsertUserSelectGacha"] user_id = data["userId"] if "userData" in upsert and len(upsert["userData"]) > 0: # check if the profile is a bright memory profile - p = self.data.profile.get_profile_data(data["userId"], self.version) + p = await self.data.profile.get_profile_data(data["userId"], self.version) if p is not None: # save the bright memory profile - self.data.profile.put_profile_data( + await self.data.profile.put_profile_data( user_id, self.version, upsert["userData"][0] ) else: # save the bright profile - self.data.profile.put_profile_data( + await self.data.profile.put_profile_data( user_id, self.version, upsert["userData"][0] ) if "userCharacterList" in upsert: for x in upsert["userCharacterList"]: - self.data.item.put_character(user_id, x) + await self.data.item.put_character(user_id, x) if "userCardList" in upsert: for x in upsert["userCardList"]: - self.data.item.put_card(user_id, x) + await self.data.item.put_card(user_id, x) if "selectGachaLogList" in data: for x in data["selectGachaLogList"]: - self.data.item.put_user_gacha( + await self.data.item.put_user_gacha( user_id, x["gachaId"], selectPoint=0, @@ -442,8 +442,8 @@ class OngekiBright(OngekiBase): return {"returnCode": 1, "apiName": "cmUpsertUserSelectGacha"} - def handle_get_game_gacha_card_by_id_api_request(self, data: Dict) -> Dict: - game_gacha_cards = self.data.static.get_gacha_cards(data["gachaId"]) + async def handle_get_game_gacha_card_by_id_api_request(self, data: Dict) -> Dict: + game_gacha_cards = await self.data.static.get_gacha_cards(data["gachaId"]) if game_gacha_cards == []: # fallback to be at least able to select that gacha return { @@ -522,7 +522,7 @@ class OngekiBright(OngekiBase): "ssrBookCalcList": [], } - def handle_get_game_theater_api_request(self, data: Dict) -> Dict: + async def handle_get_game_theater_api_request(self, data: Dict) -> Dict: """ shows a banner after every print, not sure what its used for """ @@ -548,7 +548,7 @@ class OngekiBright(OngekiBase): return {"length": 0, "gameTheaterList": [], "registIdList": []} - def handle_cm_upsert_user_print_playlog_api_request(self, data: Dict) -> Dict: + async def handle_cm_upsert_user_print_playlog_api_request(self, data: Dict) -> Dict: return { "returnCode": 1, "orderId": 0, @@ -556,7 +556,7 @@ class OngekiBright(OngekiBase): "apiName": "CMUpsertUserPrintPlaylogApi", } - def handle_cm_upsert_user_printlog_api_request(self, data: Dict) -> Dict: + async def handle_cm_upsert_user_printlog_api_request(self, data: Dict) -> Dict: return { "returnCode": 1, "orderId": 0, @@ -564,7 +564,7 @@ class OngekiBright(OngekiBase): "apiName": "CMUpsertUserPrintlogApi", } - def handle_cm_upsert_user_print_api_request(self, data: Dict) -> Dict: + async def handle_cm_upsert_user_print_api_request(self, data: Dict) -> Dict: user_print_detail = data["userPrintDetail"] # generate random serial id @@ -579,7 +579,7 @@ class OngekiBright(OngekiBase): ) # add the entry to the user print table with the random serialId - self.data.item.put_user_print_detail( + await self.data.item.put_user_print_detail( data["userId"], serial_id, user_print_detail ) @@ -589,27 +589,27 @@ class OngekiBright(OngekiBase): "apiName": "CMUpsertUserPrintApi", } - def handle_cm_upsert_user_all_api_request(self, data: Dict) -> Dict: + async def handle_cm_upsert_user_all_api_request(self, data: Dict) -> Dict: upsert = data["cmUpsertUserAll"] user_id = data["userId"] if "userData" in upsert and len(upsert["userData"]) > 0: # check if the profile is a bright memory profile - p = self.data.profile.get_profile_data(data["userId"], self.version) + p = await self.data.profile.get_profile_data(data["userId"], self.version) if p is not None: # save the bright memory profile - self.data.profile.put_profile_data( + await self.data.profile.put_profile_data( user_id, self.version, upsert["userData"][0] ) else: # save the bright profile - self.data.profile.put_profile_data( + await self.data.profile.put_profile_data( user_id, self.version, upsert["userData"][0] ) if "userActivityList" in upsert: for act in upsert["userActivityList"]: - self.data.profile.put_profile_activity( + await self.data.profile.put_profile_activity( user_id, act["kind"], act["id"], @@ -622,10 +622,10 @@ class OngekiBright(OngekiBase): if "userItemList" in upsert: for x in upsert["userItemList"]: - self.data.item.put_item(user_id, x) + await self.data.item.put_item(user_id, x) if "userCardList" in upsert: for x in upsert["userCardList"]: - self.data.item.put_card(user_id, x) + await self.data.item.put_card(user_id, x) return {"returnCode": 1, "apiName": "cmUpsertUserAll"} diff --git a/titles/ongeki/brightmemory.py b/titles/ongeki/brightmemory.py index d7103a3..6fdd60a 100644 --- a/titles/ongeki/brightmemory.py +++ b/titles/ongeki/brightmemory.py @@ -15,8 +15,8 @@ class OngekiBrightMemory(OngekiBright): super().__init__(core_cfg, game_cfg) self.version = OngekiConstants.VER_ONGEKI_BRIGHT_MEMORY - def handle_get_game_setting_api_request(self, data: Dict) -> Dict: - ret = super().handle_get_game_setting_api_request(data) + async def handle_get_game_setting_api_request(self, data: Dict) -> Dict: + ret = await super().handle_get_game_setting_api_request(data) ret["gameSetting"]["dataVersion"] = "1.35.00" ret["gameSetting"]["onlineDataVersion"] = "1.35.00" ret["gameSetting"]["maxCountCharacter"] = 50 @@ -27,8 +27,8 @@ class OngekiBrightMemory(OngekiBright): ret["gameSetting"]["maxCountRivalMusic"] = 300 return ret - def handle_get_user_memory_chapter_api_request(self, data: Dict) -> Dict: - memories = self.data.item.get_memorychapters(data["userId"]) + async def handle_get_user_memory_chapter_api_request(self, data: Dict) -> Dict: + memories = await self.data.item.get_memorychapters(data["userId"]) if not memories: return { "userId": data["userId"], @@ -134,5 +134,5 @@ class OngekiBrightMemory(OngekiBright): "userMemoryChapterList": memory_chp, } - def handle_get_game_music_release_state_api_request(self, data: Dict) -> Dict: + async def handle_get_game_music_release_state_api_request(self, data: Dict) -> Dict: return {"techScore": 0, "cardNum": 0} diff --git a/titles/ongeki/frontend.py b/titles/ongeki/frontend.py index 987776f..226f318 100644 --- a/titles/ongeki/frontend.py +++ b/titles/ongeki/frontend.py @@ -1,11 +1,12 @@ +from typing import List +from starlette.routing import Route import yaml import jinja2 -from twisted.web.http import Request +from starlette.requests import Request +from starlette.responses import Response, RedirectResponse from os import path -from twisted.web.util import redirectTo -from twisted.web.server import Session -from core.frontend import FE_Base, IUserSession +from core.frontend import FE_Base, UserSession from core.config import CoreConfig from titles.ongeki.config import OngekiConfig @@ -27,24 +28,34 @@ class OngekiFrontend(FE_Base): ) self.nav_name = "O.N.G.E.K.I." self.version_list = OngekiConstants.VERSION_NAMES + + def get_routes(self) -> List[Route]: + return [ + Route("/", self.render_GET), + Route("/version.change", self.render_POST, methods=['POST']) + ] - def render_GET(self, request: Request) -> bytes: + async def render_GET(self, request: Request) -> bytes: template = self.environment.get_template( - "titles/ongeki/frontend/ongeki_index.jinja" + "titles/ongeki/templates/ongeki_index.jinja" ) - sesh: Session = request.getSession() - usr_sesh = IUserSession(sesh) + usr_sesh = self.validate_session(request) + if not usr_sesh: + usr_sesh = UserSession() + self.version = usr_sesh.ongeki_version - if getattr(usr_sesh, "userId", 0) != 0: - profile_data =self.data.profile.get_profile_data(usr_sesh.userId, self.version) - rival_list = self.data.profile.get_rivals(usr_sesh.userId) + if usr_sesh.user_id > 0: + profile_data =await self.data.profile.get_profile_data(usr_sesh.user_id, self.version) + rival_list = await self.data.profile.get_rivals(usr_sesh.user_id) rival_data = { "userRivalList": rival_list, - "userId": usr_sesh.userId + "userId": usr_sesh.user_id } - rival_info = OngekiBase.handle_get_user_rival_data_api_request(self, rival_data) - return template.render( + # Hay1tsme 01/09/2024: ?????????????????????????????????????????????????????????????? + rival_info = await OngekiBase.handle_get_user_rival_data_api_request(self, rival_data) + + return Response(template.render( data=self.data.profile, title=f"{self.core_config.server.name} | {self.nav_name}", game_list=self.environment.globals["game_list"], @@ -54,34 +65,41 @@ class OngekiFrontend(FE_Base): version_list=self.version_list, version=self.version, sesh=vars(usr_sesh) - ).encode("utf-16") + ), media_type="text/html; charset=utf-8") else: - return redirectTo(b"/gate/", request) + return RedirectResponse("/gate/", 303) - def render_POST(self, request: Request): - uri = request.uri.decode() - sesh: Session = request.getSession() - usr_sesh = IUserSession(sesh) - if hasattr(usr_sesh, "userId"): + async def render_POST(self, request: Request): + uri = request.url.path + frm = await request.form() + usr_sesh = self.validate_session(request) + if not usr_sesh: + usr_sesh = UserSession() + + if usr_sesh.user_id > 0: if uri == "/game/ongeki/rival.add": - rival_id = request.args[b"rivalUserId"][0].decode() - self.data.profile.put_rival(usr_sesh.userId, rival_id) - # self.logger.info(f"{usr_sesh.userId} added a rival") - return redirectTo(b"/game/ongeki/", request) + rival_id = frm.get("rivalUserId") + await self.data.profile.put_rival(usr_sesh.user_id, rival_id) + # self.logger.info(f"{usr_sesh.user_id} added a rival") + return RedirectResponse(b"/game/ongeki/", 303) elif uri == "/game/ongeki/rival.delete": - rival_id = request.args[b"rivalUserId"][0].decode() - self.data.profile.delete_rival(usr_sesh.userId, rival_id) + rival_id = frm.get("rivalUserId") + await self.data.profile.delete_rival(usr_sesh.user_id, rival_id) # self.logger.info(f"{response}") - return redirectTo(b"/game/ongeki/", request) + return RedirectResponse(b"/game/ongeki/", 303) elif uri == "/game/ongeki/version.change": - ongeki_version=request.args[b"version"][0].decode() + ongeki_version=frm.get("version") if(ongeki_version.isdigit()): usr_sesh.ongeki_version=int(ongeki_version) - return redirectTo(b"/game/ongeki/", request) + enc = self.encode_session(usr_sesh) + resp = RedirectResponse("/game/ongeki/", 303) + resp.delete_cookie('ARTEMIS_SESH') + resp.set_cookie('ARTEMIS_SESH', enc) + return resp else: - return b"Something went wrong" + Response("Something went wrong", status_code=500) else: - return b"User is not logged in" + return RedirectResponse("/gate/", 303) diff --git a/titles/ongeki/index.py b/titles/ongeki/index.py index baf1ee3..3bd0e15 100644 --- a/titles/ongeki/index.py +++ b/titles/ongeki/index.py @@ -1,4 +1,6 @@ -from twisted.web.http import Request +from starlette.requests import Request +from starlette.routing import Route +from starlette.responses import Response import json import inflection import yaml @@ -120,11 +122,10 @@ class OngekiServlet(BaseServlet): return True - def get_endpoint_matchers(self) -> Tuple[List[Tuple[str, str, Dict]], List[Tuple[str, str, Dict]]]: - return ( - [], - [("render_POST", "/SDDT/{version}/{endpoint}", {})] - ) + def get_routes(self) -> List[Route]: + return [ + Route("/SDDT/{version:int}/{endpoint:str}", self.render_POST, methods=['POST']) + ] def get_allnet_info(self, game_code: str, game_ver: int, keychip: str) -> Tuple[str, str]: title_port_int = Utils.get_title_port(self.core_cfg) @@ -132,23 +133,23 @@ class OngekiServlet(BaseServlet): proto = "https" if self.game_cfg.server.use_https and game_ver >= 120 else "http" if proto == "https": - t_port = f":{title_port_ssl_int}" if title_port_ssl_int and not self.core_cfg.server.is_using_proxy else "" + t_port = f":{title_port_ssl_int}" if title_port_ssl_int != 443 else "" else: - t_port = f":{title_port_int}" if title_port_int and not self.core_cfg.server.is_using_proxy else "" + t_port = f":{title_port_int}" if title_port_int != 80 else "" return ( - f"{proto}://{self.core_cfg.title.hostname}{t_port}/{game_code}/{game_ver}/", - f"{self.core_cfg.title.hostname}{t_port}/", + f"{proto}://{self.core_cfg.server.hostname}{t_port}/{game_code}/{game_ver}/", + f"{self.core_cfg.server.hostname}{t_port}/", ) - def render_POST(self, request: Request, game_code: str, matchers: Dict) -> bytes: - endpoint = matchers['endpoint'] - version = int(matchers['version']) + async def render_POST(self, request: Request) -> bytes: + endpoint: str = request.path_params.get('endpoint', '') + version: int = request.path_params.get('version', 0) if endpoint.lower() == "ping": - return zlib.compress(b'{"returnCode": 1}') + return Response(zlib.compress(b'{"returnCode": 1}')) - req_raw = request.content.getvalue() + req_raw = await request.body() encrtped = False internal_ver = 0 client_ip = Utils.get_ip_addr(request) @@ -167,7 +168,7 @@ class OngekiServlet(BaseServlet): internal_ver = OngekiConstants.VER_ONGEKI_RED_PLUS elif version >= 130 and version < 135: # Bright internal_ver = OngekiConstants.VER_ONGEKI_BRIGHT - elif version >= 135 and version < 140: # Bright Memory + elif version >= 135 and version < 145: # Bright Memory internal_ver = OngekiConstants.VER_ONGEKI_BRIGHT_MEMORY if all(c in string.hexdigits for c in endpoint) and len(endpoint) == 32: @@ -178,13 +179,13 @@ class OngekiServlet(BaseServlet): self.logger.error( f"v{version} does not support encryption or no keys entered" ) - return zlib.compress(b'{"stat": "0"}') + return Response(zlib.compress(b'{"stat": "0"}')) elif endpoint.lower() not in self.hash_table[internal_ver]: self.logger.error( f"No hash found for v{version} endpoint {endpoint}" ) - return zlib.compress(b'{"stat": "0"}') + return Response(zlib.compress(b'{"stat": "0"}')) endpoint = self.hash_table[internal_ver][endpoint.lower()] @@ -201,7 +202,7 @@ class OngekiServlet(BaseServlet): self.logger.error( f"Failed to decrypt v{version} request to {endpoint} -> {e}" ) - return zlib.compress(b'{"stat": "0"}') + return Response(zlib.compress(b'{"stat": "0"}')) encrtped = True @@ -213,18 +214,22 @@ class OngekiServlet(BaseServlet): self.logger.error( f"Unencrypted v{version} {endpoint} request, but config is set to encrypted only: {req_raw}" ) - return zlib.compress(b'{"stat": "0"}') + return Response(zlib.compress(b'{"stat": "0"}')) - try: - unzip = zlib.decompress(req_raw) + if version < 105: + # O.N.G.E.K.I base don't use zlib + req_data = json.loads(req_raw) + else: + try: + unzip = zlib.decompress(req_raw) + + except zlib.error as e: + self.logger.error( + f"Failed to decompress v{version} {endpoint} request -> {e}" + ) + return Response(zlib.compress(b'{"stat": "0"}')) - except zlib.error as e: - self.logger.error( - f"Failed to decompress v{version} {endpoint} request -> {e}" - ) - return zlib.compress(b'{"stat": "0"}') - - req_data = json.loads(unzip) + req_data = json.loads(unzip) self.logger.info( f"v{version} {endpoint} request from {client_ip}" @@ -235,25 +240,28 @@ class OngekiServlet(BaseServlet): if not hasattr(self.versions[internal_ver], func_to_find): self.logger.warning(f"Unhandled v{version} request {endpoint}") - return zlib.compress(b'{"returnCode": 1}') + return Response(zlib.compress(b'{"returnCode": 1}')) try: handler = getattr(self.versions[internal_ver], func_to_find) - resp = handler(req_data) + resp = await handler(req_data) except Exception as e: self.logger.error(f"Error handling v{version} method {endpoint} - {e}") - return zlib.compress(b'{"stat": "0"}') + return Response(zlib.compress(b'{"stat": "0"}')) if resp == None: resp = {"returnCode": 1} self.logger.debug(f"Response {resp}") - zipped = zlib.compress(json.dumps(resp, ensure_ascii=False).encode("utf-8")) + resp_raw = json.dumps(resp, ensure_ascii=False).encode("utf-8") + zipped = zlib.compress(resp_raw) if not encrtped or version < 120: - return zipped + if version < 105: + return Response(resp_raw) + return Response(zipped) padded = pad(zipped, 16) @@ -263,4 +271,4 @@ class OngekiServlet(BaseServlet): bytes.fromhex(self.game_cfg.crypto.keys[internal_ver][1]), ) - return crypt.encrypt(padded) \ No newline at end of file + return Response(crypt.encrypt(padded)) \ No newline at end of file diff --git a/titles/ongeki/plus.py b/titles/ongeki/plus.py index 9168576..12c46b2 100644 --- a/titles/ongeki/plus.py +++ b/titles/ongeki/plus.py @@ -11,8 +11,8 @@ class OngekiPlus(OngekiBase): super().__init__(core_cfg, game_cfg) self.version = OngekiConstants.VER_ONGEKI_PLUS - def handle_get_game_setting_api_request(self, data: Dict) -> Dict: - ret = super().handle_get_game_setting_api_request(data) + async def handle_get_game_setting_api_request(self, data: Dict) -> Dict: + ret = await super().handle_get_game_setting_api_request(data) ret["gameSetting"]["dataVersion"] = "1.05.00" ret["gameSetting"]["onlineDataVersion"] = "1.05.00" return ret diff --git a/titles/ongeki/read.py b/titles/ongeki/read.py index 6e94094..a804956 100644 --- a/titles/ongeki/read.py +++ b/titles/ongeki/read.py @@ -1,16 +1,11 @@ -from decimal import Decimal -import logging import os -import re import xml.etree.ElementTree as ET -from typing import Any, Dict, List, Optional +from typing import Optional from read import BaseReader from core.config import CoreConfig from titles.ongeki.database import OngekiData from titles.ongeki.const import OngekiConstants -from titles.ongeki.config import OngekiConfig - class OngekiReader(BaseReader): def __init__( @@ -32,7 +27,7 @@ class OngekiReader(BaseReader): self.logger.error(f"Invalid ongeki version {version}") exit(1) - def read(self) -> None: + async def read(self) -> None: data_dirs = [] if self.bin_dir is not None: data_dirs += self.get_data_directories(self.bin_dir) @@ -41,12 +36,12 @@ class OngekiReader(BaseReader): data_dirs += self.get_data_directories(self.opt_dir) for dir in data_dirs: - self.read_events(f"{dir}/event") - self.read_music(f"{dir}/music") - self.read_card(f"{dir}/card") - self.read_reward(f"{dir}/reward") + await self.read_events(f"{dir}/event") + await self.read_music(f"{dir}/music") + await self.read_card(f"{dir}/card") + await self.read_reward(f"{dir}/reward") - def read_card(self, base_dir: str) -> None: + async def read_card(self, base_dir: str) -> None: self.logger.info(f"Reading cards from {base_dir}...") version_ids = { @@ -70,7 +65,7 @@ class OngekiReader(BaseReader): # skip already existing cards if ( - self.data.static.get_card( + await self.data.static.get_card( OngekiConstants.VER_ONGEKI_BRIGHT_MEMORY, card_id ) is not None @@ -100,7 +95,7 @@ class OngekiReader(BaseReader): version = version_ids[troot.find("VersionID").find("id").text] card_number = troot.find("CardNumberString").text - self.data.static.put_card( + await self.data.static.put_card( version, card_id, name=name, @@ -117,7 +112,7 @@ class OngekiReader(BaseReader): ) self.logger.info(f"Added card {card_id}") - def read_events(self, base_dir: str) -> None: + async def read_events(self, base_dir: str) -> None: self.logger.info(f"Reading events from {base_dir}...") for root, dirs, files in os.walk(base_dir): @@ -132,10 +127,10 @@ class OngekiReader(BaseReader): troot.find("EventType").text ].value - self.data.static.put_event(self.version, id, event_type, name) + await self.data.static.put_event(self.version, id, event_type, name) self.logger.info(f"Added event {id}") - def read_music(self, base_dir: str) -> None: + async def read_music(self, base_dir: str) -> None: self.logger.info(f"Reading music from {base_dir}...") for root, dirs, files in os.walk(base_dir): @@ -168,12 +163,12 @@ class OngekiReader(BaseReader): f"{fumens_data.find('FumenConstIntegerPart').text}.{fumens_data.find('FumenConstFractionalPart').text}" ) - self.data.static.put_chart( + await self.data.static.put_chart( self.version, song_id, chart_id, title, artist, genre, level ) self.logger.info(f"Added song {song_id} chart {chart_id}") - def read_reward(self, base_dir: str) -> None: + async def read_reward(self, base_dir: str) -> None: self.logger.info(f"Reading rewards from {base_dir}...") for root, dirs, files in os.walk(base_dir): @@ -195,5 +190,5 @@ class OngekiReader(BaseReader): itemKind = OngekiConstants.REWARD_TYPES[troot.find("ItemType").text].value itemId = troot.find("RewardItem").find("ItemName").find("id").text - self.data.static.put_reward(self.version, rewardId, rewardname, itemKind, itemId) + await self.data.static.put_reward(self.version, rewardId, rewardname, itemKind, itemId) self.logger.info(f"Added reward {rewardId}") diff --git a/titles/ongeki/red.py b/titles/ongeki/red.py index 52b9d59..eac20ae 100644 --- a/titles/ongeki/red.py +++ b/titles/ongeki/red.py @@ -11,8 +11,8 @@ class OngekiRed(OngekiBase): super().__init__(core_cfg, game_cfg) self.version = OngekiConstants.VER_ONGEKI_RED - def handle_get_game_setting_api_request(self, data: Dict) -> Dict: - ret = super().handle_get_game_setting_api_request(data) + async def handle_get_game_setting_api_request(self, data: Dict) -> Dict: + ret = await super().handle_get_game_setting_api_request(data) ret["gameSetting"]["dataVersion"] = "1.20.00" ret["gameSetting"]["onlineDataVersion"] = "1.20.00" return ret diff --git a/titles/ongeki/redplus.py b/titles/ongeki/redplus.py index 1f69690..99e63db 100644 --- a/titles/ongeki/redplus.py +++ b/titles/ongeki/redplus.py @@ -11,8 +11,8 @@ class OngekiRedPlus(OngekiBase): super().__init__(core_cfg, game_cfg) self.version = OngekiConstants.VER_ONGEKI_RED_PLUS - def handle_get_game_setting_api_request(self, data: Dict) -> Dict: - ret = super().handle_get_game_setting_api_request(data) + async def handle_get_game_setting_api_request(self, data: Dict) -> Dict: + ret = await super().handle_get_game_setting_api_request(data) ret["gameSetting"]["dataVersion"] = "1.25.00" ret["gameSetting"]["onlineDataVersion"] = "1.25.00" ret["gameSetting"]["maxCountCharacter"] = 50 diff --git a/titles/ongeki/schema/item.py b/titles/ongeki/schema/item.py index 55b4c68..ca2de1f 100644 --- a/titles/ongeki/schema/item.py +++ b/titles/ongeki/schema/item.py @@ -339,147 +339,147 @@ print_detail = Table( ) class OngekiItemData(BaseData): - def put_card(self, aime_id: int, card_data: Dict) -> Optional[int]: + async def put_card(self, aime_id: int, card_data: Dict) -> Optional[int]: card_data["user"] = aime_id sql = insert(card).values(**card_data) conflict = sql.on_duplicate_key_update(**card_data) - result = self.execute(conflict) + result = await self.execute(conflict) if result is None: self.logger.warning(f"put_card: Failed to update! aime_id: {aime_id}") return None return result.lastrowid - def get_cards(self, aime_id: int) -> Optional[List[Dict]]: + async def get_cards(self, aime_id: int) -> Optional[List[Dict]]: sql = select(card).where(card.c.user == aime_id) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchall() - def put_character(self, aime_id: int, character_data: Dict) -> Optional[int]: + async def put_character(self, aime_id: int, character_data: Dict) -> Optional[int]: character_data["user"] = aime_id sql = insert(character).values(**character_data) conflict = sql.on_duplicate_key_update(**character_data) - result = self.execute(conflict) + result = await self.execute(conflict) if result is None: self.logger.warning(f"put_character: Failed to update! aime_id: {aime_id}") return None return result.lastrowid - def get_characters(self, aime_id: int) -> Optional[List[Dict]]: + async def get_characters(self, aime_id: int) -> Optional[List[Dict]]: sql = select(character).where(character.c.user == aime_id) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchall() - def put_deck(self, aime_id: int, deck_data: Dict) -> Optional[int]: + async def put_deck(self, aime_id: int, deck_data: Dict) -> Optional[int]: deck_data["user"] = aime_id sql = insert(deck).values(**deck_data) conflict = sql.on_duplicate_key_update(**deck_data) - result = self.execute(conflict) + result = await self.execute(conflict) if result is None: self.logger.warning(f"put_deck: Failed to update! aime_id: {aime_id}") return None return result.lastrowid - def get_deck(self, aime_id: int, deck_id: int) -> Optional[Dict]: + async def get_deck(self, aime_id: int, deck_id: int) -> Optional[Dict]: sql = select(deck).where(and_(deck.c.user == aime_id, deck.c.deckId == deck_id)) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchone() - def get_decks(self, aime_id: int) -> Optional[List[Dict]]: + async def get_decks(self, aime_id: int) -> Optional[List[Dict]]: sql = select(deck).where(deck.c.user == aime_id) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchall() - def put_boss(self, aime_id: int, boss_data: Dict) -> Optional[int]: + async def put_boss(self, aime_id: int, boss_data: Dict) -> Optional[int]: boss_data["user"] = aime_id sql = insert(boss).values(**boss_data) conflict = sql.on_duplicate_key_update(**boss_data) - result = self.execute(conflict) + result = await self.execute(conflict) if result is None: self.logger.warning(f"put_boss: Failed to update! aime_id: {aime_id}") return None return result.lastrowid - def put_story(self, aime_id: int, story_data: Dict) -> Optional[int]: + async def put_story(self, aime_id: int, story_data: Dict) -> Optional[int]: story_data["user"] = aime_id sql = insert(story).values(**story_data) conflict = sql.on_duplicate_key_update(**story_data) - result = self.execute(conflict) + result = await self.execute(conflict) if result is None: self.logger.warning(f"put_story: Failed to update! aime_id: {aime_id}") return None return result.lastrowid - def get_stories(self, aime_id: int) -> Optional[List[Dict]]: + async def get_stories(self, aime_id: int) -> Optional[List[Dict]]: sql = select(story).where(story.c.user == aime_id) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchall() - def put_chapter(self, aime_id: int, chapter_data: Dict) -> Optional[int]: + async def put_chapter(self, aime_id: int, chapter_data: Dict) -> Optional[int]: chapter_data["user"] = aime_id sql = insert(chapter).values(**chapter_data) conflict = sql.on_duplicate_key_update(**chapter_data) - result = self.execute(conflict) + result = await self.execute(conflict) if result is None: self.logger.warning(f"put_chapter: Failed to update! aime_id: {aime_id}") return None return result.lastrowid - def get_chapters(self, aime_id: int) -> Optional[List[Dict]]: + async def get_chapters(self, aime_id: int) -> Optional[List[Dict]]: sql = select(chapter).where(chapter.c.user == aime_id) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchall() - def put_item(self, aime_id: int, item_data: Dict) -> Optional[int]: + async def put_item(self, aime_id: int, item_data: Dict) -> Optional[int]: item_data["user"] = aime_id sql = insert(item).values(**item_data) conflict = sql.on_duplicate_key_update(**item_data) - result = self.execute(conflict) + result = await self.execute(conflict) if result is None: self.logger.warning(f"put_item: Failed to update! aime_id: {aime_id}") return None return result.lastrowid - def get_item(self, aime_id: int, item_id: int, item_kind: int) -> Optional[Dict]: + async def get_item(self, aime_id: int, item_id: int, item_kind: int) -> Optional[Dict]: sql = select(item).where(and_(item.c.user == aime_id, item.c.itemId == item_id)) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchone() - def get_items(self, aime_id: int, item_kind: int = None) -> Optional[List[Dict]]: + async def get_items(self, aime_id: int, item_kind: int = None) -> Optional[List[Dict]]: if item_kind is None: sql = select(item).where(item.c.user == aime_id) else: @@ -487,73 +487,73 @@ class OngekiItemData(BaseData): and_(item.c.user == aime_id, item.c.itemKind == item_kind) ) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchall() - def put_music_item(self, aime_id: int, music_item_data: Dict) -> Optional[int]: + async def put_music_item(self, aime_id: int, music_item_data: Dict) -> Optional[int]: music_item_data["user"] = aime_id sql = insert(music_item).values(**music_item_data) conflict = sql.on_duplicate_key_update(**music_item_data) - result = self.execute(conflict) + result = await self.execute(conflict) if result is None: self.logger.warning(f"put_music_item: Failed to update! aime_id: {aime_id}") return None return result.lastrowid - def get_music_items(self, aime_id: int) -> Optional[List[Dict]]: + async def get_music_items(self, aime_id: int) -> Optional[List[Dict]]: sql = select(music_item).where(music_item.c.user == aime_id) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchall() - def put_login_bonus(self, aime_id: int, login_bonus_data: Dict) -> Optional[int]: + async def put_login_bonus(self, aime_id: int, login_bonus_data: Dict) -> Optional[int]: login_bonus_data["user"] = aime_id sql = insert(login_bonus).values(**login_bonus_data) conflict = sql.on_duplicate_key_update(**login_bonus_data) - result = self.execute(conflict) + result = await self.execute(conflict) if result is None: self.logger.warning(f"put_login_bonus: Failed to update! aime_id: {aime_id}") return None return result.lastrowid - def get_login_bonuses(self, aime_id: int) -> Optional[List[Dict]]: + async def get_login_bonuses(self, aime_id: int) -> Optional[List[Dict]]: sql = select(login_bonus).where(login_bonus.c.user == aime_id) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchall() - def put_mission_point(self, aime_id: int, version: int, mission_point_data: Dict) -> Optional[int]: + async def put_mission_point(self, aime_id: int, version: int, mission_point_data: Dict) -> Optional[int]: mission_point_data["version"] = version mission_point_data["user"] = aime_id sql = insert(mission_point).values(**mission_point_data) conflict = sql.on_duplicate_key_update(**mission_point_data) - result = self.execute(conflict) + result = await self.execute(conflict) if result is None: self.logger.warning(f"put_mission_point: Failed to update! aime_id: {aime_id}") return None return result.lastrowid - def get_mission_points(self, version: int, aime_id: int) -> Optional[List[Dict]]: + async def get_mission_points(self, version: int, aime_id: int) -> Optional[List[Dict]]: sql = select(mission_point).where(and_(mission_point.c.user == aime_id, mission_point.c.version == version)) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchall() - def put_event_point(self, aime_id: int, version: int, event_point_data: Dict) -> Optional[int]: + async def put_event_point(self, aime_id: int, version: int, event_point_data: Dict) -> Optional[int]: # We update only the newest (type: 1) entry, in official spec game watches for both latest(type:1) and previous (type:2) entries to give an additional info how many ranks has player moved up or down # This fully featured is on TODO list, at the moment we just update the tables as data comes and give out rank as request comes event_point_data["user"] = aime_id @@ -564,95 +564,95 @@ class OngekiItemData(BaseData): sql = insert(event_point).values(**event_point_data) conflict = sql.on_duplicate_key_update(**event_point_data) - result = self.execute(conflict) + result = await self.execute(conflict) if result is None: self.logger.warning(f"put_event_point: Failed to update! aime_id: {aime_id}") return None return result.lastrowid - def get_event_points(self, aime_id: int) -> Optional[List[Dict]]: + async def get_event_points(self, aime_id: int) -> Optional[List[Dict]]: sql = select(event_point).where(event_point.c.user == aime_id) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchall() - def put_scenerio(self, aime_id: int, scenerio_data: Dict) -> Optional[int]: + async def put_scenerio(self, aime_id: int, scenerio_data: Dict) -> Optional[int]: scenerio_data["user"] = aime_id sql = insert(scenerio).values(**scenerio_data) conflict = sql.on_duplicate_key_update(**scenerio_data) - result = self.execute(conflict) + result = await self.execute(conflict) if result is None: self.logger.warning(f"put_scenerio: Failed to update! aime_id: {aime_id}") return None return result.lastrowid - def get_scenerios(self, aime_id: int) -> Optional[List[Dict]]: + async def get_scenerios(self, aime_id: int) -> Optional[List[Dict]]: sql = select(scenerio).where(scenerio.c.user == aime_id) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchall() - def put_trade_item(self, aime_id: int, trade_item_data: Dict) -> Optional[int]: + async def put_trade_item(self, aime_id: int, trade_item_data: Dict) -> Optional[int]: trade_item_data["user"] = aime_id sql = insert(trade_item).values(**trade_item_data) conflict = sql.on_duplicate_key_update(**trade_item_data) - result = self.execute(conflict) + result = await self.execute(conflict) if result is None: self.logger.warning(f"put_trade_item: Failed to update! aime_id: {aime_id}") return None return result.lastrowid - def get_trade_items(self, aime_id: int) -> Optional[List[Dict]]: + async def get_trade_items(self, aime_id: int) -> Optional[List[Dict]]: sql = select(trade_item).where(trade_item.c.user == aime_id) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchall() - def put_event_music(self, aime_id: int, event_music_data: Dict) -> Optional[int]: + async def put_event_music(self, aime_id: int, event_music_data: Dict) -> Optional[int]: event_music_data["user"] = aime_id sql = insert(event_music).values(**event_music_data) conflict = sql.on_duplicate_key_update(**event_music_data) - result = self.execute(conflict) + result = await self.execute(conflict) if result is None: self.logger.warning(f"put_event_music: Failed to update! aime_id: {aime_id}") return None return result.lastrowid - def get_event_music(self, aime_id: int) -> Optional[List[Dict]]: + async def get_event_music(self, aime_id: int) -> Optional[List[Dict]]: sql = select(event_music).where(event_music.c.user == aime_id) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchall() - def put_tech_event(self, aime_id: int, version: int, tech_event_data: Dict) -> Optional[int]: + async def put_tech_event(self, aime_id: int, version: int, tech_event_data: Dict) -> Optional[int]: tech_event_data["user"] = aime_id tech_event_data["version"] = version sql = insert(tech_event).values(**tech_event_data) conflict = sql.on_duplicate_key_update(**tech_event_data) - result = self.execute(conflict) + result = await self.execute(conflict) if result is None: self.logger.warning(f"put_tech_event: Failed to update! aime_id: {aime_id}") return None return result.lastrowid - def put_tech_event_ranking(self, aime_id: int, version: int, tech_event_data: Dict) -> Optional[int]: + async def put_tech_event_ranking(self, aime_id: int, version: int, tech_event_data: Dict) -> Optional[int]: tech_event_data["user"] = aime_id tech_event_data["version"] = version tech_event_data.pop("isRankingRewarded") @@ -662,87 +662,87 @@ class OngekiItemData(BaseData): sql = insert(tech_ranking).values(**tech_event_data) conflict = sql.on_duplicate_key_update(**tech_event_data) - result = self.execute(conflict) + result = await self.execute(conflict) if result is None: self.logger.warning(f"put_tech_event_ranking: Failed to update ranking! aime_id {aime_id}") return None return result.lastrowid - def get_tech_event(self, version: int, aime_id: int) -> Optional[List[Dict]]: + async def get_tech_event(self, version: int, aime_id: int) -> Optional[List[Dict]]: sql = select(tech_event).where(and_(tech_event.c.user == aime_id, tech_event.c.version == version)) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchall() - def get_bosses(self, aime_id: int) -> Optional[List[Dict]]: + async def get_bosses(self, aime_id: int) -> Optional[List[Dict]]: sql = select(boss).where(boss.c.user == aime_id) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchall() - def put_memorychapter( + async def put_memorychapter( self, aime_id: int, memorychapter_data: Dict ) -> Optional[int]: memorychapter_data["user"] = aime_id sql = insert(memorychapter).values(**memorychapter_data) conflict = sql.on_duplicate_key_update(**memorychapter_data) - result = self.execute(conflict) + result = await self.execute(conflict) if result is None: self.logger.warning(f"put_memorychapter: Failed to update! aime_id: {aime_id}") return None return result.lastrowid - def get_memorychapters(self, aime_id: int) -> Optional[List[Dict]]: + async def get_memorychapters(self, aime_id: int) -> Optional[List[Dict]]: sql = select(memorychapter).where(memorychapter.c.user == aime_id) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchall() - def get_user_gacha(self, aime_id: int, gacha_id: int) -> Optional[Row]: + async def get_user_gacha(self, aime_id: int, gacha_id: int) -> Optional[Row]: sql = gacha.select(and_(gacha.c.user == aime_id, gacha.c.gachaId == gacha_id)) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchone() - def get_user_gachas(self, aime_id: int) -> Optional[List[Row]]: + async def get_user_gachas(self, aime_id: int) -> Optional[List[Row]]: sql = gacha.select(gacha.c.user == aime_id) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchall() - def get_user_gacha_supplies(self, aime_id: int) -> Optional[List[Row]]: + async def get_user_gacha_supplies(self, aime_id: int) -> Optional[List[Row]]: sql = gacha_supply.select(gacha_supply.c.user == aime_id) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchall() - def put_user_gacha(self, aime_id: int, gacha_id: int, **data) -> Optional[int]: + async def put_user_gacha(self, aime_id: int, gacha_id: int, **data) -> Optional[int]: sql = insert(gacha).values(user=aime_id, gachaId=gacha_id, **data) conflict = sql.on_duplicate_key_update(user=aime_id, gachaId=gacha_id, **data) - result = self.execute(conflict) + result = await self.execute(conflict) if result is None: self.logger.warning(f"put_user_gacha: Failed to insert! aime_id: {aime_id}") return None return result.lastrowid - def put_user_print_detail( + async def put_user_print_detail( self, aime_id: int, serial_id: str, user_print_data: Dict ) -> Optional[int]: sql = insert(print_detail).values( @@ -750,7 +750,7 @@ class OngekiItemData(BaseData): ) conflict = sql.on_duplicate_key_update(user=aime_id, **user_print_data) - result = self.execute(conflict) + result = await self.execute(conflict) if result is None: self.logger.warning( @@ -760,18 +760,18 @@ class OngekiItemData(BaseData): return result.lastrowid - def get_ranking_event_ranks(self, version: int, aime_id: int) -> Optional[List[Dict]]: + async def get_ranking_event_ranks(self, version: int, aime_id: int) -> Optional[List[Dict]]: # Calculates player rank on GameRequest from server, and sends it back, official spec would rank players in maintenance period, on TODO list sql = select(event_point.c.id, event_point.c.user, event_point.c.eventId, event_point.c.type, func.row_number().over(partition_by=event_point.c.eventId, order_by=event_point.c.point.desc()).label('rank'), event_point.c.date, event_point.c.point).where(event_point.c.version == version) - result = self.execute(sql) + result = await self.execute(sql) if result is None: self.logger.error(f"failed to rank aime_id: {aime_id} ranking event positions") return None return result.fetchall() - def get_tech_event_ranking(self, version: int, aime_id: int) -> Optional[List[Dict]]: + async def get_tech_event_ranking(self, version: int, aime_id: int) -> Optional[List[Dict]]: sql = select(tech_ranking.c.id, tech_ranking.c.user, tech_ranking.c.date, tech_ranking.c.eventId, func.row_number().over(partition_by=tech_ranking.c.eventId, order_by=[tech_ranking.c.totalTechScore.desc(),tech_ranking.c.totalPlatinumScore.desc()]).label('rank'), tech_ranking.c.totalTechScore, tech_ranking.c.totalPlatinumScore).where(tech_ranking.c.version == version) - result = self.execute(sql) + result = await self.execute(sql) if result is None: self.logger.warning(f"aime_id: {aime_id} has no tech ranking ranks") return None diff --git a/titles/ongeki/schema/log.py b/titles/ongeki/schema/log.py index bd5b071..fccd29f 100644 --- a/titles/ongeki/schema/log.py +++ b/titles/ongeki/schema/log.py @@ -39,7 +39,7 @@ session_log = Table( class OngekiLogData(BaseData): - def put_gp_log( + async def put_gp_log( self, aime_id: Optional[int], used_credit: int, @@ -61,7 +61,7 @@ class OngekiLogData(BaseData): currentGP=current_gp, ) - result = self.execute(sql) + result = await self.execute(sql) if result is None: self.logger.warning( f"put_gp_log: Failed to insert GP log! aime_id: {aime_id} kind {kind} pattern {pattern} current_gp {current_gp}" diff --git a/titles/ongeki/schema/profile.py b/titles/ongeki/schema/profile.py index 6071bad..b42a0a3 100644 --- a/titles/ongeki/schema/profile.py +++ b/titles/ongeki/schema/profile.py @@ -246,6 +246,26 @@ rival = Table( mysql_charset="utf8mb4", ) +rating = Table( + "ongeki_profile_rating", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), + Column("version", Integer, nullable=False), + Column("type", String(255), nullable=False), + Column("index", Integer, nullable=False), + Column("musicId", Integer), + Column("difficultId", Integer), + Column("romVersionCode", Integer), + Column("score", Integer), + UniqueConstraint("user", "version", "type", "index", name="ongeki_profile_rating_best_uk"), + mysql_charset="utf8mb4", +) + class OngekiProfileData(BaseData): def __init__(self, cfg: CoreConfig, conn: Connection) -> None: @@ -255,12 +275,12 @@ class OngekiProfileData(BaseData): ) self.date_time_format_short = "%Y-%m-%d" - def get_profile_name(self, aime_id: int, version: int) -> Optional[str]: + async def get_profile_name(self, aime_id: int, version: int) -> Optional[str]: sql = select(profile.c.userName).where( and_(profile.c.user == aime_id, profile.c.version == version) ) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None @@ -270,19 +290,19 @@ class OngekiProfileData(BaseData): return row["userName"] - def get_profile_preview(self, aime_id: int, version: int) -> Optional[Row]: + async def get_profile_preview(self, aime_id: int, version: int) -> Optional[Row]: sql = ( select([profile, option]) .join(option, profile.c.user == option.c.user) .filter(and_(profile.c.user == aime_id, profile.c.version == version)) ) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchone() - def get_profile_data(self, aime_id: int, version: int) -> Optional[Row]: + async def get_profile_data(self, aime_id: int, version: int) -> Optional[Row]: sql = select(profile).where( and_( profile.c.user == aime_id, @@ -290,40 +310,40 @@ class OngekiProfileData(BaseData): ) ) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchone() - def get_profile_options(self, aime_id: int) -> Optional[Row]: + async def get_profile_options(self, aime_id: int) -> Optional[Row]: sql = select(option).where( and_( option.c.user == aime_id, ) ) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchone() - def get_profile_recent_rating(self, aime_id: int) -> Optional[List[Row]]: + async def get_profile_recent_rating(self, aime_id: int) -> Optional[List[Row]]: sql = select(recent_rating).where(recent_rating.c.user == aime_id) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchone() - def get_profile_rating_log(self, aime_id: int) -> Optional[List[Row]]: + async def get_profile_rating_log(self, aime_id: int) -> Optional[List[Row]]: sql = select(rating_log).where(rating_log.c.user == aime_id) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchall() - def get_profile_activity( + async def get_profile_activity( self, aime_id: int, kind: int = None ) -> Optional[List[Row]]: sql = select(activity).where( @@ -333,47 +353,47 @@ class OngekiProfileData(BaseData): ) ) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchall() - def get_kop(self, aime_id: int) -> Optional[List[Row]]: + async def get_kop(self, aime_id: int) -> Optional[List[Row]]: sql = select(kop).where(kop.c.user == aime_id) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchall() - def get_rivals(self, aime_id: int) -> Optional[List[Row]]: + async def get_rivals(self, aime_id: int) -> Optional[List[Row]]: sql = select(rival.c.rivalUserId).where(rival.c.user == aime_id) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchall() - def put_profile_data(self, aime_id: int, version: int, data: Dict) -> Optional[int]: + async def put_profile_data(self, aime_id: int, version: int, data: Dict) -> Optional[int]: data["user"] = aime_id data["version"] = version data.pop("accessCode") sql = insert(profile).values(**data) conflict = sql.on_duplicate_key_update(**data) - result = self.execute(conflict) + result = await self.execute(conflict) if result is None: self.logger.warning(f"put_profile_data: Failed to update! aime_id: {aime_id}") return None return result.lastrowid - def put_profile_options(self, aime_id: int, options_data: Dict) -> Optional[int]: + async def put_profile_options(self, aime_id: int, options_data: Dict) -> Optional[int]: options_data["user"] = aime_id sql = insert(option).values(**options_data) conflict = sql.on_duplicate_key_update(**options_data) - result = self.execute(conflict) + result = await self.execute(conflict) if result is None: self.logger.warning( @@ -382,7 +402,7 @@ class OngekiProfileData(BaseData): return None return result.lastrowid - def put_profile_recent_rating( + async def put_profile_recent_rating( self, aime_id: int, recent_rating_data: List[Dict] ) -> Optional[int]: sql = insert(recent_rating).values( @@ -391,7 +411,7 @@ class OngekiProfileData(BaseData): conflict = sql.on_duplicate_key_update(recentRating=recent_rating_data) - result = self.execute(conflict) + result = await self.execute(conflict) if result is None: self.logger.warning( f"put_profile_recent_rating: failed to update recent rating! aime_id {aime_id}" @@ -399,12 +419,12 @@ class OngekiProfileData(BaseData): return None return result.lastrowid - def put_profile_bp_list( + async def put_profile_bp_list( self, aime_id: int, bp_base_list: List[Dict] ) -> Optional[int]: pass - def put_profile_rating_log( + async def put_profile_rating_log( self, aime_id: int, data_version: str, highest_rating: int ) -> Optional[int]: sql = insert(rating_log).values( @@ -413,7 +433,7 @@ class OngekiProfileData(BaseData): conflict = sql.on_duplicate_key_update(highestRating=highest_rating) - result = self.execute(conflict) + result = await self.execute(conflict) if result is None: self.logger.warning( f"put_profile_rating_log: failed to update rating log! aime_id {aime_id} data_version {data_version} highest_rating {highest_rating}" @@ -421,7 +441,7 @@ class OngekiProfileData(BaseData): return None return result.lastrowid - def put_profile_activity( + async def put_profile_activity( self, aime_id: int, kind: int, @@ -447,7 +467,7 @@ class OngekiProfileData(BaseData): sortNumber=sort_num, param1=p1, param2=p2, param3=p3, param4=p4 ) - result = self.execute(conflict) + result = await self.execute(conflict) if result is None: self.logger.warning( f"put_profile_activity: failed to put activity! aime_id {aime_id} kind {kind} activity_id {activity_id}" @@ -455,7 +475,7 @@ class OngekiProfileData(BaseData): return None return result.lastrowid - def put_profile_region(self, aime_id: int, region: int, date: str) -> Optional[int]: + async def put_profile_region(self, aime_id: int, region: int, date: str) -> Optional[int]: sql = insert(activity).values( user=aime_id, region=region, playCount=1, created=date ) @@ -464,7 +484,7 @@ class OngekiProfileData(BaseData): playCount=activity.c.playCount + 1, ) - result = self.execute(conflict) + result = await self.execute(conflict) if result is None: self.logger.warning( f"put_profile_region: failed to update! aime_id {aime_id} region {region}" @@ -472,46 +492,71 @@ class OngekiProfileData(BaseData): return None return result.lastrowid - def put_training_room(self, aime_id: int, room_detail: Dict) -> Optional[int]: + async def put_training_room(self, aime_id: int, room_detail: Dict) -> Optional[int]: room_detail["user"] = aime_id sql = insert(training_room).values(**room_detail) conflict = sql.on_duplicate_key_update(**room_detail) - result = self.execute(conflict) + result = await self.execute(conflict) if result is None: self.logger.warning(f"put_best_score: Failed to add score! aime_id: {aime_id}") return None return result.lastrowid - def put_kop(self, aime_id: int, kop_data: Dict) -> Optional[int]: + async def put_kop(self, aime_id: int, kop_data: Dict) -> Optional[int]: kop_data["user"] = aime_id sql = insert(kop).values(**kop_data) conflict = sql.on_duplicate_key_update(**kop_data) - result = self.execute(conflict) + result = await self.execute(conflict) if result is None: self.logger.warning(f"put_kop: Failed to add score! aime_id: {aime_id}") return None return result.lastrowid - def put_rival(self, aime_id: int, rival_id: int) -> Optional[int]: + async def put_rival(self, aime_id: int, rival_id: int) -> Optional[int]: sql = insert(rival).values(user=aime_id, rivalUserId=rival_id) conflict = sql.on_duplicate_key_update(rivalUserId=rival_id) - result = self.execute(conflict) + result = await self.execute(conflict) if result is None: self.logger.warning( f"put_rival: failed to update! aime_id: {aime_id}, rival_id: {rival_id}" ) return None return result.lastrowid - def delete_rival(self, aime_id: int, rival_id: int) -> Optional[int]: + + async def delete_rival(self, aime_id: int, rival_id: int) -> Optional[int]: sql = delete(rival).where(rival.c.user==aime_id, rival.c.rivalUserId==rival_id) - result = self.execute(sql) + result = await self.execute(sql) if result is None: self.logger.error(f"delete_rival: failed to delete! aime_id: {aime_id}, rival_id: {rival_id}") else: - return result.rowcount \ No newline at end of file + return result.rowcount + + async def put_profile_rating( + self, + aime_id: int, + version: int, + rating_type: str, + rating_data: List[Dict], + ): + inserted_values = [ + {"user": aime_id, "version": version, "type": rating_type, "index": i, **x} + for (i, x) in enumerate(rating_data) + ] + sql = insert(rating).values(inserted_values) + update_dict = {x.name: x for x in sql.inserted if x.name != "id"} + sql = sql.on_duplicate_key_update(**update_dict) + result = await self.execute(sql) + + if result is None: + self.logger.warn( + f"put_profile_rating_{rating_type}: Could not insert rating entries, aime_id: {aime_id}", + ) + return + + return result.lastrowid diff --git a/titles/ongeki/schema/score.py b/titles/ongeki/schema/score.py index 7c8ce15..6867133 100644 --- a/titles/ongeki/schema/score.py +++ b/titles/ongeki/schema/score.py @@ -30,7 +30,7 @@ score_best = Table( Column("isFullCombo", Boolean, nullable=False), Column("isAllBreake", Boolean, nullable=False), Column("isLock", Boolean, nullable=False), - Column("clearStatus", Boolean, nullable=False), + Column("clearStatus", Integer, nullable=False), Column("isStoryWatched", Boolean, nullable=False), Column("platinumScoreMax", Integer), UniqueConstraint("user", "musicId", "level", name="ongeki_best_score_uk"), @@ -128,52 +128,58 @@ tech_count = Table( class OngekiScoreData(BaseData): - def get_tech_count(self, aime_id: int) -> Optional[List[Dict]]: - return [] + async def get_tech_count(self, aime_id: int) -> Optional[List[Dict]]: + sql = select(tech_count).where(tech_count.c.user == aime_id) + + result = await self.execute(sql) + + if result is None: + return None + return result.fetchall() - def put_tech_count(self, aime_id: int, tech_count_data: Dict) -> Optional[int]: + async def put_tech_count(self, aime_id: int, tech_count_data: Dict) -> Optional[int]: tech_count_data["user"] = aime_id sql = insert(tech_count).values(**tech_count_data) conflict = sql.on_duplicate_key_update(**tech_count_data) - result = self.execute(conflict) + result = await self.execute(conflict) if result is None: self.logger.warning(f"put_tech_count: Failed to update! aime_id: {aime_id}") return None return result.lastrowid - def get_best_scores(self, aime_id: int) -> Optional[List[Dict]]: + async def get_best_scores(self, aime_id: int) -> Optional[List[Dict]]: sql = select(score_best).where(score_best.c.user == aime_id) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchall() - def get_best_score( + async def get_best_score( self, aime_id: int, song_id: int, chart_id: int = None ) -> Optional[List[Dict]]: return [] - def put_best_score(self, aime_id: int, music_detail: Dict) -> Optional[int]: + async def put_best_score(self, aime_id: int, music_detail: Dict) -> Optional[int]: music_detail["user"] = aime_id sql = insert(score_best).values(**music_detail) conflict = sql.on_duplicate_key_update(**music_detail) - result = self.execute(conflict) + result = await self.execute(conflict) if result is None: self.logger.warning(f"put_best_score: Failed to add score! aime_id: {aime_id}") return None return result.lastrowid - def put_playlog(self, aime_id: int, playlog_data: Dict) -> Optional[int]: + async def put_playlog(self, aime_id: int, playlog_data: Dict) -> Optional[int]: playlog_data["user"] = aime_id sql = insert(playlog).values(**playlog_data) - result = self.execute(sql) + result = await self.execute(sql) if result is None: self.logger.warning(f"put_playlog: Failed to add playlog! aime_id: {aime_id}") return None diff --git a/titles/ongeki/schema/static.py b/titles/ongeki/schema/static.py index 695d39a..85a9df4 100644 --- a/titles/ongeki/schema/static.py +++ b/titles/ongeki/schema/static.py @@ -188,26 +188,26 @@ game_point = Table( ) class OngekiStaticData(BaseData): - def put_card(self, version: int, card_id: int, **card_data) -> Optional[int]: + async def put_card(self, version: int, card_id: int, **card_data) -> Optional[int]: sql = insert(cards).values(version=version, cardId=card_id, **card_data) conflict = sql.on_duplicate_key_update(**card_data) - result = self.execute(conflict) + result = await self.execute(conflict) if result is None: self.logger.warning(f"Failed to insert card! card_id {card_id}") return None return result.lastrowid - def get_card(self, version: int, card_id: int) -> Optional[Dict]: + async def get_card(self, version: int, card_id: int) -> Optional[Dict]: sql = cards.select(and_(cards.c.version <= version, cards.c.cardId == card_id)) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchone() - def get_card_by_card_number(self, version: int, card_number: str) -> Optional[Dict]: + async def get_card_by_card_number(self, version: int, card_number: str) -> Optional[Dict]: if not card_number.startswith("[O.N.G.E.K.I.]"): card_number = f"[O.N.G.E.K.I.]{card_number}" @@ -215,36 +215,36 @@ class OngekiStaticData(BaseData): and_(cards.c.version <= version, cards.c.cardNumber == card_number) ) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchone() - def get_card_by_name(self, version: int, name: str) -> Optional[Dict]: + async def get_card_by_name(self, version: int, name: str) -> Optional[Dict]: sql = cards.select(and_(cards.c.version <= version, cards.c.name == name)) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchone() - def get_cards(self, version: int) -> Optional[List[Dict]]: + async def get_cards(self, version: int) -> Optional[List[Dict]]: sql = cards.select(cards.c.version <= version) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchall() - def get_cards_by_rarity(self, version: int, rarity: int) -> Optional[List[Dict]]: + async def get_cards_by_rarity(self, version: int, rarity: int) -> Optional[List[Dict]]: sql = cards.select(and_(cards.c.version <= version, cards.c.rarity == rarity)) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchall() - def put_gacha( + async def put_gacha( self, version: int, gacha_id: int, @@ -268,33 +268,33 @@ class OngekiStaticData(BaseData): **gacha_data, ) - result = self.execute(conflict) + result = await self.execute(conflict) if result is None: self.logger.warning(f"Failed to insert gacha! gacha_id {gacha_id}") return None return result.lastrowid - def get_gacha(self, version: int, gacha_id: int) -> Optional[Dict]: + async def get_gacha(self, version: int, gacha_id: int) -> Optional[Dict]: sql = gachas.select( and_(gachas.c.version <= version, gachas.c.gachaId == gacha_id) ) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchone() - def get_gachas(self, version: int) -> Optional[List[Dict]]: + async def get_gachas(self, version: int) -> Optional[List[Dict]]: sql = gachas.select(gachas.c.version == version).order_by( gachas.c.gachaId.asc() ) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchall() - def put_gacha_card( + async def put_gacha_card( self, gacha_id: int, card_id: int, **gacha_card ) -> Optional[int]: sql = insert(gacha_cards).values(gachaId=gacha_id, cardId=card_id, **gacha_card) @@ -303,21 +303,21 @@ class OngekiStaticData(BaseData): gachaId=gacha_id, cardId=card_id, **gacha_card ) - result = self.execute(conflict) + result = await self.execute(conflict) if result is None: self.logger.warning(f"Failed to insert gacha card! gacha_id {gacha_id}") return None return result.lastrowid - def get_gacha_cards(self, gacha_id: int) -> Optional[List[Dict]]: + async def get_gacha_cards(self, gacha_id: int) -> Optional[List[Dict]]: sql = gacha_cards.select(gacha_cards.c.gachaId == gacha_id) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchall() - def put_event( + async def put_event( self, version: int, event_id: int, event_type: int, event_name: str ) -> Optional[int]: sql = insert(events).values( @@ -332,41 +332,41 @@ class OngekiStaticData(BaseData): name=event_name, ) - result = self.execute(conflict) + result = await self.execute(conflict) if result is None: self.logger.warning(f"Failed to insert event! event_id {event_id}") return None return result.lastrowid - def get_event(self, version: int, event_id: int) -> Optional[List[Dict]]: + async def get_event(self, version: int, event_id: int) -> Optional[List[Dict]]: sql = select(events).where( and_(events.c.version == version, events.c.eventId == event_id) ) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchall() - def get_events(self, version: int) -> Optional[List[Dict]]: + async def get_events(self, version: int) -> Optional[List[Dict]]: sql = select(events).where(events.c.version == version) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchall() - def get_enabled_events(self, version: int) -> Optional[List[Dict]]: + async def get_enabled_events(self, version: int) -> Optional[List[Dict]]: sql = select(events).where( and_(events.c.version == version, events.c.enabled == True) ) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchall() - def put_chart( + async def put_chart( self, version: int, song_id: int, @@ -393,7 +393,7 @@ class OngekiStaticData(BaseData): level=level, ) - result = self.execute(conflict) + result = await self.execute(conflict) if result is None: self.logger.warning( f"Failed to insert chart! song_id: {song_id}, chart_id: {chart_id}" @@ -401,15 +401,15 @@ class OngekiStaticData(BaseData): return None return result.lastrowid - def get_chart( + async def get_chart( self, version: int, song_id: int, chart_id: int = None ) -> Optional[List[Dict]]: pass - def get_music(self, version: int) -> Optional[List[Dict]]: + async def get_music(self, version: int) -> Optional[List[Dict]]: pass - def get_music_chart( + async def get_music_chart( self, version: int, song_id: int, chart_id: int ) -> Optional[List[Row]]: sql = select(music).where( @@ -420,19 +420,19 @@ class OngekiStaticData(BaseData): ) ) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchone() - def get_ranking_list(self, version: int) -> Optional[List[Dict]]: + async def get_ranking_list(self, version: int) -> Optional[List[Dict]]: sql = select(music_ranking.c.musicId.label('id'), music_ranking.c.point, music_ranking.c.userName).where(music_ranking.c.version == version) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchall() - def put_reward(self, version: int, rewardId: int, rewardname: str, itemKind: int, itemId: int) -> Optional[int]: + async def put_reward(self, version: int, rewardId: int, rewardname: str, itemKind: int, itemId: int) -> Optional[int]: sql = insert(rewards).values( version=version, rewardId=rewardId, @@ -443,70 +443,70 @@ class OngekiStaticData(BaseData): conflict = sql.on_duplicate_key_update( rewardname=rewardname, ) - result = self.execute(conflict) + result = await self.execute(conflict) if result is None: self.logger.warning(f"Failed to insert reward! reward_id: {rewardId}") return None return result.lastrowid - def get_reward_list(self, version: int) -> Optional[List[Dict]]: + async def get_reward_list(self, version: int) -> Optional[List[Dict]]: sql = select(rewards).where(rewards.c.version == version) - result = self.execute(sql) + result = await self.execute(sql) if result is None: self.logger.warning(f"Failed to load reward list") return None return result.fetchall() - def get_present_list(self, version: int) -> Optional[List[Dict]]: + async def get_present_list(self, version: int) -> Optional[List[Dict]]: sql = select(present).where(present.c.version == version) - result = self.execute(sql) + result = await self.execute(sql) if result is None: self.logger.warning(f"Failed to load present list") return None return result.fetchall() - def get_tech_music(self, version: int) -> Optional[List[Dict]]: + async def get_tech_music(self, version: int) -> Optional[List[Dict]]: sql = select(tech_music).where(tech_music.c.version == version) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchall() - def put_client_testmode_data(self, region_id: int, client_testmode_data: Dict) -> Optional[List[Dict]]: + async def put_client_testmode_data(self, region_id: int, client_testmode_data: Dict) -> Optional[List[Dict]]: sql = insert(client_testmode).values(regionId=region_id, **client_testmode_data) conflict = sql.on_duplicate_key_update(regionId=region_id, **client_testmode_data) - result = self.execute(conflict) + result = await self.execute(conflict) if result is None: self.logger.warning(f"region_id: {region_id} Failed to update ClientTestMode data"), return None return result.lastrowid - def put_client_setting_data(self, machine_id: int, client_setting_data: Dict) -> Optional[List[Dict]]: + async def put_client_setting_data(self, machine_id: int, client_setting_data: Dict) -> Optional[List[Dict]]: sql = machine.update(machine.c.id == machine_id).values(data=client_setting_data) - result = self.execute(sql) + result = await self.execute(sql) if result is None: self.logger.warning(f"machine_id: {machine_id} Failed to update ClientSetting data"), return None return result.lastrowid - def put_static_game_point_defaults(self) -> Optional[List[Dict]]: + async def put_static_game_point_defaults(self) -> Optional[List[Dict]]: game_point_defaults = [{"type": 0, "cost": 100},{"type": 1, "cost": 230},{"type": 2, "cost": 370},{"type": 3, "cost": 120},{"type": 4, "cost": 240},{"type": 5, "cost": 360}] sql = insert(game_point).values(game_point_defaults) - result = self.execute(sql) + result = await self.execute(sql) if result is None: self.logger.warning(f"Failed to insert default GP table!") return None return result.lastrowid - def get_static_game_point(self) -> Optional[List[Dict]]: + async def get_static_game_point(self) -> Optional[List[Dict]]: sql = select(game_point.c.type, game_point.c.cost, game_point.c.startDate, game_point.c.endDate) - result = self.execute(sql) + result = await self.execute(sql) if result is None: return None return result.fetchall() diff --git a/titles/ongeki/summer.py b/titles/ongeki/summer.py index adc8c0f..c3f2ce0 100644 --- a/titles/ongeki/summer.py +++ b/titles/ongeki/summer.py @@ -11,8 +11,8 @@ class OngekiSummer(OngekiBase): super().__init__(core_cfg, game_cfg) self.version = OngekiConstants.VER_ONGEKI_SUMMER - def handle_get_game_setting_api_request(self, data: Dict) -> Dict: - ret = super().handle_get_game_setting_api_request(data) + async def handle_get_game_setting_api_request(self, data: Dict) -> Dict: + ret = await super().handle_get_game_setting_api_request(data) ret["gameSetting"]["dataVersion"] = "1.10.00" ret["gameSetting"]["onlineDataVersion"] = "1.10.00" return ret diff --git a/titles/ongeki/summerplus.py b/titles/ongeki/summerplus.py index 8b2cd03..576d615 100644 --- a/titles/ongeki/summerplus.py +++ b/titles/ongeki/summerplus.py @@ -11,8 +11,8 @@ class OngekiSummerPlus(OngekiBase): super().__init__(core_cfg, game_cfg) self.version = OngekiConstants.VER_ONGEKI_SUMMER_PLUS - def handle_get_game_setting_api_request(self, data: Dict) -> Dict: - ret = super().handle_get_game_setting_api_request(data) + async def handle_get_game_setting_api_request(self, data: Dict) -> Dict: + ret = await super().handle_get_game_setting_api_request(data) ret["gameSetting"]["dataVersion"] = "1.15.00" ret["gameSetting"]["onlineDataVersion"] = "1.15.00" return ret diff --git a/titles/ongeki/frontend/js/ongeki_scripts.js b/titles/ongeki/templates/js/ongeki_scripts.js similarity index 100% rename from titles/ongeki/frontend/js/ongeki_scripts.js rename to titles/ongeki/templates/js/ongeki_scripts.js diff --git a/titles/ongeki/frontend/ongeki_index.jinja b/titles/ongeki/templates/ongeki_index.jinja similarity index 56% rename from titles/ongeki/frontend/ongeki_index.jinja rename to titles/ongeki/templates/ongeki_index.jinja index b7b5a90..a4c4004 100644 --- a/titles/ongeki/frontend/ongeki_index.jinja +++ b/titles/ongeki/templates/ongeki_index.jinja @@ -1,7 +1,18 @@ -{% extends "core/frontend/index.jinja" %} +{% extends "core/templates/index.jinja" %} {% block content %} + +{% if sesh is defined and sesh["user_id"] > 0 %}


@@ -51,31 +62,29 @@