mirror of
https://github.com/qwerfd2/Groove_Coaster_2_Server.git
synced 2025-12-21 19:20:11 +00:00
v3 push
This commit is contained in:
18
README.md
18
README.md
@@ -10,14 +10,12 @@ A small local server for `Groove Coaster 2: Original Style`, implemented with `P
|
||||
|
||||
## Introduction
|
||||
|
||||
This project is for game preservation purposes only. Creative liberty and conveniences have been taken when it comes to specific implementation. The goal is not to ensure 1:1 behavior, but to guarrantee the minimum viability of playing this game. It is provided as-is, per the MIT license.
|
||||
This project is for game preservation purposes only. Creative liberty and conveniences have been taken when it comes to specific implementation. The goal is not to ensure 1:1 behavior, but to guarrantee the minimum viability of playing this game. It is provided as-is, per the GPLv2 license.
|
||||
|
||||
It has been rewritten multiple times to ~~nuke my poor code~~ optimize and improve. The latest iteration is 7003, which is experimental. Improvements were made but the data structure are different than the previous versions. If you are running 7002 or prior, please wait until the guide of migration is published.
|
||||
|
||||
You shall bare all the responsibility for any potential consequences as a result of running this server. If you do not agree to these requirements, you are not allowed to replicate or run this program.
|
||||
|
||||
~~It is designed as a **local** server, as Flask face issues with high concurrency. There is an optimized, `async` server, but the code is not open source. Only vetted server owners that will not violate the license terms will be given access. Contact the repo owner for more information.~~
|
||||
|
||||
The async server is now open source under the GPLv2 license. It has superceded the old server in terms of functionality and performance.
|
||||
|
||||
Inspiration: [Lost-MSth/Arcaea-server](https://github.com/Lost-MSth/Arcaea-server)
|
||||
|
||||
Special thanks: [Walter-o/gcm-downloader](https://github.com/Walter-o/gcm-downloader)
|
||||
@@ -297,14 +295,14 @@ Since this game features tons of downloadable music and stage files that cannot
|
||||
|
||||
## 简介
|
||||
|
||||
此项目的目标是保持游戏的长远可用性 (game preservation)。在具体实施上,我采取了一些便利及创意性的措施(偷懒)。此项目的目标不是确保 1:1 还原官服,而是保证游戏长久可玩。此项目在MIT许可证的“按现状” (as-is) 条件下提供。
|
||||
此项目的目标是保持游戏的长远可用性 (game preservation)。在具体实施上,我采取了一些便利及创意性的措施(偷懒)。此项目的目标不是确保 1:1 还原官服,而是保证游戏长久可玩。此项目在GPLv2许可证的“按现状” (as-is) 条件下提供。
|
||||
|
||||
It has been rewritten multiple times to ~~nuke my poor code~~ optimize and improve. The latest iteration is 7003, which is experimental. Improvements were made but the data structure are different than the previous versions. If you are running 7002 or prior, please wait until the guide of migration is published.
|
||||
|
||||
此服务器已经重写多次,以~~让屎山代码消失~~提升性能和功能。最新版为7003,尚在试验中。为提升性能,底层数据结构已经修改。如果你在运行7002及之前的版本,请稍等至指南攥写完成再进行迁移。
|
||||
|
||||
你应对因运行本服务器而产生的任何潜在后果承担全部责任。如果您不同意这些要求,则不允许您复制或运行该程序。
|
||||
|
||||
~~此服务器仅为**本地**运行设计,鉴于Flask糟糕的并发性能。一个高效,`异步`的服务器可供使用,不过代码并非开源。只有经过审核,不会违反许可条款的服务器所有者才能获得访问权限。请联系repo所有者了解更多信息。~~
|
||||
|
||||
基于`Starlette`的异步服务器已经在功能和性能上超越了老服务器,现在以`GPLv2`许可证开源。
|
||||
|
||||
灵感: [Lost-MSth/Arcaea-server](https://github.com/Lost-MSth/Arcaea-server)
|
||||
|
||||
鸣谢: [Walter-o/gcm-downloader](https://github.com/Walter-o/gcm-downloader)
|
||||
|
||||
56
new_server_7003/7003.py
Normal file
56
new_server_7003/7003.py
Normal file
@@ -0,0 +1,56 @@
|
||||
from starlette.applications import Starlette
|
||||
import os
|
||||
|
||||
# stupid loading sequence
|
||||
from api.template import init_templates
|
||||
init_templates()
|
||||
|
||||
from api.database import player_database, cache_database, init_db
|
||||
from api.misc import get_4max_version_string
|
||||
|
||||
from api.user import routes as user_routes
|
||||
from api.account import routes as account_routes
|
||||
from api.ranking import routes as rank_routes
|
||||
from api.shop import routes as shop_routes
|
||||
from api.play import routes as play_routes
|
||||
from api.batch import routes as batch_routes
|
||||
from api.web import routes as web_routes
|
||||
from api.file import routes as file_routes
|
||||
from api.discord_hook import routes as discord_routes
|
||||
from api.admin import routes as admin_routes
|
||||
|
||||
from config import DEBUG, SSL_CERT, SSL_KEY, ACTUAL_HOST, ACTUAL_PORT, BATCH_DOWNLOAD_ENABLED, AUTHORIZATION_MODE
|
||||
|
||||
if (os.path.isfile('./files/4max_ver.txt')):
|
||||
get_4max_version_string()
|
||||
|
||||
if AUTHORIZATION_MODE == 1:
|
||||
from api.email_hook import init_email
|
||||
init_email()
|
||||
|
||||
routes = []
|
||||
|
||||
routes = routes + user_routes + account_routes + rank_routes + shop_routes + play_routes + web_routes + file_routes + discord_routes + admin_routes
|
||||
|
||||
if BATCH_DOWNLOAD_ENABLED:
|
||||
routes = routes + batch_routes
|
||||
|
||||
app = Starlette(debug=DEBUG, routes=routes)
|
||||
|
||||
@app.on_event("startup")
|
||||
async def startup():
|
||||
await player_database.connect()
|
||||
await cache_database.connect()
|
||||
await init_db()
|
||||
|
||||
@app.on_event("shutdown")
|
||||
async def shutdown():
|
||||
await player_database.disconnect()
|
||||
await cache_database.disconnect()
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
ssl_context = (SSL_CERT, SSL_KEY) if SSL_CERT and SSL_KEY else None
|
||||
uvicorn.run(app, host=ACTUAL_HOST, port=ACTUAL_PORT, ssl_certfile=SSL_CERT, ssl_keyfile=SSL_KEY)
|
||||
|
||||
# Made By Tony 2025.11.21
|
||||
405
new_server_7003/api/account.py
Normal file
405
new_server_7003/api/account.py
Normal file
@@ -0,0 +1,405 @@
|
||||
from starlette.responses import Response, HTMLResponse
|
||||
from starlette.requests import Request
|
||||
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.crypt import decrypt_fields
|
||||
from config import AUTHORIZATION_MODE
|
||||
|
||||
async def name_reset(request: Request):
|
||||
form = await request.form()
|
||||
username = form.get("username")
|
||||
password = form.get("password")
|
||||
|
||||
if not username or not password:
|
||||
return inform_page("FAILED:<br>Missing username or password.", 0)
|
||||
|
||||
if len(username) < 6 or len(username) > 20:
|
||||
return inform_page("FAILED:<br>Username must be between 6 and 20 characters long.", 0)
|
||||
|
||||
if not is_alphanumeric(username):
|
||||
return inform_page("FAILED:<br>Username must consist entirely of alphanumeric characters.", 0)
|
||||
|
||||
if username == password:
|
||||
return inform_page("FAILED:<br>Username cannot be the same as password.", 0)
|
||||
|
||||
decrypted_fields, _ = await decrypt_fields(request)
|
||||
if not decrypted_fields:
|
||||
return inform_page("FAILED:<br>Invalid request data.", 0)
|
||||
|
||||
if not await check_blacklist(decrypted_fields):
|
||||
return inform_page("FAILED:<br>Your account is banned and you are not allowed to perform this action.", 0)
|
||||
|
||||
user_info, device_info = await decrypt_fields_to_user_info(decrypted_fields)
|
||||
if user_info:
|
||||
existing_user_info = await user_name_to_user_info(username)
|
||||
if existing_user_info:
|
||||
return inform_page("FAILED:<br>Another user already has this name.", 0)
|
||||
|
||||
password_hash = user_info['password_hash']
|
||||
if password_hash:
|
||||
if verify_password(password, password_hash):
|
||||
update_data = {
|
||||
"username": username
|
||||
}
|
||||
await set_user_data_using_decrypted_fields(decrypted_fields, update_data)
|
||||
return inform_page("SUCCESS:<br>Username updated.", 0)
|
||||
else:
|
||||
return inform_page("FAILED:<br>Password is not correct.<br>Please try again.", 0)
|
||||
else:
|
||||
return inform_page("FAILED:<br>User has no password hash.<br>This should not happen.", 0)
|
||||
else:
|
||||
return inform_page("FAILED:<br>User does not exist.<br>This should not happen.", 0)
|
||||
|
||||
async def password_reset(request: Request):
|
||||
form = await request.form()
|
||||
old_password = form.get("old")
|
||||
new_password = form.get("new")
|
||||
|
||||
if not old_password or not new_password:
|
||||
return inform_page("FAILED:<br>Missing old or new password.", 0)
|
||||
|
||||
decrypted_fields, _ = await decrypt_fields(request)
|
||||
if not decrypted_fields:
|
||||
return inform_page("FAILED:<br>Invalid request data.", 0)
|
||||
|
||||
user_info, device_info = await decrypt_fields_to_user_info(decrypted_fields)
|
||||
|
||||
if user_info:
|
||||
username = user_info['username']
|
||||
if username == new_password:
|
||||
return inform_page("FAILED:<br>Username cannot be the same as password.", 0)
|
||||
if len(new_password) < 6:
|
||||
return inform_page("FAILED:<br>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)
|
||||
updated_data = {
|
||||
"password_hash": hashed_new_password
|
||||
}
|
||||
await set_user_data_using_decrypted_fields(decrypted_fields, updated_data)
|
||||
return inform_page("SUCCESS:<br>Password updated.", 0)
|
||||
else:
|
||||
return inform_page("FAILED:<br>Old password is not correct.<br>Please try again.", 0)
|
||||
else:
|
||||
return inform_page("FAILED:<br>User has no password hash.<br>This should not happen.", 0)
|
||||
else:
|
||||
return inform_page("FAILED:<br>User does not exist.<br>This should not happen.", 0)
|
||||
|
||||
async def user_coin_mp(request: Request):
|
||||
form = await request.form()
|
||||
mp = form.get("coin_mp")
|
||||
|
||||
if not mp:
|
||||
return inform_page("FAILED:<br>Missing multiplier.", 0)
|
||||
|
||||
mp = int(mp)
|
||||
|
||||
if mp < 0 or mp > 5:
|
||||
return inform_page("FAILED:<br>Multiplier not acceptable.", 0)
|
||||
|
||||
decrypted_fields, _ = await decrypt_fields(request)
|
||||
if not decrypted_fields:
|
||||
return inform_page("FAILED:<br>Invalid request data.", 0)
|
||||
|
||||
user_info, _ = await decrypt_fields_to_user_info(decrypted_fields)
|
||||
|
||||
if user_info:
|
||||
update_data = {
|
||||
"coin_mp": mp
|
||||
}
|
||||
await set_user_data_using_decrypted_fields(decrypted_fields, update_data)
|
||||
return inform_page("SUCCESS:<br>Coin multiplier set to " + str(mp) + ".", 0)
|
||||
else:
|
||||
return inform_page("FAILED:<br>User does not exist.", 0)
|
||||
|
||||
async def save_migration(request: Request):
|
||||
form = await request.form()
|
||||
save_id = form.get("save_id")
|
||||
|
||||
if not save_id:
|
||||
return inform_page("FAILED:<br>Missing save_id.", 0)
|
||||
|
||||
if len(save_id) != 24 or not all(c in '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ' for c in save_id):
|
||||
return inform_page("FAILED:<br>Save ID not acceptable format.", 0)
|
||||
|
||||
decrypted_fields, _ = await decrypt_fields(request)
|
||||
if not decrypted_fields:
|
||||
return inform_page("FAILED:<br>Invalid request data.", 0)
|
||||
|
||||
should_serve_result = await should_serve(decrypted_fields)
|
||||
|
||||
if not should_serve_result:
|
||||
return inform_page("FAILED:<br>You cannot access this feature right now.", 0)
|
||||
|
||||
user_info, _ = await decrypt_fields_to_user_info(decrypted_fields)
|
||||
if user_info:
|
||||
user_id = user_info['id']
|
||||
username = user_info['username']
|
||||
existing_save_user = await get_user_from_save_id(save_id)
|
||||
|
||||
if existing_save_user['id'] == user_id:
|
||||
return inform_page("FAILED:<br>Save ID is already associated with your account.", 0)
|
||||
|
||||
existing_save_data = ""
|
||||
if existing_save_user:
|
||||
existing_save_data = await read_user_save_file(existing_save_user['id'])
|
||||
|
||||
if existing_save_data != "":
|
||||
update_data = {
|
||||
"save_crc": existing_save_user['crc'],
|
||||
"save_timestamp": existing_save_user['timestamp']
|
||||
}
|
||||
await set_user_data_using_decrypted_fields(decrypted_fields, update_data)
|
||||
await write_user_save_file(user_info['id'], existing_save_data)
|
||||
|
||||
return inform_page("SUCCESS:<br>Save migration was applied. If this was done by mistake, press the Save button now.", 0)
|
||||
else:
|
||||
return inform_page("FAILED:<br>Save ID is not associated with a save file.", 0)
|
||||
|
||||
else:
|
||||
return inform_page("FAILED:<br>User does not exist.", 0)
|
||||
|
||||
async def register(request: Request):
|
||||
form = await request.form()
|
||||
username = form.get("username")
|
||||
password = form.get("password")
|
||||
|
||||
if not username or not password:
|
||||
return inform_page("FAILED:<br>Missing username or password.", 0)
|
||||
|
||||
if username == password:
|
||||
return inform_page("FAILED:<br>Username cannot be the same as password.", 0)
|
||||
|
||||
if len(username) < 6 or len(username) > 20:
|
||||
return inform_page("FAILED:<br>Username must be between 6 and 20<br>characters long.", 0)
|
||||
|
||||
if len(password) < 6:
|
||||
return inform_page("FAILED:<br>Password must have<br>6 or above characters.", 0)
|
||||
|
||||
if not is_alphanumeric(username):
|
||||
return inform_page("FAILED:<br>Username must consist entirely of<br>alphanumeric characters.", 0)
|
||||
|
||||
decrypted_fields, _ = await decrypt_fields(request)
|
||||
if not decrypted_fields:
|
||||
return inform_page("FAILED:<br>Invalid request data.", 0)
|
||||
|
||||
user_info, _ = await decrypt_fields_to_user_info(decrypted_fields)
|
||||
|
||||
if user_info:
|
||||
return inform_page("FAILED:<br>Another user already has this name.", 0)
|
||||
|
||||
await create_user(username, hash_password(password), decrypted_fields[b'vid'][0].decode())
|
||||
|
||||
return inform_page("SUCCESS:<br>Account is registered.<br>You can now backup/restore your save file.<br>You can only log into one device at a time.", 0)
|
||||
|
||||
async def logout(request: Request):
|
||||
decrypted_fields, _ = await decrypt_fields(request)
|
||||
if not decrypted_fields:
|
||||
return inform_page("FAILED:<br>Invalid request data.", 0)
|
||||
|
||||
if not await check_blacklist(decrypted_fields):
|
||||
return inform_page("FAILED:<br>Your account is banned and you are<br>not allowed to perform this action.", 0)
|
||||
|
||||
device_id = decrypted_fields[b'vid'][0].decode()
|
||||
await logout_user(device_id)
|
||||
return inform_page("Logout success.", 0)
|
||||
|
||||
async def login(request: Request):
|
||||
form = await request.form()
|
||||
username = form.get("username")
|
||||
password = form.get("password")
|
||||
|
||||
if not username or not password:
|
||||
return inform_page("FAILED:<br>Missing username or password.", 0)
|
||||
|
||||
decrypted_fields, _ = await decrypt_fields(request)
|
||||
if not decrypted_fields:
|
||||
return inform_page("FAILED:<br>Invalid request data.", 0)
|
||||
|
||||
user_record = await user_name_to_user_info(username)
|
||||
if user_record:
|
||||
user_id = user_record['id']
|
||||
|
||||
password_hash_record = user_record['password_hash']
|
||||
if password_hash_record and verify_password(password, password_hash_record[0]):
|
||||
device_id = decrypted_fields[b'vid'][0].decode()
|
||||
await login_user(user_id, device_id)
|
||||
|
||||
return inform_page("SUCCESS:<br>You are logged in.", 0)
|
||||
else:
|
||||
return inform_page("FAILED:<br>Username or password incorrect.", 0)
|
||||
else:
|
||||
return inform_page("FAILED:<br>Username or password incorrect.", 0)
|
||||
|
||||
async def load(request: Request):
|
||||
decrypted_fields, _ = await decrypt_fields(request)
|
||||
if not decrypted_fields:
|
||||
return Response("""<response><code>10</code><message><ja>この機能を使用するには、まずアカウントを登録する必要があります。</ja><en>You need to register an account first before this feature can be used.</en><fr>Vous devez d'abord créer un compte avant de pouvoir utiliser cette fonctionnalité.</fr><it>È necessario registrare un account prima di poter utilizzare questa funzione.</it></message></response>""", media_type="application/xml")
|
||||
|
||||
should_serve_result = await should_serve(decrypted_fields)
|
||||
|
||||
if not should_serve_result:
|
||||
return Response("""<response><code>10</code><message><ja>この機能を使用するには、現在アクセスできません。</ja><en>You cannot access this feature right now.</en><fr>Vous ne pouvez pas accéder à cette fonctionnalité pour le moment.</fr><it>Non è possibile accedere a questa funzione in questo momento.</it></message></response>""", media_type="application/xml")
|
||||
|
||||
user_info, device_info = await decrypt_fields_to_user_info(decrypted_fields)
|
||||
if not user_info:
|
||||
return Response( """<response><code>10</code><message><ja>この機能を使用するには、まずアカウントを登録する必要があります。</ja><en>You need to register an account first before this feature can be used.</en><fr>Vous devez d'abord créer un compte avant de pouvoir utiliser cette fonctionnalité.</fr><it>È necessario registrare un account prima di poter utilizzare questa funzione.</it></message></response>""", media_type="application/xml")
|
||||
data = await read_user_save_file(user_info['id'])
|
||||
if data and data != "":
|
||||
crc = user_info['save_crc']
|
||||
timestamp = user_info['save_timestamp']
|
||||
xml_data = f"""<?xml version="1.0" encoding="UTF-8"?><response><code>0</code>
|
||||
<data>{data}</data>
|
||||
<crc>{crc}</crc>
|
||||
<date>{timestamp}</date>
|
||||
</response>"""
|
||||
return Response(xml_data, media_type="application/xml")
|
||||
else:
|
||||
return Response( """<response><code>12</code><message><ja>セーブデータが無いか、セーブデータが破損しているため、ロードできませんでした。</ja><en>Unable to load; either no save data exists, or the save data is corrupted.</en><fr>Chargement impossible : les données de sauvegarde sont absentes ou corrompues.</fr><it>Impossibile caricare. Non esistono dati salvati o quelli esistenti sono danneggiati.</it></message></response>""", media_type="application/xml")
|
||||
|
||||
async def save(request: Request):
|
||||
decrypted_fields, _ = await decrypt_fields(request)
|
||||
if not decrypted_fields:
|
||||
return Response("""<response><code>10</code><message><ja>この機能を使用するには、まずアカウントを登録する必要があります。</ja><en>You need to register an account first before this feature can be used.</en><fr>Vous devez d'abord créer un compte avant de pouvoir utiliser cette fonctionnalité.</fr><it>È necessario registrare un account prima di poter utilizzare questa funzione.</it></message></response>""", media_type="application/xml")
|
||||
|
||||
should_serve_result = await should_serve(decrypted_fields)
|
||||
|
||||
if not should_serve_result:
|
||||
return Response("""<response><code>10</code><message><ja>この機能を使用するには、現在アクセスできません。</ja><en>You cannot access this feature right now.</en><fr>Vous ne pouvez pas accéder à cette fonctionnalité pour le moment.</fr><it>Non è possibile accedere a questa funzione in questo momento.</it></message></response>""", media_type="application/xml")
|
||||
|
||||
data = await request.body()
|
||||
data = data.decode("utf-8")
|
||||
|
||||
user_info, _ = await decrypt_fields_to_user_info(decrypted_fields)
|
||||
|
||||
username = user_info['username']
|
||||
user_id = user_info['id']
|
||||
if username:
|
||||
crc = crc32_decimal(data)
|
||||
formatted_time = datetime.now()
|
||||
is_save_id_unique = False
|
||||
while not is_save_id_unique:
|
||||
save_id = ''.join(secrets.choice('abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ') for _ in range(24))
|
||||
|
||||
existing_user = await get_user_from_save_id(save_id)
|
||||
if not existing_user:
|
||||
is_save_id_unique = True
|
||||
|
||||
await write_user_save_file(user_info['id'], data)
|
||||
|
||||
update_data = {
|
||||
"save_crc": crc,
|
||||
"save_id": save_id,
|
||||
"save_timestamp": formatted_time
|
||||
}
|
||||
await set_user_data_using_decrypted_fields(decrypted_fields, update_data)
|
||||
|
||||
return Response("""<response><code>0</code></response>""", media_type="application/xml")
|
||||
else:
|
||||
return Response("""<response><code>10</code><message><ja>この機能を使用するには、まずアカウントを登録する必要があります。</ja><en>You need to register an account first before this feature can be used.</en><fr>Vous devez d'abord créer un compte avant de pouvoir utiliser cette fonctionnalité.</fr><it>È necessario registrare un account prima di poter utilizzare questa funzione.</it></message></response>""", media_type="application/xml")
|
||||
|
||||
async def ttag(request: Request):
|
||||
decrypted_fields, original_field = await decrypt_fields(request)
|
||||
if not decrypted_fields:
|
||||
return inform_page("FAILED:<br>Invalid request data.", 0)
|
||||
|
||||
user_info, _ = await decrypt_fields_to_user_info(decrypted_fields)
|
||||
|
||||
if user_info:
|
||||
username = user_info['username']
|
||||
user_id = user_info['id']
|
||||
gcoin_mp = user_info['coin_mp']
|
||||
savefile_id = user_info['save_id']
|
||||
if AUTHORIZATION_MODE == 0:
|
||||
bind_element = '<p>No bind required in current mode.</p>'
|
||||
elif AUTHORIZATION_MODE == 1:
|
||||
# Email auth mode
|
||||
bind_state = await get_bind(user_id)
|
||||
|
||||
if bind_state and bind_state['is_verified'] == 1:
|
||||
bind_element = f'<p>Email verified: {bind_state["bind_acc"]}\nTo remove a bind, contact the administrator.</p>'
|
||||
else:
|
||||
bind_element = f"""
|
||||
<form action="/send_email/?{original_field}" method="post">
|
||||
<div class="f60 a_center">
|
||||
<label for="email">Email:</label>
|
||||
<br>
|
||||
<input class="input" id="email" name="email" type="email">
|
||||
<br>
|
||||
<input class="bt_bg01_narrow" type="submit" value="Send Email">
|
||||
</div>
|
||||
</form>
|
||||
<form action="/verify/?{original_field}" method="post">
|
||||
<div class="f60 a_center">
|
||||
<label for="code">Verification Code:</label>
|
||||
<br>
|
||||
<input class="input" id="code" name="code">
|
||||
<br>
|
||||
<input class="bt_bg01_narrow" type="submit" value="Verify">
|
||||
</div>
|
||||
</form>
|
||||
"""
|
||||
|
||||
elif AUTHORIZATION_MODE == 2:
|
||||
bind_state = await get_bind(user_id)
|
||||
bind_code = await generate_salt(username, user_id)
|
||||
if bind_state and bind_state['is_verified'] == 1:
|
||||
bind_element = f'<p>Discord verified: {bind_state["bind_acc"]}<br>To remove a bind, contact the administrator.</p>'
|
||||
else:
|
||||
bind_element = f"""
|
||||
<p>To receive a verification code, please join our Discord server 'https://discord.gg/vugfJdc2rk' and use the !bind command with your account name and the following code. Do not leak this code to others.</p>
|
||||
<div class="f60 a_center">
|
||||
<label>Your bind code:</label>
|
||||
<br>
|
||||
<input class="input" value="{bind_code}" readonly>
|
||||
</div>
|
||||
<form action="/verify/?{original_field}" method="post">
|
||||
<div class="f60 a_center">
|
||||
<label for="code">Verification Code:</label>
|
||||
<br>
|
||||
<input class="input" id="code" name="code">
|
||||
<br>
|
||||
<input class="bt_bg01_narrow" type="submit" value="Verify">
|
||||
</div>
|
||||
</form>
|
||||
"""
|
||||
|
||||
with open("web/profile.html", "r") as file:
|
||||
html_content = file.read().format(
|
||||
bind_element=bind_element,
|
||||
pid=original_field,
|
||||
user=username,
|
||||
gcoin_mp_0='selected' if gcoin_mp == 0 else '',
|
||||
gcoin_mp_1='selected' if gcoin_mp == 1 else '',
|
||||
gcoin_mp_2='selected' if gcoin_mp == 2 else '',
|
||||
gcoin_mp_3='selected' if gcoin_mp == 3 else '',
|
||||
gcoin_mp_4='selected' if gcoin_mp == 4 else '',
|
||||
gcoin_mp_5='selected' if gcoin_mp == 5 else '',
|
||||
savefile_id=savefile_id,
|
||||
|
||||
)
|
||||
else:
|
||||
with open("web/register.html", "r") as file:
|
||||
html_content = file.read().format(pid=original_field)
|
||||
|
||||
return HTMLResponse(html_content)
|
||||
|
||||
routes = [
|
||||
Route('/name_reset/', name_reset, methods=['POST']),
|
||||
Route('/password_reset/', password_reset, methods=['POST']),
|
||||
Route('/coin_mp/', user_coin_mp, methods=['POST']),
|
||||
Route('/save_migration/', save_migration, methods=['POST']),
|
||||
Route('/register/', register, methods=['POST']),
|
||||
Route('/logout/', logout, methods=['POST']),
|
||||
Route('/login/', login, methods=['POST']),
|
||||
Route('/load.php', load, methods=['GET']),
|
||||
Route('/save.php', save, methods=['POST']),
|
||||
Route('/ttag.php', ttag, methods=['GET']),
|
||||
]
|
||||
358
new_server_7003/api/admin.py
Normal file
358
new_server_7003/api/admin.py
Normal file
@@ -0,0 +1,358 @@
|
||||
from starlette.requests import Request
|
||||
from starlette.routing import Route
|
||||
from starlette.responses import HTMLResponse, JSONResponse, RedirectResponse
|
||||
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
|
||||
|
||||
TABLE_MAP = {
|
||||
"accounts": (accounts, ["id", "username", "password_hash", "save_crc", "save_timestamp", "save_id", "coin_mp", "title", "avatar", "created_at", "updated_at"]),
|
||||
"results": (results, ["id", "device_id", "stts", "song_id", "mode", "avatar", "score", "high_score", "play_rslt", "item", "os", "os_ver", "ver", "created_at", "updated_at"]),
|
||||
"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"]),
|
||||
"logs": (logs, ["id", "user_id", "filename", "filesize", "timestamp"]),
|
||||
}
|
||||
|
||||
async def web_admin_page(request: Request):
|
||||
adm = await is_admin(request.cookies.get("token"))
|
||||
if not adm:
|
||||
response = RedirectResponse(url="/login")
|
||||
response.delete_cookie("token")
|
||||
return response
|
||||
with open("web/admin.html", "r", encoding="utf-8") as file:
|
||||
html_template = file.read()
|
||||
return HTMLResponse(content=html_template)
|
||||
|
||||
def serialize_row(row, allowed_fields):
|
||||
result = {}
|
||||
for field in allowed_fields:
|
||||
value = row[field]
|
||||
if hasattr(value, "isoformat"): # Check if it's a datetime object
|
||||
result[field] = value.isoformat()
|
||||
else:
|
||||
result[field] = value
|
||||
return result
|
||||
|
||||
async def web_admin_get_table(request: Request):
|
||||
params = request.query_params
|
||||
adm = await is_admin(request.cookies.get("token"))
|
||||
if not adm:
|
||||
return JSONResponse({"data": [], "last_page": 1, "total": 0}, status_code=400)
|
||||
|
||||
table_name = params.get("table")
|
||||
page = int(params.get("page", 1))
|
||||
size = int(params.get("size", 25))
|
||||
sort = params.get("sort")
|
||||
dir_ = params.get("dir", "asc")
|
||||
search = params.get("search", "").strip()
|
||||
schema = params.get("schema", "0") == "1"
|
||||
|
||||
if schema:
|
||||
table, _ = TABLE_MAP[table_name]
|
||||
columns = table.columns # This is a ColumnCollection
|
||||
schema = {col.name: str(col.type).upper() for col in columns}
|
||||
return JSONResponse(schema)
|
||||
|
||||
# Validate table
|
||||
if table_name not in TABLE_MAP:
|
||||
return JSONResponse({"data": [], "last_page": 1, "total": 0}, status_code=400)
|
||||
|
||||
# Validate size
|
||||
if size < 10:
|
||||
size = 10
|
||||
if size > 100:
|
||||
size = 100
|
||||
|
||||
table, allowed_fields = TABLE_MAP[table_name]
|
||||
|
||||
# Build query
|
||||
query = table.select()
|
||||
|
||||
# Search
|
||||
if search:
|
||||
search_clauses = []
|
||||
for field in allowed_fields:
|
||||
col = getattr(table.c, field, None)
|
||||
if col is not None:
|
||||
search_clauses.append(col.like(f"%{search}%"))
|
||||
if search_clauses:
|
||||
query = query.where(sqlalchemy.or_(*search_clauses))
|
||||
|
||||
# Sort
|
||||
if sort in allowed_fields:
|
||||
col = getattr(table.c, sort, None)
|
||||
if col is not None:
|
||||
if isinstance(col.type, sqlalchemy.types.String):
|
||||
if dir_ == "desc":
|
||||
query = query.order_by(sqlalchemy.func.lower(col).desc())
|
||||
else:
|
||||
query = query.order_by(sqlalchemy.func.lower(col).asc())
|
||||
else:
|
||||
if dir_ == "desc":
|
||||
query = query.order_by(col.desc())
|
||||
else:
|
||||
query = query.order_by(col.asc())
|
||||
|
||||
# Pagination
|
||||
offset = (page - 1) * size
|
||||
query = query.offset(offset).limit(size)
|
||||
|
||||
# total count
|
||||
count_query = sqlalchemy.select(sqlalchemy.func.count()).select_from(table)
|
||||
if search:
|
||||
search_clauses = []
|
||||
for field in allowed_fields:
|
||||
col = getattr(table.c, field, None)
|
||||
if col is not None:
|
||||
search_clauses.append(col.like(f"%{search}%"))
|
||||
if search_clauses:
|
||||
count_query = count_query.where(sqlalchemy.or_(*search_clauses))
|
||||
total = await player_database.fetch_val(count_query)
|
||||
last_page = max(1, (total + size - 1) // size)
|
||||
|
||||
# Fetch data
|
||||
rows = await player_database.fetch_all(query)
|
||||
data = [serialize_row(row, allowed_fields) for row in rows]
|
||||
|
||||
response_data = {"data": data, "last_page": last_page, "total": total}
|
||||
|
||||
return JSONResponse(response_data)
|
||||
|
||||
async def web_admin_table_set(request: Request):
|
||||
params = await request.json()
|
||||
adm = await is_admin(request.cookies.get("token"))
|
||||
if not adm:
|
||||
return JSONResponse({"status": "failed", "message": "Invalid token."}, status_code=400)
|
||||
|
||||
table_name = params.get("table")
|
||||
row = params.get("row")
|
||||
|
||||
if table_name not in TABLE_MAP:
|
||||
return JSONResponse({"status": "failed", "message": "Invalid table name."}, status_code=401)
|
||||
|
||||
table, _ = TABLE_MAP[table_name]
|
||||
columns = table.columns # This is a ColumnCollection
|
||||
|
||||
schema = {col.name: str(col.type) for col in columns}
|
||||
|
||||
try:
|
||||
row_data = row
|
||||
if not isinstance(row_data, dict):
|
||||
raise ValueError("Row data must be a JSON object.")
|
||||
id_field = None
|
||||
# Find primary key field (id or objectId)
|
||||
for pk in ["id"]:
|
||||
if pk in row_data:
|
||||
id_field = pk
|
||||
break
|
||||
if not id_field:
|
||||
raise ValueError("Row data must contain a primary key ('id' or 'objectId').")
|
||||
for key, value in row_data.items():
|
||||
if key not in schema:
|
||||
raise ValueError(f"Field '{key}' does not exist in table schema.")
|
||||
# Type checking
|
||||
expected_type = schema[key]
|
||||
if (value == "" or value is None) and table.c[key].nullable:
|
||||
continue # Allow null or empty for nullable fields
|
||||
|
||||
if expected_type.startswith("INTEGER"):
|
||||
if not isinstance(value, int):
|
||||
try:
|
||||
value = int(value)
|
||||
except:
|
||||
raise ValueError(f"Field '{key}' must be an integer.")
|
||||
|
||||
elif expected_type.startswith("FLOAT"):
|
||||
try:
|
||||
value = float(value)
|
||||
except:
|
||||
raise ValueError(f"Field '{key}' must be a float.")
|
||||
|
||||
elif expected_type.startswith("BOOLEAN"):
|
||||
try:
|
||||
if isinstance(value, str):
|
||||
if value.lower() in ["true", "1"]:
|
||||
value = True
|
||||
elif value.lower() in ["false", "0"]:
|
||||
value = False
|
||||
else:
|
||||
raise ValueError
|
||||
elif isinstance(value, int):
|
||||
value = bool(value)
|
||||
except:
|
||||
raise ValueError(f"Field '{key}' must be a boolean.")
|
||||
|
||||
elif expected_type.startswith("JSON"):
|
||||
if not isinstance(value, dict) and not isinstance(value, list):
|
||||
raise ValueError(f"Field '{key}' must be a JSON object or array.")
|
||||
elif expected_type.startswith("VARCHAR") or expected_type.startswith("STRING"):
|
||||
if not isinstance(value, str):
|
||||
raise ValueError(f"Field '{key}' must be a string.")
|
||||
elif expected_type.startswith("DATETIME"):
|
||||
# Try to convert to datetime object
|
||||
try:
|
||||
if isinstance(value, str):
|
||||
dt_obj = datetime.datetime.fromisoformat(value)
|
||||
row_data[key] = dt_obj
|
||||
elif isinstance(value, (int, float)):
|
||||
dt_obj = datetime.datetime.fromtimestamp(value)
|
||||
row_data[key] = dt_obj
|
||||
elif isinstance(value, datetime.datetime):
|
||||
pass # already datetime
|
||||
else:
|
||||
raise ValueError
|
||||
except Exception:
|
||||
raise ValueError(f"Field '{key}' must be a valid ISO datetime string or timestamp.")
|
||||
except Exception as e:
|
||||
return JSONResponse({"status": "failed", "message": f"Invalid row data: {str(e)}"}, status_code=402)
|
||||
|
||||
update_data = {k: v for k, v in row_data.items() if k != id_field}
|
||||
for upd_data in update_data:
|
||||
if upd_data == "" or upd_data is None:
|
||||
if not table.c[upd_data].nullable:
|
||||
return JSONResponse({"status": "failed", "message": f"Field '{upd_data}' cannot be null."}, status_code=403)
|
||||
else:
|
||||
update_data[upd_data] = None
|
||||
update_query = table.update().where(getattr(table.c, id_field) == row_data[id_field]).values(**update_data)
|
||||
await player_database.execute(update_query)
|
||||
|
||||
return JSONResponse({"status": "success", "message": "Row updated successfully."})
|
||||
|
||||
async def web_admin_table_delete(request: Request):
|
||||
params = await request.json()
|
||||
adm = await is_admin(request.cookies.get("token"))
|
||||
if not adm:
|
||||
return JSONResponse({"status": "failed", "message": "Invalid token."}, status_code=400)
|
||||
|
||||
table_name = params.get("table")
|
||||
row_id = params.get("id")
|
||||
|
||||
if table_name not in TABLE_MAP:
|
||||
return JSONResponse({"status": "failed", "message": "Invalid table name."}, status_code=401)
|
||||
|
||||
if not row_id:
|
||||
return JSONResponse({"status": "failed", "message": "Row ID is required."}, status_code=402)
|
||||
|
||||
table, _ = TABLE_MAP[table_name]
|
||||
|
||||
if table_name in ["results"]:
|
||||
delete_query = table.delete().where(table.c.rid == row_id)
|
||||
else:
|
||||
delete_query = table.delete().where(table.c.id == row_id)
|
||||
|
||||
result = await player_database.execute(delete_query)
|
||||
|
||||
return JSONResponse({"status": "success", "message": "Row deleted successfully."})
|
||||
|
||||
async def web_admin_table_insert(request: Request):
|
||||
params = await request.json()
|
||||
adm = await is_admin(request.cookies.get("token"))
|
||||
if not adm:
|
||||
return JSONResponse({"status": "failed", "message": "Invalid token."}, status_code=400)
|
||||
|
||||
table_name = params.get("table")
|
||||
row = params.get("row")
|
||||
|
||||
if table_name not in TABLE_MAP:
|
||||
return JSONResponse({"status": "failed", "message": "Invalid table name."}, status_code=401)
|
||||
|
||||
table, _ = TABLE_MAP[table_name]
|
||||
columns = table.columns
|
||||
|
||||
schema = {col.name: str(col.type) for col in columns}
|
||||
|
||||
# VERIFY that the row data conforms to the schema
|
||||
try:
|
||||
row_data = row
|
||||
if not isinstance(row_data, dict):
|
||||
raise ValueError("Row data must be a JSON object.")
|
||||
for key, value in row_data.items():
|
||||
if key not in schema:
|
||||
raise ValueError(f"Field '{key}' does not exist in table schema.")
|
||||
expected_type = schema[key]
|
||||
if expected_type.startswith("INTEGER"):
|
||||
if not isinstance(value, int):
|
||||
raise ValueError(f"Field '{key}' must be an integer.")
|
||||
elif expected_type.startswith("FLOAT"):
|
||||
if not isinstance(value, float) and not isinstance(value, int):
|
||||
raise ValueError(f"Field '{key}' must be a float.")
|
||||
elif expected_type.startswith("BOOLEAN"):
|
||||
if not isinstance(value, bool):
|
||||
raise ValueError(f"Field '{key}' must be a boolean.")
|
||||
elif expected_type.startswith("JSON"):
|
||||
try:
|
||||
json.loads(value)
|
||||
except:
|
||||
raise ValueError(f"Field '{key}' must be a valid JSON string.")
|
||||
elif expected_type.startswith("VARCHAR") or expected_type.startswith("STRING"):
|
||||
if not isinstance(value, str):
|
||||
raise ValueError(f"Field '{key}' must be a string.")
|
||||
elif expected_type.startswith("DATETIME"):
|
||||
try:
|
||||
if isinstance(value, str):
|
||||
dt_obj = datetime.datetime.fromisoformat(value)
|
||||
row_data[key] = dt_obj
|
||||
elif isinstance(value, (int, float)):
|
||||
dt_obj = datetime.datetime.fromtimestamp(value)
|
||||
row_data[key] = dt_obj
|
||||
elif isinstance(value, datetime.datetime):
|
||||
pass
|
||||
else:
|
||||
raise ValueError
|
||||
except Exception:
|
||||
raise ValueError(f"Field '{key}' must be a valid ISO datetime string or timestamp.")
|
||||
except Exception as e:
|
||||
return JSONResponse({"status": "failed", "message": f"Invalid row data: {str(e)}"}, status_code=402)
|
||||
|
||||
insert_data = {k: v for k, v in row_data.items() if k in schema}
|
||||
insert_query = table.insert().values(**insert_data)
|
||||
result = await player_database.execute(insert_query)
|
||||
return JSONResponse({"status": "success", "message": "Row inserted successfully.", "inserted_id": result})
|
||||
|
||||
async def web_admin_data_get(request: Request):
|
||||
adm = await is_admin(request.cookies.get("token"))
|
||||
if not adm:
|
||||
return JSONResponse({"status": "failed", "message": "Invalid token."}, status_code=400)
|
||||
|
||||
params = request.query_params
|
||||
uid = int(params.get("id"))
|
||||
|
||||
data = await read_user_save_file(uid)
|
||||
|
||||
return JSONResponse({"status": "success", "data": data})
|
||||
|
||||
async def web_admin_data_save(request: Request):
|
||||
adm = await is_admin(request.cookies.get("token"))
|
||||
if not adm:
|
||||
return JSONResponse({"status": "failed", "message": "Invalid token."}, status_code=400)
|
||||
|
||||
params = await request.json()
|
||||
uid = int(params['id'])
|
||||
save_data = params['data']
|
||||
|
||||
crc = crc32_decimal(save_data)
|
||||
formatted_time = datetime.datetime.now()
|
||||
|
||||
query = accounts.update().where(accounts.c.id == uid).values(save_crc=crc, save_timestamp=formatted_time)
|
||||
await player_database.execute(query)
|
||||
await write_user_save_file(uid, save_data)
|
||||
|
||||
return JSONResponse({"status": "success", "message": "Data saved successfully."})
|
||||
|
||||
routes = [
|
||||
Route("/admin", web_admin_page, methods=["GET"]),
|
||||
Route("/admin/", web_admin_page, methods=["GET"]),
|
||||
Route("/admin/table", web_admin_get_table, methods=["GET"]),
|
||||
Route("/admin/table/update", web_admin_table_set, methods=["POST"]),
|
||||
Route("/admin/table/delete", web_admin_table_delete, methods=["POST"]),
|
||||
Route("/admin/table/insert", web_admin_table_insert, methods=["POST"]),
|
||||
Route("/admin/data", web_admin_data_get, methods=["GET"]),
|
||||
Route("/admin/data/save", web_admin_data_save, methods=["POST"]),
|
||||
]
|
||||
50
new_server_7003/api/batch.py
Normal file
50
new_server_7003/api/batch.py
Normal file
@@ -0,0 +1,50 @@
|
||||
from starlette.responses import HTMLResponse
|
||||
from starlette.requests import Request
|
||||
from starlette.routing import Route
|
||||
import os
|
||||
import json
|
||||
import time
|
||||
|
||||
from api.database import player_database, batch_tokens
|
||||
from config import THREAD_COUNT
|
||||
|
||||
async def batch_handler(request: Request):
|
||||
data = await request.json()
|
||||
token = data.get("token")
|
||||
platform = data.get("platform")
|
||||
if not token:
|
||||
return HTMLResponse(content="Token is required", status_code=400)
|
||||
|
||||
if platform not in ["Android", "iOS"]:
|
||||
return HTMLResponse(content="Invalid platform", status_code=400)
|
||||
|
||||
query = batch_tokens.select().where(batch_tokens.c.token == token)
|
||||
result = await player_database.fetch_one(query)
|
||||
|
||||
if not result:
|
||||
return HTMLResponse(content="Invalid token", status_code=400)
|
||||
|
||||
if result['expire_at'] < int(time.time()):
|
||||
return HTMLResponse(content="Token expired", status_code=400)
|
||||
|
||||
with open(os.path.join('api/config/', 'download_manifest.json'), 'r', encoding='utf-8') as f:
|
||||
stage_manifest = json.load(f)
|
||||
|
||||
if platform == "Android":
|
||||
with open(os.path.join('api/config/', 'download_manifest_android.json'), 'r', encoding='utf-8') as f:
|
||||
audio_manifest = json.load(f)
|
||||
else:
|
||||
with open(os.path.join('api/config/', 'download_manifest_ios.json'), 'r', encoding='utf-8') as f:
|
||||
audio_manifest = json.load(f)
|
||||
|
||||
download_manifest = {
|
||||
"stage": stage_manifest,
|
||||
"audio": audio_manifest,
|
||||
"thread": THREAD_COUNT
|
||||
}
|
||||
|
||||
return HTMLResponse(content=json.dumps(download_manifest), status_code=200)
|
||||
|
||||
routes = [
|
||||
Route("/batch", batch_handler, methods=["POST"]),
|
||||
]
|
||||
1332
new_server_7003/api/config/avatar_list.json
Normal file
1332
new_server_7003/api/config/avatar_list.json
Normal file
File diff suppressed because it is too large
Load Diff
993
new_server_7003/api/config/download_manifest.json
Normal file
993
new_server_7003/api/config/download_manifest.json
Normal file
@@ -0,0 +1,993 @@
|
||||
{
|
||||
"10pt8tion.zip": 2364732173,
|
||||
"17k.zip": 3491384895,
|
||||
"1llusion.zip": 1409984108,
|
||||
"39music.zip": 1348103846,
|
||||
"7days.zip": 208333360,
|
||||
"8bit.zip": 2284320282,
|
||||
"8cell.zip": 2742482269,
|
||||
"8em.zip": 3952524451,
|
||||
"abby.zip": 2040725964,
|
||||
"abst.zip": 27658264,
|
||||
"acid2.zip": 827689283,
|
||||
"adr.zip": 4065932017,
|
||||
"againagain.zip": 2400435119,
|
||||
"agentcrisis.zip": 2296969836,
|
||||
"ai-zyto.zip": 258451717,
|
||||
"aid.zip": 3824991538,
|
||||
"aitai.zip": 3007514820,
|
||||
"akahitoha.zip": 3114295315,
|
||||
"akariga.zip": 2807326772,
|
||||
"akeboshi.zip": 713680816,
|
||||
"akkanbaby.zip": 2328959106,
|
||||
"akuerion.zip": 1244257778,
|
||||
"akuno.zip": 3744588613,
|
||||
"akunomeshi.zip": 2031407960,
|
||||
"alien201903291100.zip": 2897427841,
|
||||
"alkali.zip": 2643905870,
|
||||
"alright.zip": 4198485501,
|
||||
"altale.zip": 2691111759,
|
||||
"amano.zip": 3855284673,
|
||||
"ameto.zip": 537646150,
|
||||
"amnjk.zip": 1347716250,
|
||||
"analysis.zip": 1590100186,
|
||||
"ancient.zip": 458144197,
|
||||
"androgy.zip": 2426281701,
|
||||
"andrormx.zip": 528792738,
|
||||
"anohi.zip": 3779946917,
|
||||
"anone.zip": 1694985553,
|
||||
"anti.zip": 4238595349,
|
||||
"anzen.zip": 4028419719,
|
||||
"aou-flower.zip": 784304599,
|
||||
"aou-garakuta.zip": 733233076,
|
||||
"aou-saitama.zip": 3708333456,
|
||||
"aou2-fauna.zip": 1053887090,
|
||||
"aou2-fujin.zip": 1611221466,
|
||||
"aou2-ignis.zip": 2524977419,
|
||||
"aou2-vertex.zip": 4240978553,
|
||||
"ape.zip": 3696564121,
|
||||
"apocalypse.zip": 2666636974,
|
||||
"apocalypselotus.zip": 2284957669,
|
||||
"Aprils.zip": 2071318136,
|
||||
"arabes.zip": 406723382,
|
||||
"aran10th.zip": 3924188283,
|
||||
"ark.zip": 1926080813,
|
||||
"Arkanoid202504140212.zip": 3338567108,
|
||||
"Arkinv.zip": 3472193341,
|
||||
"arts.zip": 1834149096,
|
||||
"aruiwa.zip": 3952784677,
|
||||
"asaki.zip": 1579398802,
|
||||
"asgore.zip": 2902318564,
|
||||
"astro.zip": 3139871980,
|
||||
"asuno201708311100.zip": 1722248940,
|
||||
"aun.zip": 4234431913,
|
||||
"aurgelmir.zip": 703474781,
|
||||
"axe.zip": 2385860417,
|
||||
"axion.zip": 2354980101,
|
||||
"ayano201903291100.zip": 3476338461,
|
||||
"azarea.zip": 1606262962,
|
||||
"babiron.zip": 1851740753,
|
||||
"backon.zip": 2063773688,
|
||||
"badapple.zip": 2468370521,
|
||||
"badend.zip": 1293784000,
|
||||
"balerico.zip": 1398148902,
|
||||
"ballad.zip": 2678830161,
|
||||
"battle.zip": 1862669390,
|
||||
"bb2-hz.zip": 3423069643,
|
||||
"bb2-op.zip": 328227338,
|
||||
"bb2-rc.zip": 1892537828,
|
||||
"bbchrono.zip": 3427904822,
|
||||
"bbkkbkk.zip": 2121674268,
|
||||
"bbnew13.zip": 980922186,
|
||||
"bbragna201903291100.zip": 3870927770,
|
||||
"bbtaokk.zip": 619599348,
|
||||
"beatsync.zip": 3916169525,
|
||||
"beautiful.zip": 1054903700,
|
||||
"Begin.zip": 4199828556,
|
||||
"berserk.zip": 2226991095,
|
||||
"bethere.zip": 3460475831,
|
||||
"beyond.zip": 1377437804,
|
||||
"bforc.zip": 2613820570,
|
||||
"bit.zip": 3354381652,
|
||||
"blacklotus.zip": 3850575077,
|
||||
"blackmind.zip": 2176472179,
|
||||
"blackmoon.zip": 2806383343,
|
||||
"blackor.zip": 2613392795,
|
||||
"blaze.zip": 2192372038,
|
||||
"bless.zip": 1045497051,
|
||||
"blue.zip": 3444259082,
|
||||
"blued.zip": 190575639,
|
||||
"bonetrousle.zip": 1616101703,
|
||||
"border.zip": 4110383550,
|
||||
"boroboro.zip": 1052407904,
|
||||
"bousou.zip": 1412242479,
|
||||
"brain.zip": 2366189138,
|
||||
"bright.zip": 3514234609,
|
||||
"bstdensya.zip": 1426049414,
|
||||
"bstfz-den.zip": 2962950226,
|
||||
"bstfz.zip": 592671596,
|
||||
"bstrid-den.zip": 126299746,
|
||||
"bstrid-fz.zip": 1092870337,
|
||||
"bstridge.zip": 3338062265,
|
||||
"bubble.zip": 1950120495,
|
||||
"bungaku.zip": 1430134117,
|
||||
"buriki.zip": 1932319668,
|
||||
"burnalt.zip": 1322432121,
|
||||
"cando.zip": 631869971,
|
||||
"cantcome.zip": 751473283,
|
||||
"captainmura.zip": 1980891570,
|
||||
"caramel.zip": 1666969457,
|
||||
"Cardiac.zip": 3926132984,
|
||||
"casinou.zip": 184030726,
|
||||
"ccddd202504140212.zip": 3089346863,
|
||||
"celestial.zip": 2735694029,
|
||||
"ceramic.zip": 3243182262,
|
||||
"cheche.zip": 190701981,
|
||||
"chigau.zip": 233201922,
|
||||
"children.zip": 321393840,
|
||||
"chiruno.zip": 3365177961,
|
||||
"chiruno9.zip": 782340394,
|
||||
"chocho.zip": 2754434487,
|
||||
"choco.zip": 4131511875,
|
||||
"chocomint.zip": 792528046,
|
||||
"choyasei.zip": 195845123,
|
||||
"chururi.zip": 418388487,
|
||||
"cinder.zip": 608488929,
|
||||
"cinderk.zip": 3804014794,
|
||||
"cit.zip": 2991006162,
|
||||
"colors.zip": 1693245959,
|
||||
"comet.zip": 3543650143,
|
||||
"conf.zip": 3294278458,
|
||||
"connect.zip": 2606331862,
|
||||
"Contemporary.zip": 1018120223,
|
||||
"corruption.zip": 563343748,
|
||||
"cos.zip": 1172147158,
|
||||
"cosio10th.zip": 2416494947,
|
||||
"cosmic.zip": 3841430974,
|
||||
"cosmostart.zip": 1361453293,
|
||||
"cozmo.zip": 1781669639,
|
||||
"crazy.zip": 3540532330,
|
||||
"crazycrazy.zip": 992116350,
|
||||
"crepe.zip": 1090501506,
|
||||
"crepega.zip": 3740379914,
|
||||
"crime.zip": 3103649177,
|
||||
"crimson.zip": 4173476705,
|
||||
"cristalize.zip": 1085131565,
|
||||
"cross.zip": 3535280468,
|
||||
"crowded.zip": 786238736,
|
||||
"crowdedtw.zip": 1309633005,
|
||||
"cruelm.zip": 1470510403,
|
||||
"crystal.zip": 726837213,
|
||||
"cto.zip": 1249790785,
|
||||
"curry.zip": 3004387594,
|
||||
"cyan.zip": 1719762183,
|
||||
"cyber.zip": 1547324055,
|
||||
"cyberm.zip": 3634358526,
|
||||
"cybers.zip": 1311876046,
|
||||
"d4djcaptain.zip": 2916367724,
|
||||
"d4djchaos.zip": 1264892574,
|
||||
"d4djdaddy.zip": 1958456769,
|
||||
"d4djdenran.zip": 1017423175,
|
||||
"d4djguruguru.zip": 2786250101,
|
||||
"d4djlovehug.zip": 2451167998,
|
||||
"d4djphoton.zip": 2888048434,
|
||||
"d4djurban.zip": 1149097643,
|
||||
"daddy.zip": 141349449,
|
||||
"daimeiwaku.zip": 4280193246,
|
||||
"dakaraboku.zip": 2338741555,
|
||||
"dance.zip": 14222019,
|
||||
"dancing.zip": 3903714216,
|
||||
"dandan.zip": 2609487105,
|
||||
"dangan.zip": 3534530466,
|
||||
"danmaku.zip": 4233567852,
|
||||
"dappo.zip": 3303096140,
|
||||
"darekano.zip": 2731608682,
|
||||
"datsugoku.zip": 493849166,
|
||||
"datte.zip": 1924360940,
|
||||
"ddpboss.zip": 1154390446,
|
||||
"ddtp.zip": 77377797,
|
||||
"death.zip": 2768742013,
|
||||
"dekadance.zip": 507470013,
|
||||
"demparty.zip": 1170111729,
|
||||
"denden.zip": 2116164471,
|
||||
"denpare.zip": 4150928974,
|
||||
"departure20160310.zip": 2940941727,
|
||||
"desert.zip": 2211225730,
|
||||
"desktop.ini": 2937837711,
|
||||
"divine.zip": 3781648284,
|
||||
"djnobu.zip": 3041542586,
|
||||
"dontdie.zip": 3162144940,
|
||||
"dontfight.zip": 433564537,
|
||||
"double.zip": 1932595625,
|
||||
"downdown.zip": 2862823253,
|
||||
"dramatur.zip": 2213389389,
|
||||
"dream.zip": 1875129498,
|
||||
"dreamc.zip": 515687257,
|
||||
"dreamer202012031500.zip": 919554957,
|
||||
"dreaminat.zip": 633886536,
|
||||
"dreamr.zip": 3628975524,
|
||||
"drsp.zip": 496339632,
|
||||
"DrumnBass201903291100.zip": 3758912519,
|
||||
"dulla.zip": 3383544139,
|
||||
"dummy.zip": 4279270517,
|
||||
"dworiginal.zip": 269268371,
|
||||
"eaaso.zip": 3726709818,
|
||||
"echo.zip": 3343202236,
|
||||
"eclipse.zip": 1813555974,
|
||||
"edm_song01.zip": 2807171803,
|
||||
"edm_song02.zip": 281822746,
|
||||
"edm_song03.zip": 2651228814,
|
||||
"edm_song04.zip": 905117013,
|
||||
"egg2nd.zip": 3225737583,
|
||||
"egg3rd.zip": 1697904196,
|
||||
"egg4th.zip": 394253921,
|
||||
"egg5th.zip": 859389796,
|
||||
"egg6th.zip": 4219161485,
|
||||
"egg7th.zip": 8228641,
|
||||
"eggova202504121346.zip": 4078396839,
|
||||
"eggvsm.zip": 1335092594,
|
||||
"elec.zip": 3847732700,
|
||||
"endl.zip": 1739959060,
|
||||
"endlessd.zip": 1233366531,
|
||||
"ene.zip": 2351356937,
|
||||
"envy.zip": 235377615,
|
||||
"envycat.zip": 2397416061,
|
||||
"epcross.zip": 1312489969,
|
||||
"erin.zip": 1600370008,
|
||||
"erincr.zip": 2766515973,
|
||||
"estp.zip": 1447639595,
|
||||
"eternal.zip": 81883011,
|
||||
"ever.zip": 3722229358,
|
||||
"ex1.zip": 2429442729,
|
||||
"ex10.zip": 1028122300,
|
||||
"ex11.zip": 4069655124,
|
||||
"ex12.zip": 3949368213,
|
||||
"ex13.zip": 1881080776,
|
||||
"ex14.zip": 1240647124,
|
||||
"ex15.zip": 1005334284,
|
||||
"ex16.zip": 2876985976,
|
||||
"ex17.zip": 3958205002,
|
||||
"ex18.zip": 2585771411,
|
||||
"ex19.zip": 3652964779,
|
||||
"ex2.zip": 3493977675,
|
||||
"ex20.zip": 3118684744,
|
||||
"ex21.zip": 1474682285,
|
||||
"ex22.zip": 3117829800,
|
||||
"ex23.zip": 3943802416,
|
||||
"ex24.zip": 4063137912,
|
||||
"ex25.zip": 1428687488,
|
||||
"ex26.zip": 2487663852,
|
||||
"ex27.zip": 1122986521,
|
||||
"ex28.zip": 3386186424,
|
||||
"ex29.zip": 4046987226,
|
||||
"ex3.zip": 1960423303,
|
||||
"ex30.zip": 2448548072,
|
||||
"ex31.zip": 1182139851,
|
||||
"ex32.zip": 799042673,
|
||||
"ex33.zip": 4279778138,
|
||||
"ex34.zip": 3720284504,
|
||||
"ex35.zip": 165612720,
|
||||
"ex36.zip": 1332449924,
|
||||
"ex37.zip": 2177128589,
|
||||
"ex38.zip": 605698510,
|
||||
"ex39.zip": 4163480259,
|
||||
"ex4.zip": 1220923134,
|
||||
"ex40.zip": 1132487688,
|
||||
"ex41.zip": 2974781335,
|
||||
"ex42.zip": 3669052675,
|
||||
"ex43.zip": 1917353350,
|
||||
"ex44.zip": 1354603974,
|
||||
"ex45.zip": 3466868400,
|
||||
"ex46.zip": 1059479972,
|
||||
"ex47.zip": 628179419,
|
||||
"ex48.zip": 2837130364,
|
||||
"ex49.zip": 3335953351,
|
||||
"ex5.zip": 2979161060,
|
||||
"ex50.zip": 101554299,
|
||||
"ex51.zip": 223949350,
|
||||
"ex52.zip": 2825705716,
|
||||
"ex53.zip": 661748215,
|
||||
"ex54.zip": 247031865,
|
||||
"ex55.zip": 2750389402,
|
||||
"ex56.zip": 1066944413,
|
||||
"ex57.zip": 426556523,
|
||||
"ex58.zip": 3411975818,
|
||||
"ex59.zip": 929735608,
|
||||
"ex6.zip": 2400691573,
|
||||
"ex60.zip": 3627033659,
|
||||
"ex61.zip": 692763141,
|
||||
"ex62.zip": 2110666447,
|
||||
"ex63.zip": 996320804,
|
||||
"ex7.zip": 2143221734,
|
||||
"ex8.zip": 2977153649,
|
||||
"ex9.zip": 3834132157,
|
||||
"exitium.zip": 1308769219,
|
||||
"exmode.zip": 2911082029,
|
||||
"extreme.zip": 2536565200,
|
||||
"extremegrv201903291100.zip": 2236380829,
|
||||
"ezmode.zip": 2842534075,
|
||||
"faintlove.zip": 1263499415,
|
||||
"fakeprog.zip": 462017455,
|
||||
"fakermx.zip": 2921102706,
|
||||
"faketown202504140212.zip": 1088388240,
|
||||
"fantastic.zip": 648804526,
|
||||
"fd.zip": 360291234,
|
||||
"feel.zip": 3331353934,
|
||||
"fermion.zip": 168741903,
|
||||
"fha.zip": 4073148221,
|
||||
"finder.zip": 1428318156,
|
||||
"firstsnow.zip": 3035897474,
|
||||
"fixer.zip": 3614978782,
|
||||
"floor.zip": 3339888380,
|
||||
"flost.zip": 3043822086,
|
||||
"fluffy.zip": 430530877,
|
||||
"flyaway.zip": 1445319029,
|
||||
"flyng.zip": 1567458898,
|
||||
"fm.zip": 3629750370,
|
||||
"foughten.zip": 1720358675,
|
||||
"fourseason.zip": 1075291021,
|
||||
"freecon.zip": 2541541275,
|
||||
"freedom.zip": 2418711926,
|
||||
"freestyle.zip": 1834227624,
|
||||
"frey.zip": 629681899,
|
||||
"fullmetal.zip": 3776498938,
|
||||
"fullmoon.zip": 3799269698,
|
||||
"furetemitai.zip": 268243728,
|
||||
"furubo.zip": 2148392805,
|
||||
"future.zip": 3678636321,
|
||||
"gaikotu.zip": 871945506,
|
||||
"gaim.zip": 3312640846,
|
||||
"gang.zip": 3613576038,
|
||||
"gateone.zip": 2788624152,
|
||||
"GBME.zip": 1909814414,
|
||||
"GEKI.zip": 831565959,
|
||||
"gekko2.zip": 1857724030,
|
||||
"gensou.zip": 1878443504,
|
||||
"gensounisaita.zip": 3878084995,
|
||||
"gensouno.zip": 742352121,
|
||||
"georemix.zip": 1663277576,
|
||||
"gerbera.zip": 2096470848,
|
||||
"ghost20160401.zip": 3216786726,
|
||||
"ghostrmx.zip": 288575494,
|
||||
"ghostrmx2.zip": 2236194852,
|
||||
"giron.zip": 3343812019,
|
||||
"glithcre.zip": 2355741777,
|
||||
"glory.zip": 2844837462,
|
||||
"gnbl.zip": 2977189523,
|
||||
"goback.zip": 4096212037,
|
||||
"godknows.zip": 4019907209,
|
||||
"goodbounce.zip": 4260640737,
|
||||
"goodbyes.zip": 541361564,
|
||||
"goodtek.zip": 3973438937,
|
||||
"gotmore.zip": 464255160,
|
||||
"gpremix.zip": 1430296070,
|
||||
"grave.zip": 1653971304,
|
||||
"greenlights.zip": 3184351792,
|
||||
"grievous.zip": 1777989941,
|
||||
"grooveit.zip": 934881901,
|
||||
"grooveloop.zip": 1885552896,
|
||||
"grooveprayer.zip": 4150758718,
|
||||
"grooverev.zip": 1978774952,
|
||||
"groovethe.zip": 3203819005,
|
||||
"grow.zip": 3431947847,
|
||||
"gs2akiba.zip": 1914193868,
|
||||
"gs2ed.zip": 2083140136,
|
||||
"gs2kyushibuya.zip": 3775222822,
|
||||
"gs2neoshibuya.zip": 3786253402,
|
||||
"gssaitama.zip": 1547197081,
|
||||
"gsshibuya.zip": 2105891962,
|
||||
"gstrans.zip": 3262611850,
|
||||
"gsumeda.zip": 2732552585,
|
||||
"guren.zip": 3681014863,
|
||||
"gurugurumelo.zip": 2475414259,
|
||||
"gurunation.zip": 659246727,
|
||||
"gzero.zip": 666312345,
|
||||
"halcyon.zip": 535974764,
|
||||
"hanipa.zip": 1281981433,
|
||||
"Happy.zip": 1157304585,
|
||||
"happylucky.zip": 629713860,
|
||||
"happysyn.zip": 1181811033,
|
||||
"hardhead.zip": 2879273656,
|
||||
"hare.zip": 4105117236,
|
||||
"harunoumi.zip": 3489859670,
|
||||
"hata.zip": 4248069831,
|
||||
"hatara.zip": 4100746990,
|
||||
"hayabusa.zip": 2053428197,
|
||||
"hayaku.zip": 3657404898,
|
||||
"hbaccell.zip": 3661431622,
|
||||
"headphone.zip": 483618905,
|
||||
"headshot.zip": 181978373,
|
||||
"heavy.zip": 1007940308,
|
||||
"heisei.zip": 3162136381,
|
||||
"Hello31337.zip": 3091648406,
|
||||
"hellohowayou.zip": 2473949055,
|
||||
"hg.zip": 310604932,
|
||||
"hibari.zip": 1652598383,
|
||||
"hikkyou.zip": 1836382233,
|
||||
"himitsuk.zip": 3057641115,
|
||||
"Hiphop.zip": 2227463495,
|
||||
"hitogata.zip": 1421663296,
|
||||
"hitori.zip": 2526749604,
|
||||
"holo.zip": 2674028147,
|
||||
"hologram.zip": 4201327638,
|
||||
"honey202012031600.zip": 3263263189,
|
||||
"hopesand.zip": 2502875488,
|
||||
"hori.zip": 2944718885,
|
||||
"hosoe.zip": 51040834,
|
||||
"hosoisen.zip": 3097040423,
|
||||
"House.zip": 2483587954,
|
||||
"hypergoa202504120026.zip": 2310602797,
|
||||
"ia-circuit.zip": 588603554,
|
||||
"ia-star.zip": 4150464253,
|
||||
"ICNA.zip": 462750090,
|
||||
"ignotus.zip": 1958517201,
|
||||
"iiaru.zip": 2858441078,
|
||||
"iiee.zip": 3198298834,
|
||||
"ikaduchi.zip": 199831867,
|
||||
"ikasama.zip": 708041446,
|
||||
"imiss.zip": 2988289937,
|
||||
"inc.zip": 3585186587,
|
||||
"indignant202504081927.zip": 3146174136,
|
||||
"inf.zip": 383184657,
|
||||
"inmy.zip": 3820232028,
|
||||
"innocence.zip": 1398259315,
|
||||
"int.zip": 3345434976,
|
||||
"inuka.zip": 2689498793,
|
||||
"inv2003.zip": 4148289616,
|
||||
"InvadeYou.zip": 3971224548,
|
||||
"invdisco.zip": 3141104104,
|
||||
"INVGENEMIX.zip": 413945061,
|
||||
"invgirl.zip": 4189107193,
|
||||
"invisible.zip": 2149775452,
|
||||
"invisiblefre.zip": 3774373318,
|
||||
"iroha.zip": 3268009126,
|
||||
"irohauta.zip": 2856153590,
|
||||
"iscream.zip": 3082836287,
|
||||
"itazura.zip": 3914620538,
|
||||
"iwantyou.zip": 2316533891,
|
||||
"javawo.zip": 2737006537,
|
||||
"jbf.zip": 897193399,
|
||||
"JET.zip": 1920273141,
|
||||
"jingai.zip": 158791011,
|
||||
"jinsei.zip": 3831083212,
|
||||
"JNGBELL.zip": 2386777075,
|
||||
"journey.zip": 894023563,
|
||||
"joy.zip": 1308041004,
|
||||
"joyful.zip": 3541713375,
|
||||
"jukusei.zip": 2658009056,
|
||||
"jumpee201901311100.zip": 1267511079,
|
||||
"jumper.zip": 2106510610,
|
||||
"junko.zip": 555260592,
|
||||
"junky.zip": 3877040259,
|
||||
"jupiter2.zip": 3769716416,
|
||||
"juumen.zip": 2981708552,
|
||||
"kageno.zip": 3608820126,
|
||||
"kagerou.zip": 583478960,
|
||||
"kaidancranky.zip": 3435336787,
|
||||
"kaisei.zip": 2033859821,
|
||||
"kaitou.zip": 3693734980,
|
||||
"kakushi.zip": 2325334738,
|
||||
"kale.zip": 635499415,
|
||||
"kaminohi.zip": 560332698,
|
||||
"kamitoushin.zip": 1436621631,
|
||||
"kanbu.zip": 4093975277,
|
||||
"kannan.zip": 3197896870,
|
||||
"kanon.zip": 1844437125,
|
||||
"kanon2.zip": 152648430,
|
||||
"kanzenno.zip": 3573276017,
|
||||
"karakuri.zip": 1953216413,
|
||||
"karisome.zip": 1940284842,
|
||||
"keepfaith.zip": 197512519,
|
||||
"kemono201903291100.zip": 2018480742,
|
||||
"kero9201509161108.zip": 3220721908,
|
||||
"kickit.zip": 3272490363,
|
||||
"kijin.zip": 1364625763,
|
||||
"kikikaikai.zip": 4199962403,
|
||||
"kimiiro.zip": 3226978432,
|
||||
"kimiiropetal.zip": 424110768,
|
||||
"kiminostar.zip": 475116051,
|
||||
"kimitonote.zip": 1900904596,
|
||||
"kinggumi.zip": 3814382489,
|
||||
"kisaragi.zip": 870073408,
|
||||
"kiyo.zip": 3093035089,
|
||||
"knightrider.zip": 845076965,
|
||||
"kodo.zip": 2381030030,
|
||||
"kodokuna.zip": 2860482688,
|
||||
"koinegau.zip": 3365765744,
|
||||
"kokoro.zip": 1248835403,
|
||||
"konoha201903291100.zip": 3647774618,
|
||||
"konohazu.zip": 4032571331,
|
||||
"konton201903291100.zip": 2836013245,
|
||||
"kouga.zip": 506105139,
|
||||
"kousen.zip": 858727473,
|
||||
"kr-change.zip": 2746261120,
|
||||
"kr-clubmj.zip": 4237674499,
|
||||
"kr-doctor.zip": 4193944678,
|
||||
"kr-dye.zip": 1028654185,
|
||||
"kr-hajimete.zip": 1817898135,
|
||||
"kr-nizigen.zip": 3919830618,
|
||||
"kr-plugout.zip": 374141313,
|
||||
"kr-remocon.zip": 2931199177,
|
||||
"kr-setsuna.zip": 3175676339,
|
||||
"kr-starg.zip": 1554158045,
|
||||
"kungfu.zip": 4023216475,
|
||||
"kuron.zip": 1252465809,
|
||||
"kusou.zip": 330642584,
|
||||
"kyoen.zip": 2494493185,
|
||||
"kyokuken.zip": 1170608733,
|
||||
"kyoukaino.zip": 3234864886,
|
||||
"labor.zip": 2201018699,
|
||||
"laser.zip": 3303813653,
|
||||
"last.zip": 2985202631,
|
||||
"lavender.zip": 3193676548,
|
||||
"lemege.zip": 2018432381,
|
||||
"letha.zip": 1880963052,
|
||||
"letyou.zip": 1172560732,
|
||||
"libera.zip": 3291738778,
|
||||
"lightmuse.zip": 3654490145,
|
||||
"lightningdu.zip": 3269109273,
|
||||
"limit.zip": 3360424346,
|
||||
"linda.zip": 3427440882,
|
||||
"linear.zip": 3910872959,
|
||||
"link.zip": 1060660812,
|
||||
"little.zip": 537810838,
|
||||
"losstime.zip": 1240486570,
|
||||
"lostcolors.zip": 3007894597,
|
||||
"losto.zip": 1514175861,
|
||||
"lostword.zip": 1252205981,
|
||||
"lov3-battle2.zip": 1445485265,
|
||||
"lov3-battle5.zip": 1493015824,
|
||||
"lov3-main.zip": 503305134,
|
||||
"lovefor.zip": 1584398728,
|
||||
"loverpop.zip": 223560467,
|
||||
"lovetheworld.zip": 752498112,
|
||||
"lust.zip": 2390501076,
|
||||
"mad.zip": 2699059196,
|
||||
"magician.zip": 3893316204,
|
||||
"magnet.zip": 1450615240,
|
||||
"mahosyozyo.zip": 1354745522,
|
||||
"maiami.zip": 476211507,
|
||||
"majilove.zip": 2413098818,
|
||||
"majilove2.zip": 508472642,
|
||||
"marianne.zip": 3237473596,
|
||||
"marisa.zip": 71667685,
|
||||
"marryme.zip": 1005842596,
|
||||
"matara.zip": 3006973990,
|
||||
"material.zip": 2236097157,
|
||||
"matibito.zip": 984496215,
|
||||
"mato.zip": 3632695442,
|
||||
"matsuyoi.zip": 3566412973,
|
||||
"mayonaka.zip": 122059634,
|
||||
"megalo.zip": 2477241127,
|
||||
"megaton.zip": 2762410980,
|
||||
"meido.zip": 4157438180,
|
||||
"mekakushi.zip": 3101260416,
|
||||
"memesiku.zip": 3189470574,
|
||||
"merlin.zip": 902516559,
|
||||
"merm.zip": 3922566301,
|
||||
"mermaid.zip": 3695037351,
|
||||
"messiah.zip": 3101400915,
|
||||
"metallic.zip": 2201202283,
|
||||
"metamor.zip": 3902267270,
|
||||
"meteor.zip": 4246405247,
|
||||
"migikata.zip": 889494505,
|
||||
"mikaku.zip": 3699138587,
|
||||
"mikumiku.zip": 2340254086,
|
||||
"milk.zip": 1030640338,
|
||||
"mindeve.zip": 48708297,
|
||||
"mira.zip": 3101085079,
|
||||
"miracle.zip": 2162996630,
|
||||
"miracu.zip": 1058505815,
|
||||
"miraito.zip": 1115714505,
|
||||
"miserable.zip": 3881731752,
|
||||
"modeli.zip": 1675050388,
|
||||
"moeru.zip": 1605699432,
|
||||
"moerumori.zip": 201059522,
|
||||
"monogatari.zip": 118868094,
|
||||
"monster.zip": 2901226201,
|
||||
"moon.zip": 1746982303,
|
||||
"moretu.zip": 3589435711,
|
||||
"morning.zip": 4197992768,
|
||||
"mosaic.zip": 3843047093,
|
||||
"mouth.zip": 2204734087,
|
||||
"moving.zip": 1480828594,
|
||||
"mrvirtualizer.zip": 3385937358,
|
||||
"msmagic.zip": 3301634777,
|
||||
"msphantom20160401.zip": 1931224793,
|
||||
"mssp.zip": 1783304772,
|
||||
"mssphoenix.zip": 1126575717,
|
||||
"mssplanet201903291100.zip": 3359342360,
|
||||
"mugen.zip": 1082852413,
|
||||
"mujaki.zip": 225324131,
|
||||
"mukan.zip": 504156223,
|
||||
"murakumo.zip": 2688209109,
|
||||
"musicplotx.zip": 1635756069,
|
||||
"musicrev.zip": 3574849557,
|
||||
"MUSIC_PROT.zip": 2715359050,
|
||||
"musunde.zip": 1686249769,
|
||||
"myangle.zip": 3772438590,
|
||||
"mybabe.zip": 1875109198,
|
||||
"myflower.zip": 1187098508,
|
||||
"myvoice.zip": 835974723,
|
||||
"naisho.zip": 3539768495,
|
||||
"namcot202012031500.zip": 304134142,
|
||||
"namonaki.zip": 2043781110,
|
||||
"natumatu.zip": 3156457268,
|
||||
"nee.zip": 277498038,
|
||||
"negative.zip": 247546413,
|
||||
"nemurenu.zip": 1893832969,
|
||||
"Neptune.zip": 235717046,
|
||||
"netoge.zip": 541157831,
|
||||
"nevermind.zip": 3605161895,
|
||||
"neverstop.zip": 1141441859,
|
||||
"nightlife.zip": 1901610798,
|
||||
"nightmare.zip": 3776886900,
|
||||
"nightof.zip": 709740823,
|
||||
"nightofbutaotome.zip": 1286343675,
|
||||
"nightoftama.zip": 1557553406,
|
||||
"niji202504140212.zip": 504426025,
|
||||
"nisoku.zip": 1464022991,
|
||||
"no202504081927.zip": 2935866407,
|
||||
"nojarori.zip": 1816293969,
|
||||
"nolife.zip": 310713631,
|
||||
"nolimit.zip": 618615459,
|
||||
"norikenvsm.zip": 2628684370,
|
||||
"nornir.zip": 2250279032,
|
||||
"nosyo.zip": 2661973392,
|
||||
"noway.zip": 2851520437,
|
||||
"ns-battle.zip": 3824887084,
|
||||
"ns-ihou.zip": 2275154889,
|
||||
"ns-main.zip": 1588531663,
|
||||
"nuko.zip": 2163482518,
|
||||
"nyansei.zip": 2973890190,
|
||||
"nyanya.zip": 3182179741,
|
||||
"oblivion.zip": 2925990520,
|
||||
"okuuno.zip": 581179791,
|
||||
"oldheaven.zip": 3732224501,
|
||||
"omakeno.zip": 1565987606,
|
||||
"omegaax.zip": 3575105957,
|
||||
"onegai.zip": 890604983,
|
||||
"ongun.zip": 1185944767,
|
||||
"ooeyama.zip": 2869482317,
|
||||
"operation.zip": 1768608286,
|
||||
"orange.zip": 1455642304,
|
||||
"orb.zip": 2438588865,
|
||||
"orb20160310.zip": 559015124,
|
||||
"orewo.zip": 3854890307,
|
||||
"oshama.zip": 2237090226,
|
||||
"otherself.zip": 3142661313,
|
||||
"otome.zip": 3969965465,
|
||||
"otomekaibou.zip": 2111320766,
|
||||
"otsukare.zip": 2994009632,
|
||||
"otukimi.zip": 434182470,
|
||||
"ourovoros.zip": 1021543958,
|
||||
"outer201903291100.zip": 3827754529,
|
||||
"output.zip": 1862540102,
|
||||
"over.zip": 1004844309,
|
||||
"overdrive.zip": 2994255052,
|
||||
"overl.zip": 3354791930,
|
||||
"owaranai.zip": 1545987192,
|
||||
"pa3.zip": 1925354825,
|
||||
"packaged.zip": 68863826,
|
||||
"panda.zip": 3413055961,
|
||||
"panda2.zip": 2848387363,
|
||||
"paqqin.zip": 2289396311,
|
||||
"parazi.zip": 2154160881,
|
||||
"particle.zip": 1034775376,
|
||||
"particlep.zip": 2239949900,
|
||||
"party.zip": 1644116630,
|
||||
"party4u.zip": 1121510424,
|
||||
"partybegun.zip": 255189902,
|
||||
"pegasus.zip": 1574682360,
|
||||
"penet.zip": 1926096315,
|
||||
"period.zip": 280491004,
|
||||
"periodn.zip": 4035182507,
|
||||
"pers.zip": 1766784108,
|
||||
"perv.zip": 2455554268,
|
||||
"philosophy.zip": 2164410197,
|
||||
"phonedead.zip": 2766160413,
|
||||
"piko.zip": 2034540255,
|
||||
"pixel.zip": 1020709614,
|
||||
"planet.zip": 3931644607,
|
||||
"PlanetRock.zip": 1334965344,
|
||||
"plastic.zip": 2563955653,
|
||||
"platinum.zip": 2403689423,
|
||||
"plot2r3.zip": 3934340394,
|
||||
"plot3.zip": 4114144791,
|
||||
"pmneo201508182208.zip": 296410483,
|
||||
"poker.zip": 3541041500,
|
||||
"poly.zip": 3762282684,
|
||||
"poseidon.zip": 4114323270,
|
||||
"pray.zip": 135251308,
|
||||
"psychic.zip": 3250377142,
|
||||
"pumpkin.zip": 1444118886,
|
||||
"punaipu.zip": 3948249646,
|
||||
"punk202504140212.zip": 2785630338,
|
||||
"pupa.zip": 383241884,
|
||||
"putyour.zip": 122085429,
|
||||
"pzddragon20160310.zip": 4268774687,
|
||||
"pzdnj.zip": 229755922,
|
||||
"pzdwarr.zip": 1362504115,
|
||||
"pzdz.zip": 2997999724,
|
||||
"qlwa.zip": 1015681108,
|
||||
"quartet.zip": 3421563032,
|
||||
"railgun.zip": 1757881863,
|
||||
"rave.zip": 3288033471,
|
||||
"ravgirl.zip": 385331327,
|
||||
"ray.zip": 685435436,
|
||||
"rd-5.zip": 2756237538,
|
||||
"rd-light.zip": 417404195,
|
||||
"rd-tragedy.zip": 3225869535,
|
||||
"rdy.zip": 2561759148,
|
||||
"really.zip": 1196360600,
|
||||
"recaptain.zip": 34574542,
|
||||
"redalice10th.zip": 4251824366,
|
||||
"redeparture20160310.zip": 1948632406,
|
||||
"redial.zip": 3463097047,
|
||||
"reend.zip": 1021839606,
|
||||
"relo.zip": 174533348,
|
||||
"rennai.zip": 3551764933,
|
||||
"rennaiyu.zip": 1475525037,
|
||||
"retroid.zip": 4206760233,
|
||||
"reunion.zip": 2657567858,
|
||||
"reversal.zip": 1600125739,
|
||||
"reverse.zip": 2692182007,
|
||||
"reverseuni.zip": 470000272,
|
||||
"reversi.zip": 1954601858,
|
||||
"rewalking.zip": 934150180,
|
||||
"rewrite201903291100.zip": 578530448,
|
||||
"ringa.zip": 1853750484,
|
||||
"rinne.zip": 3505034365,
|
||||
"roki.zip": 3738385749,
|
||||
"rokucho.zip": 1570871767,
|
||||
"rollingirl.zip": 4245701330,
|
||||
"romance.zip": 1235398206,
|
||||
"romio.zip": 2884798423,
|
||||
"romio2.zip": 2640690391,
|
||||
"ronmei.zip": 1504902685,
|
||||
"rosin.zip": 1657409560,
|
||||
"roteen.zip": 1681747517,
|
||||
"sacri.zip": 1869865655,
|
||||
"sadomami.zip": 2058978515,
|
||||
"sadrain.zip": 3830121630,
|
||||
"saiki.zip": 1571910075,
|
||||
"sainou.zip": 1987900095,
|
||||
"saisyuu.zip": 3898439492,
|
||||
"sakuram201903291100.zip": 1802830936,
|
||||
"sandori.zip": 3560463208,
|
||||
"sangaku.zip": 988144224,
|
||||
"saraba.zip": 1021290089,
|
||||
"sasakure.zip": 414644127,
|
||||
"saso.zip": 3403183335,
|
||||
"satis-mnk.zip": 692887327,
|
||||
"satis.zip": 1232845822,
|
||||
"satorieye.zip": 2680815017,
|
||||
"say.zip": 2038002901,
|
||||
"sayaround.zip": 2782811164,
|
||||
"sayo.zip": 919571711,
|
||||
"scar.zip": 2919883519,
|
||||
"scarletkei.zip": 2410418185,
|
||||
"scream.zip": 2342214351,
|
||||
"scst-etude.zip": 4141824185,
|
||||
"scst-fifth.zip": 831427455,
|
||||
"scst-mosimo.zip": 525459608,
|
||||
"secondsight.zip": 3597981506,
|
||||
"secret.zip": 407525872,
|
||||
"seelights202504081927.zip": 3964657693,
|
||||
"seija3rd.zip": 1009893958,
|
||||
"seija4th.zip": 1781737021,
|
||||
"seikuri.zip": 3605366677,
|
||||
"seisyozyo.zip": 850973635,
|
||||
"seizya.zip": 2934095785,
|
||||
"seizya2nd.zip": 3270682829,
|
||||
"senbon.zip": 932259152,
|
||||
"senno.zip": 3028478762,
|
||||
"sensei.zip": 3407983376,
|
||||
"sensyaku.zip": 2193745826,
|
||||
"setsunat.zip": 2631012147,
|
||||
"seyana.zip": 4160216294,
|
||||
"Shadow201903291100.zip": 4061118207,
|
||||
"sharuru.zip": 3031643776,
|
||||
"shiawase.zip": 3312714051,
|
||||
"shikkokuju.zip": 1435175800,
|
||||
"shinkai.zip": 23552848,
|
||||
"shiny.zip": 2139487465,
|
||||
"shinymemory.zip": 2447053300,
|
||||
"shinysmily.zip": 3484876789,
|
||||
"shiryoku.zip": 1198234381,
|
||||
"shitworld.zip": 3026172206,
|
||||
"shiva.zip": 1903510502,
|
||||
"shiwa.zip": 544797161,
|
||||
"shonen.zip": 3704719332,
|
||||
"shootingstar.zip": 3726478933,
|
||||
"shoukon.zip": 4220242942,
|
||||
"shutterg.zip": 162410908,
|
||||
"sign.zip": 3971209484,
|
||||
"silent.zip": 362466670,
|
||||
"silenterr.zip": 700678515,
|
||||
"silver.zip": 239808364,
|
||||
"sindoi.zip": 1074908127,
|
||||
"singu.zip": 3912617806,
|
||||
"singula.zip": 3326040634,
|
||||
"siren.zip": 3581123709,
|
||||
"sisters.zip": 1100110581,
|
||||
"ska.zip": 566386219,
|
||||
"skyscraper.zip": 2902551231,
|
||||
"Sleep.zip": 834337057,
|
||||
"smash201706011100.zip": 2273983825,
|
||||
"solar.zip": 2393463597,
|
||||
"sonof.zip": 2253771985,
|
||||
"sorae.zip": 2961028507,
|
||||
"sorairo.zip": 1326160623,
|
||||
"sosite.zip": 1089402042,
|
||||
"souchi.zip": 1639863763,
|
||||
"spacearc.zip": 568497645,
|
||||
"Spacefunk.zip": 3321980069,
|
||||
"sparkling.zip": 3069662034,
|
||||
"spear.zip": 2201192963,
|
||||
"special.zip": 632790092,
|
||||
"specta.zip": 1176312624,
|
||||
"spider.zip": 1719504145,
|
||||
"spiderdance.zip": 2010956025,
|
||||
"spidersbl.zip": 3301692444,
|
||||
"sprites.zip": 3169887564,
|
||||
"SROCK.zip": 4103624135,
|
||||
"stager.zip": 2146202880,
|
||||
"stake.zip": 3904544539,
|
||||
"starcoaster.zip": 101061235,
|
||||
"stardust.zip": 2194595831,
|
||||
"stargmuse.zip": 3955723268,
|
||||
"starlight.zip": 2400203000,
|
||||
"staro.zip": 3977150069,
|
||||
"starspangled.zip": 1643772890,
|
||||
"staygold.zip": 2968419465,
|
||||
"story.zip": 2234402654,
|
||||
"strat.zip": 1425122600,
|
||||
"strboss2.zip": 2015445006,
|
||||
"stream.zip": 3130984745,
|
||||
"stro.zip": 2433167723,
|
||||
"stronger.zip": 1927622322,
|
||||
"sugar.zip": 997215082,
|
||||
"suicide.zip": 2362268068,
|
||||
"suisei.zip": 3517824706,
|
||||
"sumizome.zip": 843894801,
|
||||
"summer.zip": 2483071653,
|
||||
"sung.zip": 1289634858,
|
||||
"supernova.zip": 1689654592,
|
||||
"suta.zip": 4175385657,
|
||||
"sweet.zip": 3892837118,
|
||||
"sweetpla.zip": 1668785940,
|
||||
"symphony9.zip": 2380152480,
|
||||
"syositu2.zip": 2641755032,
|
||||
"taboo.zip": 441396610,
|
||||
"tadakimini.zip": 1843461848,
|
||||
"taiko.zip": 4142346125,
|
||||
"taiyokei.zip": 2534322087,
|
||||
"taste.zip": 2399038947,
|
||||
"tayutau.zip": 1399148697,
|
||||
"tech.zip": 2213585761,
|
||||
"tei.zip": 3700677148,
|
||||
"tellme.zip": 2738801746,
|
||||
"tellyour.zip": 3113650843,
|
||||
"temmie.zip": 3341825693,
|
||||
"tengu.zip": 565397525,
|
||||
"tenorb.zip": 3511111583,
|
||||
"tenshi.zip": 274789710,
|
||||
"tentai.zip": 2154162825,
|
||||
"teo.zip": 3156064114,
|
||||
"tete.zip": 2692862383,
|
||||
"tgm.zip": 342477732,
|
||||
"the7.zip": 198075023,
|
||||
"theworldrevolving.zip": 3360688836,
|
||||
"this.zip": 3348316243,
|
||||
"thor.zip": 725864083,
|
||||
"tiny.zip": 3110256448,
|
||||
"tobitate.zip": 2989181210,
|
||||
"today.zip": 3104185897,
|
||||
"toho3.zip": 647184525,
|
||||
"toho4.zip": 2945837013,
|
||||
"tohobeat.zip": 3369964967,
|
||||
"tohobubble.zip": 2046967009,
|
||||
"tokinowa.zip": 1106340764,
|
||||
"tokyoteddy2.zip": 2620698511,
|
||||
"torinoko.zip": 1268336368,
|
||||
"trauma.zip": 360017256,
|
||||
"traveling.zip": 2834963643,
|
||||
"treasure.zip": 109772269,
|
||||
"treeclimbers.zip": 3044500909,
|
||||
"TRIP1.zip": 15015596,
|
||||
"tropics.zip": 1640742806,
|
||||
"trrrick.zip": 3220623359,
|
||||
"tsh-star.zip": 294130999,
|
||||
"tsuchiya10th.zip": 3791028339,
|
||||
"tsukema201903291100.zip": 1720250743,
|
||||
"tsukikage.zip": 1092774038,
|
||||
"tsukiyo202012031500.zip": 1535982799,
|
||||
"tsukumo.zip": 694985679,
|
||||
"ttu.zip": 2080469462,
|
||||
"tubasa.zip": 2620825208,
|
||||
"tuti.zip": 271945285,
|
||||
"twilight.zip": 481283285,
|
||||
"twilightac.zip": 3852329796,
|
||||
"twotone.zip": 1809914508,
|
||||
"typezero.zip": 673521609,
|
||||
"uadhayako.zip": 271067877,
|
||||
"ufo.zip": 961207076,
|
||||
"uforia.zip": 521357301,
|
||||
"ukigumo.zip": 1112456617,
|
||||
"umiyuri.zip": 3916762119,
|
||||
"undawa.zip": 795823141,
|
||||
"undercont.zip": 1002255203,
|
||||
"underthe.zip": 1644628158,
|
||||
"unhappy.zip": 1376039969,
|
||||
"unknown.zip": 1058457548,
|
||||
"updown.zip": 79334500,
|
||||
"uranus.zip": 1048961637,
|
||||
"uraomote.zip": 1685969674,
|
||||
"urobo.zip": 4266427158,
|
||||
"usatei.zip": 432947099,
|
||||
"utakata.zip": 4280730004,
|
||||
"val.zip": 3837570103,
|
||||
"valedict.zip": 1856330468,
|
||||
"valkyrja.zip": 2700818983,
|
||||
"valli.zip": 3007240642,
|
||||
"vampire.zip": 4050669463,
|
||||
"vanity.zip": 501061864,
|
||||
"velvet.zip": 3769784628,
|
||||
"venom.zip": 1260357862,
|
||||
"villain.zip": 1921133926,
|
||||
"vip.zip": 195195999,
|
||||
"visio.zip": 1451133088,
|
||||
"void10th.zip": 306281456,
|
||||
"volt.zip": 1733165964,
|
||||
"walking20160310.zip": 3290405537,
|
||||
"wand.zip": 2169033843,
|
||||
"war.zip": 3266918274,
|
||||
"wavedet.zip": 2260624835,
|
||||
"wearevox.zip": 299503876,
|
||||
"welcometo.zip": 590936111,
|
||||
"wemnk.zip": 4009697145,
|
||||
"whatyou.zip": 1416716798,
|
||||
"white.zip": 1556896898,
|
||||
"whiteshining.zip": 1065651772,
|
||||
"whokilled.zip": 919553312,
|
||||
"withu.zip": 2290885290,
|
||||
"wiza.zip": 3889675391,
|
||||
"wonder.zip": 1278830233,
|
||||
"world.zip": 1016887746,
|
||||
"worldcall201903291100.zip": 453767747,
|
||||
"worldcollap.zip": 1176923028,
|
||||
"worldvanq.zip": 3069399822,
|
||||
"worr.zip": 4149428052,
|
||||
"wos.zip": 2081858235,
|
||||
"wwd.zip": 2381654136,
|
||||
"Xand.zip": 1607681165,
|
||||
"xevel.zip": 1807586778,
|
||||
"xi10th.zip": 1855614361,
|
||||
"xing.zip": 3929280934,
|
||||
"yankey.zip": 13506317,
|
||||
"yatagara.zip": 979332363,
|
||||
"ynfa.zip": 817994697,
|
||||
"yoake.zip": 3357425840,
|
||||
"yobanashi.zip": 687480460,
|
||||
"yoiyami.zip": 3995143654,
|
||||
"youand.zip": 1896805880,
|
||||
"yourbestnightmare.zip": 3807092877,
|
||||
"yowamushi201903291100.zip": 2704371024,
|
||||
"yscolabo202012031500.zip": 302281571,
|
||||
"yukei201903291100.zip": 257047572,
|
||||
"yukemuri.zip": 3891137691,
|
||||
"yumegiwa.zip": 3044349291,
|
||||
"yumeiro.zip": 3498219684,
|
||||
"yurei.zip": 2259776683,
|
||||
"yyy.zip": 1262453840,
|
||||
"zankoku.zip": 3122074237,
|
||||
"zawawa.zip": 1581569424,
|
||||
"zenryoku.zip": 3469481768,
|
||||
"zibeta.zip": 1670964129,
|
||||
"zinzou.zip": 3360478727,
|
||||
"zone1.zip": 965701857,
|
||||
"zone2.zip": 3248669969,
|
||||
"zuttomo.zip": 2112447934,
|
||||
"zyto-id.zip": 3430804251
|
||||
}
|
||||
3025
new_server_7003/api/config/download_manifest_android.json
Normal file
3025
new_server_7003/api/config/download_manifest_android.json
Normal file
File diff suppressed because it is too large
Load Diff
3025
new_server_7003/api/config/download_manifest_ios.json
Normal file
3025
new_server_7003/api/config/download_manifest_ios.json
Normal file
File diff suppressed because it is too large
Load Diff
42
new_server_7003/api/crypt.py
Normal file
42
new_server_7003/api/crypt.py
Normal file
@@ -0,0 +1,42 @@
|
||||
from Crypto.Cipher import AES
|
||||
import re
|
||||
import urllib.parse
|
||||
from starlette.requests import Request
|
||||
|
||||
# Found in: aesManager::initialize()
|
||||
# Used for: Crypting parameter bytes sent by client
|
||||
# Credit: https://github.com/Walter-o/gcm-downloader
|
||||
AES_CBC_KEY = b"oLxvgCJjMzYijWIldgKLpUx5qhUhguP1"
|
||||
|
||||
# Found in: aesManager::decryptCBC() and aesManager::encryptCBC()
|
||||
# Used for: Crypting parameter bytes sent by client
|
||||
# Credit: https://github.com/Walter-o/gcm-downloader
|
||||
AES_CBC_IV = b"6NrjyFU04IO9j9Yo"
|
||||
|
||||
# Decrypt AES encrypted data, takes in a hex string
|
||||
# Credit: https://github.com/Walter-o/gcm-downloader
|
||||
def decryptAES(data, key=AES_CBC_KEY, iv=AES_CBC_IV):
|
||||
return AES.new(key, AES.MODE_CBC, iv).decrypt(bytes.fromhex(data))
|
||||
|
||||
# Encrypt data with AES, takes in a bytes object
|
||||
# Credit: https://github.com/Walter-o/gcm-downloader
|
||||
def encryptAES(data, key=AES_CBC_KEY, iv=AES_CBC_IV):
|
||||
while len(data) % 16 != 0:
|
||||
data += b"\x00"
|
||||
encryptedData = AES.new(key, AES.MODE_CBC, iv).encrypt(data)
|
||||
return encryptedData.hex()
|
||||
|
||||
async def decrypt_fields(request: Request):
|
||||
url = str(request.url)
|
||||
try:
|
||||
match = re.search(r'\?(.*)', url)
|
||||
if match:
|
||||
original_field = match.group(1)
|
||||
filtered_field = re.sub(r'&_=\d+', '', original_field)
|
||||
decrypted_fields = urllib.parse.parse_qs(decryptAES(filtered_field)[:-1])
|
||||
|
||||
return decrypted_fields, original_field
|
||||
else:
|
||||
return None, None
|
||||
except:
|
||||
return None, None
|
||||
470
new_server_7003/api/database.py
Normal file
470
new_server_7003/api/database.py
Normal file
@@ -0,0 +1,470 @@
|
||||
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 base64
|
||||
|
||||
from config import START_COIN, SIMULTANEOUS_LOGINS
|
||||
from api.template import START_AVATARS, START_STAGES
|
||||
|
||||
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}"
|
||||
|
||||
CACHE_DB_NAME = "cache.db"
|
||||
CACHE_DB_PATH = os.path.join(os.getcwd(), CACHE_DB_NAME)
|
||||
CACHE_DATABASE_URL = f"sqlite+aiosqlite:///{CACHE_DB_PATH}"
|
||||
|
||||
cache_database = databases.Database(CACHE_DATABASE_URL)
|
||||
cache_metadata = sqlalchemy.MetaData()
|
||||
|
||||
player_database = databases.Database(DATABASE_URL)
|
||||
player_metadata = sqlalchemy.MetaData()
|
||||
|
||||
#----------------------- 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),
|
||||
Column("updated_at", DateTime, default=datetime.utcnow, onupdate=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("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("bind_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("bind_token", String(64), unique=True, nullable=False),
|
||||
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)
|
||||
)
|
||||
|
||||
ranking_cache = Table(
|
||||
"ranking_cache",
|
||||
cache_metadata,
|
||||
Column("id", Integer, primary_key=True, autoincrement=True),
|
||||
Column("key", String(16), nullable=False),
|
||||
Column("value", JSON, nullable=False),
|
||||
Column("expire_at", Integer)
|
||||
)
|
||||
|
||||
#----------------------- End of 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(CACHE_DB_PATH):
|
||||
print("[DB] Creating new cache database:", CACHE_DB_PATH)
|
||||
|
||||
cache_engine = create_async_engine(CACHE_DATABASE_URL, echo=False)
|
||||
async with cache_engine.begin() as conn:
|
||||
await conn.run_sync(cache_metadata.create_all)
|
||||
await cache_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.")
|
||||
await ensure_user_columns()
|
||||
|
||||
async def ensure_user_columns():
|
||||
import aiosqlite
|
||||
|
||||
async with aiosqlite.connect(DB_PATH) as db:
|
||||
async with db.execute("PRAGMA table_info(user);") as cursor:
|
||||
columns = [row[1] async for row in cursor]
|
||||
|
||||
alter_needed = False
|
||||
#if "save_id" not in columns:
|
||||
# await db.execute("ALTER TABLE user ADD COLUMN save_id TEXT;")
|
||||
# alter_needed = True
|
||||
#if "coin_mp" not in columns:
|
||||
# await db.execute("ALTER TABLE user ADD COLUMN coin_mp INTEGER DEFAULT 1;")
|
||||
# alter_needed = True
|
||||
if alter_needed:
|
||||
await db.commit()
|
||||
print("[DB] Added missing columns to user table.")
|
||||
|
||||
async def get_bind(user_id):
|
||||
if not user_id:
|
||||
return None
|
||||
query = binds.select().where(binds.c.user_id == user_id)
|
||||
result = await player_database.fetch_one(query)
|
||||
return dict(result) if result else None
|
||||
|
||||
async def refresh_bind(user_id):
|
||||
existing_bind = await get_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
|
||||
)
|
||||
await player_database.execute(update_query)
|
||||
|
||||
async def log_download(user_id, filename, filesize):
|
||||
query = logs.insert().values(
|
||||
user_id=user_id,
|
||||
filename=filename,
|
||||
filesize=filesize,
|
||||
timestamp=datetime.datetime.utcnow()
|
||||
)
|
||||
await player_database.execute(query)
|
||||
|
||||
async def get_downloaded_bytes(user_id, hours):
|
||||
query = select(sqlalchemy.func.sum(logs.c.filesize)).where(
|
||||
(logs.c.user_id == user_id) &
|
||||
(logs.c.timestamp >= datetime.datetime.utcnow() - datetime.timedelta(hours=hours))
|
||||
)
|
||||
result = await player_database.fetch_one(query)
|
||||
return result[0] if result[0] is not None else 0
|
||||
|
||||
async def verify_user_code(code, user_id):
|
||||
existing_bind = await get_bind(user_id)
|
||||
if existing_bind and existing_bind['is_verified'] == 1:
|
||||
return "This account is already bound to an account."
|
||||
|
||||
query = binds.select().where(
|
||||
(binds.c.bind_code == code) &
|
||||
(binds.c.user_id == user_id) &
|
||||
(binds.c.is_verified == 0) &
|
||||
(binds.c.bind_date >= datetime.datetime.utcnow() - datetime.timedelta(minutes=10))
|
||||
)
|
||||
result = await player_database.fetch_one(query)
|
||||
if not result:
|
||||
return "Invalid or expired verification code."
|
||||
|
||||
update_query = update(binds).where(binds.c.id == result['id']).values(
|
||||
is_verified=1,
|
||||
auth_token=base64.urlsafe_b64encode(os.urandom(64)).decode("utf-8")
|
||||
)
|
||||
await player_database.execute(update_query)
|
||||
return "Verified and account successfully bound."
|
||||
|
||||
async def decrypt_fields_to_user_info(decrypted_fields):
|
||||
device_id = decrypted_fields[b'vid'][0].decode()
|
||||
query = devices.select().where(devices.c.device_id == device_id)
|
||||
device_record = await player_database.fetch_one(query)
|
||||
if device_record:
|
||||
device_record = dict(device_record)
|
||||
user_query = accounts.select().where(accounts.c.id == device_record['user_id'])
|
||||
user_record = await player_database.fetch_one(user_query)
|
||||
if user_record:
|
||||
user_record = dict(user_record)
|
||||
return user_record, device_record
|
||||
|
||||
return None, device_record
|
||||
|
||||
return None, None
|
||||
|
||||
async def user_id_to_user_info(user_id):
|
||||
user_query = accounts.select().where(accounts.c.id == user_id)
|
||||
user_record = await player_database.fetch_one(user_query)
|
||||
user_record = dict(user_record) if user_record else None
|
||||
if user_record:
|
||||
user_record = dict(user_record)
|
||||
device_query = devices.select().where(devices.c.user_id == user_id)
|
||||
device_record = await player_database.fetch_all(device_query)
|
||||
device_record = [dict(d) for d in device_record]
|
||||
return user_record, device_record
|
||||
|
||||
return None, None
|
||||
|
||||
async def user_id_to_user_info_simple(user_id):
|
||||
user_query = accounts.select().where(accounts.c.id == user_id)
|
||||
user_record = await player_database.fetch_one(user_query)
|
||||
user_record = dict(user_record) if user_record else None
|
||||
return user_record
|
||||
|
||||
async def user_name_to_user_info(username):
|
||||
user_query = accounts.select().where(accounts.c.username == username)
|
||||
user_record = await player_database.fetch_one(user_query)
|
||||
user_record = dict(user_record) if user_record else None
|
||||
|
||||
return user_record
|
||||
|
||||
async def check_whitelist(decrypted_fields):
|
||||
device_id = decrypted_fields[b'vid'][0].decode()
|
||||
user_info, device_info = await decrypt_fields_to_user_info(decrypted_fields)
|
||||
query = select(whitelists.c.device_id).where((whitelists.c.device_id == device_id) | (whitelists.c.device_id == user_info['username']))
|
||||
result = await player_database.fetch_one(query)
|
||||
return result is not None
|
||||
|
||||
async def check_blacklist(decrypted_fields):
|
||||
device_id = decrypted_fields[b'vid'][0].decode()
|
||||
user_info, device_info = await decrypt_fields_to_user_info(decrypted_fields)
|
||||
query = select(blacklists.c.ban_terms).where((blacklists.c.ban_terms == device_id) | (blacklists.c.ban_terms == user_info['username']))
|
||||
result = await player_database.fetch_one(query)
|
||||
return result is None
|
||||
|
||||
async def get_user_entitlement_from_devices(user_id):
|
||||
devices_query = devices.select().where(devices.c.user_id == user_id)
|
||||
devices_list = await player_database.fetch_all(devices_query)
|
||||
devices_list = [dict(dev) for dev in devices_list] if devices_list else []
|
||||
|
||||
stage_set = set()
|
||||
avatar_set = set()
|
||||
|
||||
for dev in devices_list:
|
||||
my_stages = dev['my_stage'] if dev['my_stage'] else []
|
||||
my_avatars = dev['my_avatar'] if dev['my_avatar'] else []
|
||||
stage_set.update(my_stages)
|
||||
avatar_set.update(my_avatars)
|
||||
|
||||
return list(stage_set), list(avatar_set)
|
||||
|
||||
async def set_user_data_using_decrypted_fields(decrypted_fields, data_fields):
|
||||
data_fields['updated_at'] = datetime.utcnow()
|
||||
device_id = decrypted_fields[b'vid'][0].decode()
|
||||
device_query = devices.select().where(devices.c.device_id == device_id)
|
||||
device_result = await player_database.fetch_one(device_query)
|
||||
if device_result:
|
||||
user_id = device_result['user_id']
|
||||
query = (
|
||||
update(accounts)
|
||||
.where(accounts.c.id == user_id)
|
||||
.values(**data_fields)
|
||||
)
|
||||
await player_database.execute(query)
|
||||
|
||||
async def set_device_data_using_decrypted_fields(decrypted_fields, data_fields):
|
||||
data_fields['updated_at'] = datetime.utcnow()
|
||||
device_id = decrypted_fields[b'vid'][0].decode()
|
||||
query = (
|
||||
update(devices)
|
||||
.where(devices.c.device_id == device_id)
|
||||
.values(**data_fields)
|
||||
)
|
||||
await player_database.execute(query)
|
||||
|
||||
async def get_user_from_save_id(save_id):
|
||||
query = accounts.select().where(accounts.c.save_id == save_id)
|
||||
result = await player_database.fetch_one(query)
|
||||
result = dict(result) if result else None
|
||||
return result
|
||||
|
||||
async def create_user(username, password_hash, device_id):
|
||||
insert_query = accounts.insert().values(
|
||||
username=username,
|
||||
password_hash=password_hash,
|
||||
save_crc=None,
|
||||
save_timestamp=None,
|
||||
save_id=None,
|
||||
coin_mp=1,
|
||||
title=1,
|
||||
avatar=1,
|
||||
mobile_delta=0,
|
||||
arcade_delta=0,
|
||||
total_delta=0,
|
||||
created_at=datetime.utcnow(),
|
||||
updated_at=datetime.utcnow()
|
||||
)
|
||||
user_id = await player_database.execute(insert_query)
|
||||
await login_user(user_id, device_id)
|
||||
|
||||
async def logout_user(device_id):
|
||||
query = (
|
||||
update(devices)
|
||||
.where(devices.c.device_id == device_id)
|
||||
.values(user_id=None)
|
||||
)
|
||||
await player_database.execute(query)
|
||||
|
||||
async def login_user(user_id, device_id):
|
||||
query = (
|
||||
update(devices)
|
||||
.where(devices.c.device_id == device_id)
|
||||
.values(user_id=user_id, last_login_at=datetime.utcnow())
|
||||
)
|
||||
await player_database.execute(query)
|
||||
|
||||
_, device_list = await user_id_to_user_info(user_id)
|
||||
|
||||
if len(device_list) > SIMULTANEOUS_LOGINS:
|
||||
sorted_devices = sorted(device_list, key=lambda d: d['last_login_at'] or datetime.min)
|
||||
devices_to_logout = sorted_devices[:-SIMULTANEOUS_LOGINS]
|
||||
for device in devices_to_logout:
|
||||
await logout_user(device['device_id'])
|
||||
|
||||
async def create_device(device_id, current_time):
|
||||
insert_query = devices.insert().values(
|
||||
device_id=device_id,
|
||||
user_id=None,
|
||||
my_avatar=START_AVATARS,
|
||||
my_stage=START_STAGES,
|
||||
item=[],
|
||||
daily_day=1,
|
||||
daily_timestamp=current_time,
|
||||
coin=START_COIN,
|
||||
lvl=1,
|
||||
title=1,
|
||||
avatar=1,
|
||||
created_at=datetime.utcnow(),
|
||||
updated_at=datetime.utcnow(),
|
||||
last_login_at=None
|
||||
)
|
||||
await player_database.execute(insert_query)
|
||||
|
||||
async def is_admin(token):
|
||||
if not token:
|
||||
return False
|
||||
query = webs.select().where(webs.c.token == token)
|
||||
web_data = await player_database.fetch_one(query)
|
||||
if not web_data:
|
||||
return False
|
||||
if web_data['permission'] != 2:
|
||||
return False
|
||||
return web_data['user_id']
|
||||
|
||||
async def results_query(query_params):
|
||||
query = select(results.c.id, results.c.user_id, results.c.score, results.c.avatar)
|
||||
for key, value in query_params.items():
|
||||
query = query.where(getattr(results.c, key) == value)
|
||||
query = query.order_by(results.c.score.desc())
|
||||
records = await player_database.fetch_all(query)
|
||||
return [dict(record) for record in records]
|
||||
|
||||
async def clear_rank_cache(key):
|
||||
delete_query = ranking_cache.delete().where(ranking_cache.c.key == key)
|
||||
await cache_database.execute(delete_query)
|
||||
|
||||
async def write_rank_cache(key, value, expire_seconds=None):
|
||||
if expire_seconds:
|
||||
expire_time = datetime.utcnow() + timedelta(seconds=expire_seconds)
|
||||
else:
|
||||
expire_time = None
|
||||
|
||||
insert_query = ranking_cache.insert().values(
|
||||
key=key,
|
||||
value=value,
|
||||
expire_at=expire_time
|
||||
)
|
||||
await cache_database.execute(insert_query)
|
||||
|
||||
async def get_rank_cache(key):
|
||||
query = ranking_cache.select().where(ranking_cache.c.key == key)
|
||||
result = await cache_database.fetch_one(query)
|
||||
if result:
|
||||
expire_at = datetime.fromisoformat(result['expire_at']) if result['expire_at'] else None
|
||||
if expire_at and expire_at < datetime.utcnow():
|
||||
await clear_rank_cache(key)
|
||||
return None
|
||||
return dict(result)['value']
|
||||
return None
|
||||
40
new_server_7003/api/decorators.py
Normal file
40
new_server_7003/api/decorators.py
Normal file
@@ -0,0 +1,40 @@
|
||||
from functools import wraps
|
||||
from starlette.responses import JSONResponse
|
||||
|
||||
from starlette.requests import Request
|
||||
from config import AUTHORIZATION_MODE, DISCORD_BOT_API_KEY
|
||||
|
||||
def require_authorization(mode_required):
|
||||
def decorator(func):
|
||||
@wraps(func)
|
||||
async def wrapper(request: Request, *args, **kwargs):
|
||||
if AUTHORIZATION_MODE not in mode_required:
|
||||
print(f"Authorization mode {AUTHORIZATION_MODE} not in required modes {mode_required}")
|
||||
return JSONResponse({"state": 0, "message": "Authorization mode is not enabled."}, status_code=400)
|
||||
|
||||
return await func(request, *args, **kwargs)
|
||||
return wrapper
|
||||
return decorator
|
||||
|
||||
def validate_form_fields(required_fields):
|
||||
def decorator(func):
|
||||
@wraps(func)
|
||||
async def wrapper(request: Request, *args, **kwargs):
|
||||
form = await request.form()
|
||||
for field in required_fields:
|
||||
if not form.get(field):
|
||||
return JSONResponse({"state": 0, "message": f"Missing {field}."}, status_code=402)
|
||||
return await func(request, form, *args, **kwargs)
|
||||
return wrapper
|
||||
return decorator
|
||||
|
||||
def check_discord_api_key():
|
||||
def decorator(func):
|
||||
@wraps(func)
|
||||
async def wrapper(request: Request, *args, **kwargs):
|
||||
api_key = request.headers.get("X-API-KEY")
|
||||
if api_key != DISCORD_BOT_API_KEY:
|
||||
return JSONResponse({"state": 0, "message": "Invalid API key."}, status_code=403)
|
||||
return await func(request, *args, **kwargs)
|
||||
return wrapper
|
||||
return decorator
|
||||
189
new_server_7003/api/discord_hook.py
Normal file
189
new_server_7003/api/discord_hook.py
Normal file
@@ -0,0 +1,189 @@
|
||||
|
||||
from starlette.responses import HTMLResponse, JSONResponse
|
||||
from starlette.requests import Request
|
||||
from starlette.routing import Route
|
||||
from datetime import datetime
|
||||
|
||||
from api.misc import is_alphanumeric, inform_page, generate_salt, check_email, generate_otp
|
||||
from api.database import player_database, accounts, binds, decrypt_fields_to_user_info, get_bind, verify_user_code, user_name_to_user_info
|
||||
from api.crypt import decrypt_fields
|
||||
from api.email_hook import send_email_to_user
|
||||
from api.decorators import require_authorization, validate_form_fields, check_discord_api_key
|
||||
|
||||
@require_authorization(mode_required=[1])
|
||||
async def send_email(request: Request):
|
||||
form = await request.form()
|
||||
email = form.get("email")
|
||||
|
||||
if not email:
|
||||
return inform_page("FAILED:<br>Missing email.", 0)
|
||||
|
||||
email_valid = check_email(email)
|
||||
if not email_valid:
|
||||
return inform_page("FAILED:<br>Invalid email format.", 0)
|
||||
|
||||
decrypted_fields, _ = await decrypt_fields(request)
|
||||
if not decrypted_fields:
|
||||
return inform_page("FAILED:<br>Invalid request data.", 0)
|
||||
|
||||
account_record, device_record = await decrypt_fields_to_user_info(decrypted_fields)
|
||||
if not account_record:
|
||||
return inform_page("FAILED:<br>User does not exist.", 0)
|
||||
|
||||
bind_state = await get_bind(account_record['id'])
|
||||
if bind_state and bind_state['is_verified'] == 1:
|
||||
return inform_page("FAILED:<br>Your account is already verified.", 0)
|
||||
|
||||
response_message = await send_email_to_user(email, account_record['id'])
|
||||
return inform_page(response_message, 0)
|
||||
|
||||
@require_authorization(mode_required=[1, 2])
|
||||
async def verify_user(request: Request):
|
||||
form = await request.form()
|
||||
code = form.get("code")
|
||||
|
||||
if not code:
|
||||
return inform_page("FAILED:<br>Missing verification code.", 0)
|
||||
|
||||
decrypted_fields, _ = await decrypt_fields(request)
|
||||
if not decrypted_fields:
|
||||
return inform_page("FAILED:<br>Invalid request data.", 0)
|
||||
|
||||
account_record, device_record = await decrypt_fields_to_user_info(decrypted_fields)
|
||||
if not account_record:
|
||||
return inform_page("FAILED:<br>User does not exist.", 0)
|
||||
|
||||
bind_state = await get_bind(account_record['id'])
|
||||
if bind_state and bind_state['is_verified'] == 1:
|
||||
return inform_page("FAILED:<br>Your account is already verified.", 0)
|
||||
|
||||
response_message = await verify_user_code(code, account_record['id'])
|
||||
return inform_page(response_message, 0)
|
||||
|
||||
@require_authorization(mode_required=[2])
|
||||
@validate_form_fields(["username", "bind_token", "discord_id"])
|
||||
@check_discord_api_key()
|
||||
async def discord_get_token(request: Request, form):
|
||||
username = form.get("username")
|
||||
bind_token = form.get("bind_token")
|
||||
discord_id = form.get("discord_id")
|
||||
|
||||
if len(username) < 6 or len(username) > 20 or not is_alphanumeric(username):
|
||||
return JSONResponse({"state": 0, "message": "Invalid username."}, status_code=400)
|
||||
|
||||
user_info = await user_name_to_user_info(username)
|
||||
user_info = dict(user_info) if user_info else None
|
||||
if not user_info:
|
||||
return JSONResponse({"state": 0, "message": "User does not exist."}, status_code=404)
|
||||
|
||||
user_id = user_info['id']
|
||||
|
||||
bind_state = await get_bind(user_info['id'])
|
||||
if bind_state and bind_state['is_verified'] == 1:
|
||||
return JSONResponse({"state": 0, "message": "User is already binded. If you want to rebind, contact the administrator."}, status_code=400)
|
||||
|
||||
if bind_state and bind_state['is_verified'] < 0:
|
||||
return JSONResponse({"state": 0, "message": "This account cannot be binded now. Please contact the administrator."}, status_code=400)
|
||||
|
||||
binded_search_query = binds.select().where(binds.c.bind_acc == discord_id).where(binds.c.is_verified == 1)
|
||||
binded_search_record = await player_database.fetch_one(binded_search_query)
|
||||
|
||||
if binded_search_record:
|
||||
return JSONResponse({"state": 0, "message": "This Discord ID is already binded to another account. Please contact the administrator to remove the prior bind."}, status_code=400)
|
||||
|
||||
expected_token = await generate_salt(user_id)
|
||||
if bind_token != expected_token:
|
||||
return JSONResponse({"state": 0, "message": "Invalid bind token."}, status_code=400)
|
||||
|
||||
if bind_state:
|
||||
if (datetime.utcnow() - bind_state['bind_date']).total_seconds() < 60:
|
||||
return JSONResponse({"state": 0, "message": "Too many requests. Please wait a while before retrying."}, status_code=400)
|
||||
|
||||
verify_code, hash_code = generate_otp()
|
||||
if bind_state:
|
||||
await player_database.execute(binds.update().where(binds.c.user_id == user_id).values(
|
||||
bind_acc=discord_id,
|
||||
bind_code=verify_code,
|
||||
bind_date=datetime.utcnow()
|
||||
))
|
||||
else:
|
||||
query = binds.insert().values(
|
||||
user_id=user_id,
|
||||
bind_acc=discord_id,
|
||||
bind_code=verify_code,
|
||||
is_verified=0,
|
||||
bind_date=datetime.utcnow()
|
||||
)
|
||||
await player_database.execute(query)
|
||||
|
||||
return JSONResponse({"state": 1, "message": "Verification code generated. Enter the following code in-game: " + verify_code})
|
||||
|
||||
@require_authorization(mode_required=[2])
|
||||
@validate_form_fields(["discord_id"])
|
||||
@check_discord_api_key()
|
||||
async def discord_get_bind(request: Request, form):
|
||||
discord_id = form.get("discord_id")
|
||||
|
||||
query = binds.select().where(binds.c.bind_acc == discord_id).where(binds.c.is_verified == 1)
|
||||
bind_record = await player_database.fetch_one(query)
|
||||
bind_record = dict(bind_record) if bind_record else None
|
||||
if not bind_record:
|
||||
return JSONResponse({"state": 0, "message": "No verified bind found for this Discord ID."}, status_code=404)
|
||||
|
||||
user_query = accounts.select().where(accounts.c.id == bind_record['user_id'])
|
||||
user_record = await player_database.fetch_one(user_query)
|
||||
|
||||
user_record = dict(user_record) if user_record else None
|
||||
if not user_record:
|
||||
return JSONResponse({"state": 0, "message": "User associated with this bind does not exist."}, status_code=405)
|
||||
|
||||
return JSONResponse({"state": 1, "message": "Your account is binded to: " + user_record['username']})
|
||||
|
||||
@require_authorization(mode_required=[2])
|
||||
@validate_form_fields(["discord_id"])
|
||||
@check_discord_api_key()
|
||||
async def discord_ban(request: Request, form):
|
||||
discord_id = form.get("discord_id")
|
||||
|
||||
query = binds.select().where(binds.c.bind_acc == discord_id).where(binds.c.is_verified == 1)
|
||||
bind_record = await player_database.fetch_one(query)
|
||||
|
||||
bind_record = dict(bind_record) if bind_record else None
|
||||
|
||||
if not bind_record:
|
||||
return JSONResponse({"state": 0, "message": "No verified bind found for this Discord ID."}, status_code=404)
|
||||
|
||||
update_query = binds.update().where(binds.c.id == bind_record['id']).values(
|
||||
is_verified=-1
|
||||
)
|
||||
await player_database.execute(update_query)
|
||||
|
||||
return JSONResponse({"state": 1, "message": "The account associated with this Discord ID has been banned."})
|
||||
|
||||
@require_authorization(mode_required=[2])
|
||||
@validate_form_fields(["discord_id"])
|
||||
@check_discord_api_key()
|
||||
async def discord_unban(request: Request, form):
|
||||
discord_id = form.get("discord_id")
|
||||
|
||||
query = binds.select().where(binds.c.bind_acc == discord_id).where(binds.c.is_verified == -1)
|
||||
bind_record = await player_database.fetch_one(query)
|
||||
bind_record = dict(bind_record) if bind_record else None
|
||||
|
||||
if not bind_record:
|
||||
return JSONResponse({"state": 0, "message": "No unbannable banned bind found for this Discord ID."}, status_code=404)
|
||||
|
||||
update_query = binds.update().where(binds.c.id == bind_record['id']).values(
|
||||
is_verified=1
|
||||
)
|
||||
await player_database.execute(update_query)
|
||||
return JSONResponse({"state": 1, "message": "The account associated with this Discord ID has been unbanned."})
|
||||
|
||||
routes = [
|
||||
Route('/send_email', send_email, methods=['POST']),
|
||||
Route('/discord_get_token', discord_get_token, methods=['POST']),
|
||||
Route('/discord_get_bind', discord_get_bind, methods=['POST']),
|
||||
Route('/discord_ban', discord_ban, methods=['POST']),
|
||||
Route('/discord_unban', discord_unban, methods=['POST']),
|
||||
Route('/verify', verify_user, methods=['POST'])
|
||||
]
|
||||
81
new_server_7003/api/email_hook.py
Normal file
81
new_server_7003/api/email_hook.py
Normal file
@@ -0,0 +1,81 @@
|
||||
import smtplib
|
||||
from email.mime.text import MIMEText
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
from datetime import datetime
|
||||
import uuid
|
||||
|
||||
from starlette.responses import JSONResponse
|
||||
|
||||
from config import SMTP_HOST, SMTP_PORT, SMTP_USER, SMTP_PASSWORD
|
||||
|
||||
from api.database import player_database, binds
|
||||
from api.misc import generate_otp, check_email
|
||||
|
||||
server = None
|
||||
|
||||
def init_email():
|
||||
print("[SMTP] Initializing email server...")
|
||||
global server
|
||||
if SMTP_PORT == 25 or SMTP_PORT == 80:
|
||||
server = smtplib.SMTP(SMTP_HOST, SMTP_PORT)
|
||||
else:
|
||||
server = smtplib.SMTP_SSL(SMTP_HOST, SMTP_PORT)
|
||||
|
||||
server.ehlo()
|
||||
server.login(SMTP_USER, SMTP_PASSWORD)
|
||||
print("[SMTP] Email server initialized successfully.")
|
||||
|
||||
async def send_email(to_addr, code, lang):
|
||||
global server
|
||||
title = {"en": "Project Taiyo - Email Verification", "zh": "项目 Taiyo - 邮件验证", "tc": "專案 Taiyo - 郵件驗證", "jp": "プロジェクト Taiyo - メール認証"}
|
||||
with open(f"api/web/email_{lang}.html", "r", encoding="utf-8") as file:
|
||||
body = file.read()
|
||||
|
||||
body = body.format(code=code)
|
||||
|
||||
msg = MIMEMultipart()
|
||||
msg['From'] = SMTP_USER
|
||||
msg['To'] = to_addr
|
||||
msg['Subject'] = title.get(lang, title['en'])
|
||||
|
||||
msg.attach(MIMEText(body, 'html'))
|
||||
|
||||
try:
|
||||
server.sendmail(SMTP_USER, to_addr, msg.as_string())
|
||||
print("Email sent to ", to_addr)
|
||||
except Exception as e:
|
||||
print(f"Email error: {e}")
|
||||
|
||||
async def send_email_to_user(email, user_id):
|
||||
if not email or not check_email(email):
|
||||
return "Invalid Email."
|
||||
|
||||
verify = await player_database.fetch_one(binds.select().where(binds.c.bind_acc == email))
|
||||
if verify:
|
||||
if (datetime.utcnow() - verify['bind_date']).total_seconds() < 60:
|
||||
return "Too many requests. Please try again later."
|
||||
|
||||
verify_code, hash_code = generate_otp()
|
||||
try:
|
||||
await send_email(email, verify_code, "en")
|
||||
if verify:
|
||||
await player_database.execute(binds.update().where(binds.c.user_id == user_id).values(
|
||||
bind_acc=email,
|
||||
bind_code=verify_code,
|
||||
bind_date=datetime.utcnow()
|
||||
))
|
||||
else:
|
||||
query = binds.insert().values(
|
||||
user_id=user_id,
|
||||
bind_acc=email,
|
||||
bind_code=verify_code,
|
||||
is_verified=0,
|
||||
bind_date=datetime.utcnow()
|
||||
)
|
||||
await player_database.execute(query)
|
||||
|
||||
return "Email sent. Please enter the page again, fill in the verification code to complete the binding."
|
||||
|
||||
except Exception as e:
|
||||
print(f"Email error: {e}")
|
||||
return "Failed to send email. Please try again later."
|
||||
79
new_server_7003/api/file.py
Normal file
79
new_server_7003/api/file.py
Normal file
@@ -0,0 +1,79 @@
|
||||
from starlette.responses import Response, FileResponse
|
||||
from starlette.requests import Request
|
||||
from starlette.routing import Route
|
||||
from sqlalchemy import select
|
||||
import os
|
||||
|
||||
from api.database import player_database, devices, binds, batch_tokens, log_download, get_downloaded_bytes
|
||||
from config import AUTHORIZATION_MODE, DAILY_DOWNLOAD_LIMIT
|
||||
|
||||
async def serve_file(request: Request):
|
||||
auth_token = request.path_params['auth_token']
|
||||
folder = request.path_params['folder']
|
||||
filename = request.path_params['filename']
|
||||
|
||||
if folder not in ["audio", "stage", "pak"]:
|
||||
return Response("Unauthorized", status_code=403)
|
||||
|
||||
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)
|
||||
batch_result = await player_database.fetch_one(existing_batch_token)
|
||||
if batch_result:
|
||||
pass
|
||||
|
||||
elif AUTHORIZATION_MODE == 0:
|
||||
existing_device = select(devices).where(devices.c.device_id == auth_token)
|
||||
result = await player_database.fetch_one(existing_device)
|
||||
if not result:
|
||||
return Response("Unauthorized", status_code=403)
|
||||
else:
|
||||
pass
|
||||
|
||||
else:
|
||||
existing_bind = select(binds).where((binds.c.auth_token == auth_token) & (binds.c.is_verified == 1))
|
||||
result = await player_database.fetch_one(existing_bind)
|
||||
if not result:
|
||||
return Response("Unauthorized", status_code=403)
|
||||
else:
|
||||
daily_bytes = await get_downloaded_bytes(result['user_id'], 24)
|
||||
if daily_bytes >= DAILY_DOWNLOAD_LIMIT:
|
||||
return Response("Daily download limit exceeded", status_code=403)
|
||||
|
||||
safe_filename = os.path.realpath(os.path.join(os.getcwd(), "files", "gc2", folder, filename))
|
||||
base_directory = os.path.realpath(os.path.join(os.getcwd(), "files", "gc2", folder))
|
||||
|
||||
if not safe_filename.startswith(base_directory):
|
||||
return Response("Unauthorized", status_code=403)
|
||||
|
||||
file_path = safe_filename
|
||||
|
||||
if os.path.isfile(file_path):
|
||||
# get size of file
|
||||
if AUTHORIZATION_MODE != 0:
|
||||
file_size = os.path.getsize(file_path)
|
||||
await log_download(result['user_id'], filename, file_size)
|
||||
return FileResponse(file_path)
|
||||
else:
|
||||
return Response("File not found", status_code=404)
|
||||
|
||||
|
||||
async def serve_public_file(request: Request):
|
||||
path = request.path_params['path']
|
||||
safe_filename = os.path.realpath(os.path.join(os.getcwd(), "files", path))
|
||||
base_directory = os.path.realpath(os.path.join(os.getcwd(), "files"))
|
||||
|
||||
if not safe_filename.startswith(base_directory):
|
||||
return Response("Unauthorized", status_code=403)
|
||||
|
||||
if os.path.isfile(safe_filename):
|
||||
return FileResponse(safe_filename)
|
||||
else:
|
||||
return Response("File not found", status_code=404)
|
||||
|
||||
|
||||
routes = [
|
||||
Route("/files/gc2/{auth_token}/{folder}/{filename}", serve_file, methods=["GET"]),
|
||||
Route("/files/{path:path}", serve_public_file, methods=["GET"]),
|
||||
]
|
||||
321
new_server_7003/api/misc.py
Normal file
321
new_server_7003/api/misc.py
Normal file
@@ -0,0 +1,321 @@
|
||||
from starlette.responses import HTMLResponse
|
||||
import requests
|
||||
import json
|
||||
import binascii
|
||||
import secrets
|
||||
import bcrypt
|
||||
import hashlib
|
||||
import re
|
||||
import aiofiles
|
||||
import xml.etree.ElementTree as ET
|
||||
from config import MODEL, TUNEFILE, SKIN, AUTHORIZATION_NEEDED, AUTHORIZATION_MODE, GRANDFATHERED_ACCOUNT_LIMIT
|
||||
from api.database import get_bind, check_whitelist, check_blacklist, decrypt_fields_to_user_info, user_id_to_user_info_simple
|
||||
|
||||
FMAX_VER = None
|
||||
FMAX_RES = None
|
||||
|
||||
def get_4max_version_string():
|
||||
url = "https://studio.code.org/v3/sources/3-aKHy16Y5XaAPXQHI95RnFOKlyYT2O95ia2HN2jKIs/main.json"
|
||||
global FMAX_VER
|
||||
try:
|
||||
with open("./files/4max_ver.txt", 'r') as file:
|
||||
FMAX_VER = file.read().strip()
|
||||
except Exception as e:
|
||||
print(f"An unexpected error occurred when loading files/4max_ver.txt: {e}")
|
||||
|
||||
def fetch():
|
||||
global FMAX_RES
|
||||
try:
|
||||
response = requests.get(url)
|
||||
if 200 <= response.status_code <= 207:
|
||||
try:
|
||||
response_json = response.json()
|
||||
FMAX_RES = json.loads(response_json['source'])
|
||||
except (json.JSONDecodeError, KeyError):
|
||||
|
||||
FMAX_RES = 500
|
||||
else:
|
||||
FMAX_RES = response.status_code
|
||||
except requests.RequestException:
|
||||
FMAX_RES = 400
|
||||
|
||||
fetch()
|
||||
|
||||
def parse_res(res):
|
||||
parsed_data = []
|
||||
if isinstance(res, int) or res == None:
|
||||
return "Failed to fetch version info: Error " + str(res)
|
||||
|
||||
for item in res:
|
||||
if item.get("isOpen"):
|
||||
version = item.get("version", 0)
|
||||
changelog = "<br>".join(item.get("changeLog", {}).get("en", []))
|
||||
parsed_data.append(f"<strong>Version: {version}</strong><p><strong>Changelog:</strong><br>{changelog}</p>")
|
||||
return "".join(parsed_data)
|
||||
|
||||
def crc32_decimal(data):
|
||||
crc32_hex = binascii.crc32(data.encode())
|
||||
return int(crc32_hex & 0xFFFFFFFF)
|
||||
|
||||
def hash_password(password):
|
||||
salt = bcrypt.gensalt()
|
||||
hashed_password = bcrypt.hashpw(password.encode('utf-8'), salt)
|
||||
return hashed_password.decode('utf-8')
|
||||
|
||||
def verify_password(password, hashed_password):
|
||||
if type(hashed_password) == str:
|
||||
hashed_password = hashed_password.encode('utf-8')
|
||||
return bcrypt.checkpw(password.encode('utf-8'), hashed_password)
|
||||
|
||||
def is_alphanumeric(username):
|
||||
pattern = r"^[a-zA-Z0-9]+$"
|
||||
return bool(re.match(pattern, username))
|
||||
|
||||
async def get_model_pak(decrypted_fields, user_id):
|
||||
mid = ET.Element("model_pak")
|
||||
rid = ET.Element("date")
|
||||
uid = ET.Element("url")
|
||||
|
||||
host = await get_host_string()
|
||||
|
||||
if AUTHORIZATION_MODE == 0:
|
||||
auth_token = decrypted_fields[b'vid'][0].decode()
|
||||
rid.text = MODEL
|
||||
uid.text = host + "files/gc2/" + auth_token + "/pak/model" + MODEL + ".pak"
|
||||
else:
|
||||
if user_id:
|
||||
bind_info = await get_bind(user_id)
|
||||
if bind_info and bind_info['is_verified'] == 1:
|
||||
auth_token = bind_info['auth_token']
|
||||
rid.text = MODEL
|
||||
uid.text = host + "files/gc2/" + auth_token + "/pak/model" + MODEL + ".pak"
|
||||
else:
|
||||
rid.text = "1"
|
||||
uid.text = host + "files/gc/model1.pak"
|
||||
else:
|
||||
rid.text = "1"
|
||||
uid.text = host + "files/gc/model1.pak"
|
||||
|
||||
mid.append(rid)
|
||||
mid.append(uid)
|
||||
return mid
|
||||
|
||||
async def get_tune_pak(decrypted_fields, user_id):
|
||||
mid = ET.Element("tuneFile_pak")
|
||||
rid = ET.Element("date")
|
||||
uid = ET.Element("url")
|
||||
|
||||
host = await get_host_string()
|
||||
|
||||
if AUTHORIZATION_MODE == 0:
|
||||
auth_token = decrypted_fields[b'vid'][0].decode()
|
||||
rid.text = TUNEFILE
|
||||
uid.text = host + "files/gc2/" + auth_token + "/pak/tuneFile" + TUNEFILE + ".pak"
|
||||
else:
|
||||
if user_id:
|
||||
bind_info = await get_bind(user_id)
|
||||
if bind_info and bind_info['is_verified'] == 1:
|
||||
auth_token = bind_info['auth_token']
|
||||
rid.text = TUNEFILE
|
||||
uid.text = host + "files/gc2/" + auth_token + "/pak/tuneFile" + TUNEFILE + ".pak"
|
||||
else:
|
||||
rid.text = "1"
|
||||
uid.text = host + "files/gc/tuneFile1.pak"
|
||||
else:
|
||||
rid.text = "1"
|
||||
uid.text = host + "files/gc/tuneFile1.pak"
|
||||
|
||||
mid.append(rid)
|
||||
mid.append(uid)
|
||||
return mid
|
||||
|
||||
async def get_skin_pak(decrypted_fields, user_id):
|
||||
mid = ET.Element("skin_pak")
|
||||
rid = ET.Element("date")
|
||||
uid = ET.Element("url")
|
||||
|
||||
host = await get_host_string()
|
||||
|
||||
if AUTHORIZATION_MODE == 0:
|
||||
auth_token = decrypted_fields[b'vid'][0].decode()
|
||||
rid.text = SKIN
|
||||
uid.text = host + "files/gc2/" + auth_token + "/pak/skin" + SKIN + ".pak"
|
||||
else:
|
||||
if user_id:
|
||||
bind_info = await get_bind(user_id)
|
||||
if bind_info and bind_info['is_verified'] == 1:
|
||||
auth_token = bind_info['auth_token']
|
||||
rid.text = SKIN
|
||||
uid.text = host + "files/gc2/" + auth_token + "/pak/skin" + SKIN + ".pak"
|
||||
else:
|
||||
rid.text = "1"
|
||||
uid.text = host + "files/gc/skin1.pak"
|
||||
else:
|
||||
rid.text = "1"
|
||||
uid.text = host + "files/gc/skin1.pak"
|
||||
|
||||
mid.append(rid)
|
||||
mid.append(uid)
|
||||
return mid
|
||||
|
||||
async def get_m4a_path(decrypted_fields, user_id):
|
||||
host = await get_host_string()
|
||||
if AUTHORIZATION_MODE == 0:
|
||||
auth_token = decrypted_fields[b'vid'][0].decode()
|
||||
mid = ET.Element("m4a_path")
|
||||
mid.text = host + "files/gc2/" + auth_token + "/audio/"
|
||||
else:
|
||||
if user_id:
|
||||
bind_info = await get_bind(user_id)
|
||||
if bind_info and bind_info['is_verified'] == 1:
|
||||
mid = ET.Element("m4a_path")
|
||||
mid.text = host + "files/gc2/" + bind_info['auth_token'] + "/audio/"
|
||||
else:
|
||||
mid = ET.Element("m4a_path")
|
||||
mid.text = host
|
||||
else:
|
||||
mid = ET.Element("m4a_path")
|
||||
mid.text = host
|
||||
|
||||
return mid
|
||||
|
||||
async def get_stage_path(decrypted_data, user_id):
|
||||
host = await get_host_string()
|
||||
if AUTHORIZATION_MODE == 0:
|
||||
auth_token = decrypted_data[b'vid'][0].decode()
|
||||
mid = ET.Element("stage_path")
|
||||
mid.text = host + "files/gc2/" + auth_token + "/stage/"
|
||||
else:
|
||||
if user_id:
|
||||
bind_info = await get_bind(user_id)
|
||||
if bind_info and bind_info['is_verified'] == 1:
|
||||
mid = ET.Element("stage_path")
|
||||
mid.text = host + "files/gc2/" + bind_info['auth_token'] + "/stage/"
|
||||
else:
|
||||
mid = ET.Element("stage_path")
|
||||
mid.text = host
|
||||
else:
|
||||
mid = ET.Element("stage_path")
|
||||
mid.text = host
|
||||
|
||||
return mid
|
||||
|
||||
def get_stage_zero():
|
||||
sid = ET.Element("my_stage")
|
||||
did = ET.Element("stage_id")
|
||||
cid = ET.Element("ac_mode")
|
||||
did.text = "0"
|
||||
cid.text = "0"
|
||||
sid.append(did)
|
||||
sid.append(cid)
|
||||
return sid
|
||||
|
||||
def inform_page(text, mode):
|
||||
if mode == 0:
|
||||
mode = "/files/web/ttl_taitoid.png"
|
||||
elif mode == 1:
|
||||
mode = "/files/web/ttl_information.png"
|
||||
elif mode == 2:
|
||||
mode = "/files/web/ttl_buy.png"
|
||||
elif mode == 3:
|
||||
mode = "/files/web/ttl_title.png"
|
||||
elif mode == 4:
|
||||
mode = "/files/web/ttl_rank.png"
|
||||
elif mode == 5:
|
||||
mode = "/files/web/ttl_mission.png"
|
||||
elif mode == 6:
|
||||
mode = "/files/web/ttl_shop.png"
|
||||
with open("web/inform.html", "r") as file:
|
||||
return HTMLResponse(file.read().format(text=text, img=mode))
|
||||
|
||||
def safe_int(val):
|
||||
try:
|
||||
return int(val)
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
|
||||
def generate_otp():
|
||||
otp = ''.join(secrets.choice('0123456789') for _ in range(6))
|
||||
hashed_otp = hash_otp(otp)
|
||||
return otp, hashed_otp
|
||||
|
||||
def hash_otp(otp):
|
||||
return hashlib.sha256(otp.encode()).hexdigest()
|
||||
|
||||
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:
|
||||
should_serve = await check_whitelist(decrypted_fields) and not await check_blacklist(decrypted_fields)
|
||||
|
||||
if AUTHORIZATION_MODE and should_serve:
|
||||
user_info, device_info = await decrypt_fields_to_user_info(decrypted_fields, "id")
|
||||
bind_info = await get_bind(user_info["id"])
|
||||
if not bind_info or bind_info['is_verified'] != 1:
|
||||
should_serve = False
|
||||
|
||||
return should_serve
|
||||
|
||||
async def should_serve_init(decrypted_fields):
|
||||
should_serve = True
|
||||
if AUTHORIZATION_NEEDED:
|
||||
should_serve = await check_whitelist(decrypted_fields) and not await check_blacklist(decrypted_fields)
|
||||
|
||||
return should_serve
|
||||
|
||||
async def should_serve_web(user_id):
|
||||
user_id = safe_int(user_id)
|
||||
should_serve = True
|
||||
if AUTHORIZATION_MODE:
|
||||
bind_info = await get_bind(user_id)
|
||||
if not bind_info or bind_info['is_verified'] != 1:
|
||||
should_serve = False
|
||||
if user_id < GRANDFATHERED_ACCOUNT_LIMIT:
|
||||
should_serve = True
|
||||
|
||||
return should_serve
|
||||
|
||||
async def generate_salt(user_id):
|
||||
SALT = "jHENR3wq$zX9@LpO"
|
||||
user_info = await user_id_to_user_info_simple(user_id)
|
||||
user_pw_hash = user_info['password_hash']
|
||||
username = user_info['username']
|
||||
|
||||
combined = f"{username}{user_id}{user_pw_hash}{SALT}".encode('utf-8')
|
||||
crc32_hash = binascii.crc32(combined) & 0xFFFFFFFF
|
||||
return str(crc32_hash)
|
||||
|
||||
async def get_host_string():
|
||||
from config import OVERRIDE_HOST, HOST, PORT
|
||||
host_string = OVERRIDE_HOST if OVERRIDE_HOST is not None else ("http://" + HOST + ":" + str(PORT) + "/")
|
||||
return host_string
|
||||
193
new_server_7003/api/play.py
Normal file
193
new_server_7003/api/play.py
Normal file
@@ -0,0 +1,193 @@
|
||||
from starlette.responses import Response
|
||||
from starlette.requests import Request
|
||||
from starlette.routing import Route
|
||||
import json
|
||||
import copy
|
||||
import xml.etree.ElementTree as ET
|
||||
from datetime import datetime
|
||||
|
||||
from config import COIN_REWARD
|
||||
|
||||
from api.database import player_database, results, decrypt_fields_to_user_info, set_device_data_using_decrypted_fields, results_query, set_user_data_using_decrypted_fields, clear_rank_cache
|
||||
from api.crypt import decrypt_fields
|
||||
from api.template import START_STAGES, EXP_UNLOCKED_SONGS, RESULT_XML
|
||||
from api.misc import should_serve
|
||||
|
||||
async def score_delta(mode, old_score, new_score):
|
||||
mobile_modes = [1, 2, 3]
|
||||
arcade_modes = [11, 12, 13]
|
||||
if mode in mobile_modes:
|
||||
return new_score - old_score, 0, new_score - old_score
|
||||
elif mode in arcade_modes:
|
||||
return 0, new_score - old_score, new_score - old_score
|
||||
else:
|
||||
return 0, 0, 0
|
||||
|
||||
async def result_request(request: Request):
|
||||
decrypted_fields, _ = await decrypt_fields(request)
|
||||
if not decrypted_fields:
|
||||
return Response("""<response><code>10</code><message>Invalid request data.</message></response>""", media_type="application/xml")
|
||||
|
||||
should_serve_result = await should_serve(decrypted_fields)
|
||||
|
||||
if not should_serve_result:
|
||||
return Response("""<response><code>403</code><message>Access denied.</message></response>""", media_type="application/xml")
|
||||
|
||||
device_id = decrypted_fields[b'vid'][0].decode()
|
||||
user_info, device_info = await decrypt_fields_to_user_info(decrypted_fields)
|
||||
|
||||
tree = copy.deepcopy(RESULT_XML)
|
||||
root = tree.getroot()
|
||||
|
||||
stts = decrypted_fields[b'stts'][0].decode()
|
||||
song_id = int(decrypted_fields[b'id'][0].decode())
|
||||
mode = int(decrypted_fields[b'mode'][0].decode())
|
||||
avatar = int(decrypted_fields[b'avatar'][0].decode())
|
||||
score = int(decrypted_fields[b'score'][0].decode())
|
||||
high_score = decrypted_fields[b'high_score'][0].decode()
|
||||
play_rslt = decrypted_fields[b'play_rslt'][0].decode()
|
||||
item = int(decrypted_fields[b'item'][0].decode())
|
||||
device_os = decrypted_fields[b'os'][0].decode()
|
||||
os_ver = decrypted_fields[b'os_ver'][0].decode()
|
||||
ver = decrypted_fields[b'ver'][0].decode()
|
||||
|
||||
stts = "[" + stts + "]"
|
||||
high_score = "[" + high_score + "]"
|
||||
play_rslt = "[" + play_rslt + "]"
|
||||
|
||||
try:
|
||||
stts = json.loads(stts)
|
||||
high_score = json.loads(high_score)
|
||||
play_rslt = json.loads(play_rslt)
|
||||
except:
|
||||
return Response("""<response><code>10</code><message>Invalid request data.</message></response>""", media_type="application/xml")
|
||||
|
||||
cache_key = f"{song_id}-{mode}"
|
||||
|
||||
# delete cache with key
|
||||
|
||||
await clear_rank_cache(cache_key)
|
||||
|
||||
# Start results processing
|
||||
|
||||
target_row_id = 0
|
||||
rank = None
|
||||
|
||||
user_id = user_info['id'] if user_info else None
|
||||
|
||||
if user_id:
|
||||
|
||||
query_param = {
|
||||
"song_id": song_id,
|
||||
"mode": mode,
|
||||
"user_id": user_id
|
||||
}
|
||||
records = await results_query(query_param)
|
||||
|
||||
mobile_delta, arcade_delta, total_delta = 0, 0, 0
|
||||
|
||||
if len(records) != 0:
|
||||
# user row exists
|
||||
target_row_id = records[0]['id']
|
||||
if score > records[0]['score']:
|
||||
mobile_delta, arcade_delta, total_delta = await score_delta(mode, records[0]['score'], score)
|
||||
update_query = results.update().where(results.c.id == records[0]['id']).values(
|
||||
device_id=device_id,
|
||||
stts=stts,
|
||||
avatar=avatar,
|
||||
score=score,
|
||||
high_score=high_score,
|
||||
play_rslt=play_rslt,
|
||||
item=item,
|
||||
os=device_os,
|
||||
os_ver=os_ver,
|
||||
ver=ver
|
||||
)
|
||||
await player_database.execute(update_query)
|
||||
|
||||
else:
|
||||
# insert new row
|
||||
|
||||
mobile_delta, arcade_delta, total_delta = await score_delta(mode, 0, score)
|
||||
insert_query = results.insert().values(
|
||||
device_id=device_id,
|
||||
user_id=user_id,
|
||||
stts=stts,
|
||||
song_id=song_id,
|
||||
mode=mode,
|
||||
avatar=avatar,
|
||||
score=score,
|
||||
high_score=high_score,
|
||||
play_rslt=play_rslt,
|
||||
item=item,
|
||||
os=device_os,
|
||||
os_ver=os_ver,
|
||||
ver=ver,
|
||||
created_at=datetime.utcnow(),
|
||||
updated_at=datetime.utcnow()
|
||||
)
|
||||
result = await player_database.execute(insert_query)
|
||||
target_row_id = result
|
||||
|
||||
# Calculate final rank for client display
|
||||
|
||||
query_param = {
|
||||
"song_id": song_id,
|
||||
"mode": mode
|
||||
}
|
||||
|
||||
records = await results_query(query_param)
|
||||
|
||||
rank = None
|
||||
for idx, record in enumerate(records, start=1):
|
||||
if record["id"] == target_row_id:
|
||||
rank = idx
|
||||
break
|
||||
|
||||
# Update user score delta
|
||||
|
||||
if total_delta:
|
||||
update_data = {
|
||||
"mobile_delta": user_info['mobile_delta'] + mobile_delta,
|
||||
"arcade_delta": user_info['arcade_delta'] + arcade_delta,
|
||||
"total_delta": user_info['total_delta'] + total_delta
|
||||
}
|
||||
await set_user_data_using_decrypted_fields(decrypted_fields, update_data)
|
||||
|
||||
# Unlocking mission stages and updating avatars
|
||||
|
||||
my_stage = set(device_info["my_stage"]) if device_info and device_info["my_stage"] else set(START_STAGES)
|
||||
|
||||
current_exp = stts[0]
|
||||
for song in EXP_UNLOCKED_SONGS:
|
||||
if song["lvl"] <= current_exp:
|
||||
my_stage.add(song["id"])
|
||||
|
||||
my_stage = sorted(my_stage)
|
||||
|
||||
update_data = {
|
||||
"lvl": current_exp,
|
||||
"avatar": int(avatar),
|
||||
"my_stage": my_stage
|
||||
}
|
||||
|
||||
# add coins, skip 4max placeholder songs
|
||||
|
||||
if int(song_id) not in range(616, 1024) or int(mode) not in range(0, 4):
|
||||
coin_mp = user_info['coin_mp'] if user_info else 1
|
||||
|
||||
current_coin = device_info["coin"] if device_info and device_info["coin"] else 0
|
||||
updated_coin = current_coin + COIN_REWARD * coin_mp
|
||||
|
||||
update_data["coin"] = updated_coin
|
||||
|
||||
await set_device_data_using_decrypted_fields(decrypted_fields, update_data)
|
||||
|
||||
after_element = root.find('.//after')
|
||||
after_element.text = str(rank)
|
||||
xml_response = ET.tostring(tree.getroot(), encoding='unicode')
|
||||
return Response(xml_response, media_type="application/xml")
|
||||
|
||||
routes = [
|
||||
Route('/result.php', result_request, methods=['GET'])
|
||||
]
|
||||
353
new_server_7003/api/ranking.py
Normal file
353
new_server_7003/api/ranking.py
Normal file
@@ -0,0 +1,353 @@
|
||||
from starlette.responses import HTMLResponse, JSONResponse
|
||||
from starlette.requests import Request
|
||||
from starlette.routing import Route
|
||||
from sqlalchemy import select
|
||||
|
||||
from api.crypt import decrypt_fields
|
||||
from api.misc import inform_page, should_serve, get_host_string, inform_page
|
||||
from api.database import decrypt_fields_to_user_info, get_user_entitlement_from_devices, results_query, set_user_data_using_decrypted_fields, user_id_to_user_info_simple, accounts, player_database, write_rank_cache, get_rank_cache, set_device_data_using_decrypted_fields
|
||||
from api.template import SONG_LIST, EXP_UNLOCKED_SONGS, TITLE_LISTS, SUM_TITLE_LIST
|
||||
|
||||
async def mission(request: Request):
|
||||
decrypted_fields, _ = await decrypt_fields(request)
|
||||
if not decrypted_fields:
|
||||
return inform_page("Invalid request data", 5)
|
||||
|
||||
should_serve_result = await should_serve(decrypted_fields)
|
||||
|
||||
if not should_serve_result:
|
||||
return inform_page("Access denied", 5)
|
||||
|
||||
user_info, device_info = await decrypt_fields_to_user_info(decrypted_fields)
|
||||
if not device_info:
|
||||
return inform_page("Invalid device information", 4)
|
||||
|
||||
html = f"""<div class="f90 a_center pt50">Play Music to level up and unlock free songs!<br>Songs can only be unlocked when you play online.</div><div class='mission-list'>"""
|
||||
|
||||
for song in EXP_UNLOCKED_SONGS:
|
||||
song_id = song["id"]
|
||||
level_required = song["lvl"]
|
||||
song_name = SONG_LIST[song_id]["name_en"] if song_id < len(SONG_LIST) else "Unknown Song"
|
||||
|
||||
html += f"""
|
||||
<div class="mission-row">
|
||||
<div class="mission-level">Level {level_required}</div>
|
||||
<div class="mission-song">{song_name}</div>
|
||||
</div>
|
||||
"""
|
||||
|
||||
html += "</div>"
|
||||
try:
|
||||
with open("web/mission.html", "r", encoding="utf-8") as file:
|
||||
html_content = file.read().format(text=html)
|
||||
except FileNotFoundError:
|
||||
return HTMLResponse("""<html><body><h1>Mission file not found</h1></body></html>""", status_code=500)
|
||||
|
||||
return HTMLResponse(html_content)
|
||||
|
||||
|
||||
async def status(request: Request):
|
||||
decrypted_fields, original_fields = await decrypt_fields(request)
|
||||
if not decrypted_fields:
|
||||
return inform_page("Invalid request data", 3)
|
||||
|
||||
should_serve_result = await should_serve(decrypted_fields)
|
||||
|
||||
if not should_serve_result:
|
||||
return inform_page("Access denied", 3)
|
||||
|
||||
user_info, device_info = await decrypt_fields_to_user_info(decrypted_fields)
|
||||
if not device_info:
|
||||
return inform_page("Invalid device information", 4)
|
||||
|
||||
try:
|
||||
with open("web/status.html", "r", encoding="utf-8") as file:
|
||||
html_content = file.read().format(host_url=await get_host_string(), payload=original_fields)
|
||||
except FileNotFoundError:
|
||||
return inform_page("Status page not found", 4)
|
||||
|
||||
return HTMLResponse(html_content)
|
||||
|
||||
async def status_title_list(request: Request):
|
||||
decrypted_fields, _ = await decrypt_fields(request)
|
||||
if not decrypted_fields:
|
||||
return JSONResponse({"state": 0, "message": "Invalid request data"}, status_code=400)
|
||||
|
||||
should_serve_result = await should_serve(decrypted_fields)
|
||||
|
||||
if not should_serve_result:
|
||||
return JSONResponse({"state": 0, "message": "Access denied"}, status_code=403)
|
||||
|
||||
user_info, device_info = await decrypt_fields_to_user_info(decrypted_fields)
|
||||
if not device_info:
|
||||
return JSONResponse({"state": 0, "message": "Invalid user information"}, status_code=400)
|
||||
|
||||
username = user_info["username"] if user_info else "Guest"
|
||||
current_title = user_info["title"] if user_info else device_info['title']
|
||||
current_avatar = user_info["avatar"] if user_info else device_info['avatar']
|
||||
current_lvl = device_info['lvl']
|
||||
|
||||
player_object = {
|
||||
"username": username,
|
||||
"title": current_title,
|
||||
"avatar": current_avatar,
|
||||
"lvl": current_lvl
|
||||
}
|
||||
|
||||
payload = {
|
||||
"state": 1,
|
||||
"message": "Success",
|
||||
"data": {
|
||||
"title_list": TITLE_LISTS,
|
||||
"player_info": player_object
|
||||
}
|
||||
}
|
||||
|
||||
return JSONResponse(payload)
|
||||
|
||||
async def set_title(request: Request):
|
||||
decrypted_fields, _ = await decrypt_fields(request)
|
||||
if not decrypted_fields:
|
||||
return JSONResponse({"state": 0, "message": "Invalid request data"}, status_code=400)
|
||||
|
||||
should_serve_result = await should_serve(decrypted_fields)
|
||||
|
||||
if not should_serve_result:
|
||||
return JSONResponse({"state": 0, "message": "Access denied"}, status_code=403)
|
||||
|
||||
user_info, device_info = await decrypt_fields_to_user_info(decrypted_fields)
|
||||
if not device_info:
|
||||
return JSONResponse({"state": 0, "message": "Invalid user information"}, status_code=400)
|
||||
|
||||
post_data = await request.json()
|
||||
new_title = int(post_data.get("title", -1))
|
||||
|
||||
if new_title not in SUM_TITLE_LIST:
|
||||
return JSONResponse({"state": 0, "message": "Invalid title"}, status_code=400)
|
||||
|
||||
update_data = {
|
||||
"title": new_title
|
||||
}
|
||||
|
||||
if user_info:
|
||||
await set_user_data_using_decrypted_fields(decrypted_fields, update_data)
|
||||
|
||||
await set_device_data_using_decrypted_fields(decrypted_fields, update_data)
|
||||
|
||||
return JSONResponse({"state": 1, "message": "Title updated successfully"})
|
||||
|
||||
|
||||
async def ranking(request: Request):
|
||||
decrypted_fields, original_fields = await decrypt_fields(request)
|
||||
if not decrypted_fields:
|
||||
return inform_page("Invalid request data", 4)
|
||||
|
||||
should_serve_result = await should_serve(decrypted_fields)
|
||||
|
||||
if not should_serve_result:
|
||||
return inform_page("Access denied", 4)
|
||||
|
||||
user_info, device_info = await decrypt_fields_to_user_info(decrypted_fields)
|
||||
if not device_info:
|
||||
return inform_page("Invalid device information", 4)
|
||||
|
||||
try:
|
||||
with open("web/ranking.html", "r", encoding="utf-8") as file:
|
||||
html_content = file.read().format(host_url=await get_host_string(), payload=original_fields)
|
||||
except FileNotFoundError:
|
||||
return inform_page("Ranking page not found", 4)
|
||||
|
||||
return HTMLResponse(html_content)
|
||||
|
||||
|
||||
async def user_song_list(request: Request):
|
||||
decrypted_fields, _ = await decrypt_fields(request)
|
||||
if not decrypted_fields:
|
||||
return JSONResponse({"state": 0, "message": "Invalid request data"}, status_code=400)
|
||||
|
||||
should_serve_result = await should_serve(decrypted_fields)
|
||||
if not should_serve_result:
|
||||
return JSONResponse({"state": 0, "message": "Access denied"}, status_code=403)
|
||||
|
||||
user_info, device_info = await decrypt_fields_to_user_info(decrypted_fields)
|
||||
|
||||
my_stage = []
|
||||
if user_info:
|
||||
my_stage, _ = await get_user_entitlement_from_devices(user_info["id"])
|
||||
elif device_info:
|
||||
my_stage = device_info['my_stage']
|
||||
|
||||
payload = {
|
||||
"state": 1,
|
||||
"message": "Success",
|
||||
"data": {
|
||||
"song_list": SONG_LIST,
|
||||
"my_stage": my_stage
|
||||
}
|
||||
}
|
||||
return JSONResponse(payload)
|
||||
|
||||
async def user_ranking_individual(request: Request):
|
||||
decrypted_fields, _ = await decrypt_fields(request)
|
||||
if not decrypted_fields:
|
||||
return JSONResponse({"state": 0, "message": "Invalid request data"}, status_code=400)
|
||||
|
||||
should_serve_result = await should_serve(decrypted_fields)
|
||||
if not should_serve_result:
|
||||
return JSONResponse({"state": 0, "message": "Access denied"}, status_code=403)
|
||||
|
||||
user_info, device_info = await decrypt_fields_to_user_info(decrypted_fields)
|
||||
if not device_info:
|
||||
return JSONResponse({"state": 0, "message": "Invalid device information"}, status_code=400)
|
||||
|
||||
post_data = await request.json()
|
||||
song_id = int(post_data.get("song_id", -1))
|
||||
mode = int(post_data.get("mode", -1))
|
||||
page_number = int(post_data.get("page", 0))
|
||||
page_count = 50
|
||||
|
||||
if song_id not in range(0, 1000) or mode not in [1, 2, 3, 11, 12, 13]:
|
||||
return JSONResponse({"state": 0, "message": "Invalid song_id or mode"}, status_code=400)
|
||||
|
||||
user_id = user_info["id"] if user_info else None
|
||||
|
||||
total_count = 0
|
||||
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}"
|
||||
cached_data = await get_rank_cache(cache_key)
|
||||
|
||||
if cached_data:
|
||||
records = cached_data
|
||||
else:
|
||||
query_param = {
|
||||
"song_id": song_id,
|
||||
"mode": mode
|
||||
}
|
||||
records = await results_query(query_param)
|
||||
await write_rank_cache(cache_key, records)
|
||||
|
||||
total_count = len(records)
|
||||
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 user_id and record["user_id"] == user_id:
|
||||
player_ranking = {
|
||||
"username": user_info["username"],
|
||||
"score": record["score"],
|
||||
"position": index + 1,
|
||||
"title": user_info["title"],
|
||||
"avatar": user_info["avatar"]
|
||||
}
|
||||
|
||||
payload = {
|
||||
"state": 1,
|
||||
"message": "Success",
|
||||
"data": {
|
||||
"ranking_list": ranking_list,
|
||||
"player_ranking": player_ranking,
|
||||
"total_count": total_count
|
||||
}
|
||||
}
|
||||
|
||||
return JSONResponse(payload)
|
||||
|
||||
async def user_ranking_total(request: Request):
|
||||
decrypted_fields, _ = await decrypt_fields(request)
|
||||
if not decrypted_fields:
|
||||
return JSONResponse({"state": 0, "message": "Invalid request data"}, status_code=400)
|
||||
|
||||
should_serve_result = await should_serve(decrypted_fields)
|
||||
if not should_serve_result:
|
||||
return JSONResponse({"state": 0, "message": "Access denied"}, status_code=403)
|
||||
|
||||
user_info, device_info = await decrypt_fields_to_user_info(decrypted_fields)
|
||||
|
||||
if not device_info:
|
||||
return JSONResponse({"state": 0, "message": "Invalid device information"}, status_code=400)
|
||||
|
||||
post_data = await request.json()
|
||||
mode = int(post_data.get("mode", -1))
|
||||
page_number = int(post_data.get("page", 0))
|
||||
page_count = 50
|
||||
|
||||
if mode not in [0, 1, 2]:
|
||||
return JSONResponse({"state": 0, "message": "Invalid mode"}, status_code=400)
|
||||
|
||||
user_id = user_info["id"] if user_info else None
|
||||
|
||||
total_count = 0
|
||||
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"]}
|
||||
|
||||
score_obj = ["total_delta", "mobile_delta", "arcade_delta"]
|
||||
|
||||
cache_key = f"0-{mode}"
|
||||
cached_data = await get_rank_cache(cache_key)
|
||||
if cached_data:
|
||||
records = cached_data
|
||||
else:
|
||||
|
||||
query = select(
|
||||
accounts.c.id,
|
||||
accounts.c.username,
|
||||
accounts.c[score_obj[mode]],
|
||||
accounts.c.title,
|
||||
accounts.c.avatar,
|
||||
).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)
|
||||
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 user_id and record["id"] == user_id:
|
||||
player_ranking = {
|
||||
"username": user_info["username"],
|
||||
"score": record[score_obj[mode]],
|
||||
"position": index + 1,
|
||||
"title": user_info["title"],
|
||||
"avatar": user_info["avatar"]
|
||||
}
|
||||
|
||||
payload = {
|
||||
"state": 1,
|
||||
"message": "Success",
|
||||
"data": {
|
||||
"ranking_list": ranking_list,
|
||||
"player_ranking": player_ranking,
|
||||
"total_count": total_count
|
||||
}
|
||||
}
|
||||
|
||||
return JSONResponse(payload)
|
||||
|
||||
routes = [
|
||||
Route('/mission.php', mission, methods=['GET']),
|
||||
Route('/status.php', status, methods=['GET']),
|
||||
Route('/api/status/title_list', status_title_list, methods=['GET']),
|
||||
Route('/api/status/set_title', set_title, methods=['POST']),
|
||||
Route('/ranking.php', ranking, methods=['GET']),
|
||||
Route('/api/ranking/song_list', user_song_list, methods=['GET']),
|
||||
Route('/api/ranking/individual', user_ranking_individual, methods=['POST']),
|
||||
Route('/api/ranking/total', user_ranking_total, methods=['POST'])
|
||||
]
|
||||
313
new_server_7003/api/shop.py
Normal file
313
new_server_7003/api/shop.py
Normal file
@@ -0,0 +1,313 @@
|
||||
from starlette.responses import HTMLResponse, JSONResponse
|
||||
from starlette.requests import Request
|
||||
from starlette.routing import Route
|
||||
import os
|
||||
|
||||
from config import STAGE_PRICE, AVATAR_PRICE, ITEM_PRICE, FMAX_PRICE, EX_PRICE
|
||||
|
||||
from api.crypt import decrypt_fields
|
||||
from api.misc import inform_page, parse_res, should_serve, get_host_string
|
||||
from api.database import decrypt_fields_to_user_info, get_user_entitlement_from_devices, set_device_data_using_decrypted_fields
|
||||
from api.template import SONG_LIST, AVATAR_LIST, ITEM_LIST, EXCLUDE_STAGE_EXP
|
||||
|
||||
async def web_shop(request: Request):
|
||||
decrypted_fields, original_fields = await decrypt_fields(request)
|
||||
if not decrypted_fields:
|
||||
return inform_page("Invalid request data", 6)
|
||||
|
||||
should_serve_result = await should_serve(decrypted_fields)
|
||||
|
||||
if not should_serve_result:
|
||||
return inform_page("Access denied", 6)
|
||||
|
||||
user_info, device_info = await decrypt_fields_to_user_info(decrypted_fields)
|
||||
if not device_info:
|
||||
return inform_page("Invalid device information", 6)
|
||||
|
||||
try:
|
||||
with open("web/web_shop.html", "r", encoding="utf-8") as file:
|
||||
html_content = file.read().format(host_url=await get_host_string(), payload=original_fields)
|
||||
except FileNotFoundError:
|
||||
return inform_page("Shop page not found", 6)
|
||||
|
||||
return HTMLResponse(html_content)
|
||||
|
||||
async def api_shop_player_data(request: Request):
|
||||
from api.misc import FMAX_VER
|
||||
decrypted_fields, _ = await decrypt_fields(request)
|
||||
if not decrypted_fields:
|
||||
return JSONResponse({"state": 0, "message": "Invalid request data"}, status_code=400)
|
||||
|
||||
should_serve_result = await should_serve(decrypted_fields)
|
||||
if not should_serve_result:
|
||||
return JSONResponse({"state": 0, "message": "Access denied"}, status_code=403)
|
||||
|
||||
user_info, device_info = await decrypt_fields_to_user_info(decrypted_fields)
|
||||
|
||||
if user_info:
|
||||
my_stage, my_avatar = await get_user_entitlement_from_devices(user_info['id'])
|
||||
elif device_info:
|
||||
my_stage = device_info['my_stage']
|
||||
my_avatar = device_info['my_avatar']
|
||||
else:
|
||||
return JSONResponse({"state": 0, "message": "User and device not found"}, status_code=404)
|
||||
|
||||
is_fmax_purchased = False
|
||||
is_extra_purchased = False
|
||||
|
||||
stage_list = []
|
||||
stage_low_end = 100
|
||||
stage_high_end = 615
|
||||
|
||||
stage_list = [
|
||||
stage_id for stage_id in range(stage_low_end, stage_high_end)
|
||||
if stage_id not in EXCLUDE_STAGE_EXP and stage_id not in my_stage
|
||||
]
|
||||
|
||||
avatar_list = []
|
||||
avatar_low_end = 15
|
||||
avatar_high_end = 173 if FMAX_VER == 0 else 267
|
||||
|
||||
avatar_list = [
|
||||
avatar_id for avatar_id in range(avatar_low_end, avatar_high_end)
|
||||
if avatar_id not in my_avatar
|
||||
]
|
||||
|
||||
item_list = []
|
||||
item_low_end = 1
|
||||
item_high_end = 11
|
||||
|
||||
item_list = [
|
||||
item_id for item_id in range(item_low_end, item_high_end)
|
||||
]
|
||||
|
||||
print(my_stage)
|
||||
if 700 in my_stage and os.path.isfile('./files/4max_ver.txt'):
|
||||
is_fmax_purchased = True
|
||||
|
||||
if 980 in my_stage and os.path.isfile('./files/4max_ver.txt'):
|
||||
is_extra_purchased = True
|
||||
|
||||
payload = {
|
||||
"state": 1,
|
||||
"message": "Success",
|
||||
"data": {
|
||||
"coin": device_info['coin'],
|
||||
"stage_list": stage_list,
|
||||
"avatar_list": avatar_list,
|
||||
"item_list": item_list,
|
||||
"fmax_purchased": is_fmax_purchased,
|
||||
"extra_purchased": is_extra_purchased
|
||||
}
|
||||
}
|
||||
return JSONResponse(payload)
|
||||
|
||||
async def api_shop_item_data(request: Request):
|
||||
decrypted_fields, _ = await decrypt_fields(request)
|
||||
if not decrypted_fields:
|
||||
return JSONResponse({"state": 0, "message": "Invalid request data"}, status_code=400)
|
||||
|
||||
should_serve_result = await should_serve(decrypted_fields)
|
||||
if not should_serve_result:
|
||||
return JSONResponse({"state": 0, "message": "Access denied"}, status_code=403)
|
||||
|
||||
user_info, device_info = await decrypt_fields_to_user_info(decrypted_fields)
|
||||
if not device_info:
|
||||
return JSONResponse({"state": 0, "message": "Invalid device information"}, status_code=400)
|
||||
|
||||
list_to_use = []
|
||||
|
||||
post_data = await request.json()
|
||||
item_type = int(post_data.get("mode"))
|
||||
item_id = int(post_data.get("item_id"))
|
||||
|
||||
list_to_use = []
|
||||
price = 0
|
||||
prop_first = ""
|
||||
prop_second = ""
|
||||
prop_third = ""
|
||||
|
||||
if item_type == 0:
|
||||
list_to_use = SONG_LIST
|
||||
|
||||
elif item_type == 1:
|
||||
list_to_use = AVATAR_LIST
|
||||
|
||||
elif item_type == 2:
|
||||
list_to_use = ITEM_LIST
|
||||
|
||||
item = next((item for item in list_to_use if item['id'] == item_id), None) if list_to_use else None
|
||||
|
||||
if item or item_type in [3, 4]:
|
||||
if item_type == 0:
|
||||
price = STAGE_PRICE * 2 if len(item["difficulty_levels"]) == 6 else STAGE_PRICE
|
||||
prop_first = item['name_en']
|
||||
prop_second = item['author_en']
|
||||
prop_third = "/".join(map(str, item.get("difficulty_levels", [])))
|
||||
|
||||
elif item_type == 1:
|
||||
prop_first = item['name']
|
||||
prop_second = item['effect']
|
||||
price = AVATAR_PRICE
|
||||
|
||||
elif item_type == 2:
|
||||
prop_first = item['name']
|
||||
prop_second = item['effect']
|
||||
price = ITEM_PRICE
|
||||
|
||||
elif item_type == 3:
|
||||
from api.misc import FMAX_VER, FMAX_RES
|
||||
log = parse_res(FMAX_RES)
|
||||
prop_first = FMAX_VER
|
||||
prop_second = log
|
||||
price = FMAX_PRICE
|
||||
|
||||
elif item_type == 4:
|
||||
price = EX_PRICE
|
||||
|
||||
if item or item_type in [3, 4]:
|
||||
payload = {
|
||||
"state": 1,
|
||||
"message": "Success",
|
||||
"data": {
|
||||
"price": price,
|
||||
"property_first": prop_first,
|
||||
"property_second": prop_second,
|
||||
"property_third": prop_third
|
||||
}
|
||||
}
|
||||
|
||||
else:
|
||||
payload = {
|
||||
"state": 0,
|
||||
"message": "Item not found"
|
||||
}
|
||||
|
||||
return JSONResponse(payload)
|
||||
|
||||
async def api_shop_purchase_item(request: Request):
|
||||
decrypted_fields, _ = await decrypt_fields(request)
|
||||
if not decrypted_fields:
|
||||
return JSONResponse({"state": 0, "message": "Invalid request data"}, status_code=400)
|
||||
|
||||
should_serve_result = await should_serve(decrypted_fields)
|
||||
if not should_serve_result:
|
||||
return JSONResponse({"state": 0, "message": "Access denied"}, status_code=403)
|
||||
|
||||
list_to_use = []
|
||||
|
||||
post_data = await request.json()
|
||||
item_type = int(post_data.get("mode"))
|
||||
item_id = int(post_data.get("item_id"))
|
||||
|
||||
price = 0
|
||||
list_to_use = []
|
||||
|
||||
amount = 1 if item_type in [0, 1, 3, 4] else 10
|
||||
|
||||
if item_type == 0:
|
||||
list_to_use = SONG_LIST
|
||||
|
||||
elif item_type == 1:
|
||||
list_to_use = AVATAR_LIST
|
||||
|
||||
elif item_type == 2:
|
||||
list_to_use = ITEM_LIST
|
||||
|
||||
item = next((item for item in list_to_use if item['id'] == item_id), None) if list_to_use else None
|
||||
|
||||
if item or item_type in [3, 4]:
|
||||
if item_type == 0:
|
||||
price = STAGE_PRICE * 2 if len(item["difficulty_levels"]) == 6 else STAGE_PRICE
|
||||
|
||||
elif item_type == 1:
|
||||
price = AVATAR_PRICE
|
||||
|
||||
elif item_type == 2:
|
||||
price = ITEM_PRICE
|
||||
|
||||
elif item_type == 3:
|
||||
price = FMAX_PRICE
|
||||
|
||||
elif item_type == 4:
|
||||
price = EX_PRICE
|
||||
|
||||
if price == 0:
|
||||
payload = {
|
||||
"state": 0,
|
||||
"message": "Item not found"
|
||||
}
|
||||
|
||||
else:
|
||||
user_info, device_info = await decrypt_fields_to_user_info(decrypted_fields)
|
||||
|
||||
if user_info:
|
||||
my_stage, my_avatar = await get_user_entitlement_from_devices(user_info['id'])
|
||||
elif device_info:
|
||||
my_stage = device_info['my_stage']
|
||||
my_avatar = device_info['my_avatar']
|
||||
else:
|
||||
return JSONResponse({"state": 0, "message": "User and device not found"}, status_code=404)
|
||||
|
||||
my_stage = set(my_stage)
|
||||
my_avatar = set(my_avatar)
|
||||
item_pending = device_info['item'] or []
|
||||
|
||||
if item_type == 0 and item_id in my_stage:
|
||||
return JSONResponse({"state": 0, "message": "Stage already owned. Exit the shop and it will be added to the game."}, status_code=400)
|
||||
elif item_type == 1 and item_id in my_avatar:
|
||||
return JSONResponse({"state": 0, "message": "Avatar already owned. Exit the shop and it will be added to the game."}, status_code=400)
|
||||
elif item_type == 3 and 700 in my_stage:
|
||||
return JSONResponse({"state": 0, "message": "FMAX already owned. Exit the shop and it will be added to the game."}, status_code=400)
|
||||
elif item_type == 4 and 980 in my_stage:
|
||||
return JSONResponse({"state": 0, "message": "EXTRA already owned. Exit the shop and it will be added to the game."}, status_code=400)
|
||||
|
||||
if price > device_info['coin']:
|
||||
print("Insufficient coins for purchase.")
|
||||
return JSONResponse({"state": 0, "message": "Insufficient coins."}, status_code=400)
|
||||
|
||||
new_coin_amount = device_info['coin'] - price
|
||||
|
||||
if item_type == 0:
|
||||
my_stage.add(item_id)
|
||||
elif item_type == 1:
|
||||
my_avatar.add(item_id)
|
||||
elif item_type == 2:
|
||||
item_pending.append(item_id)
|
||||
|
||||
elif item_type == 3:
|
||||
for i in range(615, 926):
|
||||
my_stage.add(i)
|
||||
|
||||
elif item_type == 4:
|
||||
for i in range(926, 985):
|
||||
my_stage.add(i)
|
||||
|
||||
update_data = {
|
||||
"coin": new_coin_amount,
|
||||
"my_stage": list(my_stage),
|
||||
"my_avatar": list(my_avatar),
|
||||
"item": item_pending
|
||||
}
|
||||
|
||||
await set_device_data_using_decrypted_fields(decrypted_fields, update_data)
|
||||
|
||||
payload = {
|
||||
"state": 1,
|
||||
"message": "Purchase successful.",
|
||||
"data": {
|
||||
"coin": new_coin_amount
|
||||
}
|
||||
}
|
||||
|
||||
print(payload)
|
||||
|
||||
return JSONResponse(payload)
|
||||
|
||||
|
||||
routes = [
|
||||
Route('/web_shop.php', web_shop, methods=['GET', 'POST']),
|
||||
Route('/api/shop/player_data', api_shop_player_data, methods=['GET']),
|
||||
Route('/api/shop/item_data', api_shop_item_data, methods=['POST']),
|
||||
Route('/api/shop/purchase_item', api_shop_purchase_item, methods=['POST']),
|
||||
]
|
||||
95
new_server_7003/api/template.py
Normal file
95
new_server_7003/api/template.py
Normal file
@@ -0,0 +1,95 @@
|
||||
import json
|
||||
import os
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
|
||||
SONG_LIST = []
|
||||
AVATAR_LIST = []
|
||||
ITEM_LIST = []
|
||||
EXP_UNLOCKED_SONGS = []
|
||||
|
||||
START_STAGES = [7,23,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,88,89,90,91,92,93,94,95,96,97,98,99,214]
|
||||
# 214 is tutorial song.
|
||||
|
||||
START_AVATARS = []
|
||||
|
||||
EXCLUDE_STAGE_EXP = [121,134,166,167,168,169,170,213,214,215,225,277,396] # 134 and 170 unoccupied dummy tracks (filled with Departure -Remix-),
|
||||
#121 (and 93-96 lady gaga songs) removed (can be enabled by patching stageParam:isAvailable, or change the last byte before next song's name - 1 from 01 to 03 in stage_param.dat.
|
||||
# Rest are exp unlocked songs.
|
||||
EXCLUDE_AVATAR_EXP = [28,29]
|
||||
|
||||
SPECIAL_TITLES = [1, 2, 4431, 4432, 4601, 4602, 4611, 4612, 4621, 4622, 4631, 4632, 5111, 5112, 5121, 5122, 5131, 5132, 10001, 10002, 20001, 20002, 20003, 20004, 20005, 20006, 30001, 30002, 40001, 40002, 50001, 50002, 60001, 60002, 70001, 70002, 80001, 80002, 90001, 90002, 100001, 100002, 110001, 110002, 120001, 120002, 130001, 130002, 140001, 140002, 140003, 140004, 150001, 150002, 150003, 150004, 160001, 160002, 160003, 160004, 170001, 170002, 170003, 170004, 180001, 180002, 180003, 180004, 190001, 190002, 190003, 190004, 200001, 200002, 200003, 200004, 210001, 210002, 210003, 210004, 210005, 210006, 210007, 210008, 210009, 210010, 210011, 210012, 210013, 210014, 240001, 240002, 240003, 240004, 240005, 240006, 240007, 240008, 240009, 240010, 240011, 240012]
|
||||
|
||||
GOD_TITLES = [220001, 220002, 220003, 220004, 220005, 220006, 220007, 220008, 220009, 220010, 220011, 220012, 220013, 220014, 220015, 220016, 220017, 220018, 220019, 220020, 220021, 220022, 220023, 220024, 220025, 220026, 220027, 220028, 220029, 220030, 220031, 220032, 220033, 220034, 220035, 220036, 220037, 220038, 220039, 220040, 220041, 220042, 220043, 220044, 220045, 220046, 220047, 220048, 220049, 220050, 220051, 220052, 220053, 220054, 220055, 220056, 220057, 220058, 220059, 220060, 220061, 220062, 220063, 220064, 220065, 220066, 220067, 220068, 220069, 220070, 220071, 220072, 220073, 220074, 220075, 220076, 220077, 220078, 220079, 220080, 220081, 220082, 220083, 220084, 220085, 220086, 220087, 220088, 220089, 220090, 220091, 220092, 220093, 220094, 220095, 220096, 220097, 220098, 220099, 220100, 220101, 220102]
|
||||
|
||||
MASTER_TITLES = [12, 22, 32, 42, 52, 62, 72, 82, 92, 102, 112, 122, 132, 142, 152, 162, 172, 182, 192, 202, 212, 222, 232, 242, 252, 262, 272, 282, 292, 302, 312, 322, 332, 342, 352, 362, 372, 382, 392, 402, 412, 422, 432, 442, 452, 462, 472, 482, 492, 502, 512, 522, 532, 542, 552, 562, 572, 582, 592, 602, 612, 622, 632, 642, 652, 662, 672, 682, 692, 702, 712, 722, 732, 742, 752, 762, 772, 782, 792, 802, 812, 822, 832, 842, 852, 862, 872, 882, 892, 902, 912, 922, 972, 982, 992, 1002, 1012, 1022, 1032, 1042, 1052, 1062, 1072, 1082, 1092, 1102, 1112, 1122, 1132, 1142, 1152, 1162, 1172, 1182, 1192, 1202, 1222, 1232, 1242, 1252, 1262, 1272, 1282, 1292, 1302, 1312, 1322, 1332, 1342, 1352, 1362, 1372, 1382, 1392, 1402, 1412, 1422, 1432, 1442, 1452, 1462, 1472, 1482, 1492, 1502, 1512, 1522, 1532, 1542, 1552, 1562, 1572, 1582, 1592, 1602, 1612, 1622, 1632, 1642, 1652, 1662, 1672, 1682, 1692, 1702, 1712, 1722, 1732, 1742, 1752, 1762, 1772, 1782, 1792, 1802, 1812, 1822, 1832, 1842, 1852, 1862, 1872, 1882, 1892, 1902, 1912, 1922, 1932, 1942, 1952, 1962, 1972, 1982, 1992, 2002, 2012, 2022, 2032, 2042, 2052, 2062, 2072, 2082, 2092, 2102, 2112, 2122, 2132, 2152, 2162, 2172, 2182, 2192, 2202, 2212, 2222, 2232, 2242, 2252, 2262, 2272, 2282, 2292, 2302, 2312, 2322, 2332, 2342, 2352, 2362, 2372, 2382, 2392, 2402, 2412, 2422, 2432, 2442, 2452, 2462, 2472, 2482, 2492, 2502, 2512, 2522, 2532, 2542, 2552, 2562, 2572, 2582, 2592, 2602, 2612, 2622, 2632, 2642, 2652, 2662, 2672, 2682, 2692, 2702, 2712, 2722, 2732, 2742, 2752, 2762, 2782, 2792, 2802, 2812, 2822, 2832, 2842, 2852, 2862, 2872, 2882, 2892, 2902, 2912, 2922, 2932, 2942, 2952, 2962, 2972, 2982, 2992, 3002, 3012, 3022, 3032, 3042, 3052, 3062, 3072, 3082, 3092, 3102, 3112, 3122, 3132, 3142, 3152, 3162, 3172, 3182, 3192, 3202, 3212, 3222, 3232, 3242, 3252, 3262, 3272, 3282, 3292, 3302, 3312, 3322, 3332, 3342, 3352, 3362, 3372, 3382, 3392, 3402, 3412, 3422, 3432, 3442, 3452, 3462, 3472, 3482, 3492, 3502, 3512, 3522, 3532, 3542, 3552, 3562, 3572, 3582, 3592, 3602, 3612, 3622, 3632, 3642, 3652, 3662, 3672, 3682, 3692, 3702, 3712, 3722, 3732, 3742, 3752, 3762, 3772, 3782, 3792, 3802, 3812, 3822, 3832, 3842, 3852, 3862, 3872, 3882, 3892, 3902, 3912, 3922, 3932, 3942, 3952, 3962, 3982, 3992, 4002, 4012, 4022, 4032, 4042, 4052, 4062, 4072, 4082, 4092, 4102, 4112, 4122, 4132, 4142, 4152, 4162, 4172, 4182, 4192, 4202, 4212, 4222, 4232, 4242, 4252, 4262, 4272, 4282, 4292, 4302, 4312, 4322, 4332, 4342, 4352, 4362, 4372, 4382, 4392, 4402, 4412, 4422, 4442, 4452, 4462, 4472, 4482, 4492, 4502, 4512, 4522, 4532, 4542, 4552, 4562, 4572, 4582, 4592, 4642, 4652, 4662, 4672, 4682, 4692, 4702, 4712, 4722, 4732, 4742, 4752, 4762, 4772, 4782, 4792, 4802, 4812, 4822, 4832, 4842, 4862, 4872, 4882, 4892, 4902, 4912, 4922, 4932, 4942, 4952, 4962, 4972, 4982, 4992, 5002, 5012, 5022, 5032, 5042, 5052, 5062, 5072, 5082, 5092, 5102, 5142, 5152, 5162, 5172, 5182, 5192, 5202, 5212, 5222, 5232, 5242, 5252, 5262, 5272, 5282, 5292, 5302, 5312, 5322, 5332, 5342, 5352, 5362, 5372, 5382, 5392, 5402, 5412, 5422, 5432, 5442, 5452, 5462, 5472, 5482, 5492, 5502, 5512, 5522, 5532, 5542, 5552, 5562, 5572, 5582, 5592, 5602, 5612, 5622, 5632, 5642, 5652, 5662, 5672, 5682, 5692, 5702, 5712, 5722, 5732, 5742, 5752, 5762, 5772, 5782, 5792, 5802, 5812, 5822, 5832, 5842, 5852, 5862, 5872, 5882, 5892, 5902, 5912, 5922, 5932, 5942, 5952, 5962, 5972, 5982, 5992, 6002, 6012, 6022, 6032, 6042, 6052, 6062, 6072, 6082, 6092, 6102, 6112, 6122, 6132, 6142, 6152]
|
||||
|
||||
NORMAL_TITLES = [11, 21, 31, 41, 51, 61, 71, 81, 91, 101, 111, 121, 131, 141, 151, 161, 171, 181, 191, 201, 211, 221, 231, 241, 251, 261, 271, 281, 291, 301, 311, 321, 331, 341, 351, 361, 371, 381, 391, 401, 411, 421, 431, 441, 451, 461, 471, 481, 491, 501, 511, 521, 531, 541, 551, 561, 571, 581, 591, 601, 611, 621, 631, 641, 651, 661, 671, 681, 691, 701, 711, 721, 731, 741, 751, 761, 771, 781, 791, 801, 811, 821, 831, 841, 851, 861, 871, 881, 891, 901, 911, 921, 971, 981, 991, 1001, 1011, 1021, 1031, 1041, 1051, 1061, 1071, 1081, 1091, 1101, 1111, 1121, 1131, 1141, 1151, 1161, 1171, 1181, 1191, 1201, 1221, 1231, 1241, 1251, 1261, 1271, 1281, 1291, 1301, 1311, 1321, 1331, 1341, 1351, 1361, 1371, 1381, 1391, 1401, 1411, 1421, 1431, 1441, 1451, 1461, 1471, 1481, 1491, 1501, 1511, 1521, 1531, 1541, 1551, 1561, 1571, 1581, 1591, 1601, 1611, 1621, 1631, 1641, 1651, 1661, 1671, 1681, 1691, 1701, 1711, 1721, 1731, 1741, 1751, 1761, 1771, 1781, 1791, 1801, 1811, 1821, 1831, 1841, 1851, 1861, 1871, 1881, 1891, 1901, 1911, 1921, 1931, 1941, 1951, 1961, 1971, 1981, 1991, 2001, 2011, 2021, 2031, 2041, 2051, 2061, 2071, 2081, 2091, 2101, 2111, 2121, 2131, 2151, 2161, 2171, 2181, 2191, 2201, 2211, 2221, 2231, 2241, 2251, 2261, 2271, 2281, 2291, 2301, 2311, 2321, 2331, 2341, 2351, 2361, 2371, 2381, 2391, 2401, 2411, 2421, 2431, 2441, 2451, 2461, 2471, 2481, 2491, 2501, 2511, 2521, 2531, 2541, 2551, 2561, 2571, 2581, 2591, 2601, 2611, 2621, 2631, 2641, 2651, 2661, 2671, 2681, 2691, 2701, 2711, 2721, 2731, 2741, 2751, 2761, 2781, 2791, 2801, 2811, 2821, 2831, 2841, 2851, 2861, 2871, 2881, 2891, 2901, 2911, 2921, 2931, 2941, 2951, 2961, 2971, 2981, 2991, 3001, 3011, 3021, 3031, 3041, 3051, 3061, 3071, 3081, 3091, 3101, 3111, 3121, 3131, 3141, 3151, 3161, 3171, 3181, 3191, 3201, 3211, 3221, 3231, 3241, 3251, 3261, 3271, 3281, 3291, 3301, 3311, 3321, 3331, 3341, 3351, 3361, 3371, 3381, 3391, 3401, 3411, 3421, 3431, 3441, 3451, 3461, 3471, 3481, 3491, 3501, 3511, 3521, 3531, 3541, 3551, 3561, 3571, 3581, 3591, 3601, 3611, 3621, 3631, 3641, 3651, 3661, 3671, 3681, 3691, 3701, 3711, 3721, 3731, 3741, 3751, 3761, 3771, 3781, 3791, 3801, 3811, 3821, 3831, 3841, 3851, 3861, 3871, 3881, 3891, 3901, 3911, 3921, 3931, 3941, 3951, 3961, 3981, 3991, 4001, 4011, 4021, 4031, 4041, 4051, 4061, 4071, 4081, 4091, 4101, 4111, 4121, 4131, 4141, 4151, 4161, 4171, 4181, 4191, 4201, 4211, 4221, 4231, 4241, 4251, 4261, 4271, 4281, 4291, 4301, 4311, 4321, 4331, 4341, 4351, 4361, 4371, 4381, 4391, 4401, 4411, 4421, 4441, 4451, 4461, 4471, 4481, 4491, 4501, 4511, 4521, 4531, 4541, 4551, 4561, 4571, 4581, 4591, 4641, 4651, 4661, 4671, 4681, 4691, 4701, 4711, 4721, 4731, 4741, 4751, 4761, 4771, 4781, 4791, 4801, 4811, 4821, 4831, 4841, 4861, 4871, 4881, 4891, 4901, 4911, 4921, 4931, 4941, 4951, 4961, 4971, 4981, 4991, 5001, 5011, 5021, 5031, 5041, 5051, 5061, 5071, 5081, 5091, 5101, 5141, 5151, 5161, 5171, 5181, 5191, 5201, 5211, 5221, 5231, 5241, 5251, 5261, 5271, 5281, 5291, 5301, 5311, 5321, 5331, 5341, 5351, 5361, 5371, 5381, 5391, 5401, 5411, 5421, 5431, 5441, 5451, 5461, 5471, 5481, 5491, 5501, 5511, 5521, 5531, 5541, 5551, 5561, 5571, 5581, 5591, 5601, 5611, 5621, 5631, 5641, 5651, 5661, 5671, 5681, 5691, 5701, 5711, 5721, 5731, 5741, 5751, 5761, 5771, 5781, 5791, 5801, 5811, 5821, 5831, 5841, 5851, 5861, 5871, 5881, 5891, 5901, 5911, 5921, 5931, 5941, 5951, 5961, 5971, 5981, 5991, 6001, 6011, 6021, 6031, 6041, 6051, 6061, 6071, 6081, 6091, 6101, 6111, 6121, 6131, 6141, 6151]
|
||||
|
||||
START_XML = None
|
||||
SYNC_XML = None
|
||||
RESULT_XML = None
|
||||
|
||||
TITLE_LISTS = {
|
||||
0: SPECIAL_TITLES,
|
||||
1: NORMAL_TITLES,
|
||||
2: MASTER_TITLES,
|
||||
3: GOD_TITLES,
|
||||
}
|
||||
|
||||
SUM_TITLE_LIST = set(SPECIAL_TITLES + NORMAL_TITLES + MASTER_TITLES + GOD_TITLES)
|
||||
|
||||
def init_templates():
|
||||
global SONG_LIST, AVATAR_LIST, ITEM_LIST, EXP_UNLOCKED_SONGS
|
||||
global START_XML, SYNC_XML, RESULT_XML
|
||||
stage_pak_xml = None
|
||||
|
||||
base_path = 'api/config/'
|
||||
xml_path = 'files/'
|
||||
print("[TEMPLATES] Initializing templates...")
|
||||
|
||||
try:
|
||||
with open(os.path.join(base_path, 'song_list.json'), 'r', encoding='utf-8') as f:
|
||||
SONG_LIST = json.load(f)
|
||||
|
||||
with open(os.path.join(base_path, 'avatar_list.json'), 'r', encoding='utf-8') as f:
|
||||
AVATAR_LIST = json.load(f)
|
||||
|
||||
with open(os.path.join(base_path, 'item_list.json'), 'r', encoding='utf-8') as f:
|
||||
ITEM_LIST = json.load(f)
|
||||
|
||||
with open(os.path.join(base_path, 'exp_unlocked_songs.json'), 'r', encoding='utf-8') as f:
|
||||
EXP_UNLOCKED_SONGS = json.load(f)
|
||||
|
||||
with open(os.path.join(xml_path, 'stage_pak.xml'), 'r', encoding='utf-8') as f:
|
||||
stage_pak_xml = ET.parse(f)
|
||||
|
||||
with open(os.path.join(xml_path, 'start.xml'), 'r', encoding='utf-8') as f:
|
||||
START_XML = ET.parse(f)
|
||||
|
||||
with open(os.path.join(xml_path, 'sync.xml'), 'r', encoding='utf-8') as f:
|
||||
SYNC_XML = ET.parse(f)
|
||||
|
||||
with open(os.path.join(xml_path, 'result.xml'), 'r', encoding='utf-8') as f:
|
||||
RESULT_XML = ET.parse(f)
|
||||
|
||||
if stage_pak_xml is not None and START_XML is not None and SYNC_XML is not None:
|
||||
stage_pak_root = stage_pak_xml.getroot()
|
||||
start_root = START_XML.getroot()
|
||||
sync_root = SYNC_XML.getroot()
|
||||
for stage in stage_pak_root.findall("stage_pak"):
|
||||
if stage.find("id") is not None:
|
||||
start_root.append(stage)
|
||||
sync_root.append(stage)
|
||||
|
||||
|
||||
else:
|
||||
print("[TEMPLATES] Error: One or more XML files failed to load or is empty.")
|
||||
|
||||
|
||||
print("[TEMPLATES] Templates initialized successfully.")
|
||||
|
||||
except FileNotFoundError as e:
|
||||
print(f"Error: {e}")
|
||||
except json.JSONDecodeError as e:
|
||||
print(f"Error decoding JSON: {e}")
|
||||
354
new_server_7003/api/user.py
Normal file
354
new_server_7003/api/user.py
Normal file
@@ -0,0 +1,354 @@
|
||||
from starlette.responses import Response, FileResponse, HTMLResponse
|
||||
from starlette.requests import Request
|
||||
from starlette.routing import Route
|
||||
import os
|
||||
from datetime import datetime
|
||||
import xml.etree.ElementTree as ET
|
||||
import copy
|
||||
|
||||
from config import START_COIN
|
||||
|
||||
from api.misc import get_model_pak, get_tune_pak, get_skin_pak, get_m4a_path, get_stage_path, get_stage_zero, should_serve_init, inform_page
|
||||
from api.database import decrypt_fields_to_user_info, refresh_bind, get_user_entitlement_from_devices, set_device_data_using_decrypted_fields, create_device
|
||||
from api.crypt import decrypt_fields
|
||||
from api.template import START_AVATARS, START_STAGES, START_XML, SYNC_XML
|
||||
|
||||
async def info(request: Request):
|
||||
try:
|
||||
with open("web/history.html", "r", encoding="utf-8") as file:
|
||||
html_content = file.read()
|
||||
except FileNotFoundError:
|
||||
return inform_page("history.html not found", 1)
|
||||
|
||||
return HTMLResponse(html_content)
|
||||
|
||||
async def history(request: Request):
|
||||
try:
|
||||
with open("web/history.html", "r", encoding="utf-8") as file:
|
||||
html_content = file.read()
|
||||
except FileNotFoundError:
|
||||
return inform_page("history.html not found", 1)
|
||||
|
||||
return HTMLResponse(html_content)
|
||||
|
||||
async def delete_account(request):
|
||||
# This only tricks the client to clear its local data for now
|
||||
return Response(
|
||||
"""<?xml version="1.0" encoding="UTF-8"?><response><code>0</code><taito_id></taito_id></response>""",
|
||||
media_type="application/xml"
|
||||
)
|
||||
|
||||
async def tier(request: Request):
|
||||
html_path = f"files/tier.xml"
|
||||
|
||||
try:
|
||||
with open(html_path, "r", encoding="utf-8") as file:
|
||||
xml_content = file.read()
|
||||
except FileNotFoundError:
|
||||
return Response(
|
||||
"""<?xml version="1.0" encoding="UTF-8"?><response><code>0</code></response>""",
|
||||
media_type="application/xml"
|
||||
)
|
||||
|
||||
return Response(xml_content, media_type="application/xml")
|
||||
|
||||
async def reg(request: Request):
|
||||
return Response("", status_code=200)
|
||||
|
||||
async def start(request: Request):
|
||||
decrypted_fields, _ = await decrypt_fields(request)
|
||||
if not decrypted_fields:
|
||||
return Response("""<response><code>10</code><message><ja>Invalid request data.</ja><en>Invalid request data.</en></message></response>""", media_type="application/xml"
|
||||
)
|
||||
|
||||
root = copy.deepcopy(START_XML.getroot())
|
||||
|
||||
user_info, device_info = await decrypt_fields_to_user_info(decrypted_fields)
|
||||
username = user_info['username'] if user_info else None
|
||||
user_id = user_info['id'] if user_info else None
|
||||
device_id = decrypted_fields[b'vid'][0].decode()
|
||||
|
||||
should_serve_result = await should_serve_init(decrypted_fields)
|
||||
if not should_serve_result:
|
||||
return Response("""<response><code>403</code><message>Access denied.</message></response>""", media_type="application/xml")
|
||||
|
||||
if user_id:
|
||||
await refresh_bind(user_id)
|
||||
|
||||
root.append(await get_model_pak(decrypted_fields, user_id))
|
||||
root.append(await get_tune_pak(decrypted_fields, user_id))
|
||||
root.append(await get_skin_pak(decrypted_fields, user_id))
|
||||
root.append(await get_m4a_path(decrypted_fields, user_id))
|
||||
root.append(await get_stage_path(decrypted_fields, user_id))
|
||||
daily_reward_elem = root.find(".//login_bonus")
|
||||
if daily_reward_elem is None:
|
||||
return Response("""<response><code>500</code><message>Missing <login_bonus> element in XML.</message></response>""", media_type="application/xml")
|
||||
|
||||
last_count_elem = daily_reward_elem.find("last_count")
|
||||
if last_count_elem is None or not last_count_elem.text.isdigit():
|
||||
return Response("""<response><code>500</code><message>Invalid or missing last_count in XML.</message></response>""", media_type="application/xml")
|
||||
last_count = int(last_count_elem.text)
|
||||
now_count = 1
|
||||
|
||||
if device_info:
|
||||
current_day = device_info["daily_day"]
|
||||
last_timestamp = device_info["daily_timestamp"]
|
||||
current_date = datetime.now()
|
||||
|
||||
if (current_date.date() - last_timestamp.date()).days >= 1:
|
||||
now_count = current_day + 1
|
||||
if now_count > last_count:
|
||||
now_count = 1
|
||||
else:
|
||||
now_count = current_day
|
||||
|
||||
now_count_elem = daily_reward_elem.find("now_count")
|
||||
if now_count_elem is None:
|
||||
now_count_elem = ET.Element("now_count")
|
||||
daily_reward_elem.append(now_count_elem)
|
||||
now_count_elem.text = str(now_count)
|
||||
|
||||
if user_id:
|
||||
# This is a logged in user
|
||||
my_stage, my_avatar = await get_user_entitlement_from_devices(user_id)
|
||||
coin = device_info['coin'] if device_info is not None else 0
|
||||
|
||||
print("user has entitlements:", my_stage, my_avatar)
|
||||
|
||||
elif device_info:
|
||||
# This is a guest user with existing data
|
||||
my_avatar = set(device_info['my_avatar']) if device_info['my_avatar'] else START_AVATARS
|
||||
my_stage = set(device_info['my_stage']) if device_info['my_stage'] else START_STAGES
|
||||
coin = device_info['coin'] if device_info['coin'] is not None else 0
|
||||
else:
|
||||
my_avatar = START_AVATARS
|
||||
my_stage = START_STAGES
|
||||
coin = START_COIN
|
||||
|
||||
coin_elem = ET.Element("my_coin")
|
||||
coin_elem.text = str(coin)
|
||||
root.append(coin_elem)
|
||||
|
||||
for avatar_id in my_avatar:
|
||||
avatar_elem = ET.Element("my_avatar")
|
||||
avatar_elem.text = str(avatar_id)
|
||||
root.append(avatar_elem)
|
||||
|
||||
for stage_id in my_stage:
|
||||
stage_elem = ET.Element("my_stage")
|
||||
stage_id_elem = ET.Element("stage_id")
|
||||
stage_id_elem.text = str(stage_id)
|
||||
stage_elem.append(stage_id_elem)
|
||||
|
||||
ac_mode_elem = ET.Element("ac_mode")
|
||||
ac_mode_elem.text = "1"
|
||||
stage_elem.append(ac_mode_elem)
|
||||
root.append(stage_elem)
|
||||
|
||||
if username:
|
||||
tid = ET.Element("taito_id")
|
||||
tid.text = username
|
||||
root.append(tid)
|
||||
|
||||
sid_elem = ET.Element("sid")
|
||||
sid_elem.text = str(user_id)
|
||||
root.append(sid_elem)
|
||||
|
||||
try:
|
||||
sid = get_stage_zero()
|
||||
root.append(sid)
|
||||
except Exception as e:
|
||||
return Response(f"""<response><code>500</code><message>Error retrieving stage zero: {str(e)}</message></response>""", media_type="application/xml")
|
||||
|
||||
xml_response = ET.tostring(root, encoding='unicode')
|
||||
return Response(xml_response, media_type="application/xml")
|
||||
|
||||
async def sync(request: Request):
|
||||
decrypted_fields, _ = await decrypt_fields(request)
|
||||
|
||||
if not decrypted_fields:
|
||||
return Response(
|
||||
"""<response><code>10</code><message>Invalid request data.</message></response>""",
|
||||
media_type="application/xml"
|
||||
)
|
||||
|
||||
should_serve_result = await should_serve_init(decrypted_fields)
|
||||
if not should_serve_result:
|
||||
return Response(
|
||||
"""<response><code>403</code><message>Access denied.</message></response>""",
|
||||
media_type="application/xml"
|
||||
)
|
||||
|
||||
device_id = decrypted_fields[b'vid'][0].decode()
|
||||
root = copy.deepcopy(SYNC_XML.getroot())
|
||||
|
||||
user_info, device_info = await decrypt_fields_to_user_info(decrypted_fields)
|
||||
|
||||
username = user_info['username'] if user_info else None
|
||||
user_id = user_info['id'] if user_info else None
|
||||
|
||||
root.append(await get_model_pak(decrypted_fields, user_id))
|
||||
root.append(await get_tune_pak(decrypted_fields, user_id))
|
||||
root.append(await get_skin_pak(decrypted_fields, user_id))
|
||||
root.append(await get_m4a_path(decrypted_fields, user_id))
|
||||
root.append(await get_stage_path(decrypted_fields, user_id))
|
||||
if user_id:
|
||||
# This is a logged in user
|
||||
my_stage, my_avatar = await get_user_entitlement_from_devices(user_id)
|
||||
coin = device_info['coin'] if device_info['coin'] is not None else 0
|
||||
items = device_info['item'] if device_info['item'] else []
|
||||
|
||||
elif device_info:
|
||||
# This is a guest user with existing data
|
||||
my_avatar = set(device_info['my_avatar']) if device_info['my_avatar'] else START_AVATARS
|
||||
my_stage = set(device_info['my_stage']) if device_info['my_stage'] else START_STAGES
|
||||
coin = device_info['coin'] if device_info['coin'] is not None else 0
|
||||
items = device_info['item'] if device_info['item'] else []
|
||||
else:
|
||||
my_avatar = START_AVATARS
|
||||
my_stage = START_STAGES
|
||||
coin = START_COIN
|
||||
items = []
|
||||
|
||||
coin_elem = ET.Element("my_coin")
|
||||
coin_elem.text = str(coin)
|
||||
root.append(coin_elem)
|
||||
|
||||
for item in items:
|
||||
item_elem = ET.Element("add_item")
|
||||
item_id_elem = ET.Element("id")
|
||||
item_id_elem.text = str(item)
|
||||
item_elem.append(item_id_elem)
|
||||
item_num_elem = ET.Element("num")
|
||||
item_num_elem.text = "9"
|
||||
item_elem.append(item_num_elem)
|
||||
root.append(item_elem)
|
||||
|
||||
if items:
|
||||
await set_device_data_using_decrypted_fields(decrypted_fields, {"item": []})
|
||||
|
||||
for avatar_id in my_avatar:
|
||||
avatar_elem = ET.Element("my_avatar")
|
||||
avatar_elem.text = str(avatar_id)
|
||||
root.append(avatar_elem)
|
||||
|
||||
for stage_id in my_stage:
|
||||
stage_elem = ET.Element("my_stage")
|
||||
stage_id_elem = ET.Element("stage_id")
|
||||
stage_id_elem.text = str(stage_id)
|
||||
stage_elem.append(stage_id_elem)
|
||||
|
||||
ac_mode_elem = ET.Element("ac_mode")
|
||||
ac_mode_elem.text = "1"
|
||||
stage_elem.append(ac_mode_elem)
|
||||
root.append(stage_elem)
|
||||
|
||||
if username:
|
||||
tid = ET.Element("taito_id")
|
||||
tid.text = username
|
||||
root.append(tid)
|
||||
|
||||
sid = get_stage_zero()
|
||||
root.append(sid)
|
||||
|
||||
kid = ET.Element("friend_num")
|
||||
kid.text = "9"
|
||||
root.append(kid)
|
||||
|
||||
xml_response = ET.tostring(root, encoding='unicode')
|
||||
return Response(xml_response, media_type="application/xml")
|
||||
|
||||
async def bonus(request: Request):
|
||||
decrypted_fields, _ = await decrypt_fields(request)
|
||||
if not decrypted_fields:
|
||||
return Response("""<response><code>10</code><message>Invalid request data.</message></response>""", media_type="application/xml")
|
||||
|
||||
device_id = decrypted_fields[b'vid'][0].decode()
|
||||
user_info, device_info = await decrypt_fields_to_user_info(decrypted_fields)
|
||||
|
||||
root = copy.deepcopy(START_XML.getroot())
|
||||
|
||||
daily_reward_elem = root.find(".//login_bonus")
|
||||
last_count_elem = daily_reward_elem.find("last_count")
|
||||
if last_count_elem is None or not last_count_elem.text.isdigit():
|
||||
return Response("""<response><code>500</code><message>Invalid or missing last_count in XML.</message></response>""", media_type="application/xml")
|
||||
last_count = int(last_count_elem.text)
|
||||
|
||||
user_id = user_info['id'] if user_info else None
|
||||
|
||||
time = datetime.now()
|
||||
|
||||
if device_info:
|
||||
current_day = device_info["daily_day"]
|
||||
last_timestamp = device_info["daily_timestamp"]
|
||||
if user_id:
|
||||
my_stage, my_avatar = await get_user_entitlement_from_devices(user_id)
|
||||
else:
|
||||
my_avatar = set(device_info["my_avatar"]) if device_info["my_avatar"] else set()
|
||||
my_stage = set(device_info["my_stage"]) if device_info["my_stage"] else set()
|
||||
|
||||
if (time.date() - last_timestamp.date()).days >= 1:
|
||||
current_day += 1
|
||||
if current_day > last_count:
|
||||
current_day = 1
|
||||
reward_elem = daily_reward_elem.find(f".//reward[count='{current_day}']")
|
||||
if reward_elem is not None:
|
||||
cnt_type = int(reward_elem.find("cnt_type").text)
|
||||
cnt_id = int(reward_elem.find("cnt_id").text)
|
||||
|
||||
if cnt_type == 1:
|
||||
stages = set(my_stage) if my_stage else set()
|
||||
if cnt_id not in stages:
|
||||
stages.add(cnt_id)
|
||||
my_stage = list(stages)
|
||||
update_data = {
|
||||
"daily_timestamp": time,
|
||||
"daily_day": current_day,
|
||||
"my_stage": my_stage
|
||||
}
|
||||
await set_device_data_using_decrypted_fields(decrypted_fields, update_data)
|
||||
|
||||
elif cnt_type == 2:
|
||||
avatars = set(my_avatar) if my_avatar else set()
|
||||
if cnt_id not in avatars:
|
||||
avatars.add(cnt_id)
|
||||
my_avatar = list(avatars)
|
||||
update_data = {
|
||||
"daily_timestamp": time,
|
||||
"daily_day": current_day,
|
||||
"my_avatar": my_avatar
|
||||
}
|
||||
await set_device_data_using_decrypted_fields(decrypted_fields, update_data)
|
||||
|
||||
else:
|
||||
update_data = {
|
||||
"daily_timestamp": time,
|
||||
"daily_day": current_day
|
||||
}
|
||||
await set_device_data_using_decrypted_fields(decrypted_fields, update_data)
|
||||
else:
|
||||
update_data = {
|
||||
"daily_timestamp": time,
|
||||
"daily_day": current_day
|
||||
}
|
||||
await set_device_data_using_decrypted_fields(decrypted_fields, update_data)
|
||||
|
||||
xml_response = "<response><code>0</code></response>"
|
||||
else:
|
||||
xml_response = "<response><code>1</code></response>"
|
||||
else:
|
||||
await create_device(device_id, time)
|
||||
xml_response = "<response><code>0</code></response>"
|
||||
|
||||
return Response(xml_response, media_type="application/xml")
|
||||
|
||||
routes = [
|
||||
Route('/info.php', info, methods=['GET']),
|
||||
Route('/history.php', history, methods=['GET']),
|
||||
Route('/delete_account.php', delete_account, methods=['GET']),
|
||||
Route('/confirm_tier.php', tier, methods=['GET']),
|
||||
Route('/gcm/php/register.php', reg, methods=['GET']),
|
||||
Route('/start.php', start, methods=['GET']),
|
||||
Route('/sync.php', sync, methods=['GET', 'POST']),
|
||||
Route('/login_bonus.php', bonus, methods=['GET'])
|
||||
]
|
||||
132
new_server_7003/api/web.py
Normal file
132
new_server_7003/api/web.py
Normal file
@@ -0,0 +1,132 @@
|
||||
from starlette.requests import Request
|
||||
from starlette.routing import Route
|
||||
from starlette.responses import HTMLResponse, JSONResponse, RedirectResponse
|
||||
import secrets
|
||||
from datetime import datetime
|
||||
|
||||
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
|
||||
|
||||
async def is_user(request: Request):
|
||||
token = request.cookies.get("token")
|
||||
if not token:
|
||||
return False
|
||||
query = webs.select().where(webs.c.token == token)
|
||||
web_data = await player_database.fetch_one(query)
|
||||
if not web_data:
|
||||
return False
|
||||
if web_data['permission'] < 1:
|
||||
return False
|
||||
|
||||
if AUTHORIZATION_MODE > 0:
|
||||
return await should_serve_web(web_data['user_id'])
|
||||
|
||||
return True
|
||||
|
||||
async def web_login_page(request: Request):
|
||||
with open("web/login.html", "r", encoding="utf-8") as file:
|
||||
html_template = file.read()
|
||||
return HTMLResponse(content=html_template)
|
||||
|
||||
async def web_login_login(request: Request):
|
||||
form_data = await request.json()
|
||||
username = form_data.get("username")
|
||||
password = form_data.get("password")
|
||||
|
||||
user_info = await user_name_to_user_info(username)
|
||||
if not user_info:
|
||||
return JSONResponse({"status": "failed", "message": "Invalid username or password."}, status_code=400)
|
||||
|
||||
if not verify_password(password, user_info['password_hash']):
|
||||
return JSONResponse({"status": "failed", "message": "Invalid username or password."}, status_code=400)
|
||||
|
||||
should_serve = await should_serve_web(user_info['id'])
|
||||
if not should_serve:
|
||||
return JSONResponse({"status": "failed", "message": "Access denied."}, status_code=403)
|
||||
|
||||
token = secrets.token_hex(64)
|
||||
web_query = webs.select().where(webs.c.user_id == user_info['id'])
|
||||
web_result = await player_database.fetch_one(web_query)
|
||||
if web_result:
|
||||
if web_result['permission'] < 1:
|
||||
return JSONResponse({"status": "failed", "message": "Access denied."}, status_code=403)
|
||||
|
||||
query = webs.update().where(webs.c.user_id == user_info['id']).values(
|
||||
token=token
|
||||
)
|
||||
else:
|
||||
query = webs.insert().values(
|
||||
user_id=user_info['id'],
|
||||
permission=1,
|
||||
web_token=token,
|
||||
created_at=datetime.utcnow(),
|
||||
updated_at=datetime.utcnow()
|
||||
)
|
||||
|
||||
await player_database.execute(query)
|
||||
|
||||
return JSONResponse({"status": "success", "message": token})
|
||||
|
||||
|
||||
async def user_center_api(request: Request):
|
||||
form_data = await request.json()
|
||||
token = form_data.get("token")
|
||||
if not token:
|
||||
return JSONResponse({"status": "failed", "message": "Token is required."}, status_code=400)
|
||||
|
||||
query = webs.select().where(webs.c.token == token)
|
||||
web_record = await player_database.fetch_one(query)
|
||||
if not web_record:
|
||||
return JSONResponse({"status": "failed", "message": "Invalid token."}, status_code=403)
|
||||
|
||||
if web_record['permission'] == 2 and form_data.get("user_id"):
|
||||
user_id = int(form_data.get("user_id") )
|
||||
elif web_record['permission'] == 2:
|
||||
user_id = int(web_record['user_id'])
|
||||
else:
|
||||
user_id = int(web_record['user_id'])
|
||||
|
||||
action = form_data.get("action")
|
||||
|
||||
if action == "basic":
|
||||
user_info = await user_id_to_user_info_simple(user_id)
|
||||
if not user_info:
|
||||
return JSONResponse({"status": "failed", "message": "User not found."}, status_code=404)
|
||||
|
||||
response_data = {
|
||||
"username": user_info['username'],
|
||||
"last_save_export": web_record['last_save_export'].isoformat() if web_record['last_save_export'] else "None",
|
||||
}
|
||||
return JSONResponse({"status": "success", "data": response_data})
|
||||
|
||||
|
||||
else:
|
||||
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:
|
||||
response = RedirectResponse(url="/login")
|
||||
return response
|
||||
|
||||
with open("web/user.html", "r", encoding="utf-8") as file:
|
||||
html_template = file.read()
|
||||
is_adm = await is_admin(request)
|
||||
if is_adm:
|
||||
admin_button = f"""
|
||||
<div class="container mt-4">
|
||||
<button class="btn btn-light" onclick="window.location.href='/admin'">Admin Panel</button>
|
||||
</div>
|
||||
"""
|
||||
html_template += admin_button
|
||||
|
||||
return HTMLResponse(content=html_template)
|
||||
|
||||
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"])
|
||||
]
|
||||
98
new_server_7003/config.py
Normal file
98
new_server_7003/config.py
Normal file
@@ -0,0 +1,98 @@
|
||||
import os
|
||||
|
||||
'''
|
||||
Do not change the name of this file.
|
||||
不要改动这个文件的名称。
|
||||
'''
|
||||
'''
|
||||
IP and port of the server FOR THE DOWNLOAD LINKS.
|
||||
下载链接:服务器的IP和端口。
|
||||
If you want to use a domain name, set it in OVERRIDE_HOST
|
||||
若想使用域名,请在OVERRIDE_HOST中设置。
|
||||
'''
|
||||
|
||||
HOST = "127.0.0.1"
|
||||
PORT = 9068
|
||||
OVERRIDE_HOST = None
|
||||
|
||||
ACTUAL_HOST = "127.0.0.1"
|
||||
ACTUAL_PORT = 9068
|
||||
|
||||
'''
|
||||
Datecode of the 3 pak files.
|
||||
三个pak文件的时间戳。
|
||||
'''
|
||||
|
||||
MODEL = "202504125800"
|
||||
TUNEFILE = "202507315817"
|
||||
SKIN = "202404191149"
|
||||
|
||||
'''
|
||||
Groove Coin-related settings.
|
||||
GCoin相关设定。
|
||||
'''
|
||||
STAGE_PRICE = 1
|
||||
AVATAR_PRICE = 1
|
||||
ITEM_PRICE = 2
|
||||
COIN_REWARD = 1
|
||||
START_COIN = 10
|
||||
|
||||
FMAX_PRICE = 300
|
||||
EX_PRICE = 150
|
||||
|
||||
SIMULTANEOUS_LOGINS = 2
|
||||
|
||||
'''
|
||||
Only the whitelisted playerID can use the service. Blacklist has priority over whitelist.
|
||||
只有白名单的玩家ID才能使用服务。黑名单优先于白名单。
|
||||
'''
|
||||
AUTHORIZATION_NEEDED = False
|
||||
|
||||
'''
|
||||
In addition to the whitelist/blacklist, set this to use discord/email authorization.
|
||||
除了白名单/黑名单之外,设置此项以使用Discord/电子邮件授权。
|
||||
0: Default blacklist/whitelist only
|
||||
1: Email authorization w/ whitelist/blacklist
|
||||
2: Discord authorization w/ whitelist/blacklist
|
||||
'''
|
||||
|
||||
AUTHORIZATION_MODE = 0
|
||||
|
||||
# For auth mode 1
|
||||
SMTP_HOST = "smtp.test.com"
|
||||
SMTP_PORT = 465
|
||||
SMTP_USER = "test@test.com"
|
||||
SMTP_PASSWORD = "test"
|
||||
|
||||
# For auth mode 2
|
||||
DISCORD_BOT_SECRET = "test"
|
||||
DISCORD_BOT_API_KEY = "test"
|
||||
|
||||
# Daily download limit per account in bytes (only activates for AUTHORIZATION_MODE 1 and 2)
|
||||
|
||||
DAILY_DOWNLOAD_LIMIT = 1073741824 # 1 GB
|
||||
GRANDFATHERED_ACCOUNT_LIMIT = 0 # Web center access, grandfathered old accounts get access regardless of auth mode
|
||||
|
||||
'''
|
||||
SSL certificate path. If left blank, use HTTP.
|
||||
SSL证书路径 - 留空则使用HTTP
|
||||
'''
|
||||
|
||||
SSL_CERT = None
|
||||
SSL_KEY = None
|
||||
|
||||
'''
|
||||
Whether to enable batch download functionality.
|
||||
是否开启批量下载功能
|
||||
'''
|
||||
|
||||
BATCH_DOWNLOAD_ENABLED = True
|
||||
THREAD_COUNT = 3
|
||||
|
||||
|
||||
'''
|
||||
Starlette default debug
|
||||
Starlette内置Debug
|
||||
'''
|
||||
|
||||
DEBUG = True
|
||||
BIN
new_server_7003/files/gc/model1.pak
Normal file
BIN
new_server_7003/files/gc/model1.pak
Normal file
Binary file not shown.
BIN
new_server_7003/files/gc/skin1.pak
Normal file
BIN
new_server_7003/files/gc/skin1.pak
Normal file
Binary file not shown.
BIN
new_server_7003/files/gc/tuneFile1.pak
Normal file
BIN
new_server_7003/files/gc/tuneFile1.pak
Normal file
Binary file not shown.
417
new_server_7003/files/stage_pak.xml
Normal file
417
new_server_7003/files/stage_pak.xml
Normal file
@@ -0,0 +1,417 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<stage_paks>
|
||||
<stage_pak>
|
||||
<id>5</id>
|
||||
<name>DrumnBass</name>
|
||||
<date>201903291100</date>
|
||||
<update>0</update>
|
||||
</stage_pak>
|
||||
<stage_pak>
|
||||
<id>34</id>
|
||||
<name>Shadow</name>
|
||||
<date>201903291100</date>
|
||||
<update>0</update>
|
||||
</stage_pak>
|
||||
<stage_pak>
|
||||
<id>48</id>
|
||||
<name>rewrite</name>
|
||||
<date>201903291100</date>
|
||||
<update>0</update>
|
||||
</stage_pak>
|
||||
<stage_pak>
|
||||
<id>49</id>
|
||||
<name>tsukema</name>
|
||||
<date>201903291100</date>
|
||||
<update>0</update>
|
||||
</stage_pak>
|
||||
<stage_pak>
|
||||
<id>120</id>
|
||||
<name>konton</name>
|
||||
<date>201903291100</date>
|
||||
<update>0</update>
|
||||
</stage_pak>
|
||||
<stage_pak>
|
||||
<id>126</id>
|
||||
<name>konoha</name>
|
||||
<date>201903291100</date>
|
||||
<update>0</update>
|
||||
</stage_pak>
|
||||
<stage_pak>
|
||||
<id>135</id>
|
||||
<name>departure</name>
|
||||
<date>20160310</date>
|
||||
<update>0</update>
|
||||
</stage_pak>
|
||||
<stage_pak>
|
||||
<id>136</id>
|
||||
<name>walking</name>
|
||||
<date>20160310</date>
|
||||
<update>0</update>
|
||||
</stage_pak>
|
||||
<stage_pak>
|
||||
<id>137</id>
|
||||
<name>orb</name>
|
||||
<date>20160310</date>
|
||||
<update>0</update>
|
||||
</stage_pak>
|
||||
<stage_pak>
|
||||
<id>140</id>
|
||||
<name>worldcall</name>
|
||||
<date>201903291100</date>
|
||||
<update>0</update>
|
||||
</stage_pak>
|
||||
<stage_pak>
|
||||
<id>146</id>
|
||||
<name>ayano</name>
|
||||
<date>201903291100</date>
|
||||
<update>0</update>
|
||||
</stage_pak>
|
||||
<stage_pak>
|
||||
<id>147</id>
|
||||
<name>yukei</name>
|
||||
<date>201903291100</date>
|
||||
<update>0</update>
|
||||
</stage_pak>
|
||||
<stage_pak>
|
||||
<id>157</id>
|
||||
<name>sakuram</name>
|
||||
<date>201903291100</date>
|
||||
<update>0</update>
|
||||
</stage_pak>
|
||||
<stage_pak>
|
||||
<id>158</id>
|
||||
<name>mssplanet</name>
|
||||
<date>201903291100</date>
|
||||
<update>0</update>
|
||||
</stage_pak>
|
||||
<stage_pak>
|
||||
<id>164</id>
|
||||
<name>outer</name>
|
||||
<date>201903291100</date>
|
||||
<update>0</update>
|
||||
</stage_pak>
|
||||
<stage_pak>
|
||||
<id>169</id>
|
||||
<name>extremegrv</name>
|
||||
<date>201903291100</date>
|
||||
<update>0</update>
|
||||
</stage_pak>
|
||||
<stage_pak>
|
||||
<id>171</id>
|
||||
<name>pzddragon</name>
|
||||
<date>20160310</date>
|
||||
<update>0</update>
|
||||
</stage_pak>
|
||||
<stage_pak>
|
||||
<id>202</id>
|
||||
<name>bbragna</name>
|
||||
<date>201903291100</date>
|
||||
<update>0</update>
|
||||
</stage_pak>
|
||||
<stage_pak>
|
||||
<id>206</id>
|
||||
<name>yowamushi</name>
|
||||
<date>201903291100</date>
|
||||
<update>0</update>
|
||||
</stage_pak>
|
||||
<stage_pak>
|
||||
<id>215</id>
|
||||
<name>pmneo</name>
|
||||
<date>201508182208</date>
|
||||
<update>0</update>
|
||||
</stage_pak>
|
||||
<stage_pak>
|
||||
<id>238</id>
|
||||
<name>kero9</name>
|
||||
<date>201509161108</date>
|
||||
<update>0</update>
|
||||
</stage_pak>
|
||||
<stage_pak>
|
||||
<id>268</id>
|
||||
<name>msphantom</name>
|
||||
<date>20160401</date>
|
||||
<update>0</update>
|
||||
</stage_pak>
|
||||
<stage_pak>
|
||||
<id>272</id>
|
||||
<name>redeparture</name>
|
||||
<date>20160310</date>
|
||||
<update>0</update>
|
||||
</stage_pak>
|
||||
<stage_pak>
|
||||
<id>277</id>
|
||||
<name>ghost</name>
|
||||
<date>20160401</date>
|
||||
<update>0</update>
|
||||
</stage_pak>
|
||||
<stage_pak>
|
||||
<id>359</id>
|
||||
<name>smash</name>
|
||||
<date>201706011100</date>
|
||||
<update>0</update>
|
||||
</stage_pak>
|
||||
<stage_pak>
|
||||
<id>360</id>
|
||||
<name>asuno</name>
|
||||
<date>201708311100</date>
|
||||
<update>0</update>
|
||||
</stage_pak>
|
||||
<stage_pak>
|
||||
<id>385</id>
|
||||
<name>alien</name>
|
||||
<date>201903291100</date>
|
||||
<update>0</update>
|
||||
</stage_pak>
|
||||
<stage_pak>
|
||||
<id>423</id>
|
||||
<name>kemono</name>
|
||||
<date>201903291100</date>
|
||||
<update>0</update>
|
||||
</stage_pak>
|
||||
<stage_pak>
|
||||
<id>459</id>
|
||||
<name>jumpee</name>
|
||||
<date>201901311100</date>
|
||||
<update>0</update>
|
||||
</stage_pak>
|
||||
<stage_pak>
|
||||
<id>617</id>
|
||||
<name>namcot</name>
|
||||
<date>202012031500</date>
|
||||
<update>63</update>
|
||||
</stage_pak>
|
||||
<stage_pak>
|
||||
<id>712</id>
|
||||
<name>honey</name>
|
||||
<date>202012031500</date>
|
||||
<update>63</update>
|
||||
</stage_pak>
|
||||
<stage_pak>
|
||||
<id>730</id>
|
||||
<name>tsukiyo</name>
|
||||
<date>202012031500</date>
|
||||
<update>63</update>
|
||||
</stage_pak>
|
||||
<stage_pak>
|
||||
<id>821</id>
|
||||
<name>eggova</name>
|
||||
<date>202504121346</date>
|
||||
<update>63</update>
|
||||
</stage_pak>
|
||||
<stage_pak>
|
||||
<id>718</id>
|
||||
<name>hypergoa</name>
|
||||
<date>202504120026</date>
|
||||
<update>63</update>
|
||||
</stage_pak>
|
||||
<stage_pak>
|
||||
<id>833</id>
|
||||
<name>indignant</name>
|
||||
<date>202504081927</date>
|
||||
<update>63</update>
|
||||
</stage_pak>
|
||||
<stage_pak>
|
||||
<id>712</id>
|
||||
<name>honey</name>
|
||||
<date>202012031600</date>
|
||||
<update>63</update>
|
||||
</stage_pak>
|
||||
<stage_pak>
|
||||
<id>142</id>
|
||||
<name>no</name>
|
||||
<date>202504081927</date>
|
||||
<update>63</update>
|
||||
</stage_pak>
|
||||
<stage_pak>
|
||||
<id>139</id>
|
||||
<name>seelights</name>
|
||||
<date>202504081927</date>
|
||||
<update>63</update>
|
||||
</stage_pak>
|
||||
<stage_pak>
|
||||
<id>6</id>
|
||||
<name>Arkanoid</name>
|
||||
<date>202504140212</date>
|
||||
<update>63</update>
|
||||
</stage_pak>
|
||||
<stage_pak>
|
||||
<id>50</id>
|
||||
<name>punk</name>
|
||||
<date>202504140212</date>
|
||||
<update>63</update>
|
||||
</stage_pak>
|
||||
<stage_pak>
|
||||
<id>759</id>
|
||||
<name>faketown</name>
|
||||
<date>202504140212</date>
|
||||
<update>63</update>
|
||||
</stage_pak>
|
||||
<stage_pak>
|
||||
<id>750</id>
|
||||
<name>ccddd</name>
|
||||
<date>202504140212</date>
|
||||
<update>63</update>
|
||||
</stage_pak>
|
||||
<stage_pak>
|
||||
<id>615</id>
|
||||
<name>extreme</name>
|
||||
<date>202504140212</date>
|
||||
<update>63</update>
|
||||
</stage_pak>
|
||||
<stage_pak>
|
||||
<id>721</id>
|
||||
<name>moon</name>
|
||||
<date>202504140212</date>
|
||||
<update>63</update>
|
||||
</stage_pak>
|
||||
<stage_pak>
|
||||
<id>766</id>
|
||||
<name>today</name>
|
||||
<date>202504140212</date>
|
||||
<update>63</update>
|
||||
</stage_pak>
|
||||
<stage_pak>
|
||||
<id>976</id>
|
||||
<name>trauma</name>
|
||||
<date>202504140212</date>
|
||||
<update>63</update>
|
||||
</stage_pak>
|
||||
<stage_pak>
|
||||
<id>895</id>
|
||||
<name>vampire</name>
|
||||
<date>202504140212</date>
|
||||
<update>63</update>
|
||||
</stage_pak>
|
||||
<stage_pak>
|
||||
<id>845</id>
|
||||
<name>villain</name>
|
||||
<date>202504140212</date>
|
||||
<update>63</update>
|
||||
</stage_pak>
|
||||
<stage_pak>
|
||||
<id>40</id>
|
||||
<name>backon</name>
|
||||
<date>202504140212</date>
|
||||
<update>63</update>
|
||||
</stage_pak>
|
||||
<stage_pak>
|
||||
<id>65</id>
|
||||
<name>beautiful</name>
|
||||
<date>202504140212</date>
|
||||
<update>63</update>
|
||||
</stage_pak>
|
||||
<stage_pak>
|
||||
<id>58</id>
|
||||
<name>choco</name>
|
||||
<date>202504140212</date>
|
||||
<update>63</update>
|
||||
</stage_pak>
|
||||
<stage_pak>
|
||||
<id>67</id>
|
||||
<name>daimeiwaku</name>
|
||||
<date>202504140212</date>
|
||||
<update>63</update>
|
||||
</stage_pak>
|
||||
<stage_pak>
|
||||
<id>35</id>
|
||||
<name>eclipse</name>
|
||||
<date>202504140212</date>
|
||||
<update>63</update>
|
||||
</stage_pak>
|
||||
<stage_pak>
|
||||
<id>106</id>
|
||||
<name>Happy</name>
|
||||
<date>202504140212</date>
|
||||
<update>63</update>
|
||||
</stage_pak>
|
||||
<stage_pak>
|
||||
<id>4</id>
|
||||
<name>Hiphop</name>
|
||||
<date>202504140212</date>
|
||||
<update>63</update>
|
||||
</stage_pak>
|
||||
<stage_pak>
|
||||
<id>11</id>
|
||||
<name>JET</name>
|
||||
<date>202504140212</date>
|
||||
<update>63</update>
|
||||
</stage_pak>
|
||||
<stage_pak>
|
||||
<id>52</id>
|
||||
<name>joy</name>
|
||||
<date>202504140212</date>
|
||||
<update>63</update>
|
||||
</stage_pak>
|
||||
<stage_pak>
|
||||
<id>54</id>
|
||||
<name>joyful</name>
|
||||
<date>202504140212</date>
|
||||
<update>63</update>
|
||||
</stage_pak>
|
||||
<stage_pak>
|
||||
<id>59</id>
|
||||
<name>laser</name>
|
||||
<date>202504140212</date>
|
||||
<update>63</update>
|
||||
</stage_pak>
|
||||
<stage_pak>
|
||||
<id>66</id>
|
||||
<name>linda</name>
|
||||
<date>202504140212</date>
|
||||
<update>63</update>
|
||||
</stage_pak>
|
||||
<stage_pak>
|
||||
<id>60</id>
|
||||
<name>lovetheworld</name>
|
||||
<date>202504140212</date>
|
||||
<update>63</update>
|
||||
</stage_pak>
|
||||
<stage_pak>
|
||||
<id>62</id>
|
||||
<name>monogatari</name>
|
||||
<date>202504140212</date>
|
||||
<update>63</update>
|
||||
</stage_pak>
|
||||
<stage_pak>
|
||||
<id>47</id>
|
||||
<name>monster</name>
|
||||
<date>202504140212</date>
|
||||
<update>63</update>
|
||||
</stage_pak>
|
||||
<stage_pak>
|
||||
<id>61</id>
|
||||
<name>nee</name>
|
||||
<date>202504140212</date>
|
||||
<update>63</update>
|
||||
</stage_pak>
|
||||
<stage_pak>
|
||||
<id>10</id>
|
||||
<name>Neptune</name>
|
||||
<date>202504140212</date>
|
||||
<update>63</update>
|
||||
</stage_pak>
|
||||
<stage_pak>
|
||||
<id>41</id>
|
||||
<name>niji</name>
|
||||
<date>202504140212</date>
|
||||
<update>63</update>
|
||||
</stage_pak>
|
||||
<stage_pak>
|
||||
<id>42</id>
|
||||
<name>starspangled</name>
|
||||
<date>202504140212</date>
|
||||
<update>63</update>
|
||||
</stage_pak>
|
||||
<stage_pak>
|
||||
<id>55</id>
|
||||
<name>supernova</name>
|
||||
<date>202504140212</date>
|
||||
<update>63</update>
|
||||
</stage_pak>
|
||||
<stage_pak>
|
||||
<id>51</id>
|
||||
<name>traveling</name>
|
||||
<date>202504140212</date>
|
||||
<update>63</update>
|
||||
</stage_pak>
|
||||
</stage_paks>
|
||||
182
new_server_7003/files/start.xml
Normal file
182
new_server_7003/files/start.xml
Normal file
@@ -0,0 +1,182 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?><response>
|
||||
<code>0</code>
|
||||
<area>us</area>
|
||||
<is_tutorial>0</is_tutorial>
|
||||
<cm_button_level>10</cm_button_level>
|
||||
<login_bonus>
|
||||
<status>1</status>
|
||||
<last_count>28</last_count>
|
||||
<count_update>1</count_update>
|
||||
<reward>
|
||||
<count>1</count>
|
||||
<cnt_type>3</cnt_type>
|
||||
<cnt_id>1</cnt_id>
|
||||
<num>1</num>
|
||||
</reward>
|
||||
<reward>
|
||||
<count>2</count>
|
||||
<cnt_type>3</cnt_type>
|
||||
<cnt_id>2</cnt_id>
|
||||
<num>1</num>
|
||||
</reward>
|
||||
<reward>
|
||||
<count>3</count>
|
||||
<cnt_type>2</cnt_type>
|
||||
<cnt_id>10</cnt_id>
|
||||
<num>1</num>
|
||||
</reward>
|
||||
<reward>
|
||||
<count>4</count>
|
||||
<cnt_type>3</cnt_type>
|
||||
<cnt_id>3</cnt_id>
|
||||
<num>1</num>
|
||||
</reward>
|
||||
<reward>
|
||||
<count>5</count>
|
||||
<cnt_type>3</cnt_type>
|
||||
<cnt_id>4</cnt_id>
|
||||
<num>1</num>
|
||||
</reward>
|
||||
<reward>
|
||||
<count>6</count>
|
||||
<cnt_type>3</cnt_type>
|
||||
<cnt_id>5</cnt_id>
|
||||
<num>1</num>
|
||||
</reward>
|
||||
<reward>
|
||||
<count>7</count>
|
||||
<cnt_type>1</cnt_type>
|
||||
<cnt_id>213</cnt_id>
|
||||
<num>1</num>
|
||||
</reward>
|
||||
<reward>
|
||||
<count>8</count>
|
||||
<cnt_type>3</cnt_type>
|
||||
<cnt_id>6</cnt_id>
|
||||
<num>1</num>
|
||||
</reward>
|
||||
<reward>
|
||||
<count>9</count>
|
||||
<cnt_type>3</cnt_type>
|
||||
<cnt_id>7</cnt_id>
|
||||
<num>1</num>
|
||||
</reward>
|
||||
<reward>
|
||||
<count>10</count>
|
||||
<cnt_type>2</cnt_type>
|
||||
<cnt_id>11</cnt_id>
|
||||
<num>1</num>
|
||||
</reward>
|
||||
<reward>
|
||||
<count>11</count>
|
||||
<cnt_type>3</cnt_type>
|
||||
<cnt_id>8</cnt_id>
|
||||
<num>1</num>
|
||||
</reward>
|
||||
<reward>
|
||||
<count>12</count>
|
||||
<cnt_type>3</cnt_type>
|
||||
<cnt_id>9</cnt_id>
|
||||
<num>1</num>
|
||||
</reward>
|
||||
<reward>
|
||||
<count>13</count>
|
||||
<cnt_type>3</cnt_type>
|
||||
<cnt_id>10</cnt_id>
|
||||
<num>1</num>
|
||||
</reward>
|
||||
<reward>
|
||||
<count>14</count>
|
||||
<cnt_type>1</cnt_type>
|
||||
<cnt_id>277</cnt_id>
|
||||
<num>1</num>
|
||||
</reward>
|
||||
<reward>
|
||||
<count>15</count>
|
||||
<cnt_type>3</cnt_type>
|
||||
<cnt_id>1</cnt_id>
|
||||
<num>1</num>
|
||||
</reward>
|
||||
<reward>
|
||||
<count>16</count>
|
||||
<cnt_type>3</cnt_type>
|
||||
<cnt_id>2</cnt_id>
|
||||
<num>1</num>
|
||||
</reward>
|
||||
<reward>
|
||||
<count>17</count>
|
||||
<cnt_type>2</cnt_type>
|
||||
<cnt_id>12</cnt_id>
|
||||
<num>1</num>
|
||||
</reward>
|
||||
<reward>
|
||||
<count>18</count>
|
||||
<cnt_type>3</cnt_type>
|
||||
<cnt_id>3</cnt_id>
|
||||
<num>1</num>
|
||||
</reward>
|
||||
<reward>
|
||||
<count>19</count>
|
||||
<cnt_type>3</cnt_type>
|
||||
<cnt_id>4</cnt_id>
|
||||
<num>1</num>
|
||||
</reward>
|
||||
<reward>
|
||||
<count>20</count>
|
||||
<cnt_type>3</cnt_type>
|
||||
<cnt_id>5</cnt_id>
|
||||
<num>1</num>
|
||||
</reward>
|
||||
<reward>
|
||||
<count>21</count>
|
||||
<cnt_type>1</cnt_type>
|
||||
<cnt_id>397</cnt_id>
|
||||
<num>1</num>
|
||||
</reward>
|
||||
<reward>
|
||||
<count>22</count>
|
||||
<cnt_type>3</cnt_type>
|
||||
<cnt_id>6</cnt_id>
|
||||
<num>1</num>
|
||||
</reward>
|
||||
<reward>
|
||||
<count>23</count>
|
||||
<cnt_type>3</cnt_type>
|
||||
<cnt_id>7</cnt_id>
|
||||
<num>1</num>
|
||||
</reward>
|
||||
<reward>
|
||||
<count>24</count>
|
||||
<cnt_type>2</cnt_type>
|
||||
<cnt_id>13</cnt_id>
|
||||
<num>1</num>
|
||||
</reward>
|
||||
<reward>
|
||||
<count>25</count>
|
||||
<cnt_type>3</cnt_type>
|
||||
<cnt_id>8</cnt_id>
|
||||
<num>1</num>
|
||||
</reward>
|
||||
<reward>
|
||||
<count>26</count>
|
||||
<cnt_type>3</cnt_type>
|
||||
<cnt_id>9</cnt_id>
|
||||
<num>1</num>
|
||||
</reward>
|
||||
<reward>
|
||||
<count>27</count>
|
||||
<cnt_type>3</cnt_type>
|
||||
<cnt_id>10</cnt_id>
|
||||
<num>1</num>
|
||||
</reward>
|
||||
<reward>
|
||||
<count>28</count>
|
||||
<cnt_type>1</cnt_type>
|
||||
<cnt_id>121</cnt_id>
|
||||
<num>1</num>
|
||||
</reward>
|
||||
<message>Connected to private server.
|
||||
You can edit start.xml to grant items via login bonus.</message>
|
||||
</login_bonus>
|
||||
<app_data_sync>1</app_data_sync>
|
||||
</response>
|
||||
16
new_server_7003/files/sync.xml
Normal file
16
new_server_7003/files/sync.xml
Normal file
@@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?><response>
|
||||
<code>0</code>
|
||||
<area>en</area>
|
||||
<is_tutorial>0</is_tutorial>
|
||||
<icon_badge>
|
||||
<icon_no>98</icon_no>
|
||||
<icon_index>4</icon_index>
|
||||
<badge_id>0</badge_id>
|
||||
</icon_badge>
|
||||
<result_banner>0</result_banner>
|
||||
<information_badge>
|
||||
<icon_index>1</icon_index>
|
||||
<badge_id>2</badge_id>
|
||||
</information_badge>
|
||||
|
||||
</response>
|
||||
528
new_server_7003/files/web/ranking.js
Normal file
528
new_server_7003/files/web/ranking.js
Normal file
@@ -0,0 +1,528 @@
|
||||
let searchTerm = '';
|
||||
let songList = [];
|
||||
let userStage = [];
|
||||
let tmpSongList = [];
|
||||
let individualMode = 2;
|
||||
let totalMode = 0;
|
||||
|
||||
on_initialize = () => {
|
||||
fetch(HOST_URL + 'api/ranking/song_list?' + PAYLOAD + '&_=' + new Date().getTime())
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.state) {
|
||||
songList = data.data.song_list;
|
||||
userStage = data.data.my_stage;
|
||||
loadUI();
|
||||
filterList();
|
||||
loadList();
|
||||
} else {
|
||||
document.getElementById('ranking_content').innerText = data.message;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
function loadUI() {
|
||||
const searchBar = document.getElementById('ranking_searchbar');
|
||||
searchBar.addEventListener('input', (event) => {
|
||||
searchTerm = event.target.value.toLowerCase();
|
||||
filterList();
|
||||
});
|
||||
|
||||
const sortSelect = document.getElementById('ranking_sort');
|
||||
sortSelect.addEventListener('change', (event) => {
|
||||
sortList(event.target.value);
|
||||
});
|
||||
}
|
||||
|
||||
function loadList() {
|
||||
let html = `
|
||||
<li class="song-item">
|
||||
<a onclick="showDetailTotal(0, 0);" class="song-button-owned">Total Score</a>
|
||||
</li>
|
||||
`;
|
||||
for (let i = 0; i < tmpSongList.length; i++) {
|
||||
const song = tmpSongList[i];
|
||||
if (userStage.includes(song.id)) {
|
||||
html += `
|
||||
<li class="song-item">
|
||||
<a onclick="showDetailIndividual(${song.id}, ${individualMode}, 0);" class="song-button-owned">
|
||||
<div class="song-name">${song.name_en}</div>
|
||||
<div class="composer-name-owned">${song.author_en}</div>
|
||||
</a>
|
||||
</li>
|
||||
`;
|
||||
} else {
|
||||
html += `
|
||||
<li class="song-item">
|
||||
<a onclick="showDetailIndividual(${song.id}, ${individualMode}, 0);" class="song-button-unowned">
|
||||
<div class="song-name">${song.name_en}</div>
|
||||
<div class="composer-name-unowned">${song.author_en}</div>
|
||||
</a>
|
||||
</li>
|
||||
`;
|
||||
}
|
||||
|
||||
}
|
||||
document.getElementById('song_list').innerHTML = html;
|
||||
}
|
||||
|
||||
function filterList() {
|
||||
if (searchTerm.trim() === '') {
|
||||
tmpSongList = [...songList];
|
||||
} else {
|
||||
tmpSongList = songList.filter(song =>
|
||||
song.name_en.toLowerCase().includes(searchTerm) ||
|
||||
song.author_en.toLowerCase().includes(searchTerm)
|
||||
);
|
||||
}
|
||||
loadList();
|
||||
}
|
||||
|
||||
function sortList(criteria) {
|
||||
switch (criteria) {
|
||||
case 'name':
|
||||
tmpSongList.sort((a, b) => a.name_en.localeCompare(b.name_en));
|
||||
break;
|
||||
case 'name_inverse':
|
||||
tmpSongList.sort((a, b) => b.name_en.localeCompare(a.name_en));
|
||||
break;
|
||||
case 'author':
|
||||
tmpSongList.sort((a, b) => a.author_en.localeCompare(b.author_en));
|
||||
break;
|
||||
case 'author_inverse':
|
||||
tmpSongList.sort((a, b) => b.author_en.localeCompare(a.author_en));
|
||||
break;
|
||||
case 'default':
|
||||
tmpSongList = [...songList];
|
||||
break;
|
||||
case 'default_inverse':
|
||||
tmpSongList = [...songList].reverse();
|
||||
break;
|
||||
case 'ownership':
|
||||
tmpSongList.sort((a, b) => {
|
||||
const aOwned = userStage.includes(a.id) ? 1 : 0;
|
||||
const bOwned = userStage.includes(b.id) ? 1 : 0;
|
||||
return bOwned - aOwned;
|
||||
});
|
||||
break;
|
||||
}
|
||||
loadList();
|
||||
}
|
||||
|
||||
function showDetailTotal(mode, page) {
|
||||
totalMode = mode;
|
||||
let songName = "Total Score";
|
||||
let buttonLabels = ["All", "Mobile", "Arcade"];
|
||||
let buttonModes = [0, 1, 2];
|
||||
|
||||
const postData = {
|
||||
mode: mode,
|
||||
page: page
|
||||
};
|
||||
|
||||
const controller = new AbortController();
|
||||
const signal = controller.signal;
|
||||
|
||||
let html = `
|
||||
<p>Loading...</p>
|
||||
<button id="backButton" class="loading-back-button">
|
||||
Back
|
||||
</button>
|
||||
`;
|
||||
|
||||
document.getElementById('ranking_box').innerHTML = html;
|
||||
|
||||
document.getElementById('backButton').onclick = () => {
|
||||
controller.abort();
|
||||
restoreBaseStructure();
|
||||
loadUI();
|
||||
filterList();
|
||||
loadList();
|
||||
};
|
||||
|
||||
fetch(HOST_URL + 'api/ranking/total?' + PAYLOAD + '&_=' + new Date().getTime(), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(postData),
|
||||
signal: signal // Pass the abort signal
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(payload => {
|
||||
if (payload.state === 1) {
|
||||
let rankingList = payload.data.ranking_list;
|
||||
let playerRanking = payload.data.player_ranking;
|
||||
let totalCount = payload.data.total_count;
|
||||
|
||||
let pageCount = 50;
|
||||
let generatePrevButton = false;
|
||||
let generateNextButton = false;
|
||||
|
||||
if (page > 0) {
|
||||
generatePrevButton = true;
|
||||
}
|
||||
if ((page + 1) * pageCount < totalCount) {
|
||||
generateNextButton = true;
|
||||
}
|
||||
|
||||
// Mode boxes
|
||||
|
||||
html = `<div>${songName}</div>`;
|
||||
|
||||
rowStart = '<div class="button-row">'
|
||||
rowEnd = '</div>'
|
||||
rowContent = []
|
||||
|
||||
for (let i = 0; i < buttonLabels.length; i++) {
|
||||
const label = buttonLabels[i];
|
||||
const modeValue = buttonModes[i];
|
||||
|
||||
if (modeValue === mode) {
|
||||
rowContent.push(`<div class="bt_bg01_ac">${label}</div>`);
|
||||
} else {
|
||||
rowContent.push(
|
||||
`<a onclick="showDetailTotal(${modeValue});" class="bt_bg01_xnarrow">${label}</a>`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
html += rowStart + rowContent.join('') + rowEnd;
|
||||
|
||||
// Player object
|
||||
let playerRank = parseInt(playerRanking['position']);
|
||||
if (playerRank < 1) {
|
||||
playerRank = 'N/A';
|
||||
} else {
|
||||
playerRank = "#" + playerRank;
|
||||
}
|
||||
|
||||
html += `
|
||||
<div class="player-element">
|
||||
<span class="rank">You<br>${playerRank}</span>
|
||||
<img src="/files/image/icon/avatar/${playerRanking['avatar']}.png" class="avatar" alt="Player Avatar">
|
||||
<div class="player-info">
|
||||
<div class="name">${playerRanking['username']}</div>
|
||||
<img src="/files/image/title/${playerRanking['title']}.png" class="title" alt="Player Title">
|
||||
</div>
|
||||
<div class="player-score">${playerRanking['score']}</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// generate pagination buttons
|
||||
if (generatePrevButton) {
|
||||
html += `
|
||||
<button style="width: 170px; height: 40px; margin: 10px; background-color: #000000; color: #FFFFFF;"
|
||||
onclick="showDetailTotal(${mode}, ${page - 1})">
|
||||
Prev Page
|
||||
</button>
|
||||
`;
|
||||
}
|
||||
|
||||
if (generateNextButton) {
|
||||
html += `
|
||||
<button style="width: 170px; height: 40px; margin: 10px; background-color: #000000; color: #FFFFFF;"
|
||||
onclick="showDetailTotal(${mode}, ${page + 1})">
|
||||
Next Page
|
||||
</button>
|
||||
`;
|
||||
}
|
||||
|
||||
// Loop leaderboard ranks
|
||||
|
||||
for (let i = 0; i < rankingList.length; i++) {
|
||||
userData = rankingList[i];
|
||||
html += `
|
||||
<div class="leaderboard-player">
|
||||
<div class="rank">#${userData['position']}</div>
|
||||
<img class="avatar" src="/files/image/icon/avatar/${userData['avatar']}.png" alt="Avatar">
|
||||
<div class="leaderboard-info">
|
||||
<div class="name">${userData['username']}</div>
|
||||
<div class="title"><img src="/files/image/title/${userData['title']}.png" alt="Title"></div>
|
||||
</div>
|
||||
<div class="leaderboard-score">${userData['score']}</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
}
|
||||
|
||||
// Generate pagination buttons again
|
||||
|
||||
if (generatePrevButton) {
|
||||
html += `
|
||||
<button style="width: 170px; height: 40px; margin: 10px; background-color: #000000; color: #FFFFFF;"
|
||||
onclick="showDetailTotal(${mode}, ${page - 1})">
|
||||
Prev Page
|
||||
</button>
|
||||
`;
|
||||
}
|
||||
|
||||
if (generateNextButton) {
|
||||
html += `
|
||||
<button style="width: 170px; height: 40px; margin: 10px; background-color: #000000; color: #FFFFFF;"
|
||||
onclick="showDetailTotal(${mode}, ${page + 1})">
|
||||
Next Page
|
||||
</button>
|
||||
`;
|
||||
}
|
||||
|
||||
html += `
|
||||
<button onclick='restoreBaseStructure();loadUI();filterList();loadList();' style="margin-top: 20px;" class="bt_bg01">
|
||||
Back
|
||||
</button>
|
||||
`;
|
||||
|
||||
document.getElementById('ranking_box').innerHTML = html;
|
||||
|
||||
} else {
|
||||
html = `
|
||||
<br>
|
||||
<p>${payload.message}</p>
|
||||
<button onclick='restoreBaseStructure();loadUI();filterList();loadList();' style="margin-top: 20px;" class="bt_bg01">
|
||||
Back
|
||||
</button>
|
||||
`;
|
||||
|
||||
document.getElementById('ranking_box').innerHTML = html;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function showDetailIndividual(songId, mode, page = 0) {
|
||||
individualMode = mode;
|
||||
let songMeta = songList.find(song => song.id === songId);
|
||||
let songName = songMeta ? songMeta.name_en : "Error - No song name found";
|
||||
let songDiff = songMeta ? songMeta.difficulty_levels : [];
|
||||
let buttonLabels = [];
|
||||
let buttonModes = [];
|
||||
|
||||
if (songId < 615) {
|
||||
buttonLabels = ["Easy", "Normal", "Hard"];
|
||||
buttonModes = [1, 2, 3];
|
||||
} else if (mode < 10) {
|
||||
mode += 10;
|
||||
}
|
||||
|
||||
if (songDiff.length == 6) {
|
||||
buttonLabels.push("AC-Easy", "AC-Normal", "AC-Hard");
|
||||
buttonModes.push(11, 12, 13);
|
||||
} else if (mode > 10) {
|
||||
mode -= 10;
|
||||
}
|
||||
|
||||
const postData = {
|
||||
song_id: songId,
|
||||
mode: mode,
|
||||
page: page
|
||||
};
|
||||
|
||||
const controller = new AbortController();
|
||||
const signal = controller.signal;
|
||||
|
||||
let html = `
|
||||
<p>Loading...</p>
|
||||
<button id="backButton" class="loading-back-button">
|
||||
Back
|
||||
</button>
|
||||
`;
|
||||
|
||||
document.getElementById('ranking_box').innerHTML = html;
|
||||
|
||||
document.getElementById('backButton').onclick = () => {
|
||||
controller.abort();
|
||||
restoreBaseStructure();
|
||||
loadUI();
|
||||
filterList();
|
||||
loadList();
|
||||
};
|
||||
|
||||
fetch(HOST_URL + 'api/ranking/individual?' + PAYLOAD + '&_=' + new Date().getTime(), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(postData),
|
||||
signal: signal // Pass the abort signal
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(payload => {
|
||||
if (payload.state === 1) {
|
||||
let rankingList = payload.data.ranking_list;
|
||||
let playerRanking = payload.data.player_ranking;
|
||||
let totalCount = payload.data.total_count;
|
||||
|
||||
let pageCount = 50;
|
||||
let generatePrevButton = false;
|
||||
let generateNextButton = false;
|
||||
|
||||
if (page > 0) {
|
||||
generatePrevButton = true;
|
||||
}
|
||||
if ((page + 1) * pageCount < totalCount) {
|
||||
generateNextButton = true;
|
||||
}
|
||||
|
||||
// Mode boxes
|
||||
|
||||
html = `<div>${songName}</div>`;
|
||||
|
||||
rowStart = '<div class="button-row">'
|
||||
rowEnd = '</div>'
|
||||
rowContent = []
|
||||
|
||||
for (let i = 0; i < buttonLabels.length; i++) {
|
||||
if (i == 3) {
|
||||
rowContent.push(rowStart);
|
||||
}
|
||||
const label = buttonLabels[i];
|
||||
const modeValue = buttonModes[i];
|
||||
|
||||
if (modeValue === mode) {
|
||||
rowContent.push(`<div class="bt_bg01_ac">${label}</div>`);
|
||||
} else {
|
||||
rowContent.push(
|
||||
`<a onclick="showDetailIndividual(${songId}, ${modeValue}, 0);" class="bt_bg01_xnarrow">${label}</a>`
|
||||
);
|
||||
}
|
||||
if (i == 2 && buttonLabels.length > 3) {
|
||||
rowContent.push(rowEnd);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
html += rowStart + rowContent.join('') + rowEnd;
|
||||
|
||||
// Player object
|
||||
let playerRank = parseInt(playerRanking['position']);
|
||||
if (playerRank < 1) {
|
||||
playerRank = 'N/A';
|
||||
} else {
|
||||
playerRank = "#" + playerRank;
|
||||
}
|
||||
|
||||
html += `
|
||||
<div class="player-element">
|
||||
<span class="rank">You<br>${playerRank}</span>
|
||||
<img src="/files/image/icon/avatar/${playerRanking['avatar']}.png" class="avatar" alt="Player Avatar">
|
||||
<div class="player-info">
|
||||
<div class="name">${playerRanking['username']}</div>
|
||||
<img src="/files/image/title/${playerRanking['title']}.png" class="title" alt="Player Title">
|
||||
</div>
|
||||
<div class="player-score">${playerRanking['score']}</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// generate pagination buttons
|
||||
if (generatePrevButton) {
|
||||
html += `
|
||||
<button style="width: 170px; height: 40px; margin: 10px; background-color: #000000; color: #FFFFFF;"
|
||||
onclick="showDetailIndividual(${songId}, ${mode}, ${page - 1})">
|
||||
Prev Page
|
||||
</button>
|
||||
`;
|
||||
}
|
||||
|
||||
if (generateNextButton) {
|
||||
html += `
|
||||
<button style="width: 170px; height: 40px; margin: 10px; background-color: #000000; color: #FFFFFF;"
|
||||
onclick="showDetailIndividual(${songId}, ${mode}, ${page + 1})">
|
||||
Next Page
|
||||
</button>
|
||||
`;
|
||||
}
|
||||
|
||||
// Loop leaderboard ranks
|
||||
|
||||
for (let i = 0; i < rankingList.length; i++) {
|
||||
userData = rankingList[i];
|
||||
html += `
|
||||
<div class="leaderboard-player">
|
||||
<div class="rank">#${userData['position']}</div>
|
||||
<img class="avatar" src="/files/image/icon/avatar/${userData['avatar']}.png" alt="Avatar">
|
||||
<div class="leaderboard-info">
|
||||
<div class="name">${userData['username']}</div>
|
||||
<div class="title"><img src="/files/image/title/${userData['title']}.png" alt="Title"></div>
|
||||
</div>
|
||||
<div class="leaderboard-score">${userData['score']}</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
}
|
||||
|
||||
// Generate pagination buttons again
|
||||
|
||||
if (generatePrevButton) {
|
||||
html += `
|
||||
<button style="width: 170px; height: 40px; margin: 10px; background-color: #000000; color: #FFFFFF;"
|
||||
onclick="showDetailIndividual(${songId}, ${mode}, ${page - 1})">
|
||||
Prev Page
|
||||
</button>
|
||||
`;
|
||||
}
|
||||
|
||||
if (generateNextButton) {
|
||||
html += `
|
||||
<button style="width: 170px; height: 40px; margin: 10px; background-color: #000000; color: #FFFFFF;"
|
||||
onclick="showDetailIndividual(${songId}, ${mode}, ${page + 1})">
|
||||
Next Page
|
||||
</button>
|
||||
`;
|
||||
}
|
||||
|
||||
html += `
|
||||
<button onclick='restoreBaseStructure();loadUI();filterList();loadList();' style="margin-top: 20px;" class="bt_bg01">
|
||||
Back
|
||||
</button>
|
||||
`;
|
||||
|
||||
document.getElementById('ranking_box').innerHTML = html;
|
||||
|
||||
} else {
|
||||
html = `
|
||||
<br>
|
||||
<p>${payload.message}</p>
|
||||
<button onclick='restoreBaseStructure();loadUI();filterList();loadList();' style="margin-top: 20px;" class="bt_bg01">
|
||||
Back
|
||||
</button>
|
||||
`;
|
||||
|
||||
document.getElementById('ranking_box').innerHTML = html;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function restoreBaseStructure() {
|
||||
document.getElementById('ranking_box').innerHTML = `
|
||||
<div id="ranking_content" class="d_ib w100">
|
||||
<!-- Search Bar -->
|
||||
<input
|
||||
type="text"
|
||||
id="ranking_searchbar"
|
||||
placeholder="Search for songs or composers..."
|
||||
style="width: 100%; padding: 10px; font-size: 30px; box-sizing: border-box;"
|
||||
/>
|
||||
|
||||
<!-- Sort Dropdown -->
|
||||
<select
|
||||
id="ranking_sort"
|
||||
style="width: 100%; padding: 10px; font-size: 30px; margin-top: 10px; box-sizing: border-box;"
|
||||
>
|
||||
<option value="default">Default</option>
|
||||
<option value="default_inverse">Default - Inverse</option>
|
||||
<option value="name">Sort by Name</option>
|
||||
<option value="name_inverse">Sort by Name - Inverse</option>
|
||||
<option value="author">Sort by Author</option>
|
||||
<option value="author_inverse">Sort by Author - Inverse</option>
|
||||
<option value="ownership">Sort by Ownership</option>
|
||||
</select>
|
||||
<ul class='song-list' id="song_list">
|
||||
Loading...
|
||||
</ul>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
window.onload = function(){
|
||||
restoreBaseStructure();
|
||||
on_initialize();
|
||||
};
|
||||
167
new_server_7003/files/web/status.js
Normal file
167
new_server_7003/files/web/status.js
Normal file
@@ -0,0 +1,167 @@
|
||||
let titleList = {};
|
||||
let userObject = {};
|
||||
let titleMode = 0;
|
||||
|
||||
on_initialize = () => {
|
||||
fetch(HOST_URL + 'api/status/title_list?' + PAYLOAD + '&_=' + new Date().getTime())
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.state) {
|
||||
titleList = data.data.title_list;
|
||||
userObject = data.data.player_info;
|
||||
loadUI();
|
||||
} else {
|
||||
document.getElementById('status_content').innerText = data.message;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
function loadUI() {
|
||||
let html = `
|
||||
<div class="player-element">
|
||||
<img src="/files/image/icon/avatar/${userObject['avatar']}.png" class="avatar" alt="Player Avatar">
|
||||
<div class="player-info">
|
||||
<div class="name">${userObject['username']}</div>
|
||||
<img src="/files/image/title/${userObject['title']}.png" class="title" alt="Player Title">
|
||||
</div>
|
||||
<div class="player-score">Level ${userObject['lvl']}</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
let titleTypes = ["Special", "Normal", "Master", "God"];
|
||||
|
||||
let buttonsHtml = '<div class="button-row">';
|
||||
|
||||
for (let i = 0; i < titleTypes.length; i++) {
|
||||
if (i === titleMode) {
|
||||
buttonsHtml += `<div class="bt_bg01_ac">${titleTypes[i]}</div>`;
|
||||
} else {
|
||||
buttonsHtml += `<a onclick="setPage(${i})" class="bt_bg01_xnarrow">${titleTypes[i]}</a>`;
|
||||
}
|
||||
}
|
||||
|
||||
buttonsHtml += '</div>';
|
||||
html += `<div style='text-align: center; margin-top: 20px;'>${buttonsHtml}</div>`;
|
||||
|
||||
let selectedTitles = titleList[titleMode] || [];
|
||||
|
||||
let titlesHtml = '<div class="title-list">';
|
||||
|
||||
for (let i = 0; i < selectedTitles.length; i++) {
|
||||
let title = selectedTitles[i];
|
||||
if (i % 2 === 0) {
|
||||
if (i !== 0) {
|
||||
titlesHtml += '</div>';
|
||||
}
|
||||
titlesHtml += '<div class="title-row">';
|
||||
}
|
||||
if (title === userObject['title']) {
|
||||
titlesHtml += `
|
||||
<img src="/files/image/title/${title}.png" alt="Title ${title}" class="title-image-selected">
|
||||
`;
|
||||
} else {
|
||||
titlesHtml += `
|
||||
<a onclick="setTitle(${title})" class="title-link">
|
||||
<img src="/files/image/title/${title}.png" alt="Title ${title}" class="title-image">
|
||||
</a>
|
||||
`;
|
||||
}
|
||||
}
|
||||
titlesHtml += '</div></div>';
|
||||
html += titlesHtml;
|
||||
|
||||
document.getElementById('status_content').innerHTML = html;
|
||||
}
|
||||
|
||||
function setPage(mode) {
|
||||
titleMode = mode;
|
||||
restoreBaseStructure();
|
||||
}
|
||||
|
||||
function setTitle(title) {
|
||||
html = `
|
||||
<p>Would you like to change your title?<br>Current Title:</p>
|
||||
<img src="/files/image/title/${userObject['title']}.png" alt="Current Title" class="title-image">
|
||||
<p>New Title:</p>
|
||||
<img src="/files/image/title/${title}.png" alt="New Title" class="title-image">
|
||||
<div class="button-container">
|
||||
<a onclick="submitTitle(${title})" class="bt_bg01">Confirm</a>
|
||||
<a onclick="restoreBaseStructure()" class="bt_bg01">Go back</a>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.getElementById('status_content').innerHTML = html;
|
||||
}
|
||||
|
||||
function submitTitle(title) {
|
||||
const postData = {
|
||||
title: title
|
||||
};
|
||||
|
||||
const controller = new AbortController();
|
||||
const signal = controller.signal;
|
||||
|
||||
let html = `
|
||||
<p>Submitting...</p>
|
||||
<button id="backButton" class="loading-back-button">
|
||||
Back
|
||||
</button>
|
||||
`;
|
||||
|
||||
document.getElementById('status_content').innerHTML = html;
|
||||
|
||||
document.getElementById('backButton').onclick = () => {
|
||||
controller.abort();
|
||||
restoreBaseStructure();
|
||||
};
|
||||
|
||||
fetch(HOST_URL + 'api/status/set_title?' + PAYLOAD + '&_=' + new Date().getTime(), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(postData),
|
||||
signal: signal // Pass the abort signal
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(payload => {
|
||||
if (payload.state === 1) {
|
||||
userObject['title'] = title;
|
||||
restoreBaseStructure();
|
||||
} else {
|
||||
let html = `
|
||||
<p>Failed. ${payload.message}</p>
|
||||
<div class="button-container">
|
||||
<a onclick="restoreBaseStructure()" class="bt_bg01">Go back</a>
|
||||
</div>
|
||||
`;
|
||||
document.getElementById('status_content').innerHTML = html;
|
||||
}
|
||||
}
|
||||
)
|
||||
.catch(error => {
|
||||
if (error.name === 'AbortError') {
|
||||
console.log('Fetch aborted');
|
||||
} else {
|
||||
let html = `
|
||||
|
||||
<p>Network error. Please try again.</p>
|
||||
<div class="button-container">
|
||||
<a onclick="restoreBaseStructure()" class="bt_bg01">Go back</a>
|
||||
</div>
|
||||
`;
|
||||
document.getElementById('status_content').innerHTML = html;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function restoreBaseStructure() {
|
||||
document.getElementById('status_content').innerHTML = ``;
|
||||
loadUI();
|
||||
}
|
||||
|
||||
|
||||
window.onload = function(){
|
||||
restoreBaseStructure();
|
||||
on_initialize();
|
||||
};
|
||||
462
new_server_7003/files/web/style.css
Normal file
462
new_server_7003/files/web/style.css
Normal file
@@ -0,0 +1,462 @@
|
||||
.dlc_body {
|
||||
background: url('/files/web/gc4ex_bg.jpg') no-repeat center center fixed;
|
||||
background-size: cover;
|
||||
color: white;
|
||||
font-family: Arial, sans-serif;
|
||||
text-align: center;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
.dlc_container {
|
||||
max-width: 800px;
|
||||
height: 90vh;
|
||||
margin: 0px auto;
|
||||
padding: 20px;
|
||||
}
|
||||
.dlc_container_extra {
|
||||
max-width: 800px;
|
||||
height: 70vh;
|
||||
margin: 0px auto;
|
||||
padding: 20px;
|
||||
}
|
||||
.dlc_logo {
|
||||
width: 100%;
|
||||
max-width: 500px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.dlc_extra_body {
|
||||
background: url('/files/web/extra_bg.jpg') no-repeat center center fixed;
|
||||
background-size: cover;
|
||||
color: red;
|
||||
font-family: Arial, sans-serif;
|
||||
text-align: center;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
.text-content {
|
||||
font-size: 18px;
|
||||
line-height: 1.6;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
.buy-button {
|
||||
display: inline-block;
|
||||
width: 160px;
|
||||
height: 110px;
|
||||
background: url('/files/web/frame_buy.png') no-repeat center center;
|
||||
background-size: contain;
|
||||
border: none;
|
||||
color: white;
|
||||
font-size: 22px;
|
||||
font-weight: bold;
|
||||
padding: 15px;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
}
|
||||
.quit-button {
|
||||
display: inline-block;
|
||||
width: 150px;
|
||||
height: 30px;
|
||||
background: url('/files/web/quit_button.png') no-repeat center center;
|
||||
background-size: contain;
|
||||
border: none;
|
||||
color: white;
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
}
|
||||
.buy-button-extra {
|
||||
display: inline-block;
|
||||
width: 160px;
|
||||
height: 110px;
|
||||
background: url('/files/web/frame_buy_extra.png') no-repeat center center;
|
||||
background-size: contain;
|
||||
border: none;
|
||||
color: white;
|
||||
font-size: 22px;
|
||||
font-weight: bold;
|
||||
padding: 15px;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
}
|
||||
.quit-button-extra {
|
||||
display: inline-block;
|
||||
width: 150px;
|
||||
height: 30px;
|
||||
background: url('/files/web/quit_button_extra.png') no-repeat center center;
|
||||
background-size: contain;
|
||||
border: none;
|
||||
color: white;
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
}
|
||||
.coin-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin-top: 10px;
|
||||
}
|
||||
.coin-icon {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
margin-left: 5px;
|
||||
}
|
||||
body {
|
||||
background-color: #000000;
|
||||
-webkit-text-size-adjust: none;
|
||||
padding: 0px;
|
||||
margin: 0 auto;
|
||||
text-align:center;
|
||||
color: #FFFFFF;
|
||||
-webkit-text-size-adjust: auto;
|
||||
-webkit-touch-callout:none;
|
||||
-webkit-tap-highlight-color:rgba(255,255,0,0.4);
|
||||
height: 100%;
|
||||
font-family: Hiragino Kaku Gothic ProN,Hiragino Kaku Gothic Pro,Meiryo,Helvetica,Arial,sans-serif;
|
||||
font-size: 34pt;
|
||||
line-height: 36pt;
|
||||
overflow-x: hidden;
|
||||
overscroll-behavior-x: none;
|
||||
}
|
||||
html {
|
||||
overscroll-behavior-x: none;
|
||||
}
|
||||
.d_bl{display:block;}
|
||||
.a_left{text-align:left;}
|
||||
.a_center{text-align:center;}
|
||||
.w90{width:90%;}
|
||||
.w100{width:100%;}
|
||||
.d_ib{display:inline-block;}
|
||||
.mb10p{margin-bottom:10%;}
|
||||
.f90{font-size:90%;}
|
||||
.pt50{padding-top:50px;}
|
||||
.f60{font-size:60%;}
|
||||
.f70{font-size:70%;}
|
||||
.plr25{padding-left:25px;padding-right:25px;}
|
||||
.plr50{padding-left:50px;padding-right:50px;}
|
||||
.m_auto{margin: 0px auto;}
|
||||
#header {
|
||||
text-align: center;
|
||||
background-color: #000000;
|
||||
border-bottom: 2px solid #ffffff;
|
||||
top: 0px;
|
||||
width: 100%;
|
||||
padding: 0px;
|
||||
display: block;
|
||||
z-index: 4;
|
||||
position: fixed;
|
||||
}
|
||||
div#wrapper{
|
||||
width: 100%;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
.wrapper_box{
|
||||
text-align:center;
|
||||
margin-top:5%;
|
||||
margin-bottom:-10%;
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
.ttl_height{
|
||||
height:40px;
|
||||
margin:20px 0px;
|
||||
}
|
||||
.bt_bg01 {
|
||||
background: url("/files/web/bt_bg01_640.gif");
|
||||
background-color: transparent;
|
||||
background-repeat: no-repeat;
|
||||
background-size:cover;
|
||||
padding: 10px;
|
||||
font-size: 26px;
|
||||
display: block;
|
||||
text-align:center;
|
||||
font-size:70%;
|
||||
width:380px;
|
||||
height: 61px;
|
||||
color: #DDDDDD;
|
||||
margin: 20px auto;
|
||||
}
|
||||
.title-image {
|
||||
width: 40vw;
|
||||
height: auto;
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
.mission-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
margin: 20px 0;
|
||||
font-family: Arial, sans-serif;
|
||||
}
|
||||
|
||||
.mission-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 10px;
|
||||
border-bottom: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.mission-level {
|
||||
font-weight: bold;
|
||||
color: #DDD;
|
||||
}
|
||||
|
||||
.mission-song {
|
||||
color: #EEE;
|
||||
}
|
||||
|
||||
.bt_bg01_narrow {
|
||||
background: url("/files/web/bt_bg01_640.gif");
|
||||
background-color: transparent;
|
||||
background-repeat: no-repeat;
|
||||
background-size:cover;
|
||||
padding: 10px;
|
||||
display: block;
|
||||
text-align:center;
|
||||
font-size:70%;
|
||||
width:285px;
|
||||
height: 61px;
|
||||
color: #DDDDDD;
|
||||
margin: 20px auto;
|
||||
}
|
||||
.input{
|
||||
font-size: 1em;
|
||||
background-color: #000000;
|
||||
color: #dddddd;
|
||||
}
|
||||
|
||||
.bt_bg01_xnarrow {
|
||||
background: url("/files/web/bt_bg04_640.gif");
|
||||
background-color: transparent;
|
||||
background-repeat: no-repeat;
|
||||
background-size:cover;
|
||||
display: block;
|
||||
text-align:center;
|
||||
font-size:70%;
|
||||
width:250px;
|
||||
height: 42px;
|
||||
color: #DDDDDD;
|
||||
margin: 20px auto;
|
||||
}
|
||||
|
||||
.bt_bg01_ac {
|
||||
background: url("/files/web/bt_bg04_ac_640.gif");
|
||||
background-color: transparent;
|
||||
background-repeat: no-repeat;
|
||||
background-size:cover;
|
||||
display: block;
|
||||
text-align:center;
|
||||
font-size:70%;
|
||||
width:250px;
|
||||
height: 42px;
|
||||
color: #DDDDDD;
|
||||
margin: 20px auto;
|
||||
}
|
||||
.song-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.song-item {
|
||||
margin-bottom: 1px;
|
||||
}
|
||||
|
||||
.song-button-owned {
|
||||
display: block;
|
||||
padding: 15px;
|
||||
background-color: #333333;
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
text-align: left;
|
||||
font-size: 26px;
|
||||
border: none;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.song-button-unowned {
|
||||
display: block;
|
||||
padding: 15px;
|
||||
background-color: #333333;
|
||||
color: #888888;
|
||||
text-decoration: none;
|
||||
text-align: left;
|
||||
font-size: 26px;
|
||||
border: none;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.song-name {
|
||||
font-size: 26px;
|
||||
}
|
||||
|
||||
.composer-name-owned {
|
||||
font-size: 20px;
|
||||
color: #aaaaaa;
|
||||
}
|
||||
.composer-name-unowned {
|
||||
font-size: 20px;
|
||||
color: #555555;
|
||||
}
|
||||
.song-button-owned:hover {
|
||||
background-color: #444444;
|
||||
}
|
||||
|
||||
.song-button-unowned:hover {
|
||||
background-color: #444444;
|
||||
}
|
||||
.song-item + .song-item {
|
||||
border-top: 1px solid #444444;
|
||||
}
|
||||
.button-row {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.player-element {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
background: url("/files/web/PLAYERback.gif") no-repeat center center;
|
||||
background-size: cover;
|
||||
border-radius: 10px;
|
||||
padding: 10px;
|
||||
margin: 10px 0;
|
||||
height: 100px;
|
||||
width: 98%;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.player-info {
|
||||
flex-grow: 0.9;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.name {
|
||||
font-size: 36px;
|
||||
font-weight: bold;
|
||||
color: #FFFFFF;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.title {
|
||||
height: 40px;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.player-score {
|
||||
font-size: 40px;
|
||||
font-weight: bold;
|
||||
color: #FFD700;
|
||||
text-align: left;
|
||||
margin-right: 100px;
|
||||
}
|
||||
.rank {
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
color: white;
|
||||
margin-right: 10px;
|
||||
}
|
||||
.avatar {
|
||||
width: 70px;
|
||||
height: 70px;
|
||||
background-color: black;
|
||||
border: 2px solid #333;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
margin-right: 10px;
|
||||
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;
|
||||
align-items: center;
|
||||
text-align: left;
|
||||
padding: 10px;
|
||||
margin-bottom: 10px;
|
||||
border-bottom: 1px solid #333;
|
||||
color: #ddd;
|
||||
}
|
||||
|
||||
.leaderboard-info {
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
.leaderboard-score {
|
||||
font-size: 40px;
|
||||
font-weight: bold;
|
||||
text-align: left;
|
||||
margin-right: 100px;
|
||||
}
|
||||
|
||||
.title-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.title-row {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.title-image-selected {
|
||||
width: 40vw;
|
||||
height: auto;
|
||||
border: 3px solid lightgray;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.title-image:hover {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.rank-align-top {
|
||||
text-align: center;
|
||||
font-size: 36px;
|
||||
margin-bottom: 20px;
|
||||
margin-top: 50px;
|
||||
}
|
||||
|
||||
.loading-back-button {
|
||||
position: fixed;
|
||||
bottom: 20px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
z-index: 1000;
|
||||
width: 380px;
|
||||
height: 61px;
|
||||
background: url("/files/web/bt_bg01_640.gif") no-repeat center center;
|
||||
background-size: cover;
|
||||
color: #DDDDDD;
|
||||
font-size: 70%;
|
||||
text-align: center;
|
||||
line-height: 61px;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
box-shadow: 0px 4px 6px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
600
new_server_7003/files/web/web_shop.js
Normal file
600
new_server_7003/files/web/web_shop.js
Normal file
@@ -0,0 +1,600 @@
|
||||
let userCoin = 0;
|
||||
let stageList = [];
|
||||
let avatarList = [];
|
||||
let itemList = [];
|
||||
let fmaxPurchased = false;
|
||||
let extraPurchased = false;
|
||||
|
||||
let shopMode = 0; // 0: Stages, 1: Avatars, 2: Items
|
||||
let stagePage = 0;
|
||||
let avatarPage = 0;
|
||||
let pageItemCount = 40;
|
||||
|
||||
on_initialize = () => {
|
||||
fetch(HOST_URL + 'api/shop/player_data?' + PAYLOAD + '&_=' + new Date().getTime())
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.state) {
|
||||
userCoin = data.data.coin;
|
||||
stageList = data.data.stage_list;
|
||||
avatarList = data.data.avatar_list;
|
||||
itemList = data.data.item_list;
|
||||
fmaxPurchased = data.data.fmax_purchased;
|
||||
extraPurchased = data.data.extra_purchased;
|
||||
loadUI();
|
||||
} else {
|
||||
document.getElementById('content').innerText = data.message;
|
||||
document.getElementById('coinCounter').innerText = 'Error';
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
function loadUI() {
|
||||
document.getElementById('ttl').src = "/files/web/ttl_shop.png";
|
||||
document.getElementById('ttl').alt = "SHOP";
|
||||
document.getElementById('coinCounter').innerText = userCoin;
|
||||
document.getElementById('menuList').innerHTML = generateMenuList();
|
||||
document.getElementById('content').innerHTML = generateMenuContent();
|
||||
}
|
||||
|
||||
function generateMenuList() {
|
||||
const options = {
|
||||
0: [
|
||||
{ cnt_type: 1, label: "Avatar" },
|
||||
{ cnt_type: 2, label: "Item" }
|
||||
],
|
||||
1: [
|
||||
{ cnt_type: 0, label: "Tracks" },
|
||||
{ cnt_type: 2, label: "Item" }
|
||||
],
|
||||
2: [
|
||||
{ cnt_type: 0, label: "Tracks" },
|
||||
{ cnt_type: 1, label: "Avatar" }
|
||||
]
|
||||
};
|
||||
|
||||
const modeOptions = options[shopMode] || [];
|
||||
|
||||
return modeOptions.map(option => `
|
||||
<li style="display: inline-block; margin: 0 30px;">
|
||||
<a onclick="changeShopMode(${option.cnt_type})" class="bt_bg01_narrow">${option.label}</a>
|
||||
</li>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
function generateMenuContent() {
|
||||
useList = [];
|
||||
prefix = "";
|
||||
suffix = "";
|
||||
if (shopMode === 0) {
|
||||
useList = stageList;
|
||||
prefix = "shop";
|
||||
suffix = "jpg";
|
||||
} else if (shopMode === 1) {
|
||||
useList = avatarList;
|
||||
prefix = "avatar";
|
||||
suffix = "png";
|
||||
} else if (shopMode === 2) {
|
||||
useList = itemList;
|
||||
prefix = "item";
|
||||
suffix = "png";
|
||||
}
|
||||
|
||||
html = '';
|
||||
|
||||
generatePrevButton = false;
|
||||
generateNextButton = false;
|
||||
|
||||
if (shopMode !== 2) {
|
||||
const currentPage = shopMode === 0 ? stagePage : avatarPage;
|
||||
useList = useList.slice(currentPage * pageItemCount, currentPage * pageItemCount + pageItemCount);
|
||||
|
||||
if (currentPage > 0) {
|
||||
generatePrevButton = true;
|
||||
}
|
||||
if ((currentPage + 1) * pageItemCount < (shopMode === 0 ? stageList.length : avatarList.length)) {
|
||||
generateNextButton = true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if (shopMode === 0 && stagePage === 0) {
|
||||
html += `
|
||||
<a onclick="showFMax()">
|
||||
<img src="/files/web/dlc_4max.jpg" style="width: 90%; margin-bottom: 110px; margin-top: -100px;" />
|
||||
</a><br>
|
||||
<a onclick="showExtra()">
|
||||
<img src="/files/web/dlc_extra.jpg" style="width: 90%; margin-bottom: 20px; margin-top: -100px;" />
|
||||
</a><br>
|
||||
`;
|
||||
}
|
||||
|
||||
if (generatePrevButton) {
|
||||
html += `
|
||||
<button style="width: 170px; height: 40px; margin: 10px; background-color: #000000; color: #FFFFFF;"
|
||||
onclick="prevPage(${shopMode})">
|
||||
Prev Page
|
||||
</button>
|
||||
`;
|
||||
}
|
||||
|
||||
if (generateNextButton) {
|
||||
html += `
|
||||
<button style="width: 170px; height: 40px; margin: 10px; background-color: #000000; color: #FFFFFF;"
|
||||
onclick="nextPage(${shopMode})">
|
||||
Next Page
|
||||
</button>
|
||||
`;
|
||||
}
|
||||
|
||||
if (generatePrevButton || generateNextButton) {
|
||||
html += '<br>';
|
||||
}
|
||||
|
||||
useList.forEach((item, index) => {
|
||||
html += `
|
||||
<button style="width: 170px; height: 170px; margin: 10px; background-color: black; background-size: contain; background-repeat: no-repeat; background-position: center center;background-image: url('/files/image/icon/${prefix}/${item}.${suffix}');"
|
||||
onclick="showItemDetail(${shopMode}, ${item})">
|
||||
</button>
|
||||
`
|
||||
if ((index + 1) % 4 === 0) {
|
||||
html += '<br>';
|
||||
}
|
||||
});
|
||||
|
||||
if (generatePrevButton) {
|
||||
html += `
|
||||
<button style="width: 170px; height: 40px; margin: 10px; background-color: #000000; color: #FFFFFF;"
|
||||
onclick="prevPage(${shopMode})">
|
||||
Prev Page
|
||||
</button>
|
||||
`;
|
||||
}
|
||||
|
||||
if (generateNextButton) {
|
||||
html += `
|
||||
<button style="width: 170px; height: 40px; margin: 10px; background-color: #000000; color: #FFFFFF;"
|
||||
onclick="nextPage(${shopMode})">
|
||||
Next Page
|
||||
</button>
|
||||
`;
|
||||
}
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
function prevPage(mode) {
|
||||
if (mode === 0 && stagePage > 0) {
|
||||
stagePage--;
|
||||
} else if (mode === 1 && avatarPage > 0) {
|
||||
avatarPage--;
|
||||
}
|
||||
document.getElementById('content').innerHTML = generateMenuContent();
|
||||
}
|
||||
|
||||
function nextPage(mode) {
|
||||
if (mode === 0 && (stagePage + 1) * pageItemCount < stageList.length) {
|
||||
stagePage++;
|
||||
} else if (mode === 1 && (avatarPage + 1) * pageItemCount < avatarList.length) {
|
||||
avatarPage++;
|
||||
}
|
||||
document.getElementById('content').innerHTML = generateMenuContent();
|
||||
}
|
||||
|
||||
function changeShopMode(mode) {
|
||||
shopMode = mode;
|
||||
document.getElementById('menuList').innerHTML = generateMenuList();
|
||||
document.getElementById('content').innerHTML = generateMenuContent();
|
||||
}
|
||||
|
||||
function showItemDetail(mode, itemId) {
|
||||
document.getElementById('menuList').innerHTML = '';
|
||||
|
||||
document.getElementById('ttl').src = "/files/web/ttl_buy.png";
|
||||
document.getElementById('ttl').alt = "BUY";
|
||||
|
||||
const postData = {
|
||||
mode: mode,
|
||||
item_id: itemId
|
||||
};
|
||||
|
||||
const controller = new AbortController();
|
||||
const signal = controller.signal;
|
||||
|
||||
document.getElementById('content').innerHTML = `
|
||||
<p>Loading...</p>
|
||||
<button id="backButton" style="margin-top: 20px;" class="bt_bg01">
|
||||
Back
|
||||
</button>
|
||||
`;
|
||||
|
||||
document.getElementById('backButton').onclick = () => {
|
||||
controller.abort();
|
||||
loadUI();
|
||||
};
|
||||
|
||||
fetch(HOST_URL + 'api/shop/item_data?' + PAYLOAD, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(postData),
|
||||
signal: signal // Pass the abort signal
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(payload => {
|
||||
if (payload.state === 1) {
|
||||
let html = '';
|
||||
if (mode === 0) {
|
||||
// Song purchase
|
||||
html = `
|
||||
<div class="image-container">
|
||||
<img src="/files/image/icon/shop/${itemId}.jpg" alt="Item Image" style="width: 180px; height: 180px;" />
|
||||
</div>
|
||||
<p>Would you like to purchase this song?</p>
|
||||
<div>
|
||||
<p>${payload.data.property_first} - ${payload.data.property_second}</p>
|
||||
<p>Difficulty Levels: ${payload.data.property_third}</p>
|
||||
</div>
|
||||
<div>
|
||||
<img src="/files/web/coin_icon.png" class="coin-icon" style="width: 40px; height: 40px;" alt="Coin Icon" />
|
||||
<span style="color: #FFFFFF; font-size: 44px; font-family: Hiragino Kaku Gothic ProN, sans-serif;">${payload.data.price}</span>
|
||||
</div>
|
||||
`;
|
||||
} else if (mode === 1) {
|
||||
// Avatar purchase
|
||||
html = `
|
||||
<div class="image-container">
|
||||
<img src="/files/image/icon/avatar/${itemId}.png" alt="Item Image" style="width: 180px; height: 180px; background-color: black; object-fit: contain;" />
|
||||
</div>
|
||||
<p>Would you like to purchase this avatar?</p>
|
||||
<div>
|
||||
<p>${payload.data.property_first}</p>
|
||||
<p>Effect: ${payload.data.property_second}</p>
|
||||
</div>
|
||||
<div>
|
||||
<img src="/files/web/coin_icon.png" class="coin-icon" style="width: 40px; height: 40px;" alt="Coin Icon" />
|
||||
<span>${payload.data.price}</span>
|
||||
</div>
|
||||
`;
|
||||
} else if (mode === 2) {
|
||||
// Item purchase
|
||||
html = `
|
||||
<div class="image-container">
|
||||
<img src="/files/image/icon/item/${itemId}.png" alt="Item Image" style="width: 180px; height: 180px;" />
|
||||
</div>
|
||||
<p>Would you like to purchase this item?</p>
|
||||
<div>
|
||||
<p>${payload.data.property_first}</p>
|
||||
<p>Effect: ${payload.data.property_second}</p>
|
||||
</div>
|
||||
<div>
|
||||
<img src="/files/web/coin_icon.png" class="coin-icon" style="width: 40px; height: 40px;" alt="Coin Icon" />
|
||||
<span>${payload.data.price}</span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// Add purchase and back buttons
|
||||
html += `
|
||||
<div style="margin-top: 20px;">
|
||||
<a onclick="purchaseItem(${mode}, ${itemId})" class="bt_bg01">Buy</a><br>
|
||||
<a class="bt_bg01" onclick="loadUI();">Go Back</a>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.getElementById('content').innerHTML = html;
|
||||
} else {
|
||||
document.getElementById('content').innerHTML = `
|
||||
<p>Error: ${payload.message}</p>
|
||||
<div style="margin-top: 20px;">
|
||||
<a class="bt_bg01" onclick="loadUI();">Go Back</a>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
if (error.name === 'AbortError') {
|
||||
console.log('Fetch aborted');
|
||||
} else {
|
||||
console.error('Error fetching item details:', error);
|
||||
document.getElementById('content').innerHTML = '<p>Failed to load item details. Please try again later.</p>';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function purchaseItem(mode, itemId) {
|
||||
restoreBaseStructure();
|
||||
const contentElement = document.getElementById('content');
|
||||
document.getElementById('ttl').src = "/files/web/ttl_buy.png";
|
||||
document.getElementById('ttl').alt = "BUY";
|
||||
contentElement.innerHTML = `
|
||||
<p>Processing your purchase...</p>
|
||||
`;
|
||||
|
||||
const postData = {
|
||||
mode: mode,
|
||||
item_id: itemId
|
||||
};
|
||||
|
||||
fetch(HOST_URL + 'api/shop/purchase_item?' + PAYLOAD, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(postData)
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(payload => {
|
||||
if (payload.state === 1) {
|
||||
contentElement.innerHTML = `
|
||||
<p>${payload.message}</p>
|
||||
<button style="margin-top: 20px;" class="bt_bg01"
|
||||
onclick="on_initialize();">
|
||||
Back
|
||||
</button>
|
||||
`;
|
||||
document.getElementById('coinCounter').innerText = payload.data.coin;
|
||||
} else {
|
||||
contentElement.innerHTML = `
|
||||
<p>Purchase failed: ${payload.message}</p>
|
||||
<button style="margin-top: 20px;" class="bt_bg01"
|
||||
onclick="on_initialize();">
|
||||
Back
|
||||
</button>
|
||||
`;
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error processing purchase:', error);
|
||||
contentElement.innerHTML = `
|
||||
<p>Failed to process your purchase. Please try again later.</p>
|
||||
<button style="margin-top: 20px;" class="bt_bg01"
|
||||
onclick="on_initialize();">
|
||||
Back
|
||||
</button>
|
||||
`;
|
||||
});
|
||||
}
|
||||
|
||||
function restoreBaseStructure() {
|
||||
document.body.className = "";
|
||||
document.body.style.backgroundColor = "black";
|
||||
document.body.style.backgroundImage = "";
|
||||
document.body.innerHTML = `
|
||||
<body>
|
||||
<div id="header">
|
||||
<div style="position: fixed; top: 20px; left: 10px; display: flex; align-items: center; ">
|
||||
<img src="/files/web/coin_icon.png" alt="Coins" style="width: 40px; height: 40px; margin-right: 10px;">
|
||||
<span style="color: #FFFFFF; font-size: 24px; font-family: Hiragino Kaku Gothic ProN, sans-serif;" id="coinCounter">Loading</span>
|
||||
</div>
|
||||
<img class="ttl_height" id="ttl" src="/files/web/ttl_shop.png" alt="SHOP" />
|
||||
</div>
|
||||
<div id="wrapper">
|
||||
<div class="wrapper_box">
|
||||
<div class="a_left w100 d_ib mb10p">
|
||||
<ul style="list-style-type: none; padding: 0; margin-top: 100px; text-align: center;" id="menuList">
|
||||
</ul>
|
||||
<div class="f90 a_center pt50" id="content">
|
||||
Loading...
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
`
|
||||
}
|
||||
|
||||
function showFMax() {
|
||||
let htmlText = "Loading...";
|
||||
|
||||
document.body.className = "dlc_body";
|
||||
document.body.style.backgroundColor = "black";
|
||||
document.body.innerHTML = `
|
||||
<div class="dlc_container">
|
||||
<img src="/files/web/gc4ex-logo.png" alt="GC4EX Logo" class="dlc_logo"
|
||||
onload="document.body.style.backgroundImage = 'url(/files/web/gc4ex_bg.jpg)';"
|
||||
onerror="this.style.display='none';" />
|
||||
<p style="color: white; font-size: 20px; text-align: center;">${htmlText}</p>
|
||||
<button id="backButton" class="quit-button">
|
||||
Go Back
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const controller = new AbortController();
|
||||
const signal = controller.signal;
|
||||
|
||||
document.getElementById('backButton').onclick = () => {
|
||||
controller.abort();
|
||||
restoreBaseStructure();
|
||||
loadUI();
|
||||
};
|
||||
|
||||
const postData = {
|
||||
mode: 3,
|
||||
item_id: 1
|
||||
};
|
||||
|
||||
fetch(HOST_URL + 'api/shop/item_data?' + PAYLOAD, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(postData),
|
||||
signal: signal // Pass the abort signal
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(payload => {
|
||||
if (payload.state == 1) {
|
||||
let html = '';
|
||||
if (fmaxPurchased) {
|
||||
html = `
|
||||
<div class="text-content">
|
||||
<p>You have unlocked the GC4MAX expansion!</p>
|
||||
<p>Please report bugs/missing tracks to Discord: #AnTcfgss, or QQ 3421587952.</p>
|
||||
<button class="quit-button" onclick="restoreBaseStructure(); loadUI();">
|
||||
Go Back
|
||||
</button><br><br>
|
||||
<strong>This server has version ${payload.data.property_first}.</strong>
|
||||
<p>Update log: </p>
|
||||
<p>${payload.data.property_second}</p><br>
|
||||
</div>
|
||||
`;
|
||||
} else {
|
||||
html = `
|
||||
<div class="text-content">
|
||||
<p>Experience the arcade with the GC4MAX expansion! This DLC unlocks 320+ exclusive songs for your 2OS experience.</p>
|
||||
<p>Note that these songs don't have mobile difficulties. A short placeholder is used, and GCoin reward is not available for playing them. You must clear the Normal difficulty to unlock AC content.</p>
|
||||
<p>Due to technical limitations, Extra level charts cannot be ported as of now. After purchasing, you will have access to support information and update logs.</p>
|
||||
</div>
|
||||
<button class="buy-button" onclick="purchaseItem(3, 1);">
|
||||
Buy
|
||||
<div class="coin-container">
|
||||
<img src="/files/web/coin_icon.png" alt="Coin Icon" class="coin-icon">
|
||||
<span style="font-size: 22px; font-weight: bold;">${payload.data.price}</span>
|
||||
</div>
|
||||
</button>
|
||||
<br><br>
|
||||
<button class="quit-button" onclick="restoreBaseStructure(); loadUI();">
|
||||
Go Back
|
||||
</button>
|
||||
`;
|
||||
}
|
||||
|
||||
// Update the body content with the new HTML
|
||||
document.body.innerHTML = `
|
||||
<div class="dlc_container">
|
||||
<img src="/files/web/gc4ex-logo.png" alt="GC4EX Logo" class="dlc_logo">
|
||||
${html}
|
||||
</div>
|
||||
`;
|
||||
} else {
|
||||
document.body.innerHTML = `
|
||||
<div class="dlc_container">
|
||||
<img src="/files/web/gc4ex-logo.png" alt="GC4EX Logo" class="dlc_logo">
|
||||
<p style="color: white; font-size: 20px; text-align: center;">Error: ${payload.message}</p>
|
||||
<button class="quit-button" onclick="restoreBaseStructure(); loadUI();">
|
||||
Go Back
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
document.body.innerHTML = `
|
||||
<div class="dlc_container">
|
||||
<img src="/files/web/gc4ex-logo.png" alt="GC4EX Logo" class="dlc_logo">
|
||||
<p style="color: white; font-size: 20px; text-align: center;">Failed to load item details. Please try again later.</p>
|
||||
<button class="quit-button" onclick="restoreBaseStructure(); loadUI();">
|
||||
Go Back
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
}
|
||||
|
||||
function showExtra() {
|
||||
let htmlText = "Loading...";
|
||||
|
||||
document.body.className = "dlc_body";
|
||||
document.body.style.backgroundColor = "black";
|
||||
document.body.style.backgroundImage = 'url(/files/web/extra_bg.jpg)';
|
||||
document.body.innerHTML = `
|
||||
<div class="dlc_container_extra">
|
||||
${htmlText}
|
||||
<button id="backButton" class="quit-button-extra">
|
||||
Go Back
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const controller = new AbortController();
|
||||
const signal = controller.signal;
|
||||
|
||||
document.getElementById('backButton').onclick = () => {
|
||||
controller.abort();
|
||||
restoreBaseStructure();
|
||||
loadUI();
|
||||
};
|
||||
|
||||
const postData = {
|
||||
mode: 4,
|
||||
item_id: 1
|
||||
};
|
||||
|
||||
fetch(HOST_URL + 'api/shop/item_data?' + PAYLOAD, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(postData),
|
||||
signal: signal
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(payload => {
|
||||
if (payload.state === 1) {
|
||||
let html = '';
|
||||
if (extraPurchased) {
|
||||
html = `
|
||||
<br><br><br><br><br><br><br>
|
||||
<div class="text-content">
|
||||
<p>You have unlocked the EXTRA Challenge!</p>
|
||||
<p>Please report bugs/missing tracks to Discord: #AnTcfgss, or QQ 3421587952.</p>
|
||||
<button class="quit-button-extra" onclick="restoreBaseStructure(); loadUI();">
|
||||
Go Back
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
} else {
|
||||
html = `
|
||||
<br><br><br><br><br><br><br>
|
||||
<div class="text-content">
|
||||
<p>Are you looking for a bad time?</p>
|
||||
<p>If so, this is the Ultimate - Extra - Challenge.</p>
|
||||
<p>180+ Arcade Extra difficulty charts await you.</p>
|
||||
<p>You have been warned.</p>
|
||||
</div>
|
||||
|
||||
<button class="buy-button-extra" onclick="purchaseItem(4, 1);">
|
||||
Buy
|
||||
<div class="coin-container">
|
||||
<img src="/files/web/coin_icon.png" alt="Coin Icon" class="coin-icon">
|
||||
<span style="font-size: 22px; font-weight: bold;">${payload.data.price}</span>
|
||||
</div>
|
||||
</button>
|
||||
<br><br>
|
||||
<button class="quit-button-extra" onclick="restoreBaseStructure(); loadUI();">
|
||||
Go Back
|
||||
</button>
|
||||
`;
|
||||
}
|
||||
|
||||
document.body.innerHTML = html;
|
||||
} else {
|
||||
document.body.innerHTML = `
|
||||
<div class="dlc_container">
|
||||
<p style="color: white; font-size: 20px; text-align: center;">Error: ${payload.message}</p>
|
||||
<button class="quit-button-extra" onclick="restoreBaseStructure(); loadUI();">
|
||||
Go Back
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
document.body.innerHTML = `
|
||||
<div class="dlc_container">
|
||||
<p style="color: white; font-size: 20px; text-align: center;">Failed to load item details. Please try again later.</p>
|
||||
<button class="quit-button-extra" onclick="restoreBaseStructure(); loadUI();">
|
||||
Go Back
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
}
|
||||
|
||||
window.onload = function(){
|
||||
restoreBaseStructure();
|
||||
on_initialize();
|
||||
};
|
||||
599
new_server_7003/web/admin.html
Normal file
599
new_server_7003/web/admin.html
Normal file
@@ -0,0 +1,599 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>GC2OS Admin</title>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.css">
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link href="https://unpkg.com/tabulator-tables@5.5.2/dist/css/tabulator.min.css" rel="stylesheet">
|
||||
<script src="https://unpkg.com/tabulator-tables@5.5.2/dist/js/tabulator.min.js"></script>
|
||||
<style>
|
||||
body {
|
||||
background: #343a40;
|
||||
background-size: cover;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
min-height: 100vh;
|
||||
}
|
||||
.container {
|
||||
max-width: 1700px;
|
||||
}
|
||||
#tabulatorTable {
|
||||
width: 100%;
|
||||
overflow-x: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
touch-action: pan-x pan-y;
|
||||
}
|
||||
.tabulator .tabulator-header .tabulator-col {
|
||||
font-size: clamp(0.4rem, 2vw, 0.7rem);
|
||||
}
|
||||
#changeModal .modal-dialog {
|
||||
max-height: 80vh;
|
||||
margin-top: 5vh;
|
||||
margin-bottom: 5vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
#changeModal .modal-content {
|
||||
max-height: 80vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
#changeModal .modal-body {
|
||||
overflow-y: auto;
|
||||
max-height: 55vh;
|
||||
}
|
||||
|
||||
|
||||
#insertModal .modal-dialog {
|
||||
max-height: 80vh;
|
||||
margin-top: 5vh;
|
||||
margin-bottom: 5vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
#insertModal .modal-content {
|
||||
max-height: 80vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
#insertModal .modal-body {
|
||||
overflow-y: auto;
|
||||
max-height: 55vh;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<nav class="navbar navbar-expand-lg navbar-dark bg-dark mb-4">
|
||||
<div class="container-fluid">
|
||||
<span class="navbar-brand mb-0 h4 text-white">GC2OS Management</span>
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#mainNavbar" aria-controls="mainNavbar" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="mainNavbar">
|
||||
<ul class="navbar-nav ms-4">
|
||||
<li class="nav-item"><a class="nav-link" href="#" id="usersTab">Users</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="#" id="resultsTab">Results</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="#" id="dailyRewardsTab">DailyRewards</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="#" id="whitelistTab">Whitelist</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="#" id="blacklistTab">Blacklist</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="#" id="batchTokensTab">BatchTokens</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="#" id="bindTab">Bind</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="#" id="logsTab">Logs</a></li>
|
||||
</ul>
|
||||
<div class="d-flex ms-auto">
|
||||
<button class="btn btn-primary me-2" id="searchBtn">
|
||||
<i class="bi bi-search"></i>
|
||||
</button>
|
||||
<button class="btn btn-success me-2" id="addRowBtn">
|
||||
<i class="bi bi-plus-lg"></i>
|
||||
</button>
|
||||
<button class="btn btn-outline-danger" id="logoutBtn">
|
||||
<i class="bi bi-box-arrow-right"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="container">
|
||||
<div id="tabulatorTable" class="mt-4"></div>
|
||||
</div>
|
||||
|
||||
<!-- Change Modal -->
|
||||
<div class="modal fade" id="changeModal" tabindex="-1" aria-labelledby="changeModalLabel" aria-hidden="true" data-bs-backdrop="static" data-bs-keyboard="false">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="changeModalLabel"></h5>
|
||||
</div>
|
||||
<div class="modal-body" id="changeModalBody"></div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">OK</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Insert Modal -->
|
||||
<div class="modal fade" id="insertModal" tabindex="-1" aria-labelledby="insertModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="insertModalLabel">Insert New Row</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<form id="insertRowForm">
|
||||
<div class="modal-body" id="insertModalBody">
|
||||
<!-- Dynamic form fields will be inserted here -->
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="submit" class="btn btn-success">Insert</button>
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Save data Modal -->
|
||||
<div class="modal fade" id="dataModal" tabindex="-1" aria-labelledby="dataModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="dataModalLabel">Edit Save Data</h5>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<textarea id="dataModalTextarea" class="form-control" style="height: 300px; width: 100%;"></textarea>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-success" id="dataModalSaveBtn">Save</button>
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Search Modal -->
|
||||
<div class="modal fade" id="searchModal" tabindex="-1" aria-labelledby="searchModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="searchModalLabel">Search</h5>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<input type="text" id="searchInput" class="form-control" placeholder="Enter search term">
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-primary" id="searchModalBtn">Search</button>
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
window.currentTableName = null;
|
||||
let dataUserId = -1;
|
||||
let searchTerm = "";
|
||||
function showTable(tabName) {
|
||||
window.currentTableName = tabName;
|
||||
if (window.currentTable) {
|
||||
window.currentTable.destroy();
|
||||
}
|
||||
setCookie("lastTab", tabName, {path: '/'});
|
||||
|
||||
window.currentTable = new Tabulator("#tabulatorTable", {
|
||||
ajaxURL: "/admin/table",
|
||||
ajaxConfig: "GET",
|
||||
pagination: true,
|
||||
paginationMode:"remote",
|
||||
paginationSize: 25,
|
||||
paginationSizeSelector: [25, 50, 100],
|
||||
layout: "fitColumns",
|
||||
height: window.innerHeight * 0.9 + "px",
|
||||
autoColumns: true,
|
||||
responsiveLayout: "collapse",
|
||||
sortMode: "remote",
|
||||
autoColumnsDefinitions: function(definitions){
|
||||
if (tabName === "users") {
|
||||
definitions.push({
|
||||
title: "data",
|
||||
field: "__data",
|
||||
hozAlign: "center",
|
||||
formatter: function(cell, formatterParams, onRendered){
|
||||
return `<button class="btn btn-sm btn-info">View</button>`;
|
||||
},
|
||||
cellClick: async function(e, cell){
|
||||
const rowData = cell.getRow().getData();
|
||||
const userId = rowData.id;
|
||||
dataUserId = userId;
|
||||
const resp = await fetch(`/admin/data?id=${userId}`);
|
||||
const result = await resp.json();
|
||||
document.getElementById('dataModalTextarea').value = typeof result.data === "object"
|
||||
? JSON.stringify(result.data, null, 2)
|
||||
: result.data;
|
||||
const modal = bootstrap.Modal.getOrCreateInstance(document.getElementById('dataModal'));
|
||||
modal.show();
|
||||
}
|
||||
});
|
||||
}
|
||||
definitions.push({
|
||||
title: "Delete",
|
||||
field: "__delete",
|
||||
hozAlign: "center",
|
||||
formatter: function(cell, formatterParams, onRendered){
|
||||
return `<button class="btn btn-sm btn-danger">Delete</button>`;
|
||||
},
|
||||
cellClick: function(e, cell){
|
||||
const rowData = cell.getRow().getData();
|
||||
const idValue = rowData.id !== undefined ? rowData.id : rowData.objectId;
|
||||
const tableName = window.currentTableName;
|
||||
|
||||
document.getElementById('changeModalLabel').innerText = "Confirm Delete";
|
||||
document.getElementById('changeModalBody').innerHTML =
|
||||
`<strong>ID:</strong> ${idValue}<br>
|
||||
<strong>Table:</strong> ${tableName}<br>
|
||||
<p class="text-danger">Are you sure you want to delete this row?</p>`;
|
||||
|
||||
const modalFooter = document.querySelector("#changeModal .modal-footer");
|
||||
modalFooter.innerHTML = "";
|
||||
const confirmBtn = document.createElement('button');
|
||||
confirmBtn.className = "btn btn-danger";
|
||||
confirmBtn.textContent = "Delete";
|
||||
confirmBtn.onclick = async function() {
|
||||
modal.hide();
|
||||
const response = await fetch("/admin/table/delete", {
|
||||
method: "POST",
|
||||
headers: {"Content-Type": "application/json"},
|
||||
body: JSON.stringify({
|
||||
table: tableName,
|
||||
id: rowData.id !== undefined ? rowData.id : rowData.objectId
|
||||
})
|
||||
});
|
||||
const result = await response.json();
|
||||
if (result.status === "failed") {
|
||||
alert(result.message || "Delete failed.");
|
||||
} else {
|
||||
showTable(tableName);
|
||||
}
|
||||
};
|
||||
modalFooter.appendChild(confirmBtn);
|
||||
const cancelBtn = document.createElement('button');
|
||||
cancelBtn.className = "btn btn-secondary ms-2";
|
||||
cancelBtn.textContent = "Cancel";
|
||||
cancelBtn.setAttribute("data-bs-dismiss", "modal");
|
||||
modalFooter.appendChild(cancelBtn);
|
||||
|
||||
const modal = bootstrap.Modal.getOrCreateInstance(document.getElementById('changeModal'));
|
||||
modal.show();
|
||||
}
|
||||
});
|
||||
definitions.forEach(function(col){
|
||||
col.formatter = function(cell){
|
||||
const value = cell.getValue();
|
||||
if (typeof value === "object" && value !== null) {
|
||||
let str = JSON.stringify(value);
|
||||
if (str.length > 60) {
|
||||
str = str.substring(0, 57) + "...";
|
||||
}
|
||||
return `<span title="${JSON.stringify(value, null, 2)}">${str}</span>`;
|
||||
}
|
||||
return value;
|
||||
};
|
||||
if (col.field !== "id" && col.field !== "objectId" && col.field !== "__delete" && col.field !== "__data") {
|
||||
col.editor = function(cell, onRendered, success, cancel){
|
||||
const value = cell.getValue();
|
||||
let input;
|
||||
if ((typeof value === "object" && value !== null) ||
|
||||
(typeof value === "string" && value.includes('\n'))) {
|
||||
input = document.createElement("textarea");
|
||||
input.style.width = "100%";
|
||||
input.style.height = "8em";
|
||||
input.value = (typeof value === "object" && value !== null) ? JSON.stringify(value, null, 2) : value;
|
||||
} else {
|
||||
input = document.createElement("input");
|
||||
input.type = "text";
|
||||
input.style.width = "100%";
|
||||
input.value = value;
|
||||
}
|
||||
|
||||
onRendered(function(){
|
||||
input.focus();
|
||||
input.select();
|
||||
});
|
||||
|
||||
function parseValue(original, edited) {
|
||||
if (typeof original === "boolean") {
|
||||
return (edited === "true");
|
||||
}
|
||||
else if (typeof original === "number" && Number.isInteger(original)) {
|
||||
let parsed = parseInt(edited, 10);
|
||||
return isNaN(parsed) ? original : parsed;
|
||||
}
|
||||
else if (typeof original === "number") {
|
||||
let parsed = parseFloat(edited);
|
||||
return isNaN(parsed) ? original : parsed;
|
||||
}
|
||||
else if (typeof original === "object" && original !== null) {
|
||||
try {
|
||||
return JSON.parse(edited);
|
||||
} catch {
|
||||
alert("Invalid JSON format. Please correct your input.");
|
||||
return "__CANCEL__";
|
||||
}
|
||||
}
|
||||
return edited;
|
||||
}
|
||||
|
||||
function handleEdit() {
|
||||
let original = cell.getValue();
|
||||
let edited = input.value;
|
||||
let parsed = parseValue(original, edited);
|
||||
|
||||
if (parsed === "__CANCEL__") {
|
||||
cancel();
|
||||
return;
|
||||
}
|
||||
|
||||
let changed = false;
|
||||
if (typeof original === "object" && original !== null) {
|
||||
changed = JSON.stringify(original) !== JSON.stringify(parsed);
|
||||
} else {
|
||||
changed = original !== parsed;
|
||||
}
|
||||
|
||||
if (changed) {
|
||||
success(parsed);
|
||||
} else {
|
||||
cancel();
|
||||
}
|
||||
}
|
||||
|
||||
input.addEventListener("keydown", function(e){
|
||||
if(e.keyCode == 13 && !(input instanceof HTMLTextAreaElement)){
|
||||
handleEdit();
|
||||
} else if(e.keyCode == 27){
|
||||
cancel();
|
||||
}
|
||||
});
|
||||
input.addEventListener("blur", handleEdit);
|
||||
|
||||
return input;
|
||||
};
|
||||
}
|
||||
});
|
||||
return definitions;
|
||||
},
|
||||
ajaxURLGenerator: function(url, config, params) {
|
||||
let query = [];
|
||||
query.push(`table=${tabName.toLowerCase()}`);
|
||||
if (params.page) query.push(`page=${params.page}`);
|
||||
if (params.size) query.push(`size=${params.size}`);
|
||||
if (params.sort && params.sort.length) {
|
||||
query.push(`sort=${params.sort[0].field}`);
|
||||
query.push(`dir=${params.sort[0].dir}`);
|
||||
}
|
||||
if (params.filters && params.filters.length) {
|
||||
query.push(`filter=${params.filters.map(f => `${f.field}:${f.value}`).join(",")}`);
|
||||
}
|
||||
if (searchTerm) {
|
||||
query.push(`search=${encodeURIComponent(searchTerm)}`);
|
||||
}
|
||||
return url + "?" + query.join("&");
|
||||
},
|
||||
ajaxResponse: function(url, params, response) {
|
||||
return response;
|
||||
},
|
||||
paginationDataReceived: {
|
||||
last_page: "last_page",
|
||||
total: "total"
|
||||
}
|
||||
});
|
||||
|
||||
window.currentTable.on('cellEdited', (cell) => {
|
||||
const updatedRow = cell.getRow().getData();
|
||||
const field = cell.getField();
|
||||
const newValue = cell.getValue();
|
||||
const idValue = updatedRow.id !== undefined ? updatedRow.id : updatedRow.objectId;
|
||||
|
||||
window.pendingUpdateRow = updatedRow;
|
||||
window.pendingUpdateTable = cell.getTable().options.ajaxParams.table;
|
||||
window.pendingUpdateCell = cell;
|
||||
window.pendingOldValue = cell.getOldValue();
|
||||
|
||||
const oldType = typeof window.pendingOldValue;
|
||||
const newType = typeof newValue;
|
||||
|
||||
document.getElementById('changeModalLabel').innerText = "Confirm Update";
|
||||
document.getElementById('changeModalBody').innerHTML =
|
||||
`<strong>ID:</strong> ${idValue}<br>
|
||||
<strong>Field:</strong> ${field}<br>
|
||||
<strong>Old Value:</strong> <pre>${typeof window.pendingOldValue === "object" ? JSON.stringify(window.pendingOldValue, null, 2) : window.pendingOldValue}</pre>
|
||||
<strong>Old Type:</strong> ${oldType}<br>
|
||||
<strong>New Value:</strong> <pre>${typeof newValue === "object" ? JSON.stringify(newValue, null, 2) : newValue}</pre>
|
||||
<strong>New Type:</strong> ${newType}<br>
|
||||
<p>Do you want to update this cell?</p>`;
|
||||
|
||||
const modalFooter = document.querySelector("#changeModal .modal-footer");
|
||||
modalFooter.innerHTML = "";
|
||||
const confirmBtn = document.createElement('button');
|
||||
confirmBtn.className = "btn btn-primary";
|
||||
confirmBtn.textContent = "Confirm";
|
||||
confirmBtn.onclick = async function() {
|
||||
modal.hide();
|
||||
const response = await fetch("/admin/table/update", {
|
||||
method: "POST",
|
||||
headers: {"Content-Type": "application/json"},
|
||||
body: JSON.stringify({
|
||||
table: window.currentTableName,
|
||||
row: window.pendingUpdateRow
|
||||
})
|
||||
});
|
||||
const result = await response.json();
|
||||
if (result.status === "failed") {
|
||||
alert(result.message || "Update failed.");
|
||||
} else {
|
||||
showTable(window.currentTableName);
|
||||
}
|
||||
};
|
||||
modalFooter.appendChild(confirmBtn);
|
||||
const cancelBtn = document.createElement('button');
|
||||
cancelBtn.className = "btn btn-secondary ms-2";
|
||||
cancelBtn.textContent = "Cancel";
|
||||
cancelBtn.setAttribute("data-bs-dismiss", "modal");
|
||||
cancelBtn.onclick = function() {
|
||||
window.pendingUpdateCell.setValue(window.pendingOldValue, true);
|
||||
};
|
||||
modalFooter.appendChild(cancelBtn);
|
||||
|
||||
const modal = bootstrap.Modal.getOrCreateInstance(document.getElementById('changeModal'));
|
||||
modal.show();
|
||||
});
|
||||
}
|
||||
|
||||
document.getElementById('dataModalSaveBtn').onclick = async function() {
|
||||
const dataContent = document.getElementById('dataModalTextarea').value;
|
||||
|
||||
const resp = await fetch("/admin/data/save", {
|
||||
method: "POST",
|
||||
headers: {"Content-Type": "application/json"},
|
||||
body: JSON.stringify({
|
||||
id: dataUserId,
|
||||
data: dataContent
|
||||
})
|
||||
});
|
||||
const result = await resp.json();
|
||||
if (result.status === "failed") {
|
||||
alert(result.message || "Save failed.");
|
||||
} else {
|
||||
const modal = bootstrap.Modal.getOrCreateInstance(document.getElementById('dataModal'));
|
||||
modal.hide();
|
||||
showTable("users");
|
||||
}
|
||||
};
|
||||
|
||||
document.getElementById('usersTab').onclick = () => showTable("users");
|
||||
document.getElementById('resultsTab').onclick = () => showTable("results");
|
||||
document.getElementById('dailyRewardsTab').onclick = () => showTable("daily_rewards");
|
||||
document.getElementById('whitelistTab').onclick = () => showTable("whitelist");
|
||||
document.getElementById('blacklistTab').onclick = () => showTable("blacklist");
|
||||
document.getElementById('batchTokensTab').onclick = () => showTable("batch_tokens");
|
||||
document.getElementById('bindTab').onclick = () => showTable("bind");
|
||||
document.getElementById('logsTab').onclick = () => showTable("logs");
|
||||
|
||||
document.getElementById('logoutBtn').addEventListener('click', function() {
|
||||
document.cookie = "token=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;";
|
||||
window.location.href = "/Login";
|
||||
});
|
||||
|
||||
document.getElementById('searchBtn').onclick = function() {
|
||||
const modal = bootstrap.Modal.getOrCreateInstance(document.getElementById('searchModal'));
|
||||
document.getElementById('searchInput').value = searchTerm;
|
||||
modal.show();
|
||||
};
|
||||
|
||||
document.getElementById('searchModalBtn').onclick = function() {
|
||||
searchTerm = document.getElementById('searchInput').value.trim();
|
||||
const modal = bootstrap.Modal.getOrCreateInstance(document.getElementById('searchModal'));
|
||||
modal.hide();
|
||||
showTable(window.currentTableName);
|
||||
|
||||
// Highlight searchBtn if search is active
|
||||
const searchBtn = document.getElementById('searchBtn');
|
||||
if (searchTerm) {
|
||||
searchBtn.style.backgroundColor = "yellow";
|
||||
searchBtn.style.color = "black";
|
||||
} else {
|
||||
searchBtn.style.backgroundColor = "";
|
||||
searchBtn.style.color = "";
|
||||
}
|
||||
};
|
||||
|
||||
document.getElementById('addRowBtn').onclick = async function() {
|
||||
const tableName = window.currentTableName;
|
||||
const schemaResp = await fetch(`/admin/table?table=${tableName}&schema=1`);
|
||||
const schema = await schemaResp.json();
|
||||
|
||||
const modalBody = document.getElementById('insertModalBody');
|
||||
modalBody.innerHTML = "";
|
||||
Object.entries(schema).forEach(([field, type]) => {
|
||||
if (field === "id" || field === "objectId") return;
|
||||
let inputType = "text";
|
||||
if (type.startsWith("INTEGER")) inputType = "number";
|
||||
if (type.startsWith("BOOLEAN")) inputType = "checkbox";
|
||||
modalBody.innerHTML += `
|
||||
<div class="mb-3">
|
||||
<label class="form-label">${field} (${type})</label>
|
||||
${inputType === "checkbox"
|
||||
? `<input type="checkbox" class="form-check-input" name="${field}">`
|
||||
: `<input type="${inputType}" class="form-control" name="${field}">`
|
||||
}
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
const modal = bootstrap.Modal.getOrCreateInstance(document.getElementById('insertModal'));
|
||||
modal.show();
|
||||
};
|
||||
|
||||
document.getElementById('insertRowForm').onsubmit = async function(e) {
|
||||
e.preventDefault();
|
||||
const tableName = window.currentTableName;
|
||||
const form = e.target;
|
||||
const data = {};
|
||||
Array.from(form.elements).forEach(el => {
|
||||
if (!el.name) return;
|
||||
if (el.type === "checkbox") {
|
||||
data[el.name] = el.checked;
|
||||
} else if (el.type === "number") {
|
||||
data[el.name] = el.value ? parseInt(el.value, 10) : null;
|
||||
} else {
|
||||
data[el.name] = el.value;
|
||||
}
|
||||
});
|
||||
|
||||
const response = await fetch("/admin/table/insert", {
|
||||
method: "POST",
|
||||
headers: {"Content-Type": "application/json"},
|
||||
body: JSON.stringify({
|
||||
table: tableName,
|
||||
row: data
|
||||
})
|
||||
});
|
||||
const result = await response.json();
|
||||
if (result.status === "failed") {
|
||||
alert(result.message || "Insert failed.");
|
||||
} else {
|
||||
const modal = bootstrap.Modal.getOrCreateInstance(document.getElementById('insertModal'));
|
||||
modal.hide();
|
||||
showTable(tableName);
|
||||
}
|
||||
};
|
||||
|
||||
const validTabs = ["users", "results", "daily_rewards", "whitelist", "blacklist", "batch_tokens", "bind", "logs"];
|
||||
const lastTab = getCookie("lastTab");
|
||||
if (validTabs.includes(lastTab)) {
|
||||
showTable(lastTab);
|
||||
} else {
|
||||
showTable("users");
|
||||
}
|
||||
|
||||
function getCookie(name) {
|
||||
const value = `; ${document.cookie}`;
|
||||
const parts = value.split(`; ${name}=`);
|
||||
if (parts.length === 2) return parts.pop().split(';').shift();
|
||||
return null;
|
||||
}
|
||||
|
||||
function setCookie(name, value, days) {
|
||||
let expires = "";
|
||||
if (days) {
|
||||
const date = new Date();
|
||||
date.setTime(date.getTime() + (days*24*60*60*1000));
|
||||
expires = "; expires=" + date.toUTCString();
|
||||
}
|
||||
document.cookie = name + "=" + (value || "") + expires + "; path=/";
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
142
new_server_7003/web/history.html
Normal file
142
new_server_7003/web/history.html
Normal file
@@ -0,0 +1,142 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||
<meta content="user-scalable=0" name="viewport" />
|
||||
<link rel="stylesheet" href="/files/web/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="header">
|
||||
<img class="ttl_height" src="./files/web//ttl_information.png" alt="INFORMATION" />
|
||||
</div>
|
||||
|
||||
<div id="wrapper">
|
||||
<div class="wrapper_box">
|
||||
<div class="a_left w100 d_ib mb10p">
|
||||
<div class="f90 a_center pt50">This is a Private Server</div>
|
||||
<hr />
|
||||
<div class="f70 plr25">Your text here</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="wrapper">
|
||||
<div class="wrapper_box">
|
||||
<div class="a_left w100 d_ib mb10p">
|
||||
<div class="f90 a_center pt50">About Coins</div>
|
||||
<hr />
|
||||
<div class="f70 plr25">Coins are an in-game currency used within GROOVE COASTER ZERO.
|
||||
They may be used to purchase special tracks, Avatars, and Items unavailable through regular game play.
|
||||
Spent Coins will not be refunded under any circumstances.
|
||||
Please note that Coin-related records are stored online, so a network connection is required for Coin use.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="wrapper">
|
||||
<div class="wrapper_box">
|
||||
<div class="a_left w100 d_ib mb10p">
|
||||
<div class="f90 a_center pt50">Regarding Data Backup</div>
|
||||
<hr />
|
||||
<div class="f70 plr25">Please note that while the Backup/TAITO ID Registration option may be used to save play data.
|
||||
Item stock information and replays are not saved.
|
||||
Contrary to the official server, you can log out of the TAITO ID, change username, and password.
|
||||
However, each account can only be linked to one device at a time.
|
||||
When logging on to another device, the previous device will be automatically removed.
|
||||
Thus, please refrain from sharing credentials, and keep your password strong, as the username is on leaderboard display.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="wrapper">
|
||||
<div class="wrapper_box">
|
||||
<div class="a_left w100 d_ib mb10p">
|
||||
<div class="f90 a_center pt50">Regarding Online Use</div>
|
||||
<hr />
|
||||
<div class="f70 plr25">Once the application's tutorial is complete the game may be played offline (without a network connection).
|
||||
However, please note that a network connection is required for certain operations, including track data downloads, SHOP use, save backup/restore, etc.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="wrapper">
|
||||
<div class="wrapper_box">
|
||||
<div class="a_left w100 d_ib mb10p">
|
||||
<div class="f90 a_center pt50">About HELP and OPTIONS</div>
|
||||
<hr />
|
||||
<div class="f70 plr25">Tapping on the question mark (?) in the top left of the screen will bring up the application's help and settings.</div>
|
||||
<br />
|
||||
<div class="f70 plr25">There are many different settings under Options in the Help menu for you to customize to your liking.</div>
|
||||
<br />
|
||||
<div class="f70 plr25">- SCREEN SETTINGS</div>
|
||||
<div class="f60 plr50">This menu is where you may adjust the visuals if you feel like the frame rate is unstable, or you want to disable AD-LIBS or Hit SFX when there's no TARGET to hit.</div>
|
||||
<br />
|
||||
<div class="f70 plr25">- SOUND AND VISUAL TIMING ADJUSTMENT</div>
|
||||
<div class="f60 plr50">Adjust the settings here when you feel like the visuals or audio is not in sync.</div>
|
||||
<div class="f60 plr50">If you feel the visuals are ahead of the sound press the - button, if they're behind then use the + button. These buttons will adjust the visual display timing with the audio.</div>
|
||||
<br />
|
||||
<div class="f70 plr25">- INPUT REGISTRY ADJUSTMENT</div>
|
||||
<div class="f60 plr50">Adjust these settings if even though you feel like you're right in time with the beat, you still don't get a GREAT rating.</div>
|
||||
<div class="f60 plr50">
|
||||
Press the - button if you feel like you have to press earlier than you should for a GREAT rating, or press the + button if you feel like you have to press later than you should for a GREAT rating.
|
||||
</div>
|
||||
<br />
|
||||
<div class="f70 plr25">■MICROPHONE INPUT SENSITIVITY</div>
|
||||
<div class="f60 plr50">This setting will help to make playing ORIGINAL STYLE more smooth.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="wrapper">
|
||||
<div class="wrapper_box">
|
||||
<div class="a_left w100 d_ib mb10p">
|
||||
<p class="a_center">About Arcade Mode</p>
|
||||
<hr />
|
||||
<p class="f60 plr50">
|
||||
Now you can play Arcade Mode which lets you enjoy a the same experience you'd have on a Groove Coaster arcade machine!<br />
|
||||
<br />
|
||||
*Please be aware that you will not be able to play Arcade Mode, even if you own it, if you have not yet cleared the NORMAL difficulty on the specified track.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="wrapper">
|
||||
<div class="wrapper_box">
|
||||
<div class="a_left w100 d_ib mb10p">
|
||||
<p class="a_center">Play the game by making sounds!</p>
|
||||
<hr />
|
||||
<p class="f60 plr50">
|
||||
★What's Original Style?<br />
|
||||
<img src="./files/web/Info1.gif" class="w90 d_bl m_auto" /><br />
|
||||
It gives you a brand new way to play the game by making sounds!<br />
|
||||
Enjoy playing by clapping, drumming on the table or whatever ways to make sounds!<br />
|
||||
<br />
|
||||
<br />
|
||||
★How can I play Original Style?<br />
|
||||
<img src="./files/web/Info2.png" class="w90 d_bl m_auto" /><br />
|
||||
Tap the icon in the bottom right of the Mode (difficulty selection) screen, and switch from TAP STYLE to ORIGINAL STYLE!<br />
|
||||
<br />
|
||||
<br />
|
||||
★We recommend using earphones or external speakers!<br />
|
||||
<img src="./files/web/Info3.png" class="w90 d_bl m_auto" /><br />
|
||||
Using device's speaker may adversely affect responsiveness.<br />
|
||||
We recommend using earphones or external speakers to improve accuracy of sound detection.<br />
|
||||
<br />
|
||||
<br />
|
||||
★Lower volume is recommended when using device's speaker.<br />
|
||||
<br />
|
||||
As the game registers any sounds made by device, we recommend lowering volume when using device's speaker.<br />
|
||||
Here is our recommended volume adjustment:<br />
|
||||
<br />
|
||||
Smartphone series: 1/4 of maximum volume<br />
|
||||
Tablet series: 1/2 of maximum volume<br />
|
||||
<br />
|
||||
<br />
|
||||
★Enjoy together!<br />
|
||||
<img src="./files/web/Info4.jpg" class="w90 d_bl m_auto" /><br />
|
||||
The game can be played by two or more people.<br />
|
||||
Find out your own way and enjoy the game with your friends and family!
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
21
new_server_7003/web/inform.html
Normal file
21
new_server_7003/web/inform.html
Normal file
@@ -0,0 +1,21 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||
<meta content="user-scalable=0" name="viewport" />
|
||||
<link rel="stylesheet" href="/files/web/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="header">
|
||||
<img class="ttl_height" src="{img}" alt="title image" />
|
||||
</div>
|
||||
<div id="wrapper">
|
||||
<div class="wrapper_box">
|
||||
<div class="a_left w100 d_ib mb10p">
|
||||
<br>
|
||||
<div class="f90 a_center pt50">{text}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
115
new_server_7003/web/login.html
Normal file
115
new_server_7003/web/login.html
Normal file
@@ -0,0 +1,115 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>GC2OS Login</title>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.css">
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<style>
|
||||
body {
|
||||
background-color: #1a1a1b;
|
||||
background-size: cover;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
min-height: 100vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
.login-card {
|
||||
background: rgba(255,255,255,0.9);
|
||||
border-radius: 12px;
|
||||
padding: 2rem;
|
||||
max-width: 400px;
|
||||
margin: 5% auto;
|
||||
box-shadow: 0 4px 24px rgba(0,0,0,0.2);
|
||||
}
|
||||
.form-control {
|
||||
font-family: monospace;
|
||||
font-size: 1rem;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="login-card">
|
||||
<div class="d-flex justify-content-center align-items-center mb-4">
|
||||
<h3 class="text-center mb-0">Admin Login</h3>
|
||||
<button type="button" class="btn btn-link ms-2 p-0" id="infoBtn" aria-label="Info" style="font-size:1.5rem;">
|
||||
<span class="bi bi-info-circle" style="vertical-align:middle;"></span>
|
||||
</button>
|
||||
</div>
|
||||
<form id="loginForm">
|
||||
<div class="mb-3">
|
||||
<label for="username" class="form-label">Username</label>
|
||||
<input type="text" class="form-control" id="username" name="username" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="password" class="form-label">Password</label>
|
||||
<input type="password" class="form-control" id="password" name="password" required>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary w-100">Submit</button>
|
||||
</form>
|
||||
<div id="result" class="mt-3"></div>
|
||||
</div>
|
||||
<div class="modal fade" id="loginModal" tabindex="-1" aria-labelledby="loginModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="loginModalLabel"></h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body" id="loginModalBody"></div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">OK</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
document.getElementById('loginForm').addEventListener('submit', async function(e) {
|
||||
e.preventDefault();
|
||||
const username = document.getElementById('username').value;
|
||||
const password = document.getElementById('password').value;
|
||||
|
||||
const response = await fetch('/login/login', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({"username": username, "password": password})
|
||||
});
|
||||
const result = await response.json();
|
||||
|
||||
if (result.status === "success") {
|
||||
// Set token cookie and redirect
|
||||
document.cookie = `token=${result.message}; path=/;`;
|
||||
window.location.href = "/usercenter";
|
||||
} else {
|
||||
// Show modal with error message
|
||||
document.getElementById('loginModalLabel').innerText = "Login Failed";
|
||||
document.getElementById('loginModalBody').innerHTML =
|
||||
`${result.message}`;
|
||||
const modal = new bootstrap.Modal(document.getElementById('loginModal'));
|
||||
modal.show();
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('infoBtn').addEventListener('click', function() {
|
||||
document.getElementById('loginModalLabel').innerText = "User Center Login";
|
||||
document.getElementById('loginModalBody').innerHTML =
|
||||
`<p>Don't do anything funny! Don't even think about it!</p>`;
|
||||
const modal = new bootstrap.Modal(document.getElementById('loginModal'));
|
||||
modal.show();
|
||||
});
|
||||
|
||||
function getCookie(name) {
|
||||
const value = `; ${document.cookie}`;
|
||||
const parts = value.split(`; ${name}=`);
|
||||
if (parts.length === 2) return parts.pop().split(';').shift();
|
||||
return null;
|
||||
}
|
||||
|
||||
if (getCookie("token")) {
|
||||
window.location.href = "/usercenter";
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
20
new_server_7003/web/mission.html
Normal file
20
new_server_7003/web/mission.html
Normal file
@@ -0,0 +1,20 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||
<meta content="user-scalable=0" name="viewport" />
|
||||
<link rel="stylesheet" href="/files/web/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="header">
|
||||
<img class="ttl_height" src="/files/web/ttl_mission.png" alt="MISSION" />
|
||||
</div>
|
||||
<div id="wrapper">
|
||||
<div class="wrapper_box">
|
||||
<div class="a_left w100 d_ib mb10p">
|
||||
<br>{text}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
89
new_server_7003/web/profile.html
Normal file
89
new_server_7003/web/profile.html
Normal file
@@ -0,0 +1,89 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||
<meta content="user-scalable=0" name="viewport" />
|
||||
<link rel="stylesheet" href="/files/web/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="header">
|
||||
<img class="ttl_height" src="./files/web/ttl_taitoid.png" alt="TAITO ID" />
|
||||
</div>
|
||||
|
||||
<div id="wrapper">
|
||||
<div class="wrapper_box">
|
||||
<div class="a_left w100 d_ib mb10p">
|
||||
<br>
|
||||
<div class="f90 a_center pt50">Logged in as {user}</div>
|
||||
<form action=/logout/?{pid} method=post>
|
||||
<input class="bt_bg01_narrow" type=submit value="Log Out">
|
||||
</form>
|
||||
{bind_element}
|
||||
<form action=/password_reset/?{pid} method=post>
|
||||
<div class="f60 a_center">
|
||||
<label for=old>Old Password:</label>
|
||||
<br>
|
||||
<input class="input" id=old name=old>
|
||||
<br>
|
||||
</div>
|
||||
<div class="f60 a_center">
|
||||
<label for=new>New Password:</label>
|
||||
<br>
|
||||
<input class="input" id=new name=new>
|
||||
<br>
|
||||
</div>
|
||||
<input class="bt_bg01_narrow" type=submit value='Reset Password'>
|
||||
</form>
|
||||
<br>
|
||||
<form action=/name_reset/?{pid} method=post>
|
||||
<div class="f60 a_center">
|
||||
<label for=username>New Username:</label>
|
||||
<br>
|
||||
<input class="input" id=username name=username>
|
||||
<br>
|
||||
</div>
|
||||
<div class="f60 a_center">
|
||||
<label for=password>Password:</label>
|
||||
<br>
|
||||
<input class="input" id=password name=password>
|
||||
<br>
|
||||
</div>
|
||||
<input class="bt_bg01_narrow" type=submit value='Edit Username'>
|
||||
</form>
|
||||
<form action=/coin_mp/?{pid} method=post>
|
||||
<div class="f60 a_center">
|
||||
<label for="coin_mp">GCoin multiplier:</label>
|
||||
<br>
|
||||
<select class="input" id="coin_mp" name="coin_mp">
|
||||
<option value="0" {gcoin_mp_0}>0x</option>
|
||||
<option value="1" {gcoin_mp_1}>1x</option>
|
||||
<option value="2" {gcoin_mp_2}>2x</option>
|
||||
<option value="3" {gcoin_mp_3}>3x</option>
|
||||
<option value="4" {gcoin_mp_4}>4x</option>
|
||||
<option value="5" {gcoin_mp_5}>5x</option>
|
||||
</select>
|
||||
<input class="bt_bg01_narrow" type=submit value='Edit Multiplier'>
|
||||
<br>
|
||||
</div>
|
||||
</form>
|
||||
<div class="f60 a_center" style="margin-top: 20px;">
|
||||
<label for="savefile_id">savefile ID:</label>
|
||||
<br>
|
||||
<button type="button" onclick="document.getElementById('savefile_id_container').style.display='block'; this.style.display='none';" class="bt_bg01_narrow">
|
||||
Reveal
|
||||
</button>
|
||||
<div id="savefile_id_container" style="display:none; margin-top:10px;">
|
||||
<input id="savefile_id" type="text" value="{savefile_id}" readonly style="text-align: center;" onclick="this.select();" />
|
||||
</div>
|
||||
</div>
|
||||
<form action="/save_migration/?{pid}" method="post" class="f60 a_center" style="margin-top: 10px;">
|
||||
<label for="save_id">Enter savefile ID:</label>
|
||||
<br>
|
||||
<input class="input" id="save_id" name="save_id"/>
|
||||
<input class="bt_bg01_narrow" type="submit" value="Migrate Save">
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
26
new_server_7003/web/ranking.html
Normal file
26
new_server_7003/web/ranking.html
Normal file
@@ -0,0 +1,26 @@
|
||||
<!DOCTYPE html>
|
||||
<!-- The page content is entirely controlled by ranking.js -->
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||
<meta content="user-scalable=0" name="viewport" />
|
||||
<link rel="stylesheet" href="/files/web/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="header">
|
||||
<img class="ttl_height" src="/files/web/ttl_rank.png" alt="RANK" />
|
||||
</div>
|
||||
<div id="wrapper">
|
||||
<div class="wrapper_box">
|
||||
<div class="a_left w100 d_ib mb10p rank-align-top" id="ranking_box">
|
||||
Loading...
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
<script>
|
||||
HOST_URL = "{host_url}";
|
||||
PAYLOAD = "{payload}";
|
||||
</script>
|
||||
<script src="/files/web/ranking.js"></script>
|
||||
</html>
|
||||
57
new_server_7003/web/register.html
Normal file
57
new_server_7003/web/register.html
Normal file
@@ -0,0 +1,57 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||
<meta content="user-scalable=0" name="viewport" />
|
||||
<link rel="stylesheet" href="/files/web/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="header">
|
||||
<img class="ttl_height" src="./files/web/ttl_taitoid.png" alt="TAITO ID" />
|
||||
</div>
|
||||
|
||||
<div id="wrapper">
|
||||
<div class="wrapper_box">
|
||||
<div class="a_left w100 d_ib mb10p">
|
||||
<br>
|
||||
<div class="f90 a_center pt50">Log in/Register</div>
|
||||
<div class="f60 a_center">Username must consist of alphanumeric characters only.</div>
|
||||
<div class="f60 a_center">Username length must be between 6 and 20 characters.</div>
|
||||
<div class="f60 a_center">Password must have 6 or above characters.</div>
|
||||
<br>
|
||||
<form action=/register/?{pid} method=post>
|
||||
<div class="f60 a_center">
|
||||
<label for=username>Username:</label>
|
||||
<br>
|
||||
<input class="input" id=username name=username>
|
||||
<br>
|
||||
</div>
|
||||
<div class="f60 a_center">
|
||||
<label for=password>Password:</label>
|
||||
<br>
|
||||
<input class="input" id=password name=password>
|
||||
<br>
|
||||
</div>
|
||||
<input class="bt_bg01_narrow" type=submit value='Register'>
|
||||
</form>
|
||||
<br>
|
||||
<form action=/login/?{pid} method=post>
|
||||
<div class="f60 a_center">
|
||||
<label for=username>Username:</label>
|
||||
<br>
|
||||
<input class="input" id=username name=username>
|
||||
<br>
|
||||
</div>
|
||||
<div class="f60 a_center">
|
||||
<label for=password>Password:</label>
|
||||
<br>
|
||||
<input class="input" id=password name=password>
|
||||
<br>
|
||||
</div>
|
||||
<input class="bt_bg01_narrow" type=submit value='Log In'>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
26
new_server_7003/web/status.html
Normal file
26
new_server_7003/web/status.html
Normal file
@@ -0,0 +1,26 @@
|
||||
<!DOCTYPE html>
|
||||
<!-- The page content is entirely controlled by status.js -->
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||
<meta content="user-scalable=0" name="viewport" />
|
||||
<link rel="stylesheet" href="/files/web/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="header">
|
||||
<img class="ttl_height" src="/files/web/ttl_title.png" alt="TITLE" />
|
||||
</div>
|
||||
<div id="wrapper">
|
||||
<div class="wrapper_box">
|
||||
<div class="a_left w100 d_ib mb10p rank-align-top" id="status_content">
|
||||
Loading...
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
HOST_URL = "{host_url}";
|
||||
PAYLOAD = "{payload}";
|
||||
</script>
|
||||
<script src="/files/web/status.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
79
new_server_7003/web/user.html
Normal file
79
new_server_7003/web/user.html
Normal file
@@ -0,0 +1,79 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>User Information</title>
|
||||
<!-- Include Bootstrap CSS -->
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<style>
|
||||
body {
|
||||
background-color: #000000;
|
||||
}
|
||||
#welcome-title {
|
||||
color: #ffffff;
|
||||
font-family: Arial, sans-serif;
|
||||
margin-top: 20px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container mt-5">
|
||||
<div class="text-center">
|
||||
<!-- Title -->
|
||||
<h1 id="welcome-title">Hello, {username}</h1>
|
||||
<!-- Export Data Button -->
|
||||
<button id="export-button" class="btn btn-primary mt-3">Export Data</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Function to get a cookie value by name
|
||||
function getCookie(name) {
|
||||
const value = `; ${document.cookie}`;
|
||||
const parts = value.split(`; ${name}=`);
|
||||
if (parts.length === 2) return parts.pop().split(';').shift();
|
||||
}
|
||||
|
||||
// Fetch user data from the API
|
||||
async function fetchUserData() {
|
||||
const token = getCookie("token"); // Read the token from the cookie
|
||||
if (!token) {
|
||||
console.error("Token not found in cookies.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Query the API
|
||||
const response = await fetch("/usercenter/api", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({
|
||||
token: token,
|
||||
action: "basic"
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
console.error("Failed to fetch user data:", response.statusText);
|
||||
return;
|
||||
}
|
||||
|
||||
// Parse the response data
|
||||
const response_data = await response.json();
|
||||
const username = response_data.data.username || "User";
|
||||
|
||||
// Update the username in the title
|
||||
document.getElementById("welcome-title").textContent = `Hello, ${username}`;
|
||||
} catch (error) {
|
||||
console.error("Error fetching user data:", error);
|
||||
}
|
||||
}
|
||||
|
||||
// Call the function to fetch user data when the page loads
|
||||
document.addEventListener("DOMContentLoaded", fetchUserData);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
14
new_server_7003/web/web_shop.html
Normal file
14
new_server_7003/web/web_shop.html
Normal file
@@ -0,0 +1,14 @@
|
||||
<!DOCTYPE html>
|
||||
<!-- The page content is entirely controlled by web_shop.js -->
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||
<meta content="user-scalable=0" name="viewport" />
|
||||
<link rel="stylesheet" href="/files/web/style.css">
|
||||
</head>
|
||||
<script>
|
||||
HOST_URL = "{host_url}";
|
||||
PAYLOAD = "{payload}";
|
||||
</script>
|
||||
<script src="/files/web/web_shop.js"></script>
|
||||
</html>
|
||||
62
old_server_7002/api/config/exp_unlocked_songs.json
Normal file
62
old_server_7002/api/config/exp_unlocked_songs.json
Normal file
@@ -0,0 +1,62 @@
|
||||
[
|
||||
{"id": 1, "lvl": 5},
|
||||
{"id": 2, "lvl": 10},
|
||||
{"id": 3, "lvl": 15},
|
||||
{"id": 4, "lvl": 20},
|
||||
{"id": 5, "lvl": 25},
|
||||
{"id": 6, "lvl": 30},
|
||||
{"id": 8, "lvl": 35},
|
||||
{"id": 9, "lvl": 40},
|
||||
{"id": 10, "lvl": 45},
|
||||
{"id": 11, "lvl": 50},
|
||||
{"id": 12, "lvl": 55},
|
||||
{"id": 13, "lvl": 60},
|
||||
{"id": 14, "lvl": 65},
|
||||
{"id": 15, "lvl": 70},
|
||||
{"id": 16, "lvl": 75},
|
||||
{"id": 17, "lvl": 80},
|
||||
{"id": 18, "lvl": 85},
|
||||
{"id": 19, "lvl": 90},
|
||||
{"id": 20, "lvl": 95},
|
||||
{"id": 21, "lvl": 100},
|
||||
{"id": 22, "lvl": 105},
|
||||
{"id": 24, "lvl": 110},
|
||||
{"id": 25, "lvl": 115},
|
||||
{"id": 26, "lvl": 120},
|
||||
{"id": 27, "lvl": 125},
|
||||
{"id": 28, "lvl": 130},
|
||||
{"id": 29, "lvl": 135},
|
||||
{"id": 30, "lvl": 140},
|
||||
{"id": 31, "lvl": 145},
|
||||
{"id": 32, "lvl": 150},
|
||||
{"id": 33, "lvl": 155},
|
||||
{"id": 34, "lvl": 160},
|
||||
{"id": 35, "lvl": 165},
|
||||
{"id": 53, "lvl": 170},
|
||||
{"id": 54, "lvl": 175},
|
||||
{"id": 74, "lvl": 180},
|
||||
{"id": 75, "lvl": 185},
|
||||
{"id": 76, "lvl": 190},
|
||||
{"id": 77, "lvl": 195},
|
||||
{"id": 78, "lvl": 200},
|
||||
{"id": 79, "lvl": 205},
|
||||
{"id": 80, "lvl": 210},
|
||||
{"id": 81, "lvl": 215},
|
||||
{"id": 82, "lvl": 220},
|
||||
{"id": 83, "lvl": 225},
|
||||
{"id": 84, "lvl": 230},
|
||||
{"id": 85, "lvl": 235},
|
||||
{"id": 86, "lvl": 240},
|
||||
{"id": 87, "lvl": 245},
|
||||
{"id": 93, "lvl": 250},
|
||||
{"id": 94, "lvl": 255},
|
||||
{"id": 95, "lvl": 260},
|
||||
{"id": 96, "lvl": 265},
|
||||
{"id": 121, "lvl": 270},
|
||||
{"id": 166, "lvl": 275},
|
||||
{"id": 167, "lvl": 280},
|
||||
{"id": 168, "lvl": 285},
|
||||
{"id": 169, "lvl": 290},
|
||||
{"id": 215, "lvl": 295},
|
||||
{"id": 225, "lvl": 300}
|
||||
]
|
||||
52
old_server_7002/api/config/item_list.json
Normal file
52
old_server_7002/api/config/item_list.json
Normal file
@@ -0,0 +1,52 @@
|
||||
[
|
||||
{
|
||||
"id": 1,
|
||||
"name": "FOLLOW",
|
||||
"effect": "Change up to 10 MISS results to GOOD. *Consumed upon use."
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "UNUSED",
|
||||
"effect": "Unused item. *Consumed upon use."
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"name": "CHANGE",
|
||||
"effect": "Turns Flicks into Tap TARGETS. *Consumed upon use."
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"name": "VISIBLE",
|
||||
"effect": "Displays AD-LIB timing hints. *Consumed upon use."
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"name": "MIRROR",
|
||||
"effect": "The game screen will be flipped horizontally. *Consumed upon use."
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"name": "JUST",
|
||||
"effect": "Everything but GREAT timing will be counted as a MISS. *Consumed upon use."
|
||||
},
|
||||
{
|
||||
"id": 7,
|
||||
"name": "HIDDEN",
|
||||
"effect": "Targets disappear as they get closer to your AVATAR. *Consumed upon use."
|
||||
},
|
||||
{
|
||||
"id": 8,
|
||||
"name": "SUDDEN",
|
||||
"effect": "Targets are not visible until just before they reach your AVATAR. *Consumed upon use."
|
||||
},
|
||||
{
|
||||
"id": 9,
|
||||
"name": "STEALTH",
|
||||
"effect": "TARGETS are completely invisible. *Consumed upon use."
|
||||
},
|
||||
{
|
||||
"id": 10,
|
||||
"name": "NO WAY",
|
||||
"effect": "Your AVATAR's path is invisible. *Consumed upon use."
|
||||
}
|
||||
]
|
||||
24431
old_server_7002/api/config/song_list.json
Normal file
24431
old_server_7002/api/config/song_list.json
Normal file
File diff suppressed because it is too large
Load Diff
0
old_server_7002/files/.nomedia
Normal file
0
old_server_7002/files/.nomedia
Normal file
6
old_server_7002/files/result.xml
Normal file
6
old_server_7002/files/result.xml
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?><response>
|
||||
<code>0</code>
|
||||
<ranking>
|
||||
<after></after>
|
||||
</ranking>
|
||||
</response>
|
||||
40
old_server_7002/files/tier.xml
Normal file
40
old_server_7002/files/tier.xml
Normal file
@@ -0,0 +1,40 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?><response>
|
||||
<code>0</code>
|
||||
|
||||
<tier>
|
||||
<product_id>jp.co.taito.groovecoaster2.tier001</product_id>
|
||||
</tier>
|
||||
<tier>
|
||||
<product_id>jp.co.taito.groovecoaster2.tier002</product_id>
|
||||
</tier>
|
||||
<tier>
|
||||
<product_id>jp.co.taito.groovecoaster2.tier003</product_id>
|
||||
</tier>
|
||||
<tier>
|
||||
<product_id>jp.co.taito.groovecoaster2.tier004</product_id>
|
||||
</tier>
|
||||
<tier>
|
||||
<product_id>jp.co.taito.groovecoaster2.tier005</product_id>
|
||||
</tier>
|
||||
<tier>
|
||||
<product_id>jp.co.taito.groovecoaster2.tier006</product_id>
|
||||
</tier>
|
||||
<tier>
|
||||
<product_id>jp.co.taito.groovecoaster2.tier007</product_id>
|
||||
</tier>
|
||||
<tier>
|
||||
<product_id>jp.co.taito.groovecoaster2.tier008</product_id>
|
||||
</tier>
|
||||
<tier>
|
||||
<product_id>jp.co.taito.groovecoaster2.tier009</product_id>
|
||||
</tier>
|
||||
<tier>
|
||||
<product_id>jp.co.taito.groovecoaster2.tier010</product_id>
|
||||
</tier>
|
||||
<tier>
|
||||
<product_id>jp.co.taito.groovecoaster2.tier011</product_id>
|
||||
</tier>
|
||||
<tier>
|
||||
<product_id>jp.co.taito.groovecoaster2.tier012</product_id>
|
||||
</tier>
|
||||
</response>
|
||||
Reference in New Issue
Block a user