From aacbea209676a0bee588c8d2a026ab959a78977f Mon Sep 17 00:00:00 2001 From: UnitedAirforce Date: Thu, 27 Nov 2025 14:52:43 +0800 Subject: [PATCH] mid-push --- new_server_7003/api/account.py | 7 +- new_server_7003/api/admin.py | 10 +- new_server_7003/api/batch.py | 10 + new_server_7003/api/database.py | 74 +++- new_server_7003/api/file.py | 35 +- new_server_7003/api/misc.py | 29 +- new_server_7003/api/play.py | 6 +- new_server_7003/api/ranking.py | 44 ++- new_server_7003/api/web.py | 79 ++++- new_server_7003/db-conv.py | 492 ++++++++++++++++++++++++++ new_server_7003/files/web/ranking.js | 131 +++---- new_server_7003/files/web/style.css | 139 ++++++-- new_server_7003/files/web/web_shop.js | 70 ++-- new_server_7003/web/history.html | 50 +-- new_server_7003/web/login.html | 9 +- new_server_7003/web/user.html | 170 ++++++--- 16 files changed, 1059 insertions(+), 296 deletions(-) create mode 100644 new_server_7003/db-conv.py diff --git a/new_server_7003/api/account.py b/new_server_7003/api/account.py index 660732f..3e6baf8 100644 --- a/new_server_7003/api/account.py +++ b/new_server_7003/api/account.py @@ -4,8 +4,8 @@ from starlette.routing import Route from datetime import datetime import secrets -from api.misc import is_alphanumeric, inform_page, verify_password, hash_password, crc32_decimal, read_user_save_file, write_user_save_file, should_serve, generate_salt -from api.database import check_blacklist, user_name_to_user_info, decrypt_fields_to_user_info, set_user_data_using_decrypted_fields, get_user_from_save_id, create_user, logout_user, login_user, get_bind +from api.misc import is_alphanumeric, inform_page, verify_password, hash_password, crc32_decimal, should_serve, generate_salt +from api.database import check_blacklist, user_name_to_user_info, decrypt_fields_to_user_info, set_user_data_using_decrypted_fields, get_user_from_save_id, create_user, logout_user, login_user, get_bind, read_user_save_file, write_user_save_file from api.crypt import decrypt_fields from config import AUTHORIZATION_MODE @@ -76,7 +76,6 @@ async def password_reset(request: Request): return inform_page("FAILED:
Password must have 6 or more characters.", 0) old_hash = user_info['password_hash'] - print("hash type", type(old_hash)) if old_hash: if verify_password(old_password, old_hash): hashed_new_password = hash_password(new_password) @@ -228,7 +227,7 @@ async def login(request: Request): user_id = user_record['id'] password_hash_record = user_record['password_hash'] - if password_hash_record and verify_password(password, password_hash_record[0]): + if password_hash_record and verify_password(password, password_hash_record): device_id = decrypted_fields[b'vid'][0].decode() await login_user(user_id, device_id) diff --git a/new_server_7003/api/admin.py b/new_server_7003/api/admin.py index b884559..9ce9d2d 100644 --- a/new_server_7003/api/admin.py +++ b/new_server_7003/api/admin.py @@ -5,8 +5,8 @@ import sqlalchemy import json import datetime -from api.database import player_database, accounts, results, devices, whitelists, blacklists, batch_tokens, binds, webs, logs, is_admin -from api.misc import crc32_decimal, read_user_save_file, write_user_save_file +from api.database import player_database, accounts, results, devices, whitelists, blacklists, batch_tokens, binds, webs, logs, is_admin, read_user_save_file, write_user_save_file +from api.misc import crc32_decimal TABLE_MAP = { "accounts": (accounts, ["id", "username", "password_hash", "save_crc", "save_timestamp", "save_id", "coin_mp", "title", "avatar", "created_at", "updated_at"]), @@ -14,9 +14,9 @@ TABLE_MAP = { "devices": (devices, ["device_id", "user_id", "my_stage", "my_avatar", "item", "daily_day", "coin", "lvl", "title", "avatar", "mobile_sum", "arcade_sum", "total_sum", "created_at", "updated_at", "last_login_at"]), "whitelist": (whitelists, ["id", "device_id"]), "blacklist": (blacklists, ["id", "ban_terms", "reason"]), - "batch_tokens": (batch_tokens, ["id", "bind_token", "expire_at", "uses_left", "auth_id", "created_at", "updated_at"]), - "binds": (binds, ["id", "user_id", "bind_account", "bind_code", "is_verified", "bind_token", "created_at", "updated_at"]), - "webs": (webs, ["id", "user_id", "permission", "web_token", "created_at", "updated_at"]), + "batch_tokens": (batch_tokens, ["id", "batch_token", "expire_at", "uses_left", "auth_id", "created_at", "updated_at"]), + "binds": (binds, ["id", "user_id", "bind_account", "bind_code", "is_verified", "auth_token", "created_at", "updated_at"]), + "webs": (webs, ["id", "user_id", "permission", "web_token", "last_save_export", "created_at", "updated_at"]), "logs": (logs, ["id", "user_id", "filename", "filesize", "timestamp"]), } diff --git a/new_server_7003/api/batch.py b/new_server_7003/api/batch.py index 077ffb4..e7ba6c4 100644 --- a/new_server_7003/api/batch.py +++ b/new_server_7003/api/batch.py @@ -1,6 +1,7 @@ from starlette.responses import HTMLResponse from starlette.requests import Request from starlette.routing import Route +from datetime import datetime import os import json import time @@ -27,6 +28,15 @@ async def batch_handler(request: Request): if result['expire_at'] < int(time.time()): return HTMLResponse(content="Token expired", status_code=400) + if result['uses_left'] <= 0: + return HTMLResponse(content="No uses left for this token", status_code=400) + + update_query = batch_tokens.update().where(batch_tokens.c.token == token).values( + uses_left=result['uses_left'] - 1, + updated_at=datetime.utcnow() + ) + await player_database.execute(update_query) + with open(os.path.join('api/config/', 'download_manifest.json'), 'r', encoding='utf-8') as f: stage_manifest = json.load(f) diff --git a/new_server_7003/api/database.py b/new_server_7003/api/database.py index 2b3d952..64b0837 100644 --- a/new_server_7003/api/database.py +++ b/new_server_7003/api/database.py @@ -1,8 +1,10 @@ import sqlalchemy from sqlalchemy import Table, Column, Integer, String, DateTime, JSON, ForeignKey, Index from sqlalchemy.ext.asyncio import create_async_engine -from sqlalchemy import select, update, insert +from sqlalchemy import select, update import base64 +import aiofiles +import json from config import START_COIN, SIMULTANEOUS_LOGINS from api.template import START_AVATARS, START_STAGES @@ -82,8 +84,7 @@ results = Table( Column("os", String(8), nullable=False), Column("os_ver", String(16), nullable=False), Column("ver", String(8), nullable=False), - Column("created_at", DateTime, default=datetime.utcnow), - Column("updated_at", DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) + Column("created_at", DateTime, default=datetime.utcnow) ) Index( @@ -100,6 +101,7 @@ webs = Table( Column("user_id", Integer, ForeignKey("accounts.id")), Column("permission", Integer, default=1), Column("web_token", String(128), unique=True, nullable=False), + Column("last_save_export", Integer, nullable=True), Column("created_at", DateTime, default=datetime.utcnow), Column("updated_at", DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) ) @@ -108,7 +110,7 @@ batch_tokens = Table( "batch_tokens", player_metadata, Column("id", Integer, primary_key=True, autoincrement=True), - Column("bind_token", String(128), unique=True, nullable=False), + Column("batch_token", String(128), unique=True, nullable=False), Column("expire_at", DateTime, nullable=False), Column("uses_left", Integer, default=1), Column("auth_id", String(64), nullable=False), @@ -139,7 +141,7 @@ binds = Table( Column("bind_account", String(128), unique=True, nullable=False), Column("bind_code", String(6), nullable=False), Column("is_verified", Integer, default=0), - Column("bind_token", String(64), unique=True, nullable=False), + Column("auth_token", String(64), unique=True), Column("bind_date", DateTime, default=datetime.utcnow) ) @@ -215,7 +217,7 @@ async def refresh_bind(user_id): if existing_bind and existing_bind['is_verified'] == 1: new_auth_token = base64.urlsafe_b64encode(os.urandom(64)).decode("utf-8") update_query = update(binds).where(binds.c.id == existing_bind['id']).values( - bind_token=new_auth_token + auth_token=new_auth_token ) await player_database.execute(update_query) @@ -425,7 +427,7 @@ async def create_device(device_id, current_time): async def is_admin(token): if not token: return False - query = webs.select().where(webs.c.token == token) + query = webs.select().where(webs.c.web_token == token) web_data = await player_database.fetch_one(query) if not web_data: return False @@ -467,4 +469,60 @@ async def get_rank_cache(key): await clear_rank_cache(key) return None return dict(result)['value'] - return None \ No newline at end of file + return None + +async def get_user_export_data(user_id): + user_data = {} + user_info, device_list = await user_id_to_user_info(user_id) + + user_save = await read_user_save_file(user_id) + user_info['save_data'] = user_save + + user_data['account'] = [user_info] + user_data['devices'] = device_list + + all_results_query = results.select().where(results.c.user_id == user_id) + all_results = await player_database.fetch_all(all_results_query) + user_data['results'] = [dict(result) for result in all_results] + + user_binds_query = binds.select().where(binds.c.user_id == user_id) + user_binds = await player_database.fetch_all(user_binds_query) + + user_data['binds'] = [dict(bind) for bind in user_binds] + # Convert JSON fields to strings + for key, value in user_data.items(): + if isinstance(value, list): + for item in value: + if isinstance(item, dict): + for field, field_value in item.items(): + if isinstance(field_value, (dict, list)): + item[field] = json.dumps(field_value) + + return user_data + +async def read_user_save_file(user_id): + if user_id is None: + return "" + elif type(user_id) != int: + return "" + else: + try: + async with aiofiles.open(f"./save/{user_id}.dat", "rb") as file: + result = await file.read() + result = result.decode("utf-8") + return result + + except FileNotFoundError: + return "" + +async def write_user_save_file(user_id, data): + if user_id is None: + return + elif type(user_id) != int: + return + else: + try: + async with aiofiles.open(f"./save/{user_id}.dat", "wb") as file: + await file.write(data.encode("utf-8")) + except Exception as e: + print(f"An error occurred while writing the file: {e}") \ No newline at end of file diff --git a/new_server_7003/api/file.py b/new_server_7003/api/file.py index e6fb3e2..4cce9a3 100644 --- a/new_server_7003/api/file.py +++ b/new_server_7003/api/file.py @@ -2,6 +2,8 @@ from starlette.responses import Response, FileResponse from starlette.requests import Request from starlette.routing import Route from sqlalchemy import select +import openpyxl +from io import BytesIO import os from api.database import player_database, devices, binds, batch_tokens, log_download, get_downloaded_bytes @@ -18,7 +20,7 @@ async def serve_file(request: Request): if not filename.endswith(".zip") and not filename.endswith(".pak"): return Response("Unauthorized", status_code=403) - existing_batch_token = select(batch_tokens).where(batch_tokens.c.bind_token == auth_token) + existing_batch_token = select(batch_tokens).where(batch_tokens.c.batch_token == auth_token) batch_result = await player_database.fetch_one(existing_batch_token) if batch_result: pass @@ -71,7 +73,38 @@ async def serve_public_file(request: Request): return FileResponse(safe_filename) else: return Response("File not found", status_code=404) + +async def convert_user_export_data(data): + wb = openpyxl.Workbook() + # Remove default sheet + default_sheet = wb.active + wb.remove(default_sheet) + + # Create sheet for each top-level key + for sheet_name, rows in data.items(): + ws = wb.create_sheet(title=sheet_name) + + # rows expected to be list[dict] + if isinstance(rows, list): + await write_dict_list_to_sheet(ws, rows) + + stream = BytesIO() + wb.save(stream) + stream.seek(0) + return stream + +async def write_dict_list_to_sheet(ws, rows): + if not rows: + return + + # headers + headers = list(rows[0].keys()) + ws.append(headers) + + # rows + for row in rows: + ws.append([row.get(h, "") for h in headers]) routes = [ Route("/files/gc2/{auth_token}/{folder}/{filename}", serve_file, methods=["GET"]), diff --git a/new_server_7003/api/misc.py b/new_server_7003/api/misc.py index 2a7b636..06739df 100644 --- a/new_server_7003/api/misc.py +++ b/new_server_7003/api/misc.py @@ -65,6 +65,8 @@ def hash_password(password): def verify_password(password, hashed_password): if type(hashed_password) == str: hashed_password = hashed_password.encode('utf-8') + + print("hashed_password:", hashed_password) return bcrypt.checkpw(password.encode('utf-8'), hashed_password) def is_alphanumeric(username): @@ -246,33 +248,6 @@ def check_email(email): STRICT_EMAIL_REGEX = r"^[A-Za-z0-9]+(?:[._-][A-Za-z0-9]+)*@[A-Za-z0-9]+(?:-[A-Za-z0-9]+)*(?:\.[A-Za-z0-9]+(?:-[A-Za-z0-9]+)*)*\.[A-Za-z]{2,}$" return re.match(STRICT_EMAIL_REGEX, email) is not None -async def read_user_save_file(user_id): - if user_id is None: - return "" - elif type(user_id) != int: - return "" - else: - try: - async with aiofiles.open(f"./save/{user_id}.dat", "rb") as file: - result = await file.read() - result = result.decode("utf-8") - return result - - except FileNotFoundError: - return "" - -async def write_user_save_file(user_id, data): - if user_id is None: - return - elif type(user_id) != int: - return - else: - try: - async with aiofiles.open(f"./save/{user_id}.dat", "wb") as file: - await file.write(data.encode("utf-8")) - except Exception as e: - print(f"An error occurred while writing the file: {e}") - async def should_serve(decrypted_fields): should_serve = True if AUTHORIZATION_NEEDED: diff --git a/new_server_7003/api/play.py b/new_server_7003/api/play.py index e081ea8..7ea24cb 100644 --- a/new_server_7003/api/play.py +++ b/new_server_7003/api/play.py @@ -101,7 +101,8 @@ async def result_request(request: Request): item=item, os=device_os, os_ver=os_ver, - ver=ver + ver=ver, + created_at=datetime.utcnow() ) await player_database.execute(update_query) @@ -123,8 +124,7 @@ async def result_request(request: Request): os=device_os, os_ver=os_ver, ver=ver, - created_at=datetime.utcnow(), - updated_at=datetime.utcnow() + created_at=datetime.utcnow() ) result = await player_database.execute(insert_query) target_row_id = result diff --git a/new_server_7003/api/ranking.py b/new_server_7003/api/ranking.py index fdc7f85..8877131 100644 --- a/new_server_7003/api/ranking.py +++ b/new_server_7003/api/ranking.py @@ -215,7 +215,7 @@ async def user_ranking_individual(request: Request): ranking_list = [] player_ranking = {"username": user_info["username"] if user_info else "Guest (Not Ranked)", "score": 0, "position": -1, "title": user_info["title"] if user_info else device_info['title'], "avatar": device_info["avatar"]} - cache_key = f"{song_id}-{mode}" + cache_key = f"{str(song_id)}-{str(mode)}" cached_data = await get_rank_cache(cache_key) if cached_data: @@ -232,13 +232,14 @@ async def user_ranking_individual(request: Request): for index, record in enumerate(records): if index >= page_number * page_count and index < (page_number + 1) * page_count: rank_user = await user_id_to_user_info_simple(record["user_id"]) - ranking_list.append({ - "position": index + 1, - "username": rank_user["username"], - "score": record["score"], - "title": rank_user["title"], - "avatar": record["avatar"] - }) + if rank_user: + ranking_list.append({ + "position": index + 1, + "username": rank_user["username"], + "score": record["score"], + "title": rank_user["title"], + "avatar": record["avatar"] + }) if user_id and record["user_id"] == user_id: player_ranking = { "username": user_info["username"], @@ -258,6 +259,8 @@ async def user_ranking_individual(request: Request): } } + print("toital_count: " + str(total_count)) + return JSONResponse(payload) async def user_ranking_total(request: Request): @@ -290,7 +293,7 @@ async def user_ranking_total(request: Request): score_obj = ["total_delta", "mobile_delta", "arcade_delta"] - cache_key = f"0-{mode}" + cache_key = f"0-{str(mode)}" cached_data = await get_rank_cache(cache_key) if cached_data: records = cached_data @@ -302,24 +305,29 @@ async def user_ranking_total(request: Request): accounts.c[score_obj[mode]], accounts.c.title, accounts.c.avatar, - ).order_by(accounts.c[score_obj[mode]].desc()) + ).where( + accounts.c[score_obj[mode]] > 0 + ).order_by( + accounts.c[score_obj[mode]].desc() + ) records = await player_database.fetch_all(query) records = [dict(record) for record in records] await write_rank_cache(cache_key, records, expire_seconds=120) total_count = len(records) + print("total_count fetched: " + str(total_count)) for index, record in enumerate(records): - print(record) if index >= page_number * page_count and index < (page_number + 1) * page_count: rank_user = await user_id_to_user_info_simple(record["id"]) - ranking_list.append({ - "position": index + 1, - "username": rank_user["username"], - "score": record[score_obj[mode]], - "title": rank_user["title"], - "avatar": record["avatar"] - }) + if rank_user: + ranking_list.append({ + "position": index + 1, + "username": rank_user["username"], + "score": record[score_obj[mode]], + "title": rank_user["title"], + "avatar": record["avatar"] + }) if user_id and record["id"] == user_id: player_ranking = { "username": user_info["username"], diff --git a/new_server_7003/api/web.py b/new_server_7003/api/web.py index 59c82ff..feca4f1 100644 --- a/new_server_7003/api/web.py +++ b/new_server_7003/api/web.py @@ -1,28 +1,33 @@ from starlette.requests import Request from starlette.routing import Route -from starlette.responses import HTMLResponse, JSONResponse, RedirectResponse +from starlette.responses import HTMLResponse, JSONResponse, RedirectResponse, StreamingResponse import secrets from datetime import datetime +import time -from api.database import player_database, webs, is_admin, user_name_to_user_info, user_id_to_user_info_simple -from api.misc import read_user_save_file, verify_password, should_serve_web -from config import AUTHORIZATION_MODE +from api.database import player_database, webs, is_admin, user_name_to_user_info, user_id_to_user_info_simple, get_user_export_data +from api.misc import verify_password, should_serve_web +from api.file import convert_user_export_data +from config import AUTHORIZATION_MODE, SAVE_EXPORT_COOLDOWN async def is_user(request: Request): token = request.cookies.get("token") if not token: - return False - query = webs.select().where(webs.c.token == token) + return False, None + query = webs.select().where(webs.c.web_token == token) web_data = await player_database.fetch_one(query) if not web_data: - return False + return False, None if web_data['permission'] < 1: - return False + return False, None if AUTHORIZATION_MODE > 0: - return await should_serve_web(web_data['user_id']) + result = await should_serve_web(web_data['user_id']) + if not result: + return False, None - return True + + return True, web_data async def web_login_page(request: Request): with open("web/login.html", "r", encoding="utf-8") as file: @@ -53,13 +58,15 @@ async def web_login_login(request: Request): return JSONResponse({"status": "failed", "message": "Access denied."}, status_code=403) query = webs.update().where(webs.c.user_id == user_info['id']).values( - token=token + web_token=token, + updated_at=datetime.utcnow() ) else: query = webs.insert().values( user_id=user_info['id'], permission=1, web_token=token, + last_save_export=0, created_at=datetime.utcnow(), updated_at=datetime.utcnow() ) @@ -75,7 +82,7 @@ async def user_center_api(request: Request): if not token: return JSONResponse({"status": "failed", "message": "Token is required."}, status_code=400) - query = webs.select().where(webs.c.token == token) + query = webs.select().where(webs.c.web_token == token) web_record = await player_database.fetch_one(query) if not web_record: return JSONResponse({"status": "failed", "message": "Invalid token."}, status_code=403) @@ -96,7 +103,7 @@ async def user_center_api(request: Request): response_data = { "username": user_info['username'], - "last_save_export": web_record['last_save_export'].isoformat() if web_record['last_save_export'] else "None", + "next_save_export": web_record['last_save_export'] + SAVE_EXPORT_COOLDOWN, } return JSONResponse({"status": "success", "data": response_data}) @@ -105,28 +112,64 @@ async def user_center_api(request: Request): return JSONResponse({"status": "failed", "message": "Invalid action."}, status_code=400) async def user_center_page(request: Request): - usr = await is_user(request) - if not usr: + serve, web_data = await is_user(request) + if not serve: response = RedirectResponse(url="/login") + response.delete_cookie(key="token") return response with open("web/user.html", "r", encoding="utf-8") as file: html_template = file.read() - is_adm = await is_admin(request) + is_adm = await is_admin(request.cookies.get("token")) if is_adm: admin_button = f"""
- +
""" html_template += admin_button return HTMLResponse(content=html_template) +async def user_center_export_data(request: Request): + serve, web_data = await is_user(request) + if not serve: + response = RedirectResponse(url="/login") + response.delete_cookie(key="token") + return response + + user_id = web_data['user_id'] + last_save_export = web_data['last_save_export'] + current_time = time.time() + + if current_time - last_save_export < SAVE_EXPORT_COOLDOWN: + wait_time = int(SAVE_EXPORT_COOLDOWN - (current_time - last_save_export)) + return JSONResponse({"status": "failed", "message": f"Please wait {wait_time} seconds before exporting again."}, status_code=429) + + user_json_data_set = await get_user_export_data(user_id) + user_xlsx_stream = await convert_user_export_data(user_json_data_set) + + headers = { + "Content-Disposition": 'attachment; filename="export.xlsx"' + } + + update_query = webs.update().where(webs.c.user_id == user_id).values( + last_save_export=int(current_time), + updated_at=datetime.utcnow() + ) + await player_database.execute(update_query) + + return StreamingResponse( + user_xlsx_stream, + media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + headers=headers, + ) + routes = [ Route("/login", web_login_page, methods=["GET"]), Route("/login/", web_login_page, methods=["GET"]), Route("/login/login", web_login_login, methods=["POST"]), Route("/usercenter", user_center_page, methods=["GET"]), - Route("/usercenter/api", user_center_api, methods=["POST"]) + Route("/usercenter/api", user_center_api, methods=["POST"]), + Route("/usercenter/export_data", user_center_export_data, methods=["GET"]), ] \ No newline at end of file diff --git a/new_server_7003/db-conv.py b/new_server_7003/db-conv.py new file mode 100644 index 0000000..f89131e --- /dev/null +++ b/new_server_7003/db-conv.py @@ -0,0 +1,492 @@ +import sqlalchemy +from sqlalchemy import Table, Column, Integer, String, DateTime, JSON, ForeignKey, Index +from sqlalchemy.ext.asyncio import create_async_engine +from sqlalchemy import select, update, insert +import json +import os + +START_COIN = 10 + +import os +import databases +from datetime import datetime, timedelta + +DB_NAME = "player.db" +DB_PATH = os.path.join(os.getcwd(), DB_NAME) +DATABASE_URL = f"sqlite+aiosqlite:///{DB_PATH}" + +OLD_DB_NAME = "player_d.db" +OLD_DB_PATH = os.path.join(os.getcwd(), OLD_DB_NAME) +OLD_DATABASE_URL = f"sqlite+aiosqlite:///{OLD_DB_PATH}" + +old_database = databases.Database(OLD_DATABASE_URL) +old_metadata = sqlalchemy.MetaData() + +player_database = databases.Database(DATABASE_URL) +player_metadata = sqlalchemy.MetaData() + +#----------------------- Old Table definitions -----------------------# + +user = Table( + "user", + old_metadata, + Column("id", Integer, primary_key=True, autoincrement=True), + Column("username", String(20), unique=True, nullable=False), + Column("password_hash", String(255), nullable=False), + Column("device_id", String(512)), + Column("data", String, nullable=True), + Column("save_id", String, nullable=True), + Column("crc", Integer, nullable=True), + Column("timestamp", DateTime, default=None), + Column("coin_mp", Integer, default=1) +) + +daily_reward = Table( + "daily_reward", + old_metadata, + Column("id", Integer, primary_key=True, autoincrement=True), + Column("device_id", String(512)), + Column("timestamp", DateTime, default=datetime.utcnow), + Column("my_stage", JSON), + Column("my_avatar", JSON), + Column("item", JSON), + Column("day", Integer), + Column("coin", Integer), + Column("lvl", Integer), + Column("title", Integer), + Column("avatar", Integer), +) + +result = Table( + "result", + old_metadata, + Column("rid", Integer, primary_key=True, autoincrement=True), + Column("vid", String(512), nullable=False), + Column("tid", String(512), nullable=False), + Column("sid", Integer, nullable=False), + Column("stts", String(64)), + Column("id", Integer), + Column("mode", Integer), + Column("avatar", Integer), + Column("score", Integer), + Column("high_score", String(128)), + Column("play_rslt", String(128)), + Column("item", Integer), + Column("os", String(16)), + Column("os_ver", String(16)), + Column("ver", String(16)), + Column("mike", Integer), +) + +whitelist = Table( + "whitelist", + old_metadata, + Column("id", String(512), primary_key=True) +) +blacklist = Table( + "blacklist", + old_metadata, + Column("id", String(512), primary_key=True), + Column("reason", String(256)) +) + +bind = Table( + "bind", + old_metadata, + Column("id", Integer, primary_key=True, autoincrement=True), + Column("user_id", Integer, sqlalchemy.ForeignKey("user.id")), + Column("bind_acc", String(256), unique=True, nullable=False), + Column("bind_code", String(16), nullable=False), + Column("is_verified", Integer, default=0), + Column("auth_token", String(256), nullable=True), + Column("bind_date", DateTime, default=datetime.utcnow) +) + +logs = Table( + "logs", + old_metadata, + Column("id", Integer, primary_key=True, autoincrement=True), + Column("user_id", Integer, sqlalchemy.ForeignKey("user.id")), + Column("filename", String(256)), + Column("filesize", Integer), + Column("timestamp", DateTime, default=datetime.utcnow) +) + +device_list = Table( + "device_list", + old_metadata, + Column("id", Integer, primary_key=True, autoincrement=True), + Column("user_id", Integer, sqlalchemy.ForeignKey("user.id")), + Column("device_id", String(512), unique=True, nullable=False), + Column("last_login", DateTime, default=datetime.utcnow) +) + +#----------------------- End of old Table definitions -----------------------# + +#----------------------- New Table definitions -----------------------# + +accounts = Table( + "accounts", + player_metadata, + Column("id", Integer, primary_key=True, autoincrement=True), + Column("username", String(20), unique=True, nullable=False), + Column("password_hash", String(255), nullable=False), + Column("save_crc", String(24), nullable=True), + Column("save_timestamp", DateTime, nullable=True), + Column("save_id", String(24), nullable=True), + Column("coin_mp", Integer, default=0), + Column("title", Integer, default=1), + Column("avatar", Integer, default=1), + Column("mobile_delta", Integer, default=0), + Column("arcade_delta", Integer, default=0), + Column("total_delta", Integer, default=0), + Column("created_at", DateTime, default=datetime.utcnow), + Column("updated_at", DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) +) + +devices = Table( + "devices", + player_metadata, + Column("device_id", String(64), primary_key=True), + Column("user_id", Integer, ForeignKey("accounts.id")), + Column("my_stage", JSON, default=[]), + Column("my_avatar", JSON, default=[]), + Column("item", JSON, default=[]), + Column("daily_day", Integer, default=0), + Column("daily_timestamp", DateTime, default=datetime.min), + Column("coin", Integer, default=START_COIN), + Column("lvl", Integer, default=1), + Column("title", Integer, default=1), + Column("avatar", Integer, default=1), + Column("created_at", DateTime, default=datetime.utcnow), + Column("updated_at", DateTime, default=datetime.utcnow, onupdate=datetime.utcnow), + Column("last_login_at", DateTime, default=None) +) + +results = Table( + "results", + player_metadata, + Column("id", Integer, primary_key=True, autoincrement=True), + Column("device_id", String(64), ForeignKey("devices.device_id")), + Column("user_id", Integer, ForeignKey("accounts.id")), + Column("stts", JSON, nullable=False), + Column("song_id", Integer, nullable=False), + Column("mode", Integer, nullable=False), + Column("avatar", Integer, nullable=False), + Column("score", Integer, nullable=False), + Column("high_score", JSON, nullable=False), + Column("play_rslt", JSON, nullable=False), + Column("item", Integer, nullable=False), + Column("os", String(8), nullable=False), + Column("os_ver", String(16), nullable=False), + Column("ver", String(8), nullable=False), + Column("created_at", DateTime, default=datetime.utcnow) +) + +Index( + "idx_results_song_mode_score", + results.c.song_id, + results.c.mode, + results.c.score.desc(), +) + +webs = Table( + "webs", + player_metadata, + Column("id", Integer, primary_key=True, autoincrement=True), + Column("user_id", Integer, ForeignKey("accounts.id")), + Column("permission", Integer, default=1), + Column("web_token", String(128), unique=True, nullable=False), + Column("last_save_export", Integer, nullable=True), + Column("created_at", DateTime, default=datetime.utcnow), + Column("updated_at", DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) +) + +batch_tokens = Table( + "batch_tokens", + player_metadata, + Column("id", Integer, primary_key=True, autoincrement=True), + Column("batch_token", String(128), unique=True, nullable=False), + Column("expire_at", DateTime, nullable=False), + Column("uses_left", Integer, default=1), + Column("auth_id", String(64), nullable=False), + Column("created_at", DateTime, default=datetime.utcnow), + Column("updated_at", DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) +) + +whitelists = Table( + "whitelists", + player_metadata, + Column("id", Integer, primary_key=True, autoincrement=True), + Column("device_id", String(64), ForeignKey("devices.device_id")), +) + +blacklists = Table( + "blacklists", + player_metadata, + Column("id", Integer, primary_key=True, autoincrement=True), + Column("ban_terms", String(128), unique=True, nullable=False), + Column("reason", String(255), nullable=True) +) + +binds = Table( + "binds", + player_metadata, + Column("id", Integer, primary_key=True, autoincrement=True), + Column("user_id", Integer, ForeignKey("accounts.id")), + Column("bind_account", String(128), unique=True, nullable=False), + Column("bind_code", String(6), nullable=False), + Column("is_verified", Integer, default=0), + Column("auth_token", String(64), unique=True), + Column("bind_date", DateTime, default=datetime.utcnow) +) + +logs = Table( + "logs", + player_metadata, + Column("id", Integer, primary_key=True, autoincrement=True), + Column("user_id", Integer, ForeignKey("accounts.id")), + Column("filename", String(255), nullable=False), + Column("filesize", Integer, nullable=False), + Column("timestamp", DateTime, default=datetime.utcnow) +) + +#----------------------- End of new Table definitions -----------------------# + +async def init_db(): + if not os.path.exists(DB_PATH): + print("[DB] Creating new database:", DB_PATH) + + if not os.path.exists(OLD_DB_PATH): + print("[DB] Old db must exist as player_d.db") + + old_engine = create_async_engine(OLD_DATABASE_URL, echo=False) + async with old_engine.begin() as conn: + await conn.run_sync(old_metadata.create_all) + await old_engine.dispose() + + engine = create_async_engine(DATABASE_URL, echo=False) + + async with engine.begin() as conn: + await conn.run_sync(player_metadata.create_all) + + await engine.dispose() + print("[DB] Database initialized successfully.") + +async def save_user_data(user_id, user_data): + save_dir = "save" + save_file = os.path.join(save_dir, f"{user_id}.dat") + + if not os.path.exists(save_dir): + os.makedirs(save_dir) + + with open(save_file, "w") as file: + file.write(user_data) + +async def convert_db(): + await old_database.connect() + await player_database.connect() + + # Conversion logic goes here + print("[DB] Starting database conversion...") + + # Convert user -> accounts + print("[DB] Converting users to accounts...") + query = select(old_metadata.tables["user"]) + all_users = await old_database.fetch_all(query) + all_users = [dict(u) for u in all_users] + for user in all_users: + user_device_id = user['device_id'] + + user_data = user.get('data') if 'data' in user else None + if user_data: + user_id = user['id'] + await save_user_data(user_id, user_data) + + + user_old_device = await old_database.fetch_one( + select(old_metadata.tables["daily_reward"]).where(old_metadata.tables["daily_reward"].c.device_id == user_device_id) + ) + user_old_device = dict(user_old_device) if user_old_device else None + + insert_query = insert(accounts).values( + username=user['username'], + password_hash=user['password_hash'], + save_crc=user['crc'], + save_timestamp=user['timestamp'], + save_id=user['save_id'], + coin_mp=user['coin_mp'], + title=user_old_device['title'] if user_old_device else 1, + avatar=user_old_device['avatar'] if user_old_device else 1, + mobile_delta=0, + arcade_delta=0, + total_delta=0, + created_at=datetime.utcnow(), + updated_at=datetime.utcnow() + ) + await player_database.execute(insert_query) + + # Compute score deltas + print("[DB] Computing score deltas for accounts...") + all_new_users = await player_database.fetch_all(select(accounts)) + all_new_users = [dict(u) for u in all_new_users] + for new_user in all_new_users: + username = new_user['username'] + old_user_object = await old_database.fetch_one( + select(old_metadata.tables["user"]).where(old_metadata.tables["user"].c.username == username) + ) + old_user_id = old_user_object['id'] + old_user_results_mobile = await old_database.fetch_all( + select(old_metadata.tables["result"]).where( + (old_metadata.tables["result"].c.sid == old_user_id) & + (old_metadata.tables["result"].c.mode.in_([1, 2, 3])) + ) + ) + old_user_mobile_sum = sum([int(r['score']) for r in old_user_results_mobile]) + old_user_results_arcade = await old_database.fetch_all( + select(old_metadata.tables["result"]).where( + (old_metadata.tables["result"].c.sid == old_user_id) & + (old_metadata.tables["result"].c.mode.in_([11, 12, 13])) + ) + ) + old_user_arcade_sum = sum([int(r['score']) for r in old_user_results_arcade]) + old_user_results_total = old_user_mobile_sum + old_user_arcade_sum + + update_query = ( + update(accounts).where(accounts.c.id == new_user['id']).values( + mobile_delta=old_user_mobile_sum, + arcade_delta=old_user_arcade_sum, + total_delta=old_user_results_total + ) + ) + await player_database.execute(update_query) + + # Convert daily_reward -> devices + print("[DB] Converting daily rewards to devices...") + all_devices = await old_database.fetch_all(select(old_metadata.tables["daily_reward"])) + all_devices = [dict(d) for d in all_devices] + for device in all_devices: + connected_user = await old_database.fetch_one( + select(old_metadata.tables["user"]).where(old_metadata.tables["user"].c.device_id == device['device_id']) + ) + insert_query = insert(devices).values( + device_id=device['device_id'], + user_id=connected_user['id'] if connected_user else None, + my_stage=device['my_stage'], + my_avatar=device['my_avatar'], + item=device['item'], + daily_day=device['day'], + daily_timestamp=device['timestamp'], + coin=device['coin'], + lvl=device['lvl'], + title=device['title'], + avatar=device['avatar'], + created_at=datetime.utcnow(), + updated_at=datetime.utcnow(), + last_login_at=datetime.utcnow() + ) + await player_database.execute(insert_query) + + # convert result -> results + print("[DB] Converting results to results...") + all_results = await old_database.fetch_all(select(old_metadata.tables["result"])) + all_results = [dict(r) for r in all_results] + for result in all_results: + if result['sid'] is not None and result['sid'] != 0: + # Skip guests, they are no longer ranked + old_user_object = await old_database.fetch_one( + select(old_metadata.tables["user"]).where(old_metadata.tables["user"].c.id == result['sid']) + ) + old_user_username = old_user_object['username'] if old_user_object else None + if old_user_username: + new_user_object = await player_database.fetch_one( + select(accounts).where(accounts.c.username == old_user_username) + ) + new_user_id = new_user_object['id'] + insert_query = insert(results).values( + device_id=result['vid'], + user_id=new_user_id, + stts=convert_array(result['stts']), + song_id=result['id'], + mode=result['mode'], + avatar=result['avatar'], + score=result['score'], + high_score=convert_array(result['high_score']), + play_rslt=convert_array(result['play_rslt']), + item=result['item'], + os=result['os'], + os_ver=result['os_ver'], + ver=result['ver'], + created_at=datetime.utcnow(), + ) + await player_database.execute(insert_query) + + # convert bwlist + print("[DB] Converting blacklists and whitelists...") + old_blacklists = await old_database.fetch_all(select(old_metadata.tables["blacklist"])) + old_blacklists = [dict(b) for b in old_blacklists] + + for bl in old_blacklists: + insert_query = insert(blacklists).values( + ban_terms=bl['id'], + reason=bl['reason'] + ) + await player_database.execute(insert_query) + + old_whitelists = await old_database.fetch_all(select(old_metadata.tables["whitelist"])) + old_whitelists = [dict(w) for w in old_whitelists] + for wl in old_whitelists: + insert_query = insert(whitelists).values( + device_id=wl['id'] + ) + await player_database.execute(insert_query) + + # convert binds (optional) + + if 'bind' in old_metadata.tables: + print("[DB] Converting binds...") + old_binds = await old_database.fetch_all(select(old_metadata.tables["bind"])) + old_binds = [dict(b) for b in old_binds] + for b in old_binds: + insert_query = insert(binds).values( + user_id=b['user_id'], + bind_account=b['bind_acc'], + bind_code=b['bind_code'], + is_verified=b['is_verified'], + auth_token=b['auth_token'], + bind_date=b['bind_date'] + ) + await player_database.execute(insert_query) + + # convert logs (optional) + + if 'logs' in old_metadata.tables: + print("[DB] Converting logs...") + old_logs = await old_database.fetch_all(select(old_metadata.tables["logs"])) + old_logs = [dict(l) for l in old_logs] + for l in old_logs: + insert_query = insert(logs).values( + user_id=l['user_id'], + filename=l['filename'], + filesize=l['filesize'], + timestamp=l['timestamp'] + ) + await player_database.execute(insert_query) + + # tables not converted: batch_tokens (new field added), webs (auto created) + + await old_database.disconnect() + await player_database.disconnect() + print("[DB] Database conversion completed successfully.") + +def convert_array(old_array): + old_array = "[" + old_array + "]" + old_array = json.loads(old_array) + return old_array + + + +if __name__ == "__main__": + import asyncio + asyncio.run(init_db()) + asyncio.run(convert_db()) \ No newline at end of file diff --git a/new_server_7003/files/web/ranking.js b/new_server_7003/files/web/ranking.js index 69d94a5..3d721cb 100644 --- a/new_server_7003/files/web/ranking.js +++ b/new_server_7003/files/web/ranking.js @@ -168,9 +168,11 @@ function showDetailTotal(mode, page) { // Mode boxes - html = `
${songName}
`; + html = `
+
${songName}
+ `; - rowStart = '
' + rowStart = '
' rowEnd = '
' rowContent = [] @@ -182,7 +184,7 @@ function showDetailTotal(mode, page) { rowContent.push(`
${label}
`); } else { rowContent.push( - `${label}` + `${label}` ); } } @@ -210,25 +212,29 @@ function showDetailTotal(mode, page) { `; // generate pagination buttons - if (generatePrevButton) { - html += ` - - `; - } + html += ` +
+ ${generatePrevButton ? ` + + ` : '
'} - if (generateNextButton) { - html += ` - - `; - } + ${generateNextButton ? ` + + ` : ''} +
+ `; // Loop leaderboard ranks + html += `
`; for (let i = 0; i < rankingList.length; i++) { userData = rankingList[i]; @@ -245,32 +251,8 @@ function showDetailTotal(mode, page) { `; } - - // Generate pagination buttons again - if (generatePrevButton) { - html += ` - - `; - } - - if (generateNextButton) { - html += ` - - `; - } - - html += ` - - `; + html += `
`; document.getElementById('ranking_box').innerHTML = html; @@ -358,6 +340,7 @@ function showDetailIndividual(songId, mode, page = 0) { if (page > 0) { generatePrevButton = true; } + if ((page + 1) * pageCount < totalCount) { generateNextButton = true; } @@ -413,25 +396,29 @@ function showDetailIndividual(songId, mode, page = 0) { `; // generate pagination buttons - if (generatePrevButton) { - html += ` - - `; - } + html += ` +
+ ${generatePrevButton ? ` + + ` : '
'} - if (generateNextButton) { - html += ` - - `; - } + ${generateNextButton ? ` + + ` : '
'} +
+ `; // Loop leaderboard ranks + html += `
`; for (let i = 0; i < rankingList.length; i++) { userData = rankingList[i]; @@ -448,33 +435,9 @@ function showDetailIndividual(songId, mode, page = 0) { `; } + + html += `
`; - // Generate pagination buttons again - - if (generatePrevButton) { - html += ` - - `; - } - - if (generateNextButton) { - html += ` - - `; - } - - html += ` - - `; - document.getElementById('ranking_box').innerHTML = html; } else { diff --git a/new_server_7003/files/web/style.css b/new_server_7003/files/web/style.css index 1e4bc26..77021bf 100644 --- a/new_server_7003/files/web/style.css +++ b/new_server_7003/files/web/style.css @@ -20,8 +20,8 @@ padding: 20px; } .dlc_logo { - width: 100%; - max-width: 500px; + width: 70vw; + max-width: 1000px; margin-bottom: 10px; } .dlc_extra_body { @@ -34,19 +34,19 @@ padding: 0; } .text-content { - font-size: 18px; + font-size: 36px; line-height: 1.6; margin-bottom: 30px; } .buy-button { display: inline-block; - width: 160px; - height: 110px; + width: 30vw; + height: 20vw; background: url('/files/web/frame_buy.png') no-repeat center center; background-size: contain; border: none; color: white; - font-size: 22px; + font-size: 30px; font-weight: bold; padding: 15px; cursor: pointer; @@ -54,26 +54,26 @@ } .quit-button { display: inline-block; - width: 150px; - height: 30px; + width: 33vw; + height: 10vw; background: url('/files/web/quit_button.png') no-repeat center center; background-size: contain; border: none; color: white; - font-size: 20px; + font-size: 30px; font-weight: bold; cursor: pointer; position: relative; } .buy-button-extra { display: inline-block; - width: 160px; - height: 110px; + width: 30vw; + height: 20vw; background: url('/files/web/frame_buy_extra.png') no-repeat center center; background-size: contain; border: none; color: white; - font-size: 22px; + font-size: 30px; font-weight: bold; padding: 15px; cursor: pointer; @@ -81,13 +81,13 @@ } .quit-button-extra { display: inline-block; - width: 150px; - height: 30px; + width: 33vw; + height: 10vw; background: url('/files/web/quit_button_extra.png') no-repeat center center; background-size: contain; border: none; color: white; - font-size: 20px; + font-size: 30px; font-weight: bold; cursor: pointer; position: relative; @@ -99,8 +99,13 @@ margin-top: 10px; } .coin-icon { - width: 30px; - height: 30px; + width: 7vw; + height: 7vw; + margin-left: 5px; +} +.coin-price { + font-size: 30px; + font-weight: bold; margin-left: 5px; } body { @@ -131,7 +136,12 @@ html { .d_ib{display:inline-block;} .mb10p{margin-bottom:10%;} .f90{font-size:90%;} +.f80{font-size:70%;} .pt50{padding-top:50px;} +.f20{font-size:20%;} +.f30{font-size:30%;} +.f40{font-size:40%;} +.f50{font-size:50%;} .f60{font-size:60%;} .f70{font-size:70%;} .plr25{padding-left:25px;padding-right:25px;} @@ -156,7 +166,6 @@ div#wrapper{ .wrapper_box{ text-align:center; margin-top:5%; - margin-bottom:-10%; display: inline-block; position: relative; width: 100%; @@ -261,6 +270,12 @@ div#wrapper{ list-style: none; padding: 0; margin: 0; + flex-grow: 1; + overflow-y: auto; + height: 0; + margin: 0; + padding: 0; + list-style: none; } .song-item { @@ -295,6 +310,11 @@ div#wrapper{ .song-name { font-size: 26px; + flex-shrink: 0; +} + +.mode-buttons { + flex-shrink: 0; } .composer-name-owned { @@ -319,7 +339,6 @@ div#wrapper{ display: flex; justify-content: center; gap: 10px; - margin-bottom: 20px; } .player-element { display: flex; @@ -333,6 +352,7 @@ div#wrapper{ height: 100px; width: 98%; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3); + flex-shrink: 0; } .player-info { @@ -376,14 +396,6 @@ div#wrapper{ border-radius: 8px; padding: 20px; } -.leaderboard-container { - height: calc(53vh); - overflow-y: auto; - border: 1px solid #444; - margin: 20px auto; - padding: 10px; - background-color: #111; -} .leaderboard-player { display: flex; @@ -459,4 +471,77 @@ div#wrapper{ border: none; cursor: pointer; box-shadow: 0px 4px 6px rgba(0, 0, 0, 0.2); +} + + +#ranking_content { + display: flex; + flex-direction: column; + flex: 1 1 auto; + min-height: 0; +} + +.song-list { + flex: 1 1 auto; + min-height: 0; + overflow-y: auto; + padding: 0; + margin: 0; + list-style: none; +} + +#ranking_searchbar, +#ranking_sort { + flex-shrink: 0; +} + +#ranking_box { + display: flex; + flex-direction: column; + height: 90vh; + overflow: hidden; +} + +.leaderboard-players { + flex-grow: 1; + height: 90%; + overflow-y: auto; + margin: 0; + list-style: none; +} + +.pagination-container { + display: flex; + justify-content: space-between; + align-items: center; + width: 100%; + margin: 10px 0; +} + +.pagination-button { + width: 170px; + height: 40px; + background-color: #000000; + color: #FFFFFF; + border: 1px solid #808080; + cursor: pointer; + font-size: 30px; + text-align: center; +} + +.pagination-button:hover { + background-color: #333333; +} + +.placeholder { + flex: 0.25; +} + +.pagination-button-shop { + width: 170px; + height: 40px; + margin: 10px; + background-color: #000000; + color: #FFFFFF; + font-size: 24px; } \ No newline at end of file diff --git a/new_server_7003/files/web/web_shop.js b/new_server_7003/files/web/web_shop.js index 27adeb8..77f7b60 100644 --- a/new_server_7003/files/web/web_shop.js +++ b/new_server_7003/files/web/web_shop.js @@ -101,17 +101,17 @@ function generateMenuContent() { if (shopMode === 0 && stagePage === 0) { html += ` - +
- +
`; } if (generatePrevButton) { html += ` - @@ -120,7 +120,7 @@ function generateMenuContent() { if (generateNextButton) { html += ` - @@ -144,7 +144,7 @@ function generateMenuContent() { if (generatePrevButton) { html += ` - @@ -153,7 +153,7 @@ function generateMenuContent() { if (generateNextButton) { html += ` - @@ -231,10 +231,10 @@ function showItemDetail(mode, itemId) {
Item Image
-

Would you like to purchase this song?

+

Would you like to purchase this song?

-

${payload.data.property_first} - ${payload.data.property_second}

-

Difficulty Levels: ${payload.data.property_third}

+

${payload.data.property_first} - ${payload.data.property_second}

+

Difficulty Levels: ${payload.data.property_third}

Coin Icon @@ -247,10 +247,10 @@ function showItemDetail(mode, itemId) {
Item Image
-

Would you like to purchase this avatar?

+

Would you like to purchase this avatar?

-

${payload.data.property_first}

-

Effect: ${payload.data.property_second}

+

${payload.data.property_first}

+

Effect: ${payload.data.property_second}

Coin Icon @@ -263,10 +263,10 @@ function showItemDetail(mode, itemId) {
Item Image
-

Would you like to purchase this item?

+

Would you like to purchase this item?

-

${payload.data.property_first}

-

Effect: ${payload.data.property_second}

+

${payload.data.property_first}

+

Effect: ${payload.data.property_second}

Coin Icon @@ -329,19 +329,17 @@ function purchaseItem(mode, itemId) { if (payload.state === 1) { contentElement.innerHTML = `

${payload.message}

- +
+ Back
+
`; document.getElementById('coinCounter').innerText = payload.data.coin; } else { contentElement.innerHTML = `

Purchase failed: ${payload.message}

- +
+ Back
+
`; } }) @@ -349,10 +347,9 @@ function purchaseItem(mode, itemId) { console.error('Error processing purchase:', error); contentElement.innerHTML = `

Failed to process your purchase. Please try again later.

- +
+ Back
+
`; }); } @@ -373,7 +370,7 @@ function restoreBaseStructure() {
-