diff --git a/core/__init__.py b/core/__init__.py index c72ba0a..717de33 100644 --- a/core/__init__.py +++ b/core/__init__.py @@ -4,3 +4,4 @@ from core.aimedb import AimedbFactory from core.title import TitleServlet from core.utils import Utils from core.mucha import MuchaServlet +from core.frontend import FrontendServlet \ No newline at end of file diff --git a/core/data/database.py b/core/data/database.py index 800b5d0..70fc3e0 100644 --- a/core/data/database.py +++ b/core/data/database.py @@ -4,11 +4,14 @@ from sqlalchemy.orm import scoped_session, sessionmaker from sqlalchemy.exc import SQLAlchemyError from sqlalchemy import create_engine from logging.handlers import TimedRotatingFileHandler +from datetime import datetime +import importlib, os, json from hashlib import sha256 from core.config import CoreConfig from core.data.schema import * +from core.utils import Utils class Data: def __init__(self, cfg: CoreConfig) -> None: @@ -28,7 +31,7 @@ class Data: self.arcade = ArcadeData(self.config, self.session) self.card = CardData(self.config, self.session) self.base = BaseData(self.config, self.session) - self.schema_ver_latest = 1 + self.schema_ver_latest = 2 log_fmt_str = "[%(asctime)s] %(levelname)s | Database | %(message)s" log_fmt = logging.Formatter(log_fmt_str) @@ -50,4 +53,1702 @@ class Data: coloredlogs.install(cfg.database.loglevel, logger=self.logger, fmt=log_fmt_str) self.logger.handler_set = True # type: ignore - \ No newline at end of file + 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: + title_db = game_mod.database(self.config) + metadata.create_all(self.__engine.connect()) + + self.base.set_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.schema_ver_latest}") + self.base.set_schema_ver(self.schema_ver_latest) + + 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) + + 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 root, dirs, files in os.walk("./titles"): + for dir in dirs: + if not dir.startswith("__"): + try: + mod = importlib.import_module(f"titles.{dir}") + + try: + title_db = mod.database(self.config) + metadata.drop_all(self.__engine.connect()) + + except Exception as e: + self.logger.warning(f"Could not load database schema from {dir} - {e}") + + 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: int, action: str) -> None: + old_ver = self.base.get_schema_ver(game) + sql = "" + + 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 not os.path.exists(f"core/data/schema/versions/{game.upper()}_{version}_{action}.sql"): + self.logger.error(f"Could not find {action} script {game.upper()}_{version}_{action}.sql in core/data/schema/versions folder") + return + + with open(f"core/data/schema/versions/{game.upper()}_{version}_{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 dump_db(self): + dbname = self.config.database.name + + self.logger.info("Database dumper for use with the reworked schema") + self.logger.info("Dumping users...") + + sql = f"SELECT * FROM `{dbname}`.`user`" + + result = self.base.execute(sql) + if result is None: + self.logger.error("Failed") + return None + users = result.fetchall() + + user_list: List[Dict[str, Any]] = [] + for usr in users: + user_list.append({ + "id": usr["id"], + "username": usr["username"], + "email": usr["email"], + "password": usr["password"], + "permissions": usr["permissions"], + "created_date": datetime.strftime(usr["created_date"], "%Y-%m-%d %H:%M:%S"), + "last_login_date": datetime.strftime(usr["accessed_date"], "%Y-%m-%d %H:%M:%S"), + }) + + self.logger.info(f"Done, found {len(user_list)} users") + with open("dbdump-user.json", "w", encoding="utf-8") as f: + f.write(json.dumps(user_list)) + self.logger.info(f"Saved as dbdump-user.json") + + self.logger.info("Dumping cards...") + + sql = f"SELECT * FROM `{dbname}`.`card`" + + result = self.base.execute(sql) + if result is None: + self.logger.error("Failed") + return None + cards = result.fetchall() + + card_list: List[Dict[str, Any]] = [] + for crd in cards: + card_list.append({ + "id": crd["id"], + "user": crd["user"], + "access_code": crd["access_code"], + "is_locked": crd["is_locked"], + "is_banned": crd["is_banned"], + "created_date": datetime.strftime(crd["created_date"], "%Y-%m-%d %H:%M:%S"), + "last_login_date": datetime.strftime(crd["accessed_date"], "%Y-%m-%d %H:%M:%S"), + }) + + self.logger.info(f"Done, found {len(card_list)} cards") + with open("dbdump-card.json", "w", encoding="utf-8") as f: + f.write(json.dumps(card_list)) + self.logger.info(f"Saved as dbdump-card.json") + + self.logger.info("Dumping arcades...") + + sql = f"SELECT * FROM `{dbname}`.`arcade`" + + result = self.base.execute(sql) + if result is None: + self.logger.error("Failed") + return None + arcades = result.fetchall() + + arcade_list: List[Dict[str, Any]] = [] + for arc in arcades: + arcade_list.append({ + "id": arc["id"], + "name": arc["name"], + "nickname": arc["name"], + "country": None, + "country_id": None, + "state": None, + "city": None, + "region_id": None, + "timezone": None, + }) + + self.logger.info(f"Done, found {len(arcade_list)} arcades") + with open("dbdump-arcade.json", "w", encoding="utf-8") as f: + f.write(json.dumps(arcade_list)) + self.logger.info(f"Saved as dbdump-arcade.json") + + self.logger.info("Dumping machines...") + + sql = f"SELECT * FROM `{dbname}`.`machine`" + + result = self.base.execute(sql) + if result is None: + self.logger.error("Failed") + return None + machines = result.fetchall() + + machine_list: List[Dict[str, Any]] = [] + for mech in machines: + if "country" in mech["data"]: + country = mech["data"]["country"] + else: + country = None + + if "ota_enable" in mech["data"]: + ota_enable = mech["data"]["ota_enable"] + else: + ota_enable = None + + machine_list.append({ + "id": mech["id"], + "arcade": mech["arcade"], + "serial": mech["keychip"], + "game": mech["game"], + "board": None, + "country": country, + "timezone": None, + "ota_enable": ota_enable, + "is_cab": False, + }) + + self.logger.info(f"Done, found {len(machine_list)} machines") + with open("dbdump-machine.json", "w", encoding="utf-8") as f: + f.write(json.dumps(machine_list)) + self.logger.info(f"Saved as dbdump-machine.json") + + self.logger.info("Dumping arcade owners...") + + sql = f"SELECT * FROM `{dbname}`.`arcade_owner`" + + result = self.base.execute(sql) + if result is None: + self.logger.error("Failed") + return None + arcade_owners = result.fetchall() + + owner_list: List[Dict[str, Any]] = [] + for owner in owner_list: + owner_list.append(owner._asdict()) + + self.logger.info(f"Done, found {len(owner_list)} arcade owners") + with open("dbdump-arcade_owner.json", "w", encoding="utf-8") as f: + f.write(json.dumps(owner_list)) + self.logger.info(f"Saved as dbdump-arcade_owner.json") + + self.logger.info("Dumping profiles...") + + sql = f"SELECT * FROM `{dbname}`.`profile`" + + result = self.base.execute(sql) + if result is None: + self.logger.error("Failed") + return None + profiles = result.fetchall() + + profile_list: Dict[List[Dict[str, Any]]] = {} + for pf in profiles: + game = pf["game"] + + if game not in profile_list: + profile_list[game] = [] + + profile_list[game].append({ + "id": pf["id"], + "user": pf["user"], + "version": pf["version"], + "use_count": pf["use_count"], + "name": pf["name"], + "game_id": pf["game_id"], + "mods": pf["mods"], + "data": pf["data"], + }) + + self.logger.info(f"Done, found profiles for {len(profile_list)} games") + with open("dbdump-profile.json", "w", encoding="utf-8") as f: + f.write(json.dumps(profile_list)) + self.logger.info(f"Saved as dbdump-profile.json") + + self.logger.info("Dumping scores...") + + sql = f"SELECT * FROM `{dbname}`.`score`" + + result = self.base.execute(sql) + if result is None: + self.logger.error("Failed") + return None + scores = result.fetchall() + + score_list: Dict[List[Dict[str, Any]]] = {} + for sc in scores: + game = sc["game"] + + if game not in score_list: + score_list[game] = [] + + score_list[game].append({ + "id": sc["id"], + "user": sc["user"], + "version": sc["version"], + "song_id": sc["song_id"], + "chart_id": sc["chart_id"], + "score1": sc["score1"], + "score2": sc["score2"], + "fc1": sc["fc1"], + "fc2": sc["fc2"], + "cleared": sc["cleared"], + "grade": sc["grade"], + "data": sc["data"], + }) + + self.logger.info(f"Done, found scores for {len(score_list)} games") + with open("dbdump-score.json", "w", encoding="utf-8") as f: + f.write(json.dumps(score_list)) + self.logger.info(f"Saved as dbdump-score.json") + + self.logger.info("Dumping achievements...") + + sql = f"SELECT * FROM `{dbname}`.`achievement`" + + result = self.base.execute(sql) + if result is None: + self.logger.error("Failed") + return None + achievements = result.fetchall() + + achievement_list: Dict[List[Dict[str, Any]]] = {} + for ach in achievements: + game = ach["game"] + + if game not in achievement_list: + achievement_list[game] = [] + + achievement_list[game].append({ + "id": ach["id"], + "user": ach["user"], + "version": ach["version"], + "type": ach["type"], + "achievement_id": ach["achievement_id"], + "data": ach["data"], + }) + + self.logger.info(f"Done, found achievements for {len(achievement_list)} games") + with open("dbdump-achievement.json", "w", encoding="utf-8") as f: + f.write(json.dumps(achievement_list)) + self.logger.info(f"Saved as dbdump-achievement.json") + + self.logger.info("Dumping items...") + + sql = f"SELECT * FROM `{dbname}`.`item`" + + result = self.base.execute(sql) + if result is None: + self.logger.error("Failed") + return None + items = result.fetchall() + + item_list: Dict[List[Dict[str, Any]]] = {} + for itm in items: + game = itm["game"] + + if game not in item_list: + item_list[game] = [] + + item_list[game].append({ + "id": itm["id"], + "user": itm["user"], + "version": itm["version"], + "type": itm["type"], + "item_id": itm["item_id"], + "data": ach["data"], + }) + + self.logger.info(f"Done, found items for {len(item_list)} games") + with open("dbdump-item.json", "w", encoding="utf-8") as f: + f.write(json.dumps(item_list)) + self.logger.info(f"Saved as dbdump-item.json") + + def restore_from_old_schema(self): + # Import the tables we expect to be there + from core.data.schema.user import aime_user + from core.data.schema.card import aime_card + from core.data.schema.arcade import arcade, machine, arcade_owner + from sqlalchemy.dialects.mysql import Insert + + # Make sure that all the tables we're trying to access exist + self.create_database() + + # Import the data, making sure that dependencies are accounted for + if os.path.exists("dbdump-user.json"): + users = [] + with open("dbdump-user.json", "r", encoding="utf-8") as f: + users = json.load(f) + + self.logger.info(f"Load {len(users)} users") + + for user in users: + sql = Insert(aime_user).values(**user) + + conflict = sql.on_duplicate_key_update(**user) + + result = self.base.execute(conflict) + if result is None: + self.logger.error(f"Failed to insert user {user['id']}") + continue + self.logger.info(f"Inserted user {user['id']} -> {result.lastrowid}") + + if os.path.exists("dbdump-card.json"): + cards = [] + with open("dbdump-card.json", "r", encoding="utf-8") as f: + cards = json.load(f) + + self.logger.info(f"Load {len(cards)} cards") + + for card in cards: + sql = Insert(aime_card).values(**card) + + conflict = sql.on_duplicate_key_update(**card) + + result = self.base.execute(conflict) + if result is None: + self.logger.error(f"Failed to insert card {card['id']}") + continue + self.logger.info(f"Inserted card {card['id']} -> {result.lastrowid}") + + if os.path.exists("dbdump-arcade.json"): + arcades = [] + with open("dbdump-arcade.json", "r", encoding="utf-8") as f: + arcades = json.load(f) + + self.logger.info(f"Load {len(arcades)} arcades") + + for ac in arcades: + sql = Insert(arcade).values(**ac) + + conflict = sql.on_duplicate_key_update(**ac) + + result = self.base.execute(conflict) + if result is None: + self.logger.error(f"Failed to insert arcade {ac['id']}") + continue + self.logger.info(f"Inserted arcade {ac['id']} -> {result.lastrowid}") + + if os.path.exists("dbdump-arcade_owner.json"): + ac_owners = [] + with open("dbdump-arcade_owner.json", "r", encoding="utf-8") as f: + ac_owners = json.load(f) + + self.logger.info(f"Load {len(ac_owners)} arcade owners") + + for owner in ac_owners: + sql = Insert(arcade_owner).values(**owner) + + conflict = sql.on_duplicate_key_update(**owner) + + result = self.base.execute(conflict) + if result is None: + self.logger.error(f"Failed to insert arcade_owner {owner['user']}") + continue + self.logger.info(f"Inserted arcade_owner {owner['user']} -> {result.lastrowid}") + + if os.path.exists("dbdump-machine.json"): + mechs = [] + with open("dbdump-machine.json", "r", encoding="utf-8") as f: + mechs = json.load(f) + + self.logger.info(f"Load {len(mechs)} machines") + + for mech in mechs: + sql = Insert(machine).values(**mech) + + conflict = sql.on_duplicate_key_update(**mech) + + result = self.base.execute(conflict) + if result is None: + self.logger.error(f"Failed to insert machine {mech['id']}") + continue + self.logger.info(f"Inserted machine {mech['id']} -> {result.lastrowid}") + + # Now the fun part, grabbing all our scores, profiles, items, and achievements and trying + # to conform them to our current, freeform schema. This will be painful... + profiles = {} + items = {} + scores = {} + achievements = {} + + if os.path.exists("dbdump-profile.json"): + with open("dbdump-profile.json", "r", encoding="utf-8") as f: + profiles = json.load(f) + + self.logger.info(f"Load {len(profiles)} profiles") + + if os.path.exists("dbdump-item.json"): + with open("dbdump-item.json", "r", encoding="utf-8") as f: + items = json.load(f) + + self.logger.info(f"Load {len(items)} items") + + if os.path.exists("dbdump-score.json"): + with open("dbdump-score.json", "r", encoding="utf-8") as f: + scores = json.load(f) + + self.logger.info(f"Load {len(scores)} scores") + + if os.path.exists("dbdump-achievement.json"): + with open("dbdump-achievement.json", "r", encoding="utf-8") as f: + achievements = json.load(f) + + self.logger.info(f"Load {len(achievements)} achievements") + + # Chuni / Chusan + if os.path.exists("titles/chuni/schema"): + from titles.chuni.schema.item import character, item, duel, map, map_area + from titles.chuni.schema.profile import profile, profile_ex, option, option_ex + from titles.chuni.schema.profile import recent_rating, activity, charge, emoney + from titles.chuni.schema.profile import overpower + from titles.chuni.schema.score import best_score, course + + chuni_profiles = [] + chuni_items = [] + chuni_scores = [] + + if "SDBT" in profiles: + chuni_profiles = profiles["SDBT"] + if "SDBT" in items: + chuni_items = items["SDBT"] + if "SDBT" in scores: + chuni_scores = scores["SDBT"] + if "SDHD" in profiles: + chuni_profiles += profiles["SDHD"] + if "SDHD" in items: + chuni_items += items["SDHD"] + if "SDHD" in scores: + chuni_scores += scores["SDHD"] + + self.logger.info(f"Importing {len(chuni_profiles)} chunithm/chunithm new profiles") + + for pf in chuni_profiles: + if type(pf["data"]) is not dict: + pf["data"] = json.loads(pf["data"]) + pf_data = pf["data"] + + # data + if "userData" in pf_data: + pf_data["userData"]["userName"] = bytes([ord(c) for c in pf_data["userData"]["userName"]]).decode("utf-8") + pf_data["userData"]["user"] = pf["user"] + pf_data["userData"]["version"] = pf["version"] + pf_data["userData"].pop("accessCode") + + if pf_data["userData"]["lastRomVersion"].startswith("2."): + pf_data["userData"]["version"] += 10 + + pf_data["userData"] = self.base.fix_bools(pf_data["userData"]) + + sql = Insert(profile).values(**pf_data["userData"]) + conflict = sql.on_duplicate_key_update(**pf_data["userData"]) + + result = self.base.execute(conflict) + if result is None: + self.logger.error(f"Failed to insert chuni profile data for {pf['user']}") + continue + self.logger.info(f"Inserted chuni profile for {pf['user']} ->{result.lastrowid}") + + # data_ex + if "userDataEx" in pf_data and len(pf_data["userDataEx"]) > 0: + pf_data["userDataEx"][0]["user"] = pf["user"] + pf_data["userDataEx"][0]["version"] = pf["version"] + + pf_data["userDataEx"] = self.base.fix_bools(pf_data["userDataEx"][0]) + + sql = Insert(profile_ex).values(**pf_data["userDataEx"]) + conflict = sql.on_duplicate_key_update(**pf_data["userDataEx"]) + + result = self.base.execute(conflict) + if result is None: + self.logger.error(f"Failed to insert chuni profile data_ex for {pf['user']}") + continue + self.logger.info(f"Inserted chuni profile data_ex for {pf['user']} ->{result.lastrowid}") + + # option + if "userGameOption" in pf_data: + pf_data["userGameOption"]["user"] = pf["user"] + + pf_data["userGameOption"] = self.base.fix_bools(pf_data["userGameOption"]) + + sql = Insert(option).values(**pf_data["userGameOption"]) + conflict = sql.on_duplicate_key_update(**pf_data["userGameOption"]) + + result = self.base.execute(conflict) + if result is None: + self.logger.error(f"Failed to insert chuni profile options for {pf['user']}") + continue + self.logger.info(f"Inserted chuni profile options for {pf['user']} ->{result.lastrowid}") + + # option_ex + if "userGameOptionEx" in pf_data and len(pf_data["userGameOptionEx"]) > 0: + pf_data["userGameOptionEx"][0]["user"] = pf["user"] + + pf_data["userGameOptionEx"] = self.base.fix_bools(pf_data["userGameOptionEx"][0]) + + sql = Insert(option_ex).values(**pf_data["userGameOptionEx"]) + conflict = sql.on_duplicate_key_update(**pf_data["userGameOptionEx"]) + + result = self.base.execute(conflict) + if result is None: + self.logger.error(f"Failed to insert chuni profile option_ex for {pf['user']}") + continue + self.logger.info(f"Inserted chuni profile option_ex for {pf['user']} ->{result.lastrowid}") + + # recent_rating + if "userRecentRatingList" in pf_data: + rr = { + "user": pf["user"], + "recentRating": pf_data["userRecentRatingList"] + } + + sql = Insert(recent_rating).values(**rr) + conflict = sql.on_duplicate_key_update(**rr) + + result = self.base.execute(conflict) + if result is None: + self.logger.error(f"Failed to insert chuni profile recent_rating for {pf['user']}") + continue + self.logger.info(f"Inserted chuni profile recent_rating for {pf['user']} ->{result.lastrowid}") + + # activity + if "userActivityList" in pf_data: + for act in pf_data["userActivityList"]: + act["user"] = pf["user"] + + sql = Insert(activity).values(**act) + conflict = sql.on_duplicate_key_update(**act) + + result = self.base.execute(conflict) + if result is None: + self.logger.error(f"Failed to insert chuni profile activity for {pf['user']}") + else: + self.logger.info(f"Inserted chuni profile activity for {pf['user']} ->{result.lastrowid}") + + # charge + if "userChargeList" in pf_data: + for cg in pf_data["userChargeList"]: + cg["user"] = pf["user"] + + cg = self.base.fix_bools(cg) + + sql = Insert(charge).values(**cg) + conflict = sql.on_duplicate_key_update(**cg) + + result = self.base.execute(conflict) + if result is None: + self.logger.error(f"Failed to insert chuni profile charge for {pf['user']}") + else: + self.logger.info(f"Inserted chuni profile charge for {pf['user']} ->{result.lastrowid}") + + # emoney + if "userEmoneyList" in pf_data: + for emon in pf_data["userEmoneyList"]: + emon["user"] = pf["user"] + + sql = Insert(emoney).values(**emon) + conflict = sql.on_duplicate_key_update(**emon) + + result = self.base.execute(conflict) + if result is None: + self.logger.error(f"Failed to insert chuni profile emoney for {pf['user']}") + else: + self.logger.info(f"Inserted chuni profile emoney for {pf['user']} ->{result.lastrowid}") + + # overpower + if "userOverPowerList" in pf_data: + for op in pf_data["userOverPowerList"]: + op["user"] = pf["user"] + + sql = Insert(overpower).values(**op) + conflict = sql.on_duplicate_key_update(**op) + + result = self.base.execute(conflict) + if result is None: + self.logger.error(f"Failed to insert chuni profile overpower for {pf['user']}") + else: + self.logger.info(f"Inserted chuni profile overpower for {pf['user']} ->{result.lastrowid}") + + # map_area + if "userMapAreaList" in pf_data: + for ma in pf_data["userMapAreaList"]: + ma["user"] = pf["user"] + + ma = self.base.fix_bools(ma) + + sql = Insert(map_area).values(**ma) + conflict = sql.on_duplicate_key_update(**ma) + + result = self.base.execute(conflict) + if result is None: + self.logger.error(f"Failed to insert chuni map_area for {pf['user']}") + else: + self.logger.info(f"Inserted chuni map_area for {pf['user']} ->{result.lastrowid}") + + #duel + if "userDuelList" in pf_data: + for ma in pf_data["userDuelList"]: + ma["user"] = pf["user"] + + ma = self.base.fix_bools(ma) + + sql = Insert(duel).values(**ma) + conflict = sql.on_duplicate_key_update(**ma) + + result = self.base.execute(conflict) + if result is None: + self.logger.error(f"Failed to insert chuni duel for {pf['user']}") + else: + self.logger.info(f"Inserted chuni duel for {pf['user']} ->{result.lastrowid}") + + # map + if "userMapList" in pf_data: + for ma in pf_data["userMapList"]: + ma["user"] = pf["user"] + + ma = self.base.fix_bools(ma) + + sql = Insert(map).values(**ma) + conflict = sql.on_duplicate_key_update(**ma) + + result = self.base.execute(conflict) + if result is None: + self.logger.error(f"Failed to insert chuni map for {pf['user']}") + else: + self.logger.info(f"Inserted chuni map for {pf['user']} ->{result.lastrowid}") + + self.logger.info(f"Importing {len(chuni_items)} chunithm/chunithm new items") + + for i in chuni_items: + if type(i["data"]) is not dict: + i["data"] = json.loads(i["data"]) + i_data = i["data"] + + i_data["user"] = i["user"] + + i_data = self.base.fix_bools(i_data) + + try: i_data.pop("assignIllust") + except: pass + + try: i_data.pop("exMaxLv") + except: pass + + if i["type"] == 20: #character + sql = Insert(character).values(**i_data) + else: + sql = Insert(item).values(**i_data) + + conflict = sql.on_duplicate_key_update(**i_data) + + result = self.base.execute(conflict) + if result is None: + self.logger.error(f"Failed to insert chuni item for user {i['user']}") + + else: + self.logger.info(f"Inserted chuni item for user {i['user']} {i['item_id']} -> {result.lastrowid}") + + self.logger.info(f"Importing {len(chuni_scores)} chunithm/chunithm new scores") + + for sc in chuni_scores: + if type(sc["data"]) is not dict: + sc["data"] = json.loads(sc["data"]) + + score_data = self.base.fix_bools(sc["data"]) + + try: score_data.pop("theoryCount") + except: pass + + try: score_data.pop("ext1") + except: pass + + score_data["user"] = sc["user"] + + sql = Insert(best_score).values(**score_data) + conflict = sql.on_duplicate_key_update(**score_data) + + result = self.base.execute(conflict) + if result is None: + self.logger.error(f"Failed to put chuni score for user {sc['user']}") + else: + self.logger.info(f"Inserted chuni score for user {sc['user']} {sc['song_id']}/{sc['chart_id']} -> {result.lastrowid}") + + else: + self.logger.info(f"Chuni/Chusan not found, skipping...") + + # CXB + if os.path.exists("titles/cxb/schema"): + from titles.cxb.schema.item import energy + from titles.cxb.schema.profile import profile + from titles.cxb.schema.score import score, ranking + + cxb_profiles = [] + cxb_items = [] + cxb_scores = [] + + if "SDCA" in profiles: + cxb_profiles = profiles["SDCA"] + if "SDCA" in items: + cxb_items = items["SDCA"] + if "SDCA" in scores: + cxb_scores = scores["SDCA"] + + self.logger.info(f"Importing {len(cxb_profiles)} CXB profiles") + + for pf in cxb_profiles: + user = pf["user"] + version = pf["version"] + pf_data = pf["data"]["data"] + pf_idx = pf["data"]["index"] + + for x in range(len(pf_data)): + sql = Insert(profile).values( + user = user, + version = version, + index = int(pf_idx[x]), + data = json.loads(pf_data[x]) if type(pf_data[x]) is not dict else pf_data[x] + ) + + conflict = sql.on_duplicate_key_update( + user = user, + version = version, + index = int(pf_idx[x]), + data = json.loads(pf_data[x]) if type(pf_data[x]) is not dict else pf_data[x] + ) + result = self.base.execute(conflict) + if result is None: + self.logger.error(f"Failed to insert CXB profile for user {user} Index {pf_idx[x]}") + + self.logger.info(f"Importing {len(cxb_scores)} CXB scores") + + for sc in cxb_scores: + user = sc["user"] + version = sc["version"] + mcode = sc["data"]["mcode"] + index = sc["data"]["index"] + + sql = Insert(score).values( + user = user, + game_version = version, + song_mcode = mcode, + song_index = index, + data = sc["data"] + ) + + conflict = sql.on_duplicate_key_update( + user = user, + game_version = version, + song_mcode = mcode, + song_index = index, + data = sc["data"] + ) + + result = self.base.execute(conflict) + if result is None: + self.logger.error(f"Failed to insert CXB score for user {user} mcode {mcode}") + + self.logger.info(f"Importing {len(cxb_items)} CXB items") + + for it in cxb_items: + user = it["user"] + + if it["type"] == 3: # energy + sql = Insert(energy).values( + user = user, + energy = it["data"]["total"] + ) + + conflict = sql.on_duplicate_key_update( + user = user, + energy = it["data"]["total"] + ) + + result = self.base.execute(conflict) + if result is None: + self.logger.error(f"Failed to insert CXB energy for user {user}") + + elif it["type"] == 2: + sql = Insert(ranking).values( + user = user, + rev_id = it["data"]["rid"], + song_id = it["data"]["sc"][1] if len(it["data"]["sc"]) > 1 else None, + score = it["data"]["sc"][0], + clear = it["data"]["clear"], + ) + + conflict = sql.on_duplicate_key_update( + user = user, + rev_id = it["data"]["rid"], + song_id = it["data"]["sc"][1] if len(it["data"]["sc"]) > 1 else None, + score = it["data"]["sc"][0], + clear = it["data"]["clear"], + ) + + result = self.base.execute(conflict) + if result is None: + self.logger.error(f"Failed to insert CXB ranking for user {user}") + + else: + self.logger.error(f"Unknown CXB item type {it['type']} for user {user}") + + else: + self.logger.info(f"CXB not found, skipping...") + + # Diva + if os.path.exists("titles/diva/schema"): + from titles.diva.schema.profile import profile + from titles.diva.schema.score import score + from titles.diva.schema.item import shop + + diva_profiles = [] + diva_scores = [] + + if "SBZV" in profiles: + diva_profiles = profiles["SBZV"] + if "SBZV" in scores: + diva_scores = scores["SBZV"] + + self.logger.info(f"Importing {len(diva_profiles)} Diva profiles") + + for pf in diva_profiles: + pf["data"]["user"] = pf["user"] + pf["data"]["version"] = pf["version"] + pf_data = pf["data"] + + if "mdl_eqp_ary" in pf["data"]: + sql = Insert(shop).values( + user = user, + version = version, + mdl_eqp_ary = pf["data"]["mdl_eqp_ary"], + ) + conflict = sql.on_duplicate_key_update( + user = user, + version = version, + mdl_eqp_ary = pf["data"]["mdl_eqp_ary"] + ) + self.base.execute(conflict) + pf["data"].pop("mdl_eqp_ary") + + sql = Insert(profile).values(**pf_data) + conflict = sql.on_duplicate_key_update(**pf_data) + result = self.base.execute(conflict) + if result is None: + self.logger.error(f"Failed to insert diva profile for {pf['user']}") + + self.logger.info(f"Importing {len(diva_scores)} Diva scores") + + for sc in diva_scores: + user = sc["user"] + + clr_kind = -1 + for x in sc["data"]["stg_clr_kind"].split(","): + if x != "-1": + clr_kind = x + + cool_ct = 0 + for x in sc["data"]["stg_cool_cnt"].split(","): + if x != "0": + cool_ct = x + + fine_ct = 0 + for x in sc["data"]["stg_fine_cnt"].split(","): + if x != "0": + fine_ct = x + + safe_ct = 0 + for x in sc["data"]["stg_safe_cnt"].split(","): + if x != "0": + safe_ct = x + + sad_ct = 0 + for x in sc["data"]["stg_sad_cnt"].split(","): + if x != "0": + sad_ct = x + + worst_ct = 0 + for x in sc["data"]["stg_wt_wg_cnt"].split(","): + if x != "0": + worst_ct = x + + max_cmb = 0 + for x in sc["data"]["stg_max_cmb"].split(","): + if x != "0": + max_cmb = x + + sql = Insert(score).values( + user = user, + version = sc["version"], + pv_id = sc["song_id"], + difficulty = sc["chart_id"], + score = sc["score1"], + atn_pnt = sc["score2"], + clr_kind = clr_kind, + sort_kind = sc["data"]["sort_kind"], + cool = cool_ct, + fine = fine_ct, + safe = safe_ct, + sad = sad_ct, + worst = worst_ct, + max_combo = max_cmb, + ) + + conflict = sql.on_duplicate_key_update(user = user, + version = sc["version"], + pv_id = sc["song_id"], + difficulty = sc["chart_id"], + score = sc["score1"], + atn_pnt = sc["score2"], + clr_kind = clr_kind, + sort_kind = sc["data"]["sort_kind"], + cool = cool_ct, + fine = fine_ct, + safe = safe_ct, + sad = sad_ct, + worst = worst_ct, + max_combo = max_cmb + ) + + result = self.base.execute(conflict) + if result is None: + self.logger.error(f"Failed to insert diva score for {pf['user']}") + + else: + self.logger.info(f"Diva not found, skipping...") + + # Ongeki + if os.path.exists("titles/ongeki/schema"): + from titles.ongeki.schema.item import card, deck, character, boss, story + from titles.ongeki.schema.item import chapter, item, music_item, login_bonus + from titles.ongeki.schema.item import event_point, mission_point, scenerio + from titles.ongeki.schema.item import trade_item, event_music, tech_event + from titles.ongeki.schema.profile import profile, option, activity, recent_rating + from titles.ongeki.schema.profile import rating_log, training_room, kop + from titles.ongeki.schema.score import score_best, tech_count, playlog + from titles.ongeki.schema.log import session_log + + item_types = { + "character": 20, + "story": 21, + "card": 22, + "deck": 23, + "login": 24, + "chapter": 25 + } + + ongeki_profiles = [] + ongeki_items = [] + ongeki_scores = [] + + if "SDDT" in profiles: + ongeki_profiles = profiles["SDDT"] + if "SDDT" in items: + ongeki_items = items["SDDT"] + if "SDDT" in scores: + ongeki_scores = scores["SDDT"] + + self.logger.info(f"Importing {len(ongeki_profiles)} ongeki profiles") + + for pf in ongeki_profiles: + user = pf["user"] + version = pf["version"] + pf_data = pf["data"] + + pf_data["userData"]["user"] = user + pf_data["userData"]["version"] = version + pf_data["userData"].pop("accessCode") + + sql = Insert(profile).values(**pf_data["userData"]) + conflict = sql.on_duplicate_key_update(**pf_data["userData"]) + result = self.base.execute(conflict) + if result is None: + self.logger.error(f"Failed to insert ongeki profile data for user {pf['user']}") + continue + + pf_data["userOption"]["user"] = user + + sql = Insert(option).values(**pf_data["userOption"]) + conflict = sql.on_duplicate_key_update(**pf_data["userOption"]) + result = self.base.execute(conflict) + if result is None: + self.logger.error(f"Failed to insert ongeki profile options for user {pf['user']}") + continue + + for pf_list in pf_data["userActivityList"]: + pf_list["user"] = user + + sql = Insert(activity).values(**pf_list) + conflict = sql.on_duplicate_key_update(**pf_list) + result = self.base.execute(conflict) + if result is None: + self.logger.error(f"Failed to insert ongeki profile activity for user {pf['user']}") + continue + + sql = Insert(recent_rating).values( + user = user, + recentRating = pf_data["userRecentRatingList"] + ) + + conflict = sql.on_duplicate_key_update( + user = user, + recentRating = pf_data["userRecentRatingList"] + ) + result = self.base.execute(conflict) + + if result is None: + self.logger.error(f"Failed to insert ongeki profile recent rating for user {pf['user']}") + continue + + for pf_list in pf_data["userRatinglogList"]: + pf_list["user"] = user + + sql = Insert(rating_log).values(**pf_list) + + conflict = sql.on_duplicate_key_update(**pf_list) + result = self.base.execute(conflict) + if result is None: + self.logger.error(f"Failed to insert ongeki profile rating log for user {pf['user']}") + continue + + for pf_list in pf_data["userTrainingRoomList"]: + pf_list["user"] = user + + sql = Insert(training_room).values(**pf_list) + conflict = sql.on_duplicate_key_update(**pf_list) + result = self.base.execute(conflict) + if result is None: + self.logger.error(f"Failed to insert ongeki profile training room for user {pf['user']}") + continue + + if "userKopList" in pf_data: + for pf_list in pf_data["userKopList"]: + pf_list["user"] = user + + sql = Insert(kop).values(**pf_list) + conflict = sql.on_duplicate_key_update(**pf_list) + result = self.base.execute(conflict) + if result is None: + self.logger.error(f"Failed to insert ongeki profile training room for user {pf['user']}") + continue + + for pf_list in pf_data["userBossList"]: + pf_list["user"] = user + + sql = Insert(boss).values(**pf_list) + conflict = sql.on_duplicate_key_update(**pf_list) + result = self.base.execute(conflict) + if result is None: + self.logger.error(f"Failed to insert ongeki item boss for user {pf['user']}") + continue + + for pf_list in pf_data["userDeckList"]: + pf_list["user"] = user + + sql = Insert(deck).values(**pf_list) + conflict = sql.on_duplicate_key_update(**pf_list) + result = self.base.execute(conflict) + if result is None: + self.logger.error(f"Failed to insert ongeki item deck for user {pf['user']}") + continue + + for pf_list in pf_data["userStoryList"]: + pf_list["user"] = user + + sql = Insert(story).values(**pf_list) + conflict = sql.on_duplicate_key_update(**pf_list) + result = self.base.execute(conflict) + if result is None: + self.logger.error(f"Failed to insert ongeki item story for user {pf['user']}") + continue + + for pf_list in pf_data["userChapterList"]: + pf_list["user"] = user + + sql = Insert(chapter).values(**pf_list) + conflict = sql.on_duplicate_key_update(**pf_list) + result = self.base.execute(conflict) + if result is None: + self.logger.error(f"Failed to insert ongeki item chapter for user {pf['user']}") + continue + + for pf_list in pf_data["userPlaylogList"]: + pf_list["user"] = user + + sql = Insert(playlog).values(**pf_list) + conflict = sql.on_duplicate_key_update(**pf_list) + result = self.base.execute(conflict) + if result is None: + self.logger.error(f"Failed to insert ongeki score playlog for user {pf['user']}") + continue + + for pf_list in pf_data["userMusicItemList"]: + pf_list["user"] = user + + sql = Insert(music_item).values(**pf_list) + conflict = sql.on_duplicate_key_update(**pf_list) + result = self.base.execute(conflict) + if result is None: + self.logger.error(f"Failed to insert ongeki item music item for user {pf['user']}") + continue + + for pf_list in pf_data["userTechCountList"]: + pf_list["user"] = user + + sql = Insert(tech_count).values(**pf_list) + conflict = sql.on_duplicate_key_update(**pf_list) + result = self.base.execute(conflict) + if result is None: + self.logger.error(f"Failed to insert ongeki item tech count for user {pf['user']}") + continue + + if "userTechEventList" in pf_data: + for pf_list in pf_data["userTechEventList"]: + pf_list["user"] = user + + sql = Insert(tech_event).values(**pf_list) + conflict = sql.on_duplicate_key_update(**pf_list) + result = self.base.execute(conflict) + if result is None: + self.logger.error(f"Failed to insert ongeki item tech event for user {pf['user']}") + continue + + for pf_list in pf_data["userTradeItemList"]: + pf_list["user"] = user + + sql = Insert(trade_item).values(**pf_list) + conflict = sql.on_duplicate_key_update(**pf_list) + result = self.base.execute(conflict) + if result is None: + self.logger.error(f"Failed to insert ongeki item trade item for user {pf['user']}") + continue + + if "userEventMusicList" in pf_data: + for pf_list in pf_data["userEventMusicList"]: + pf_list["user"] = user + + sql = Insert(event_music).values(**pf_list) + conflict = sql.on_duplicate_key_update(**pf_list) + result = self.base.execute(conflict) + if result is None: + self.logger.error(f"Failed to insert ongeki item event music for user {pf['user']}") + continue + + if "userEventPointList" in pf_data: + for pf_list in pf_data["userEventPointList"]: + pf_list["user"] = user + + sql = Insert(event_point).values(**pf_list) + conflict = sql.on_duplicate_key_update(**pf_list) + result = self.base.execute(conflict) + if result is None: + self.logger.error(f"Failed to insert ongeki item event point for user {pf['user']}") + continue + + for pf_list in pf_data["userLoginBonusList"]: + pf_list["user"] = user + + sql = Insert(login_bonus).values(**pf_list) + conflict = sql.on_duplicate_key_update(**pf_list) + result = self.base.execute(conflict) + if result is None: + self.logger.error(f"Failed to insert ongeki item login bonus for user {pf['user']}") + continue + + for pf_list in pf_data["userMissionPointList"]: + pf_list["user"] = user + + sql = Insert(mission_point).values(**pf_list) + conflict = sql.on_duplicate_key_update(**pf_list) + result = self.base.execute(conflict) + if result is None: + self.logger.error(f"Failed to insert ongeki item mission point for user {pf['user']}") + continue + + for pf_list in pf_data["userScenarioList"]: + pf_list["user"] = user + + sql = Insert(scenerio).values(**pf_list) + conflict = sql.on_duplicate_key_update(**pf_list) + result = self.base.execute(conflict) + if result is None: + self.logger.error(f"Failed to insert ongeki item scenerio for user {pf['user']}") + continue + + if "userSessionlogList" in pf_data: + for pf_list in pf_data["userSessionlogList"]: + pf_list["user"] = user + + sql = Insert(session_log).values(**pf_list) + conflict = sql.on_duplicate_key_update(**pf_list) + result = self.base.execute(conflict) + if result is None: + self.logger.error(f"Failed to insert ongeki log session for user {pf['user']}") + continue + + self.logger.info(f"Importing {len(ongeki_items)} ongeki items") + + for it in ongeki_items: + user = it["user"] + it_type = it["type"] + it_id = it["item_id"] + it_data = it["data"] + it_data["user"] = user + + if it_type == item_types["character"] and "characterId" in it_data: + sql = Insert(character).values(**it_data) + + elif it_type == item_types["story"]: + sql = Insert(story).values(**it_data) + + elif it_type == item_types["card"]: + sql = Insert(card).values(**it_data) + + elif it_type == item_types["deck"]: + sql = Insert(deck).values(**it_data) + + elif it_type == item_types["login"]: # login bonus + sql = Insert(login_bonus).values(**it_data) + + elif it_type == item_types["chapter"]: + sql = Insert(chapter).values(**it_data) + + else: + sql = Insert(item).values(**it_data) + + conflict = sql.on_duplicate_key_update(**it_data) + result = self.base.execute(conflict) + if result is None: + self.logger.error(f"Failed to insert ongeki item {it_id} kind {it_type} for user {user}") + + self.logger.info(f"Importing {len(ongeki_scores)} ongeki scores") + + for sc in ongeki_scores: + user = sc["user"] + sc_data = sc["data"] + sc_data["user"] = user + + sql = Insert(score_best).values(**sc_data) + conflict = sql.on_duplicate_key_update(**sc_data) + + result = self.base.execute(conflict) + if result is None: + self.logger.error(f"Failed to insert ongeki score for user {user}: {sc['song_id']}/{sc['chart_id']}") + + else: + self.logger.info(f"Ongeki not found, skipping...") + + # Wacca + if os.path.exists("titles/wacca/schema"): + from titles.wacca.schema.profile import profile, option, bingo, gate, favorite + from titles.wacca.schema.item import item, ticket, song_unlock, trophy + from titles.wacca.schema.score import best_score, stageup + from titles.wacca.reverse import WaccaReverse + from titles.wacca.const import WaccaConstants + + default_opts = WaccaReverse.OPTIONS_DEFAULTS + opts = WaccaConstants.OPTIONS + item_types = WaccaConstants.ITEM_TYPES + + wacca_profiles = [] + wacca_items = [] + wacca_scores = [] + wacca_achievements = [] + + if "SDFE" in profiles: + wacca_profiles = profiles["SDFE"] + if "SDFE" in items: + wacca_items = items["SDFE"] + if "SDFE" in scores: + wacca_scores = scores["SDFE"] + if "SDFE" in achievements: + wacca_achievements = achievements["SDFE"] + + self.logger.info(f"Importing {len(wacca_profiles)} wacca profiles") + + for pf in wacca_profiles: + if pf["version"] == 0 or pf["version"] == 1: + season = 1 + elif pf["version"] == 2 or pf["version"] == 3: + season = 2 + elif pf["version"] >= 4: + season = 3 + + if type(pf["data"]) is not dict: + pf["data"] = json.loads(pf["data"]) + + try: + sql = Insert(profile).values( + id = pf["id"], + user = pf["user"], + version = pf["version"], + season = season, + username = pf["data"]["profile"]["username"] if "username" in pf["data"]["profile"] else pf["name"], + xp = pf["data"]["profile"]["xp"], + xp_season = pf["data"]["profile"]["xp"], + wp = pf["data"]["profile"]["wp"], + wp_season = pf["data"]["profile"]["wp"], + wp_total = pf["data"]["profile"]["total_wp_gained"], + dan_type = pf["data"]["profile"]["dan_type"], + dan_level = pf["data"]["profile"]["dan_level"], + title_0 = pf["data"]["profile"]["title_part_ids"][0], + title_1 = pf["data"]["profile"]["title_part_ids"][1], + title_2 = pf["data"]["profile"]["title_part_ids"][2], + rating = pf["data"]["profile"]["rating"], + vip_expire_time = datetime.fromtimestamp(pf["data"]["profile"]["vip_expire_time"]) if "vip_expire_time" in pf["data"]["profile"] else None, + login_count = pf["use_count"], + playcount_single = pf["use_count"], + playcount_single_season = pf["use_count"], + last_game_ver = pf["data"]["profile"]["last_game_ver"], + last_song_id = pf["data"]["profile"]["last_song_info"][0] if "last_song_info" in pf["data"]["profile"] else 0, + last_song_difficulty = pf["data"]["profile"]["last_song_info"][1] if "last_song_info" in pf["data"]["profile"] else 0, + last_folder_order = pf["data"]["profile"]["last_song_info"][2] if "last_song_info" in pf["data"]["profile"] else 0, + last_folder_id = pf["data"]["profile"]["last_song_info"][3] if "last_song_info" in pf["data"]["profile"] else 0, + last_song_order = pf["data"]["profile"]["last_song_info"][4] if "last_song_info" in pf["data"]["profile"] else 0, + last_login_date = datetime.fromtimestamp(pf["data"]["profile"]["last_login_timestamp"]), + ) + + conflict = sql.on_duplicate_key_update( + id = pf["id"], + user = pf["user"], + version = pf["version"], + season = season, + username = pf["data"]["profile"]["username"] if "username" in pf["data"]["profile"] else pf["name"], + xp = pf["data"]["profile"]["xp"], + xp_season = pf["data"]["profile"]["xp"], + wp = pf["data"]["profile"]["wp"], + wp_season = pf["data"]["profile"]["wp"], + wp_total = pf["data"]["profile"]["total_wp_gained"], + dan_type = pf["data"]["profile"]["dan_type"], + dan_level = pf["data"]["profile"]["dan_level"], + title_0 = pf["data"]["profile"]["title_part_ids"][0], + title_1 = pf["data"]["profile"]["title_part_ids"][1], + title_2 = pf["data"]["profile"]["title_part_ids"][2], + rating = pf["data"]["profile"]["rating"], + vip_expire_time = datetime.fromtimestamp(pf["data"]["profile"]["vip_expire_time"]) if "vip_expire_time" in pf["data"]["profile"] else None, + login_count = pf["use_count"], + playcount_single = pf["use_count"], + playcount_single_season = pf["use_count"], + last_game_ver = pf["data"]["profile"]["last_game_ver"], + last_song_id = pf["data"]["profile"]["last_song_info"][0] if "last_song_info" in pf["data"]["profile"] else 0, + last_song_difficulty = pf["data"]["profile"]["last_song_info"][1] if "last_song_info" in pf["data"]["profile"] else 0, + last_folder_order = pf["data"]["profile"]["last_song_info"][2] if "last_song_info" in pf["data"]["profile"] else 0, + last_folder_id = pf["data"]["profile"]["last_song_info"][3] if "last_song_info" in pf["data"]["profile"] else 0, + last_song_order = pf["data"]["profile"]["last_song_info"][4] if "last_song_info" in pf["data"]["profile"] else 0, + last_login_date = datetime.fromtimestamp(pf["data"]["profile"]["last_login_timestamp"]), + ) + + result = self.base.execute(conflict) + if result is None: + self.logger.error(f"Failed to insert wacca profile for user {pf['user']}") + continue + + for opt, val in pf["data"]["option"].items(): + if val != default_opts[opt]: + opt_id = opts[opt] + sql = Insert(option).values( + user = pf["user"], + opt_id = opt_id, + value = val, + ) + + conflict = sql.on_duplicate_key_update( + user = pf["user"], + opt_id = opt_id, + value = val, + ) + result = self.base.execute(conflict) + if result is None: + self.logger.error(f"Failed to insert wacca option for user {pf['user']} {opt} -> {val}") + + except KeyError as e: + self.logger.warn(f"Outdated wacca profile, skipping: {e}") + + if "gate" in pf["data"]: + for profile_gate in pf["data"]["gate"]: + sql = Insert(gate).values( + user = pf["user"], + gate_id = profile_gate["id"], + page = profile_gate["page"], + loops = profile_gate["loops"], + progress = profile_gate["progress"], + last_used = datetime.fromtimestamp(profile_gate["last_used"]), + mission_flag = profile_gate["mission_flag"], + total_points = profile_gate["total_points"], + ) + + conflict = sql.on_duplicate_key_update( + user = pf["user"], + gate_id = profile_gate["id"], + page = profile_gate["page"], + loops = profile_gate["loops"], + progress = profile_gate["progress"], + last_used = datetime.fromtimestamp(profile_gate["last_used"]), + mission_flag = profile_gate["mission_flag"], + total_points = profile_gate["total_points"], + ) + result = self.base.execute(conflict) + if result is None: + self.logger.error(f"Failed to insert wacca gate for user {pf['user']} -> {profile_gate['id']}") + continue + + if "favorite" in pf["data"]: + for profile_favorite in pf["data"]["favorite"]: + sql = Insert(favorite).values( + user = pf["user"], + song_id = profile_favorite + ) + + conflict = sql.on_duplicate_key_update( + user = pf["user"], + song_id = profile_favorite + ) + result = self.base.execute(conflict) + if result is None: + self.logger.error(f"Failed to insert wacca favorite songs for user {pf['user']} -> {profile_favorite}") + continue + + for it in wacca_items: + user = it["user"] + item_type = it["type"] + item_id = it["item_id"] + + if type(it["data"]) is not dict: + it["data"] = json.loads(it["data"]) + + if item_type == item_types["ticket"]: + if "quantity" in it["data"]: + for x in range(it["data"]["quantity"]): + sql = Insert(ticket).values( + user = user, + ticket_id = item_id, + ) + + conflict = sql.on_duplicate_key_update( + user = user, + ticket_id = item_id, + ) + result = self.base.execute(conflict) + if result is None: + self.logger.warn(f"Wacca: Failed to insert ticket {item_id} for user {user}") + + elif item_type == item_types["music_unlock"] or item_type == item_types["music_difficulty_unlock"]: + diff = 0 + if "difficulty" in it["data"]: + for x in it["data"]["difficulty"]: + if x == 1: + diff += 1 + else: + break + + sql = Insert(song_unlock).values( + user = user, + song_id = item_id, + highest_difficulty = diff, + ) + + conflict = sql.on_duplicate_key_update( + user = user, + song_id = item_id, + highest_difficulty = diff, + ) + result = self.base.execute(conflict) + if result is None: + self.logger.warn(f"Wacca: Failed to insert song unlock {item_id} {diff} for user {user}") + + elif item_type == item_types["trophy"]: + season = int(item_id / 100000) + sql = Insert(trophy).values( + user = user, + trophy_id = item_id, + season = season, + progress = 0 if "progress" not in it["data"] else it["data"]["progress"], + badge_type = 0 # ??? + ) + + conflict = sql.on_duplicate_key_update( + user = user, + trophy_id = item_id, + season = season, + progress = 0 if "progress" not in it["data"] else it["data"]["progress"], + badge_type = 0 # ??? + ) + result = self.base.execute(conflict) + if result is None: + self.logger.warn(f"Wacca: Failed to insert trophy {item_id} for user {user}") + + else: + sql = Insert(item).values( + user = user, + item_id = item_id, + type = item_type, + acquire_date = datetime.fromtimestamp(it["data"]["obtainedDate"]) if "obtainedDate" in it["data"] else datetime.now(), + use_count = it["data"]["uses"] if "uses" in it["data"] else 0, + use_count_season = it["data"]["season_uses"] if "season_uses" in it["data"] else 0 + ) + + conflict = sql.on_duplicate_key_update( + user = user, + item_id = item_id, + type = item_type, + acquire_date = datetime.fromtimestamp(it["data"]["obtainedDate"]) if "obtainedDate" in it["data"] else datetime.now(), + use_count = it["data"]["uses"] if "uses" in it["data"] else 0, + use_count_season = it["data"]["season_uses"] if "season_uses" in it["data"] else 0 + ) + result = self.base.execute(conflict) + if result is None: + self.logger.warn(f"Wacca: Failed to insert trophy {item_id} for user {user}") + + for sc in wacca_scores: + if type(sc["data"]) is not dict: + sc["data"] = json.loads(sc["data"]) + + sql = Insert(best_score).values( + user = sc["user"], + song_id = int(sc["song_id"]), + chart_id = sc["chart_id"], + score = sc["score1"], + play_ct = 1 if "play_count" not in sc["data"] else sc["data"]["play_count"], + clear_ct = 1 if sc["cleared"] & 0x01 else 0, + missless_ct = 1 if sc["cleared"] & 0x02 else 0, + fullcombo_ct = 1 if sc["cleared"] & 0x04 else 0, + allmarv_ct = 1 if sc["cleared"] & 0x08 else 0, + grade_d_ct = 1 if sc["grade"] & 0x01 else 0, + grade_c_ct = 1 if sc["grade"] & 0x02 else 0, + grade_b_ct = 1 if sc["grade"] & 0x04 else 0, + grade_a_ct = 1 if sc["grade"] & 0x08 else 0, + grade_aa_ct = 1 if sc["grade"] & 0x10 else 0, + grade_aaa_ct = 1 if sc["grade"] & 0x20 else 0, + grade_s_ct = 1 if sc["grade"] & 0x40 else 0, + grade_ss_ct = 1 if sc["grade"] & 0x80 else 0, + grade_sss_ct = 1 if sc["grade"] & 0x100 else 0, + grade_master_ct = 1 if sc["grade"] & 0x200 else 0, + grade_sp_ct = 1 if sc["grade"] & 0x400 else 0, + grade_ssp_ct = 1 if sc["grade"] & 0x800 else 0, + grade_sssp_ct = 1 if sc["grade"] & 0x1000 else 0, + best_combo = 0 if "max_combo" not in sc["data"] else sc["data"]["max_combo"], + lowest_miss_ct = 0 if "lowest_miss_count" not in sc["data"] else sc["data"]["lowest_miss_count"], + rating = 0 if "rating" not in sc["data"] else sc["data"]["rating"], + ) + + conflict = sql.on_duplicate_key_update( + user = sc["user"], + song_id = int(sc["song_id"]), + chart_id = sc["chart_id"], + score = sc["score1"], + play_ct = 1 if "play_count" not in sc["data"] else sc["data"]["play_count"], + clear_ct = 1 if sc["cleared"] & 0x01 else 0, + missless_ct = 1 if sc["cleared"] & 0x02 else 0, + fullcombo_ct = 1 if sc["cleared"] & 0x04 else 0, + allmarv_ct = 1 if sc["cleared"] & 0x08 else 0, + grade_d_ct = 1 if sc["grade"] & 0x01 else 0, + grade_c_ct = 1 if sc["grade"] & 0x02 else 0, + grade_b_ct = 1 if sc["grade"] & 0x04 else 0, + grade_a_ct = 1 if sc["grade"] & 0x08 else 0, + grade_aa_ct = 1 if sc["grade"] & 0x10 else 0, + grade_aaa_ct = 1 if sc["grade"] & 0x20 else 0, + grade_s_ct = 1 if sc["grade"] & 0x40 else 0, + grade_ss_ct = 1 if sc["grade"] & 0x80 else 0, + grade_sss_ct = 1 if sc["grade"] & 0x100 else 0, + grade_master_ct = 1 if sc["grade"] & 0x200 else 0, + grade_sp_ct = 1 if sc["grade"] & 0x400 else 0, + grade_ssp_ct = 1 if sc["grade"] & 0x800 else 0, + grade_sssp_ct = 1 if sc["grade"] & 0x1000 else 0, + best_combo = 0 if "max_combo" not in sc["data"] else sc["data"]["max_combo"], + lowest_miss_ct = 0 if "lowest_miss_count" not in sc["data"] else sc["data"]["lowest_miss_count"], + rating = 0 if "rating" not in sc["data"] else sc["data"]["rating"], + ) + result = self.base.execute(conflict) + if result is None: + self.logger.error(f"Failed to insert wacca score for user {sc['user']} {int(sc['song_id'])} {sc['chart_id']}") + + for ach in wacca_achievements: + if ach["version"] == 0 or ach["version"] == 1: + season = 1 + elif ach["version"] == 2 or ach["version"] == 3: + season = 2 + elif ach["version"] >= 4: + season = 3 + + if type(ach["data"]) is not dict: + ach["data"] = json.loads(ach["data"]) + + sql = Insert(stageup).values( + user = ach["user"], + season = season, + stage_id = ach["achievement_id"], + clear_status = 0 if "clear" not in ach["data"] else ach["data"]["clear"], + clear_song_ct = 0 if "clear_song_ct" not in ach["data"] else ach["data"]["clear_song_ct"], + song1_score = 0 if "score1" not in ach["data"] else ach["data"]["score1"], + song2_score = 0 if "score2" not in ach["data"] else ach["data"]["score2"], + song3_score = 0 if "score3" not in ach["data"] else ach["data"]["score3"], + play_ct = 1 if "attemps" not in ach["data"] else ach["data"]["attemps"], + ) + + conflict = sql.on_duplicate_key_update( + user = ach["user"], + season = season, + stage_id = ach["achievement_id"], + clear_status = 0 if "clear" not in ach["data"] else ach["data"]["clear"], + clear_song_ct = 0 if "clear_song_ct" not in ach["data"] else ach["data"]["clear_song_ct"], + song1_score = 0 if "score1" not in ach["data"] else ach["data"]["score1"], + song2_score = 0 if "score2" not in ach["data"] else ach["data"]["score2"], + song3_score = 0 if "score3" not in ach["data"] else ach["data"]["score3"], + play_ct = 1 if "attemps" not in ach["data"] else ach["data"]["attemps"], + ) + result = self.base.execute(conflict) + if result is None: + self.logger.error(f"Failed to insert wacca achievement for user {ach['user']}") + + else: + self.logger.info(f"Wacca not found, skipping...") diff --git a/core/data/schema/user.py b/core/data/schema/user.py index 7d76bbe..9e79891 100644 --- a/core/data/schema/user.py +++ b/core/data/schema/user.py @@ -1,9 +1,14 @@ from enum import Enum from typing import Dict, Optional -from sqlalchemy import Table, Column +from sqlalchemy import Table, Column, and_ from sqlalchemy.types import Integer, String, TIMESTAMP from sqlalchemy.sql.schema import ForeignKey from sqlalchemy.sql import func +from sqlalchemy.dialects.mysql import insert +from sqlalchemy.sql import func, select, Delete +from uuid import uuid4 +from datetime import datetime, timedelta +from sqlalchemy.engine import Row from core.data.schema.base import BaseData, metadata @@ -26,6 +31,7 @@ frontend_session = Table( metadata, Column("id", Integer, primary_key=True, unique=True), Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False), + Column("ip", String(15)), Column('session_cookie', String(32), nullable=False, unique=True), Column("expires", TIMESTAMP, nullable=False), mysql_charset='utf8mb4' @@ -37,21 +43,83 @@ class PermissionBits(Enum): PermSysAdmin = 4 class UserData(BaseData): - def create_user(self, username: str = None, email: str = None, password: str = None) -> Optional[int]: - + def create_user(self, id: int = None, username: str = None, email: str = None, password: str = None, permission: int = 1) -> Optional[int]: if email is None: - permission = None - else: - permission = 0 + permission = 1 - sql = aime_user.insert().values(username=username, email=email, password=password, permissions=permission) + if id is None: + sql = insert(aime_user).values( + username=username, + email=email, + password=password, + permissions=permission + ) + else: + sql = insert(aime_user).values( + id=id, + username=username, + email=email, + password=password, + permissions=permission + ) + + conflict = sql.on_duplicate_key_update( + username=username, + email=email, + password=password, + permissions=permission + ) - result = self.execute(sql) + result = self.execute(conflict) if result is None: return None return result.lastrowid + def login(self, user_id: int, passwd: bytes = None, ip: str = "0.0.0.0") -> Optional[str]: + sql = select(aime_user).where(and_(aime_user.c.id == user_id, aime_user.c.password == passwd)) + + result = self.execute(sql) + if result is None: return None + + usr = result.fetchone() + if usr is None: return None + + return self.create_session(user_id, ip) + + def check_session(self, cookie: str, ip: str = "0.0.0.0") -> Optional[Row]: + sql = select(frontend_session).where( + and_( + frontend_session.c.session_cookie == cookie, + frontend_session.c.ip == ip + ) + ) + + result = self.execute(sql) + if result is None: return None + return result.fetchone() + + def delete_session(self, session_id: int) -> bool: + sql = Delete(frontend_session).where(frontend_session.c.id == session_id) + + result = self.execute(sql) + if result is None: return False + return True + + def create_session(self, user_id: int, ip: str = "0.0.0.0", expires: datetime = datetime.now() + timedelta(days=1)) -> Optional[str]: + cookie = uuid4().hex + + sql = insert(frontend_session).values( + user = user_id, + ip = ip, + session_cookie = cookie, + expires = expires + ) + + result = self.execute(sql) + if result is None: + return None + return cookie + def reset_autoincrement(self, ai_value: int) -> None: - # Didn't feel like learning how to do this the right way - # if somebody wants a free PR go nuts I guess + # 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) \ No newline at end of file diff --git a/core/data/schema/versions/CORE_1_rollback.sql b/core/data/schema/versions/CORE_1_rollback.sql new file mode 100644 index 0000000..8a1144b --- /dev/null +++ b/core/data/schema/versions/CORE_1_rollback.sql @@ -0,0 +1,2 @@ +ALTER TABLE `frontend_session` +DROP COLUMN `ip`; \ No newline at end of file diff --git a/core/data/schema/versions/CORE_2_upgrade.sql b/core/data/schema/versions/CORE_2_upgrade.sql new file mode 100644 index 0000000..44deb6d --- /dev/null +++ b/core/data/schema/versions/CORE_2_upgrade.sql @@ -0,0 +1,2 @@ +ALTER TABLE `frontend_session` +ADD `ip` CHAR(15); \ No newline at end of file diff --git a/core/frontend.py b/core/frontend.py new file mode 100644 index 0000000..554df13 --- /dev/null +++ b/core/frontend.py @@ -0,0 +1,176 @@ +import logging, coloredlogs +from typing import Any, Dict +from twisted.web import resource +from twisted.web.util import redirectTo +from twisted.web.http import Request +from logging.handlers import TimedRotatingFileHandler +import jinja2 +import bcrypt + +from core.config import CoreConfig +from core.data import Data +from core.utils import Utils + +class FrontendServlet(resource.Resource): + children: Dict[str, Any] = {} + def getChild(self, name: bytes, request: Request): + self.logger.debug(f"{request.getClientIP()} -> {name.decode()}") + if name == b'': + return self + return resource.Resource.getChild(self, name, request) + + 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("core/frontend")) + + 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) + + 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) + + fe_game = FE_Game(cfg, self.environment) + games = Utils.get_all_titles() + for game_dir, game_mod in games.items(): + if hasattr(game_mod, "frontend"): + try: + fe_game.putChild(game_dir.encode(), game_mod.frontend(cfg, self.environment, config_dir)) + except: + raise + + self.putChild(b"gate", FE_Gate(cfg, self.environment)) + self.putChild(b"user", FE_User(cfg, self.environment)) + self.putChild(b"game", fe_game) + + 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"{request.getClientIP()} -> {request.uri.decode()}") + template = self.environment.get_template("index.jinja") + return template.render(server_name=self.config.server.name, title=self.config.server.name).encode("utf-16") + +class FE_Base(resource.Resource): + """ + 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, cfg_dir: str = None) -> None: + self.core_config = cfg + self.data = Data(cfg) + self.logger = logging.getLogger('frontend') + self.environment = environment + +class FE_Gate(FE_Base): + def render_GET(self, request: Request): + self.logger.debug(f"{request.getClientIP()} -> {request.uri.decode()}") + uri: str = request.uri.decode() + if uri.startswith("/gate/create"): + return self.create_user(request) + + if b'e' in request.args: + try: + err = int(request.args[b'e'][0].decode()) + except: + err = 0 + + else: err = 0 + + template = self.environment.get_template("gate/gate.jinja") + return template.render(title=f"{self.core_config.server.name} | Login Gate", error=err).encode("utf-16") + + def render_POST(self, request: Request): + uri = request.uri.decode() + ip = request.getClientAddress().host + + if uri == "/gate/gate.login": + access_code: str = request.args[b"access_code"][0].decode() + passwd: str = request.args[b"passwd"][0] + if passwd == b"": + passwd = None + + uid = self.data.card.get_user_id_from_card(access_code) + if uid is None: + return redirectTo(b"/gate?e=1", request) + + if passwd is None: + sesh = self.data.user.login(uid, ip=ip) + + if sesh is not None: + return redirectTo(f"/gate/create?ac={access_code}".encode(), request) + return redirectTo(b"/gate?e=1", request) + + salt = bcrypt.gensalt() + hashed = bcrypt.hashpw(passwd, salt) + sesh = self.data.user.login(uid, hashed, ip) + + if sesh is None: + return redirectTo(b"/gate?e=1", request) + + request.addCookie('session', sesh) + 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: str = 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, hashed.decode(), 1) + if result is None: + return redirectTo(b"/gate?e=3", request) + + sesh = self.data.user.login(uid, hashed, ip) + if sesh is None: + return redirectTo(b"/gate", request) + request.addCookie('session', sesh) + + 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() + + template = self.environment.get_template("gate/create.jinja") + return template.render(title=f"{self.core_config.server.name} | Create User", code=ac).encode("utf-16") + +class FE_User(FE_Base): + def render_GET(self, request: Request): + template = self.environment.get_template("user/index.jinja") + return template.render().encode("utf-16") + if b'session' not in request.cookies: + return redirectTo(b"/gate", request) + +class FE_Game(FE_Base): + isLeaf = False + children: Dict[str, Any] = {} + + def getChild(self, name: bytes, request: Request): + if name == b'': + return self + return resource.Resource.getChild(self, name, request) + + def render_GET(self, request: Request) -> bytes: + return redirectTo(b"/user", request) \ No newline at end of file diff --git a/core/frontend/gate/create.jinja b/core/frontend/gate/create.jinja new file mode 100644 index 0000000..f5b78ae --- /dev/null +++ b/core/frontend/gate/create.jinja @@ -0,0 +1,24 @@ +{% extends "index.jinja" %} +{% block content %} +