New server open source

This commit is contained in:
UnitedAirforce
2025-06-17 20:03:03 +08:00
parent 621dbd7ffa
commit e6f6a56f4b
14 changed files with 2241 additions and 15 deletions
+56
View File
@@ -0,0 +1,56 @@
from starlette.applications import Starlette
from starlette.responses import FileResponse, Response
from starlette.routing import Route
import os
# stupid loading sequence
from api.templates import init_templates
init_templates()
from api.database import database, init_db
from api.misc import get_4max_version_string
from api.user import routes as user_routes
from api.ranking import routes as rank_routes
from api.shop import routes as shop_routes
from config import HOST, PORT, DEBUG, SSL_CERT, SSL_KEY, ROOT_FOLDER, ACTUAL_HOST, ACTUAL_PORT
if (os.path.isfile('./files/dlc_4max.html')):
get_4max_version_string()
allowed_folders = ["files"]
async def serve_file(request):
path = request.path_params['path']
for folder in allowed_folders:
if path.startswith(folder):
file_path = os.path.join(ROOT_FOLDER, path)
if os.path.isfile(file_path):
return FileResponse(file_path)
return Response("", status_code=404)
routes = []
routes = routes + user_routes + rank_routes + shop_routes
routes.append(Route("/{path:path}", serve_file))
app = Starlette(debug=DEBUG, routes=routes)
@app.on_event("startup")
async def startup():
await database.connect()
await init_db()
@app.on_event("shutdown")
async def shutdown():
await 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.5.10
+21 -14
View File
@@ -2,7 +2,7 @@
A small local server for `Groove Coaster 2: Original Style`, implemented with `Python` and `Flask`.
一个基于`Python``Flask`的微型`Groove Coaster 2: Original Style`本地服务器。
一个基于`Python`~~`Flask`~~`Starlette`的微型`Groove Coaster 2: Original Style`本地服务器。
<details>
<summary>English</summary>
@@ -14,7 +14,9 @@ This project is for game preservation purposes only. Creative liberty and conven
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.
~~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)
@@ -65,7 +67,7 @@ The server owner must install the update on their instance. They can download it
- Python
- Flask
- ~~Flask~~ `Starlette`
- Crypto (pycryptodome)
@@ -99,13 +101,13 @@ Note that MAC uses `python3`. Code examples in this document will use the defaul
Open command on Windows (MAC open terminal). Type `ipconfig` (MAC `ifconfig`), and obtain your IPV4 address. This assumes that you are connected to a WIFI, and it should start with 192 or 172.
Open the `config_old.py` of the private server, and change the `IP` accordingly.
Open the `config.env` of the private server, and change the `IP` accordingly.
Type `cmd` in the file directory on the top of the file explorer, and press enter. A command prompt will be opened for that directory.
Type `pip install -r requirements.txt` to install all the dependencies.
Type `python 7001.py` to start the server. If an error pops up, resolve it now did you install all the dependencies? Is the IP correct?
Type `python 7002.py` to start the server. If an error pops up, resolve it now did you install all the dependencies? Is the IP correct?
### Android (Harder)
@@ -125,7 +127,7 @@ Use
`pip install ...`
to install `rust`, `flask`, `passlib`, `pycryptodome`, `requests`.
to install `rust`, `starlette`, `passlib`, `pycryptodome`, `requests`.
If ssl errors pop up, you might need to ``pkg up ssl -y``.
@@ -135,7 +137,7 @@ change `config.py`'s `IP` to `127.0.0.1` (this is `loopback`. Feel free to use y
`cd storage/shared/.... (server location on android file system)`
`python 7001.py` to start the server.
`python 7002.py` to start the server.
</details>
@@ -263,7 +265,9 @@ Note that this data is for analytics only, and the functionality to embed this d
你应对因运行本服务器而产生的任何潜在后果承担全部责任。如果您不同意这些要求,则不允许您复制或运行该程序。
此服务器仅为**本地**运行设计,鉴于Flask糟糕的并发性能。一个高效,`异步`的服务器可供使用,不过代码并非开源。只有经过审核,不会违反许可条款的服务器所有者才能获得访问权限。请联系repo所有者了解更多信息。
~~此服务器仅为**本地**运行设计,鉴于Flask糟糕的并发性能。一个高效,`异步`的服务器可供使用,不过代码并非开源。只有经过审核,不会违反许可条款的服务器所有者才能获得访问权限。请联系repo所有者了解更多信息。~~
基于`Starlette`的异步服务器已经在功能和性能上超越了老服务器,现在以`GPLv2`许可证开源。
灵感: [Lost-MSth/Arcaea-server](https://github.com/Lost-MSth/Arcaea-server)
@@ -313,7 +317,7 @@ Note that this data is for analytics only, and the functionality to embed this d
- Python
- Flask
- ~~Flask~~`Starlette`
- Crypto (pycryptodome)
@@ -348,11 +352,11 @@ PC/MAC安装 `python`,安装 `pip`。
PC打开 `cmd` 输入 `ipconfig`。MAC 打开 `terminal` 输入 `ifconfig`。获得你的`IPV4`,一串为192或172开头的数字。
PC用文本编辑器打开服务器文件夹的 `config.py`,将`IPV4`填写至`IP`。`PORT`(端口)也可以更改。
PC用文本编辑器打开服务器文件夹的 `config.env`,将`IPV4`填写至`IP`。`PORT`(端口)也可以更改。
文件管理器上方的文件夹路径清空,输入 `cmd`。命令行窗口会弹出。
输入 `python 7001.py`来开启服务器。如果出现错误,就解决他们吧。检查依赖项是否安装,网络配置是否正确。
输入 `python 7002.py`来开启服务器。如果出现错误,就解决他们吧。检查依赖项是否安装,网络配置是否正确。
### 安卓(稍难)
@@ -372,7 +376,7 @@ PC用文本编辑器打开服务器文件夹的 `config.py`,将`IPV4`填写至
`pip install ...`
来安装 `rust`, `flask`, `bcrypt`, `pycryptodome`, `requests`.
来安装 `rust`, `starlette`, `bcrypt`, `pycryptodome`, `requests`.
如果出现ssl问题,可能需要``pkg up ssl -y``.
@@ -382,7 +386,7 @@ PC用文本编辑器打开服务器文件夹的 `config.py`,将`IPV4`填写至
`cd storage/shared/.... (服务器在安卓文件系统的位置)`
`python 7001.py` 来运行服务器。
`python 7002.py` 来运行服务器。
</details>
@@ -516,8 +520,11 @@ server/
│ │ └─ title
│ ├─ web/ (found in common.zip)
│ │ └─ webpage assets
├─ 7001.py (main script)
├─ 7002.py (main script)
├─ api/ (API scripts)
│ ├─ config/ (various configuration files)
├─ getCrypt.py (debug purpose only)
├─ old_server (Flask old server (depricated))
└─ config.py (configuration script)
</pre>
</details>
+38
View File
@@ -0,0 +1,38 @@
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)
match = re.search(r'\?(.*)', url)
if match:
original_field = match.group(1)
decrypted_fields = urllib.parse.parse_qs(decryptAES(match.group(1))[:-1])
return decrypted_fields, original_field
else:
return None, None
+128
View File
@@ -0,0 +1,128 @@
from starlette.responses import JSONResponse, Response
from starlette.requests import Request
import sqlalchemy
from sqlalchemy import Table, Column, Integer, String, DateTime, ForeignKey
from sqlalchemy.ext.asyncio import create_async_engine
from sqlalchemy import select, update
import os
import databases
import datetime
DB_NAME = "player.db"
DB_PATH = os.path.join(os.getcwd(), DB_NAME)
DATABASE_URL = f"sqlite+aiosqlite:///{DB_PATH}"
database = databases.Database(DATABASE_URL)
metadata = sqlalchemy.MetaData()
user = Table(
"user",
metadata,
Column("id", Integer, primary_key=True, autoincrement=True),
Column("username", String(20), unique=True, nullable=False),
Column("password_hash", String(255), nullable=False),
Column("device_id", String(512)),
Column("data", String, nullable=True),
Column("save_id", String, nullable=True),
Column("crc", Integer, nullable=True),
Column("timestamp", DateTime, default=datetime.datetime.utcnow),
Column("coin_mp", Integer, default=1),
)
daily_reward = Table(
"daily_reward",
metadata,
Column("id", Integer, primary_key=True, autoincrement=True),
Column("device_id", String(512)),
Column("timestamp", DateTime, default=datetime.datetime.utcnow),
Column("my_stage", String),
Column("my_avatar", String),
Column("item", String),
Column("day", Integer),
Column("coin", Integer),
Column("lvl", Integer),
Column("title", Integer),
Column("avatar", Integer),
)
result = Table(
"result",
metadata,
Column("rid", Integer, primary_key=True, autoincrement=True),
Column("vid", String(512), nullable=False),
Column("tid", String(512), nullable=False),
Column("sid", String(512), nullable=False),
Column("stts", String(64)),
Column("id", String(8)),
Column("mode", String(4)),
Column("avatar", String(4)),
Column("score", String(16)),
Column("high_score", String(128)),
Column("play_rslt", String(128)),
Column("item", String(16)),
Column("os", String(16)),
Column("os_ver", String(16)),
Column("ver", String(16)),
Column("mike", String(8)),
)
whitelist = Table(
"whitelist",
metadata,
Column("id", String(512), primary_key=True)
)
blacklist = Table(
"blacklist",
metadata,
Column("id", String(512), primary_key=True),
Column("reason", String(256))
)
async def init_db():
if not os.path.exists(DB_PATH):
print("[DB] Creating new database:", DB_PATH)
engine = create_async_engine(DATABASE_URL, echo=False)
async with engine.begin() as conn:
await conn.run_sync(metadata.create_all)
await engine.dispose()
print("[DB] Database initialized successfully.")
async def get_user_data(uid, data_field):
query = select(user.c[data_field]).where(user.c.device_id == uid[b'vid'][0].decode())
async with database.transaction():
result = await database.fetch_one(query)
return result[data_field] if result else None
async def set_user_data(uid, data_field, new_data):
query = (
update(user)
.where(user.c.device_id == uid[b'vid'][0].decode())
.values({data_field: new_data})
)
async with database.transaction():
await database.execute(query)
async def check_whitelist(uid):
query = select(whitelist.c.id).where(whitelist.c.id == uid[b'vid'][0].decode())
async with database.transaction():
result = await database.fetch_one(query)
return result is not None
async def check_blacklist(uid):
device_id = uid[b'vid'][0].decode()
user_data = await get_user_data(uid, "username")
username = user_data[0] if user_data else None
query = select(blacklist.c.id).where(
(blacklist.c.id == device_id) | (blacklist.c.id == username)
)
async with database.transaction():
result = await database.fetch_one(query)
return result is None
+127
View File
@@ -0,0 +1,127 @@
import requests
import json
import binascii
import bcrypt
import re
import xml.etree.ElementTree as ET
from config import MODEL, TUNEFILE, SKIN
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:
FMAX_RES = json.loads(json.loads(response.text)['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):
return "Failed to fetch version info: Error " + str(res)
for item in res:
if item.get("isOpen"):
version = item.get("version", "Unknown Version")
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
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))
def get_model_pak(host):
mid = ET.Element("model_pak")
rid = ET.Element("date")
uid = ET.Element("url")
rid.text = MODEL
uid.text = host + "files/gc2/model" + MODEL + ".pak"
mid.append(rid)
mid.append(uid)
return mid
def get_tune_pak(host):
mid = ET.Element("tuneFile_pak")
rid = ET.Element("date")
uid = ET.Element("url")
rid.text = TUNEFILE
uid.text = host + "files/gc2/tuneFile" + TUNEFILE + ".pak"
mid.append(rid)
mid.append(uid)
return mid
def get_skin_pak(host):
mid = ET.Element("skin_pak")
rid = ET.Element("date")
uid = ET.Element("url")
rid.text = SKIN
uid.text = host + "files/gc2/skin" + SKIN + ".pak"
mid.append(rid)
mid.append(uid)
return mid
def get_m4a_path(host):
mid = ET.Element("m4a_path")
mid.text = host + "files/gc2/audio/"
return mid
def get_stage_path(host):
mid = ET.Element("stage_path")
mid.text = host + "files/gc2/stage/"
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"
with open("files/inform.html", "r") as file:
return file.read().format(text=text, img=mode)
+498
View File
@@ -0,0 +1,498 @@
from starlette.responses import HTMLResponse
from starlette.requests import Request
from starlette.routing import Route
import os
from sqlalchemy import select, update
from config import AUTHORIZATION_NEEDED
from api.database import database, user, result, daily_reward, check_blacklist, check_whitelist
from api.crypt import decrypt_fields, encryptAES
from api.templates import EXP_UNLOCKED_SONGS, TITLE_LISTS, SONG_LIST
from api.misc import inform_page
async def ranking(request: Request):
decrypted_fields, _ = await decrypt_fields(request)
if not decrypted_fields:
return HTMLResponse("""<html><body><h1>Invalid request data</h1></body></html>""", status_code=400)
should_serve = True
if AUTHORIZATION_NEEDED:
should_serve = await check_whitelist(decrypted_fields) and not await check_blacklist(decrypted_fields)
if should_serve:
device_id = decrypted_fields[b'vid'][0].decode()
html = "<ul class='song-list'>"
encrypted_mass = encryptAES(("vid=" + device_id + "&song_id=-1&mode=1&dummy=").encode("utf-8"))
href = f"/ranking_detail.php?{encrypted_mass}"
html += f'''
<li class="song-item">
<a href="{href}" class="song-button">Total Score</a>
</li>
'''
for index, song in enumerate(SONG_LIST):
encrypted_mass = encryptAES(("vid=" + device_id + "&song_id=" + str(index) + "&mode=3&dummy=").encode("utf-8"))
song_name = song.get("name_en", "Unknown")
href = f"/ranking_detail.php?{encrypted_mass}"
html += f'''
<li class="song-item">
<a href="{href}" class="song-button">{song_name}</a>
</li>
'''
html += "</ul>"
file_path = os.path.join("files", "ranking.html")
try:
with open(file_path, "r", encoding="utf-8") as file:
html_content = file.read().format(text=html)
except FileNotFoundError:
return HTMLResponse("""<html><body><h1>Ranking file not found</h1></body></html>""", status_code=500)
return HTMLResponse(html_content)
else:
return HTMLResponse("""<html><body><h1>Access denied</h1></body></html>""", status_code=403)
async def ranking_detail(request: Request):
decrypted_fields, _ = await decrypt_fields(request)
if not decrypted_fields:
return HTMLResponse("""<html><body><h1>Invalid request data</h1></body></html>""", status_code=400)
should_serve = True
if AUTHORIZATION_NEEDED:
should_serve = await check_whitelist(decrypted_fields) and not await check_blacklist(decrypted_fields)
if should_serve:
device_id = decrypted_fields[b'vid'][0].decode()
song_id = int(decrypted_fields[b'song_id'][0].decode())
mode = int(decrypted_fields[b'mode'][0].decode())
button_labels = []
difficulty_levels = []
song_name = ""
if (song_id == -1):
song_name = "Total Score"
difficulty_levels = []
button_labels = ["All", "Mobile", "Arcade"]
else:
song_name = SONG_LIST[song_id]["name_en"]
difficulty_levels = SONG_LIST[song_id]["difficulty_levels"]
button_labels = ["Easy", "Normal", "Hard"]
html = f"""<div style="text-align: center; font-size: 36px; margin-bottom: 20px;">{song_name}</div>"""
button_modes = [1, 2, 3]
print(len(difficulty_levels))
if (len(difficulty_levels) == 6):
button_labels.extend(["AC-Easy", "AC-Normal", "AC-Hard"])
button_modes.extend([11, 12, 13])
if song_id > 615:
button_modes = [x for x in button_modes if x not in [1, 2, 3]]
button_labels = [x for x in button_labels if x not in ["Easy", "Normal", "Hard"]]
row_start = '<div class="button-row">'
row_end = '</div>'
row_content = []
for i, (label, mode_value) in enumerate(zip(button_labels, button_modes)):
if mode_value == mode:
row_content.append(f"""<div class="bt_bg01_ac">{label}</div>""")
else:
encrypted_mass = encryptAES(("vid=" + device_id + "&song_id=" + str(song_id) + "&mode=" + str(mode_value) + "&dummy=").encode("utf-8"))
row_content.append(f"""<a href="/ranking_detail.php?{encrypted_mass}" class="bt_bg01_xnarrow">{label}</a>""")
if len(row_content) == 3:
html += row_start + ''.join(row_content) + row_end
row_content = [] # Reset row content
play_results = None
user_result = None
device_result = None
if (song_id == -1):
# Filter out the mobile/AC modes
if (mode == 1):
exclude = []
elif mode == 2:
exclude = [11, 12, 13]
else:
exclude = [1, 2, 3]
query = select(result.c.vid, result.c.sid, result.c.mode, result.c.avatar, result.c.score)
play_results = await database.fetch_all(query)
query = select(daily_reward.c.device_id, daily_reward.c.title, daily_reward.c.avatar)
device_results_raw = await database.fetch_all(query)
device_results = {row["device_id"]: {"title": row["title"], "avatar": row["avatar"]} for row in device_results_raw}
query = select(user.c.id, user.c.username, user.c.device_id)
user_results_raw = await database.fetch_all(query)
user_results = {row["id"]: {"username": row["username"], "device_id": row["device_id"]} for row in user_results_raw}
query = select(user).where(user.c.device_id == device_id)
cur_user = await database.fetch_one(query)
player_scores = {}
filtered_play_results = [play for play in play_results if int(play[2]) not in exclude]
for play in filtered_play_results:
did = play[0]
sid = play[1]
avatar = play[3]
score = play[4]
username, title = None, None
if sid:
sid = int(sid)
if sid in user_results:
username = user_results[sid]["username"]
did = user_results[sid]["device_id"]
else: # Guest
username = f"Guest({did[-6:]})"
# title is device-specific
title = device_results.get(did, {}).get("title", "1")
if username in player_scores:
player_scores[username]["score"] += int(score)
player_scores[username]["avatar"] = avatar # But avatar is based on latest play submission
player_scores[username]["title"] = title
else:
player_scores[username] = {"score": int(score), "avatar": avatar, "title": title}
sorted_players = sorted(player_scores.items(), key=lambda x: x[1]["score"], reverse=True)
username = cur_user[1] if cur_user else f"Guest({device_id[-6:]})"
player_rank = None
user_score = 0
avatar = "1"
title = "1"
for rank, (player_name, data) in enumerate(sorted_players, start=1):
if player_name == username:
player_rank = rank
user_score = data["score"]
avatar = data["avatar"]
title = data["title"]
break
if player_rank is None:
device_data = next((device for device in device_results if device[1] == device_id), None)
if device_data:
avatar = device_data["avatar"]
title = device_data["title"]
html += f"""
<div class="player-element">
<span class="rank">You<br>{"#" + str(player_rank) if player_rank else "N/A"}</span>
<img src="/files/image/icon/avatar/{avatar}.png" class="avatar" alt="Player Avatar">
<div class="player-info">
<div class="name">{username}</div>
<img src="/files/image/title/{title}.png" class="title" alt="Player Title">
</div>
<div class="player-score">{user_score}</div>
</div>
"""
html += """
<div class="leaderboard-container">
"""
# Loop leaderboard
for rank, (username, data) in enumerate(sorted_players, start=1):
html += f"""
<div class="leaderboard-player">
<div class="rank">#{rank}</div>
<img class="avatar" src="/files/image/icon/avatar/{data['avatar']}.png" alt="Avatar">
<div class="leaderboard-info">
<div class="name">{username}</div>
<div class="title"><img src="/files/image/title/{data['title']}.png" alt="Title"></div>
</div>
<div class="leaderboard-score">{data['score']}</div>
</div>
"""
else:
query = select(result).where(
(result.c.id == song_id) & (result.c.mode == mode)
).order_by(result.c.score.desc())
play_results = await database.fetch_all(query)
query = select(user).where(user.c.device_id == device_id)
user_result = await database.fetch_one(query)
query = select(daily_reward).where(daily_reward.c.device_id == device_id)
device_result = await database.fetch_one(query)
user_id = user_result[0] if user_result else None
username = user_result[1] if user_result else f"Guest({device_id[-6:]})"
play_record = None
if user_id:
play_record = next((record for record in play_results if int(record[3]) == user_id), None)
if not play_record:
play_record = next((record for record in play_results if record[1] == device_id and record[3] is None), None)
player_rank = None
avatar_index = str(play_record[7]) if play_record else "1"
user_score = play_record[8] if play_record else 0
for rank, result_obj in enumerate(play_results, start=1):
if user_result and int(result_obj[3]) == user_id:
player_rank = rank
break
elif result_obj[1] == device_id and result_obj[3] is None:
player_rank = rank
break
html += f"""
<div class="player-element">
<span class="rank">You<br>{"#" + str(player_rank) if player_rank else "N/A"}</span>
<img src="/files/image/icon/avatar/{avatar_index}.png" class="avatar" alt="Player Avatar">
<div class="player-info">
<div class="name">{username}</div>
<img src="/files/image/title/{device_result[9]}.png" class="title" alt="Player Title">
</div>
<div class="player-score">{user_score}</div>
</div>
"""
html += """
<div class="leaderboard-container">
"""
for rank, record in enumerate(play_results, start=1):
username = f"Guest({record[1][-6:]})"
device_info = None
if record[3]:
query = select(user.c.username).where(user.c.id == record[3])
user_data = await database.fetch_one(query)
if user_data:
username = user_data["username"]
query = select(daily_reward.c.title).where(daily_reward.c.device_id == record[1])
device_title = await database.fetch_one(query)
if device_title:
device_info = device_title["title"]
else:
device_info = "1"
avatar_id = record[7] if record[7] else 1
avatar_url = f"/files/image/icon/avatar/{avatar_id}.png"
score = record[8]
html += f"""
<div class="leaderboard-player">
<div class="rank">#{rank}</div>
<img class="avatar" src="{avatar_url}" alt="Avatar">
<div class="leaderboard-info">
<div class="name">{username}</div>
<div class="title"><img src="/files/image/title/{device_info}.png" alt="Title"></div>
</div>
<div class="leaderboard-score">{score}</div>
</div>
"""
html += "</div>"
encrypted_mass = encryptAES(("vid=" + device_id + "&dummy=").encode("utf-8"))
html += f"""
<a href="/ranking.php?{encrypted_mass}" class="bt_bg01" style="margin: 20px auto; display: block; text-align: center;">
Go Back
</a>
"""
file_path = os.path.join("files", "ranking.html")
try:
with open(file_path, "r", encoding="utf-8") as file:
html_content = file.read().format(text=html)
except FileNotFoundError:
return HTMLResponse("""<html><body><h1>Ranking file not found</h1></body></html>""", status_code=500)
return HTMLResponse(html_content)
else:
return HTMLResponse("""<html><body><h1>Access denied</h1></body></html>""", status_code=403)
async def status(request: Request):
decrypted_fields, _ = await decrypt_fields(request)
if not decrypted_fields:
return HTMLResponse("""<html><body><h1>Invalid request data</h1></body></html>""", status_code=400)
should_serve = True
if AUTHORIZATION_NEEDED:
should_serve = await check_whitelist(decrypted_fields) and not await check_blacklist(decrypted_fields)
if should_serve:
device_id = decrypted_fields[b'vid'][0].decode()
set_title = int(decrypted_fields[b'set_title'][0].decode()) if b'set_title' in decrypted_fields else None
page_id = int(decrypted_fields[b'page_id'][0].decode()) if b'page_id' in decrypted_fields else 0
if set_title:
update_query = (
update(daily_reward)
.where(daily_reward.c.device_id == device_id)
.values(title=set_title)
)
await database.execute(update_query)
query = select(daily_reward).where(daily_reward.c.device_id == device_id)
user_data = await database.fetch_one(query)
user_name = f"Guest({device_id[-6:]})"
if user_data:
query = select(user.c.username).where(user.c.device_id == device_id)
user_result = await database.fetch_one(query)
if user_result:
user_name = user_result["username"]
html = ""
if user_data:
player_element = f"""
<div class="player-element">
<img src="/files/image/icon/avatar/{user_data['avatar']}.png" class="avatar" alt="Player Avatar">
<div class="player-info">
<div class="name">{user_name}</div>
<img src="/files/image/title/{user_data['title']}.png" class="title" alt="Player Title">
</div>
<div class="player-score">Level {user_data['lvl']}</div>
</div>
"""
html += player_element
page_name = ["Special", "Normal", "Master", "God"]
buttons_html = '<div class="button-row">'
for i, name in enumerate(page_name):
if i == page_id:
buttons_html += f"""
<div class="bt_bg01_ac">{name}</div>
"""
else:
encrypted_mass = encryptAES(f"vid={device_id}&page_id={i}&dummy=".encode("utf-8"))
buttons_html += f"""
<a href="/status.php?{encrypted_mass}" class="bt_bg01_xnarrow">{name}</a>
"""
buttons_html += '</div>'
html += f"<div style='text-align: center; margin-top: 20px;'>{buttons_html}</div>"
selected_titles = TITLE_LISTS.get(page_id, [])
titles_html = '<div class="title-list">'
for index, num in enumerate(selected_titles):
if index % 2 == 0:
if index != 0:
titles_html += '</div>'
titles_html += '<div class="title-row">'
if num == user_data["title"]:
titles_html += f"""
<img src="/files/image/title/{num}.png" alt="Title {num}" class="title-image-selected">
"""
else:
encrypted_mass = encryptAES(f"vid={device_id}&title_id={num}&page_id={page_id}&dummy=".encode("utf-8"))
titles_html += f"""
<a href="/set_title.php?{encrypted_mass}" class="title-link">
<img src="/files/image/title/{num}.png" alt="Title {num}" class="title-image">
</a>
"""
titles_html += '</div></div>'
html += titles_html
file_path = os.path.join("files", "status.html")
try:
with open(file_path, "r", encoding="utf-8") as file:
html_content = file.read().format(text=html)
except FileNotFoundError:
return HTMLResponse("""<html><body><h1>Status file not found</h1></body></html>""", status_code=500)
return HTMLResponse(html_content)
else:
return HTMLResponse("""<html><body><h1>Access denied</h1></body></html>""", status_code=403)
async def set_title(request: Request):
decrypted_fields, _ = await decrypt_fields(request)
if not decrypted_fields:
return HTMLResponse("""<html><body><h1>Invalid request data</h1></body></html>""", status_code=400)
should_serve = True
if AUTHORIZATION_NEEDED:
should_serve = await check_whitelist(decrypted_fields) and not await check_blacklist(decrypted_fields)
if should_serve:
device_id = decrypted_fields[b'vid'][0].decode()
page_id = decrypted_fields[b'page_id'][0].decode()
title_id = decrypted_fields[b'title_id'][0].decode()
current_title = 1
query = select(daily_reward.c.title).where(daily_reward.c.device_id == device_id)
row = await database.fetch_one(query)
if row:
current_title = row["title"]
confirm_url = encryptAES(
f"vid={device_id}&page_id={page_id}&set_title={title_id}&dummy=".encode("utf-8")
)
go_back_url = encryptAES(
f"vid={device_id}&page_id={page_id}&dummy=".encode("utf-8")
)
html = f"""
<p>Would you like to change your title?<br>Current Title:</p>
<img src="/files/image/title/{current_title}.png" alt="Current Title" class="title-image">
<p>New Title:</p>
<img src="/files/image/title/{title_id}.png" alt="New Title" class="title-image">
<div class="button-container">
<a href="/status.php?{confirm_url}" class="bt_bg01">Confirm</a>
<a href="/status.php?{go_back_url}" class="bt_bg01">Go back</a>
</div>
"""
return HTMLResponse(inform_page(html, 1))
else:
return HTMLResponse("""<html><body><h1>Access denied</h1></body></html>""", status_code=403)
async def mission(request: Request):
decrypted_fields, _ = await decrypt_fields(request)
if not decrypted_fields:
return HTMLResponse("""<html><body><h1>Invalid request data</h1></body></html>""", status_code=400)
should_serve = True
if AUTHORIZATION_NEEDED:
should_serve = await check_whitelist(decrypted_fields) and not await check_blacklist(decrypted_fields)
if should_serve:
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>"
file_path = os.path.join("files", "mission.html")
try:
with open(file_path, "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)
else:
return HTMLResponse("""<html><body><h1>Access denied</h1></body></html>""", status_code=403)
routes = [
Route('/ranking.php', ranking, methods=['GET']),
Route('/ranking_detail.php', ranking_detail, methods=['GET']),
Route('/set_title.php', set_title, methods=['GET']),
Route('/mission.php', mission, methods=['GET']),
Route('/status.php', status, methods=['GET']),
]
+408
View File
@@ -0,0 +1,408 @@
from starlette.responses import Response, FileResponse, HTMLResponse
from starlette.requests import Request
from starlette.routing import Route
import os
import json
import math
from sqlalchemy import select, update
import xml.etree.ElementTree as ET
from config import START_COIN, AUTHORIZATION_NEEDED, STAGE_PRICE, START_COIN, AVATAR_PRICE, ITEM_PRICE
from api.crypt import decrypt_fields
from api.misc import inform_page, parse_res, FMAX_VER, FMAX_RES
from api.database import database, daily_reward, check_blacklist, check_whitelist
from api.templates import START_AVATARS, START_STAGES, EXCLUDE_STAGE_EXP, SONG_LIST, AVATAR_LIST, ITEM_LIST
async def web_shop(request: Request):
decrypted_fields, _ = await decrypt_fields(request)
if not decrypted_fields:
return HTMLResponse("""<html><body><h1>Invalid request data</h1></body></html>""", status_code=400)
should_serve = True
if AUTHORIZATION_NEEDED:
should_serve = await check_whitelist(decrypted_fields) and not await check_blacklist(decrypted_fields)
if should_serve:
cnt_type = decrypted_fields[b'cnt_type'][0].decode()
device_id = decrypted_fields[b'vid'][0].decode()
if (decrypted_fields.get(b'page')):
page = int(decrypted_fields.get(b'page')[0].decode())
else:
page = 0
inc = 0
fmax_inc = 0
buttons_html = ""
spawn_prev_page = False
spawn_next_page = False
query = select(daily_reward.c.my_stage, daily_reward.c.my_avatar, daily_reward.c.coin).where(daily_reward.c.device_id == device_id)
result = await database.fetch_one(query)
my_stage = set(json.loads(result["my_stage"])) if result and result["my_stage"] else START_STAGES
my_avatar = set(json.loads(result["my_avatar"])) if result and result["my_avatar"] else START_AVATARS
coin = result["coin"] if result and result["coin"] else START_COIN
if cnt_type == "1":
low_range = 100
up_range = 616
if page < 0 or page > math.ceil(up_range - low_range) / 80:
return HTMLResponse("""<html><body><h1>Invalid page number</h1></body></html>""", status_code=400)
if page > 0:
spawn_prev_page = True
if page < math.ceil(up_range - low_range) / 80 - 1:
spawn_next_page = True
low_range = low_range + page * 80
up_range = min(up_range, low_range + 80)
if 700 not in my_stage and os.path.isfile('./files/dlc_4max.html'):
buttons_html += """
<a href="wwic://web_shop_detail?&cnt_type=1&cnt_id=-1">
<img src="/files/web/dlc_4max.jpg" style="width: 84%; margin-bottom: 20px; margin-top: -100px;" />
</a><br>
"""
fmax_inc = 1
elif 700 in my_stage and os.path.isfile('./files/dlc_4max.html'):
buttons_html += """
<a href="wwic://web_shop_detail?&cnt_type=1&cnt_id=-2">
<img src="/files/web/dlc_4max.jpg" style="width: 84%; margin-bottom: 20px; margin-top: -100px;" />
</a><br>
"""
for i in range(low_range, up_range):
if i not in my_stage and i not in EXCLUDE_STAGE_EXP:
buttons_html += f"""
<button style="width: 170px; height: 170px; margin: 10px; background-size: cover; background-image: url('/files/image/icon/shop/{i}.jpg');"
onclick="window.location.href='wwic://web_shop_detail?&cnt_type={cnt_type}&cnt_id={i}'">
</button>
"""
inc += 1
if inc % 4 == 0:
buttons_html += "<br>"
if spawn_prev_page:
buttons_html += """<br>
<button style="width: 170px; height: 40px; margin: 10px; background-color: #000000; color: #FFFFFF;"
onclick="window.location.href='wwic://web_shop?&cnt_type=1&page={}'">
Prev Page
</button>
""".format(page - 1)
if spawn_next_page:
buttons_html += """<br>
<button style="width: 170px; height: 40px; margin: 10px; background-color: #000000; color: #FFFFFF;"
onclick="window.location.href='wwic://web_shop?&cnt_type=1&page={}'">
Next Page
</button>
""".format(page + 1)
elif cnt_type == "2":
low_range = 15
up_range = 173 if FMAX_VER == 0 else 267
if page < 0 or page > math.ceil(up_range - low_range) / 80:
return HTMLResponse("""<html><body><h1>Invalid page number</h1></body></html>""", status_code=400)
if page > 0:
spawn_prev_page = True
if page < math.ceil(up_range - low_range) / 80 - 1:
spawn_next_page = True
low_range = low_range + page * 80
up_range = min(up_range, low_range + 80)
for i in range(low_range, up_range):
if i not in my_avatar and i not in EXCLUDE_STAGE_EXP:
buttons_html += f"""
<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/avatar/{i}.png');"
onclick="window.location.href='wwic://web_shop_detail?&cnt_type={cnt_type}&cnt_id={i}'">
</button>
"""
inc += 1
if inc % 4 == 0:
buttons_html += "<br>"
if spawn_prev_page:
buttons_html += """<br>
<button style="width: 170px; height: 40px; margin: 10px; background-color: #000000; color: #FFFFFF;"
onclick="window.location.href='wwic://web_shop?&cnt_type=2&page={}'">
Prev Page
</button>
""".format(page - 1)
if spawn_next_page:
buttons_html += """<br>
<button style="width: 170px; height: 40px; margin: 10px; background-color: #000000; color: #FFFFFF;"
onclick="window.location.href='wwic://web_shop?&cnt_type=2&page={}'">
Next Page
</button>
""".format(page + 1)
elif cnt_type == "3":
for i in range(1, 11):
buttons_html += f"""
<button style="width: 170px; height: 170px; margin: 10px; background-size: cover; background-image: url('/files/image/icon/item/{i}.png');"
onclick="window.location.href='wwic://web_shop_detail?&cnt_type={cnt_type}&cnt_id={i}'">
</button>
"""
if i % 4 == 0:
buttons_html += "<br>"
if inc == 0 and fmax_inc == 0 and cnt_type != "3":
buttons_html += """<div>Everything has been purchased!</div>"""
html_path = f"files/web_shop_{cnt_type}.html"
try:
with open(html_path, "r", encoding="utf-8") as file:
html_content = file.read().format(text=buttons_html, coin=coin)
except FileNotFoundError:
return HTMLResponse("""<html><body><h1>Shop template not found</h1></body></html>""", status_code=500)
return HTMLResponse(html_content)
else:
return HTMLResponse("""<html><body><h1>Access denied</h1></body></html>""", status_code=403)
async def web_shop_detail(request: Request):
decrypted_fields, _ = await decrypt_fields(request)
if not decrypted_fields:
return HTMLResponse("""<html><body><h1>Invalid request data</h1></body></html>""", status_code=400)
should_serve = True
if AUTHORIZATION_NEEDED:
should_serve = await check_whitelist(decrypted_fields) and not await check_blacklist(decrypted_fields)
if should_serve:
cnt_type = decrypted_fields[b'cnt_type'][0].decode()
cnt_id = int(decrypted_fields[b'cnt_id'][0].decode())
device_id = decrypted_fields[b'vid'][0].decode()
query = select(daily_reward.c.coin).where(daily_reward.c.device_id == device_id)
result = await database.fetch_one(query)
coin = result["coin"] if result and result["coin"] else 0
html = ""
if cnt_type == "1":
if cnt_id > -1:
song = SONG_LIST[cnt_id]
difficulty_levels = "/".join(map(str, song.get("difficulty_levels", [])))
song_stage_price = STAGE_PRICE * 2 if len(song["difficulty_levels"]) == 6 else STAGE_PRICE
html = f"""
<div class="image-container">
<img src="/files/image/icon/shop/{cnt_id}.jpg" alt="Item Image" style="width: 180px; height: 180px;" />
</div>
<p>Would you like to purchase this song?</p>
<div>
<p>{song.get("name_en")} - {song.get("author_en")}</p>
<p>Difficulty Levels: {difficulty_levels}</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;">{song_stage_price}</span>
</div>
"""
elif cnt_id == -2:
log = parse_res(FMAX_RES)
html = f"""
<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="window.location.href='wwic://web_shop?&cnt_type=1'">
Go Back
</button><br>
<strong>This server has version {FMAX_VER}.</strong>
<p>Update log: </p>
<p>{log}<p><br>
</div>
"""
elif cnt_id == -1:
html = f"""
<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="window.location.href='wwic://web_purchase_coin?&cnt_type=1&cnt_id=-1&num=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;"> 300</span>
</div>
</button>
<br><br>
<button class="quit-button" onclick="window.location.href='wwic://web_shop?&cnt_type=1'">
Go Back
</button>
"""
elif cnt_type == "2":
avatar = next((item for item in AVATAR_LIST if item.get("id") == cnt_id), None)
if avatar:
html = f"""
<div class="image-container">
<img src="/files/image/icon/avatar/{cnt_id}.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>{avatar.get("name")}</p>
<p>Effect: {avatar.get("effect")}</p>
</div>
<div>
<img src="/files/web/coin_icon.png" class="coin-icon" style="width: 40px; height: 40px;" alt="Coin Icon" />
<span>{AVATAR_PRICE}</span>
</div>
"""
else:
html = "<p>Avatar not found.</p>"
elif cnt_type == "3":
item = next((item for item in ITEM_LIST if item.get("id") == cnt_id), None)
if item:
html = f"""
<div class="image-container">
<img src="/files/image/icon/item/{cnt_id}.png" alt="Item Image" style="width: 180px; height: 180px;" />
</div>
<p>Would you like to purchase this item?</p>
<div>
<p>{item.get("name")}</p>
<p>Effect: {item.get("effect")}</p>
</div>
<div>
<img src="/files/web/coin_icon.png" class="coin-icon" style="width: 40px; height: 40px;" alt="Coin Icon" />
<span>{ITEM_PRICE}</span>
</div>
"""
else:
html = "<p>Item not found.</p>"
if cnt_type == "1" and cnt_id < 0:
source_html = f"files/dlc_4max.html"
else:
source_html = f"files/web_shop_detail.html"
html += f"""
<br>
<div class="buttons" style="margin-top: 20px;">
<a href="wwic://web_purchase_coin?cnt_type={cnt_type}&cnt_id={cnt_id}&num=1" class="bt_bg01" >Buy</a><br>
<a href="wwic://web_shop?cnt_type={cnt_type}" class="bt_bg01" >Go Back</a>
</div>
"""
try:
with open(source_html, "r", encoding="utf-8") as file:
html_content = file.read().format(text=html, coin=coin)
except FileNotFoundError:
return HTMLResponse("""<html><body><h1>Shop detail template not found</h1></body></html>""", status_code=500)
return HTMLResponse(html_content)
else:
return HTMLResponse("""<html><body><h1>Access denied</h1></body></html>""", status_code=403)
async def buy_by_coin(request: Request):
decrypted_fields, _ = await decrypt_fields(request)
if not decrypted_fields:
return Response("""<?xml version="1.0" encoding="UTF-8"?><response><code>1</code><result_url>coin_error.php</result_url></response>""", media_type="application/xml")
should_serve = True
if AUTHORIZATION_NEEDED:
should_serve = await check_whitelist(decrypted_fields) and not await check_blacklist(decrypted_fields)
if should_serve:
cnt_type = decrypted_fields[b'cnt_type'][0].decode()
cnt_id = int(decrypted_fields[b'cnt_id'][0].decode())
num = int(decrypted_fields[b'num'][0].decode())
device_id = decrypted_fields[b'vid'][0].decode()
fail_url = """<?xml version="1.0" encoding="UTF-8"?><response><code>1</code><result_url>coin_error.php</result_url></response>"""
query = select(daily_reward.c.my_stage, daily_reward.c.my_avatar, daily_reward.c.coin, daily_reward.c.item).where(daily_reward.c.device_id == device_id)
result = await database.fetch_one(query)
if not result:
return Response(fail_url, media_type="application/xml")
my_stage = set(json.loads(result["my_stage"])) if result["my_stage"] else set()
my_avatar = set(json.loads(result["my_avatar"])) if result["my_avatar"] else set()
coin = int(result["coin"]) if result["coin"] else 0
item = json.loads(result["item"]) if result["item"] else []
if cnt_type == "1":
if cnt_id == -1:
song_stage_price = 300
if coin < song_stage_price:
return Response(fail_url, media_type="application/xml")
for i in range(616, 950):
my_stage.add(i)
coin -= song_stage_price
else:
song_stage_price = STAGE_PRICE * 2 if len(SONG_LIST[cnt_id]["difficulty_levels"]) == 6 else STAGE_PRICE
if coin < song_stage_price or cnt_id in my_stage:
return Response(fail_url, media_type="application/xml")
coin -= song_stage_price
my_stage.add(cnt_id)
elif cnt_type == "2":
if coin < AVATAR_PRICE or cnt_id in my_avatar:
return Response(fail_url, media_type="application/xml")
coin -= AVATAR_PRICE
my_avatar.add(cnt_id)
elif cnt_type == "3":
if coin < ITEM_PRICE:
return Response(fail_url, media_type="application/xml")
coin -= ITEM_PRICE
item.append(cnt_id)
else:
return Response(fail_url, media_type="application/xml")
update_query = (
update(daily_reward)
.where(daily_reward.c.device_id == device_id)
.values(
my_stage=json.dumps(list(my_stage)),
my_avatar=json.dumps(list(my_avatar)),
coin=coin,
item=json.dumps(item)
)
)
await database.execute(update_query)
response = ET.Element("response")
ET.SubElement(response, "code").text = "0"
ET.SubElement(response, "result_url").text = "web_shop_result.php"
ET.SubElement(response, "cnt_type").text = cnt_type
ET.SubElement(response, "cnt_id").text = str(cnt_id)
ET.SubElement(response, "num").text = str(num)
if cnt_type == "1":
ET.SubElement(response, "stage_id").text = str(cnt_id)
response_string = ET.tostring(response, encoding="utf-8", method="xml").decode("utf-8")
return Response(response_string, media_type="application/xml")
else:
return Response("""<?xml version="1.0" encoding="UTF-8"?><response><code>1</code><result_url>coin_error.php</result_url></response>""", media_type="application/xml")
async def web_shop_result(request: Request):
decrypted_fields, _ = await decrypt_fields(request)
cnt_type = decrypted_fields[b'cnt_type'][0].decode()
return HTMLResponse(inform_page(f"""SUCCESS:<br>Purchase successful.<br>Please close this page and the reward will arrive shortly.<br>If it took too long, try restarting the game.<br><a href='wwic://web_shop?cnt_type={cnt_type}' class='bt_bg01' >Go Back</a>""", 2))
async def coin_error(request: Request):
return HTMLResponse(inform_page(f"""FAILED:<br>Either you don't have enough coin,<br>or there were a duplicate order, and the reward will arrive shortly.""", 2))
routes = [
Route('/web_shop.php', web_shop, methods=['GET', 'POST']),
Route('/web_shop_detail.php', web_shop_detail, methods=['GET', 'POST']),
Route('/buy_by_coin.php', buy_by_coin, methods=['GET']),
Route('/web_shop_result.php', web_shop_result, methods=['GET']),
Route('/coin_error.php', coin_error, methods=['GET']),
]
+58
View File
@@ -0,0 +1,58 @@
import json
import os
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,397] # 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]
TITLE_LISTS = {
0: SPECIAL_TITLES,
1: NORMAL_TITLES,
2: MASTER_TITLES,
3: GOD_TITLES,
}
def init_templates():
global SONG_LIST, AVATAR_LIST, ITEM_LIST, EXP_UNLOCKED_SONGS
base_path = 'api/config/'
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)
print("[TEMPLATES] Templates initialized successfully.")
except FileNotFoundError as e:
print(f"Error: {e}")
except json.JSONDecodeError as e:
print(f"Error decoding JSON: {e}")
+825
View File
@@ -0,0 +1,825 @@
from starlette.responses import Response, FileResponse, HTMLResponse
from starlette.requests import Request
from starlette.routing import Route
import os
from datetime import datetime
import json
import secrets
from sqlalchemy import select, update, insert
import xml.etree.ElementTree as ET
from config import ROOT_FOLDER, START_COIN, COIN_REWARD, AUTHORIZATION_NEEDED, HOST, PORT
from api.misc import is_alphanumeric, inform_page, verify_password, hash_password, crc32_decimal, get_model_pak, get_tune_pak, get_skin_pak, get_m4a_path, get_stage_path, get_stage_zero
from api.database import database, user, daily_reward, result, get_user_data, set_user_data, check_blacklist, check_whitelist
from api.crypt import decrypt_fields
from api.templates import START_AVATARS, START_STAGES, EXP_UNLOCKED_SONGS
async def info(request: Request):
file_path = os.path.join(ROOT_FOLDER, "files/history.html")
return FileResponse(file_path)
async def history(request: Request):
file_path = os.path.join(ROOT_FOLDER, "files/history.html")
return FileResponse(file_path)
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):
file_path = os.path.join(ROOT_FOLDER, "files/tier.xml")
return FileResponse(file_path)
async def reg(request: Request):
return Response("", status_code=200)
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 HTMLResponse(inform_page("FAILED:<br>Missing username or password.", 0))
if len(username) < 6 or len(username) > 20:
return HTMLResponse(inform_page("FAILED:<br>Username must be between 6 and 20 characters long.", 0))
if not is_alphanumeric(username):
return HTMLResponse(inform_page("FAILED:<br>Username must consist entirely of alphanumeric characters.", 0))
if username == password:
return HTMLResponse(inform_page("FAILED:<br>Username cannot be the same as password.", 0))
decrypted_fields, _ = await decrypt_fields(request)
if not decrypted_fields:
return HTMLResponse(inform_page("FAILED:<br>Invalid request data.", 0))
if not await check_blacklist(decrypted_fields):
return HTMLResponse(inform_page("FAILED:<br>Your account is banned and you are not allowed to perform this action.", 0))
user_exist = await get_user_data(decrypted_fields, "username")
if user_exist:
query = select(user.c.id).where(user.c.username == username)
existing_user = await database.fetch_one(query)
if existing_user:
return HTMLResponse(inform_page("FAILED:<br>Another user already has this name.", 0))
password_hash = await get_user_data(decrypted_fields, "password_hash")
if password_hash:
if verify_password(password, password_hash):
await set_user_data(decrypted_fields, "username", username)
return HTMLResponse(inform_page("SUCCESS:<br>Username updated.", 0))
else:
return HTMLResponse(inform_page("FAILED:<br>Password is not correct.<br>Please try again.", 0))
else:
return HTMLResponse(inform_page("FAILED:<br>User has no password hash.<br>This should not happen.", 0))
else:
return HTMLResponse(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 HTMLResponse(inform_page("FAILED:<br>Missing old or new password.", 0))
decrypted_fields, _ = await decrypt_fields(request)
if not decrypted_fields:
return HTMLResponse(inform_page("FAILED:<br>Invalid request data.", 0))
username = await get_user_data(decrypted_fields, "username")
if username:
if username == new_password:
return HTMLResponse(inform_page("FAILED:<br>Username cannot be the same as password.", 0))
if len(new_password) < 6:
return HTMLResponse(inform_page("FAILED:<br>Password must have 6 or more characters.", 0))
old_hash = await get_user_data(decrypted_fields, "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)
await set_user_data(decrypted_fields, "password_hash", hashed_new_password)
return HTMLResponse(inform_page("SUCCESS:<br>Password updated.", 0))
else:
return HTMLResponse(inform_page("FAILED:<br>Old password is not correct.<br>Please try again.", 0))
else:
return HTMLResponse(inform_page("FAILED:<br>User has no password hash.<br>This should not happen.", 0))
else:
return HTMLResponse(inform_page("FAILED:<br>User does not exist.<br>This should not happen.", 0))
async def coin_mp(request: Request):
form = await request.form()
mp = int(form.get("coin_mp"))
if not mp:
return HTMLResponse(inform_page("FAILED:<br>Missing multiplier.", 0))
if mp < 0 or mp > 5:
return HTMLResponse(inform_page("FAILED:<br>Multiplier not acceptable.", 0))
decrypted_fields, _ = await decrypt_fields(request)
if not decrypted_fields:
return HTMLResponse(inform_page("FAILED:<br>Invalid request data.", 0))
user_exist = await get_user_data(decrypted_fields, "username")
if user_exist:
await set_user_data(decrypted_fields, "coin_mp", mp)
return HTMLResponse(inform_page("SUCCESS:<br>Coin multiplier set to " + str(mp) + ".", 0))
else:
return HTMLResponse(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 HTMLResponse(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 HTMLResponse(inform_page("FAILED:<br>Save ID not acceptable format.", 0))
decrypted_fields, _ = await decrypt_fields(request)
if not decrypted_fields:
return HTMLResponse(inform_page("FAILED:<br>Invalid request data.", 0))
user_exist = await get_user_data(decrypted_fields, "username")
if user_exist:
query = select(user.c.data, user.c.crc).where(user.c.save_id == save_id)
existing_save_data = await database.fetch_one(query)
if existing_save_data:
query = update(user).where(user.c.device_id == decrypted_fields[b'vid'][0].decode()).values(
data=existing_save_data["data"],
crc=existing_save_data["crc"],
timestamp=datetime.now()
)
await database.execute(query)
return HTMLResponse(inform_page("SUCCESS:<br>Save migration was applied. If this was done by mistake, press the Save button now.", 0))
else:
return HTMLResponse(inform_page("FAILED:<br>Save ID is not associated with a save file.", 0))
else:
return HTMLResponse(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 HTMLResponse(inform_page("FAILED:<br>Missing username or password.", 0))
if username == password:
return HTMLResponse(inform_page("FAILED:<br>Username cannot be the same as password.", 0))
if len(username) < 6 or len(username) > 20:
return HTMLResponse(inform_page("FAILED:<br>Username must be between 6 and 20<br>characters long.", 0))
if len(password) < 6:
return HTMLResponse(inform_page("FAILED:<br>Password must have<br>6 or above characters.", 0))
if not is_alphanumeric(username):
return HTMLResponse(inform_page("FAILED:<br>Username must consist entirely of<br>alphanumeric characters.", 0))
decrypted_fields, _ = await decrypt_fields(request)
if not decrypted_fields:
return HTMLResponse(inform_page("FAILED:<br>Invalid request data.", 0))
query = select(user.c.id).where(user.c.username == username)
existing_user = await database.fetch_one(query)
if existing_user:
return HTMLResponse(inform_page("FAILED:<br>Another user already has this name.", 0))
insert_query = insert(user).values(
username=username,
password_hash=hash_password(password),
device_id=decrypted_fields[b'vid'][0].decode(),
data="",
crc=0,
coin_mp=1,
)
await database.execute(insert_query)
return HTMLResponse(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 HTMLResponse(inform_page("FAILED:<br>Invalid request data.", 0))
if not await check_blacklist(decrypted_fields):
return HTMLResponse(inform_page("FAILED:<br>Your account is banned and you are<br>not allowed to perform this action.", 0))
await set_user_data(decrypted_fields, "device_id", "")
return HTMLResponse(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 HTMLResponse(inform_page("FAILED:<br>Missing username or password.", 0))
decrypted_fields, _ = await decrypt_fields(request)
if not decrypted_fields:
return HTMLResponse(inform_page("FAILED:<br>Invalid request data.", 0))
query = select(user.c.id).where(user.c.username == username)
user_record = await database.fetch_one(query)
if user_record:
user_id = user_record[0]
query = select(user.c.password_hash).where(user.c.id == user_id)
password_hash_record = await database.fetch_one(query)
if password_hash_record and verify_password(password, password_hash_record[0]):
update_query = (
update(user)
.where(user.c.id == user_id)
.values(device_id=decrypted_fields[b'vid'][0].decode())
)
await database.execute(update_query)
return HTMLResponse(inform_page("SUCCESS:<br>You are logged in.", 0))
else:
return HTMLResponse(inform_page("FAILED:<br>Username or password incorrect.", 0))
else:
return HTMLResponse(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")
data = await get_user_data(decrypted_fields, "data")
if data and data != "":
crc = await get_user_data(decrypted_fields, "crc")
timestamp = await get_user_data(decrypted_fields, "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></response>""", media_type="application/xml")
data = await request.body()
data = data.decode("utf-8")
username = await get_user_data(decrypted_fields, "username")
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))
check_query = select(user.c.id).where(user.c.save_id == save_id)
existing_user = await database.fetch_one(check_query)
if not existing_user:
is_save_id_unique = True
update_query = (
update(user)
.where(user.c.device_id == decrypted_fields[b'vid'][0].decode())
.values(data=data, crc=crc, save_id=save_id, timestamp=formatted_time)
)
await database.execute(update_query)
return Response("""<response><code>0</code></response>""", media_type="application/xml")
else:
return Response("""<response><code>10</code></response>""", media_type="application/xml")
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"
)
file_path = os.path.join(ROOT_FOLDER, "files/start.xml")
try:
tree = ET.parse(file_path)
root = tree.getroot()
except Exception as e:
return Response(f"""<response><code>500</code><message>Error parsing XML: {str(e)}</message></response>""", media_type="application/xml")
username = await get_user_data(decrypted_fields, "username")
user_id = await get_user_data(decrypted_fields, "id")
should_serve = True
if AUTHORIZATION_NEEDED:
should_serve = await check_whitelist(decrypted_fields) and not await check_blacklist(decrypted_fields)
if not should_serve:
return Response("""<response><code>403</code><message>Access denied.</message></response>""", media_type="application/xml")
host_string = "http://" + HOST + ":" + str(PORT) + "/"
device_id = decrypted_fields[b'vid'][0].decode()
for generator in [get_model_pak, get_tune_pak, get_skin_pak, get_m4a_path, get_stage_path]:
try:
root.append(generator(host_string))
except Exception as e:
return Response(f"""<response><code>500</code><message>Error generating XML element: {str(e)}</message></response>""", media_type="application/xml")
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
query = select(daily_reward.c.day, daily_reward.c.timestamp).where(daily_reward.c.device_id == device_id)
row = await database.fetch_one(query)
if row:
current_day = row["day"]
last_timestamp = row["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)
query = select(daily_reward.c.my_avatar, daily_reward.c.my_stage, daily_reward.c.coin).where(daily_reward.c.device_id == device_id)
result_obj = await database.fetch_one(query)
if result_obj:
my_avatar = set(json.loads(result_obj[0])) if result_obj[0] else START_AVATARS
my_stage = set(json.loads(result_obj[1])) if result_obj[1] else START_STAGES
coin = result_obj[2] if result_obj[2] is not None else START_COIN
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"
)
device_id = decrypted_fields[b'vid'][0].decode()
file_path = os.path.join(ROOT_FOLDER, "files/sync.xml")
try:
tree = ET.parse(file_path)
root = tree.getroot()
except Exception as e:
return Response(
f"""<response><code>500</code><message>Error parsing XML: {str(e)}</message></response>""",
media_type="application/xml"
)
username = await get_user_data(decrypted_fields, "username")
should_serve = True
if AUTHORIZATION_NEEDED:
should_serve = await check_whitelist(decrypted_fields) and not await check_blacklist(decrypted_fields)
if not should_serve:
return Response(
"""<response><code>403</code><message>Access denied.</message></response>""",
media_type="application/xml"
)
host_string = "http://" + HOST + ":" + str(PORT) + "/"
root.append(get_model_pak(host_string))
root.append(get_tune_pak(host_string))
root.append(get_skin_pak(host_string))
root.append(get_m4a_path(host_string))
root.append(get_stage_path(host_string))
query = select(daily_reward.c.my_avatar, daily_reward.c.my_stage, daily_reward.c.coin, daily_reward.c.item).where(daily_reward.c.device_id == device_id)
result_obj = await database.fetch_one(query)
if result_obj:
my_avatar = set(json.loads(result_obj[0])) if result_obj[0] else START_AVATARS
my_stage = set(json.loads(result_obj[1])) if result_obj[1] else START_STAGES
coin = result_obj[2] if result_obj[2] is not None else START_COIN
items = json.loads(result_obj[3]) if result_obj[3] 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:
update_query = (
update(daily_reward)
.where(daily_reward.c.device_id == device_id)
.values(item="[]")
)
await database.execute(update_query)
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 ttag(request: Request):
decrypted_fields, original_field = await decrypt_fields(request)
if not decrypted_fields:
return HTMLResponse("""<html><body><h1>Invalid request data</h1></body></html>""", status_code=400)
username = await get_user_data(decrypted_fields, "username")
if username:
gcoin_mp = await get_user_data(decrypted_fields, "coin_mp")
savefile_id = await get_user_data(decrypted_fields, "save_id")
with open("files/profile.html", "r") as file:
html_content = file.read().format(
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("files/register.html", "r") as file:
html_content = file.read().format(pid=original_field)
return HTMLResponse(html_content)
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()
file_path = os.path.join(ROOT_FOLDER, "files/start.xml")
try:
tree = ET.parse(file_path)
root = tree.getroot()
except Exception as e:
return Response(f"""<response><code>500</code><message>Error parsing XML: {str(e)}</message></response>""", media_type="application/xml")
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)
query = select(daily_reward.c.day, daily_reward.c.timestamp, daily_reward.c.my_avatar, daily_reward.c.my_stage).where(daily_reward.c.device_id == device_id)
row = await database.fetch_one(query)
time = datetime.now()
if row:
current_day = row["day"]
last_timestamp = row["timestamp"]
my_avatar = set(json.loads(row["my_avatar"])) if row["my_avatar"] else set()
my_stage = set(json.loads(row["my_stage"])) if row["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(json.loads(my_stage)) if my_stage else set()
if cnt_id not in stages:
stages.add(cnt_id)
my_stage = json.dumps(list(stages))
update_query = (
update(daily_reward)
.where(daily_reward.c.device_id == device_id)
.values(timestamp=time, day=current_day, my_stage=my_stage)
)
await database.execute(update_query)
elif cnt_type == 2:
avatars = set(json.loads(my_avatar)) if my_avatar else set()
if cnt_id not in avatars:
avatars.add(cnt_id)
my_avatar = json.dumps(list(avatars))
update_query = (
update(daily_reward)
.where(daily_reward.c.device_id == device_id)
.values(timestamp=time, day=current_day, my_avatar=my_avatar)
)
await database.execute(update_query)
else:
update_query = (
update(daily_reward)
.where(daily_reward.c.device_id == device_id)
.values(timestamp=time, day=current_day)
)
await database.execute(update_query)
else:
update_query = (
update(daily_reward)
.where(daily_reward.c.device_id == device_id)
.values(timestamp=time, day=current_day)
)
await database.execute(update_query)
xml_response = "<response><code>0</code></response>"
else:
xml_response = "<response><code>1</code></response>"
else:
insert_query = insert(daily_reward).values(
device_id=device_id,
day=1,
timestamp=time,
my_avatar=json.dumps(START_AVATARS),
my_stage=json.dumps(START_STAGES),
coin=START_COIN,
item="[]",
lvl=1,
title=1,
avatar=1
)
await database.execute(insert_query)
xml_response = "<response><code>0</code></response>"
return Response(xml_response, media_type="application/xml")
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 = True
if AUTHORIZATION_NEEDED:
should_serve = await check_whitelist(decrypted_fields) and not await check_blacklist(decrypted_fields)
if not should_serve:
return Response("""<response><code>403</code><message>Access denied.</message></response>""", media_type="application/xml")
device_id = decrypted_fields[b'vid'][0].decode()
file_path = os.path.join(ROOT_FOLDER, "files/result.xml")
try:
tree = ET.parse(file_path)
root = tree.getroot()
except Exception as e:
return Response(f"""<response><code>500</code><message>Error parsing XML: {str(e)}</message></response>""", media_type="application/xml")
vid = decrypted_fields[b'vid'][0].decode()
stts = decrypted_fields[b'stts'][0].decode()
track_id = decrypted_fields[b'id'][0].decode()
mode = decrypted_fields[b'mode'][0].decode()
avatar = 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 = decrypted_fields[b'item'][0].decode()
device_os = decrypted_fields[b'os'][0].decode()
os_ver = decrypted_fields[b'os_ver'][0].decode()
tid = decrypted_fields[b'tid'][0].decode()
ver = decrypted_fields[b'ver'][0].decode()
mike = decrypted_fields[b'mike'][0].decode()
if int(track_id) not in range(616, 1024) or int(mode) not in range(0, 4):
query = select(daily_reward.c.coin).where(daily_reward.c.device_id == device_id)
row = await database.fetch_one(query)
query = select(user.c.coin_mp).where(user.c.device_id == device_id)
coin_mp_row = await database.fetch_one(query)
current_coin = row["coin"] if row and row["coin"] else START_COIN
updated_coin = current_coin + COIN_REWARD * coin_mp_row["coin_mp"]
update_query = (
update(daily_reward)
.where(daily_reward.c.device_id == device_id)
.values(coin=updated_coin)
)
await database.execute(update_query)
query = select(user.c.id).where(user.c.device_id == vid)
user_row = await database.fetch_one(query)
sid = user_row["id"] if user_row else ""
do_insert = False
do_update_sid = False
do_update_vid = False
last_row_id = 0
if sid:
query = select(result.c.rid, result.c.score).where(
(result.c.id == track_id) &
(result.c.mode == mode) &
(result.c.sid == sid)
).order_by(result.c.score.desc())
records = await database.fetch_all(query)
if records:
last_row_id = records[0]["rid"]
if score > int(records[0]["score"]):
do_update_sid = True
else:
do_insert = True
else:
query = select(result.c.rid, result.c.score).where(
(result.c.id == track_id) &
(result.c.mode == mode) &
(result.c.sid == "") &
(result.c.vid == vid)
).order_by(result.c.score.desc())
records = await database.fetch_all(query)
if records:
last_row_id = records[0]["rid"]
if score > records[0]["score"]:
do_update_vid = True
else:
do_insert = True
if do_insert:
insert_query = insert(result).values(
vid=vid, stts=stts, id=track_id, mode=mode, avatar=avatar,
score=score, high_score=high_score, play_rslt=play_rslt, item=item,
os=device_os, os_ver=os_ver, tid=tid, sid=sid, ver=ver, mike=mike
)
result_obj = await database.execute(insert_query)
last_row_id = result_obj
elif do_update_sid:
update_query = (
update(result)
.where((result.c.sid == sid) & (result.c.id == track_id) & (result.c.mode == mode))
.values(
stts=stts, avatar=avatar, score=score, high_score=high_score,
play_rslt=play_rslt, item=item, os=device_os, os_ver=os_ver,
tid=tid, ver=ver, mike=mike, vid=vid
)
)
await database.execute(update_query)
elif do_update_vid:
update_query = (
update(result)
.where((result.c.vid == vid) & (result.c.id == track_id) & (result.c.mode == mode))
.values(
stts=stts, avatar=avatar, score=score, high_score=high_score,
play_rslt=play_rslt, item=item, os=device_os, os_ver=os_ver,
sid=sid, ver=ver, mike=mike
)
)
await database.execute(update_query)
query = select(daily_reward.c.my_stage).where(daily_reward.c.device_id == device_id)
row = await database.fetch_one(query)
my_stage = set(json.loads(row["my_stage"])) if row and row["my_stage"] else set(START_STAGES)
current_exp = int(stts.split(",")[0])
for song in EXP_UNLOCKED_SONGS:
if song["lvl"] <= current_exp:
my_stage.add(song["id"])
my_stage = sorted(my_stage)
update_query = (
update(daily_reward)
.where(daily_reward.c.device_id == device_id)
.values(lvl=current_exp, avatar=int(avatar), my_stage=json.dumps(my_stage))
)
await database.execute(update_query)
query = select(result.c.rid, result.c.score).where(
(result.c.id == track_id) & (result.c.mode == mode)
).order_by(result.c.score.desc())
records = await database.fetch_all(query)
rank = None
for idx, record in enumerate(records, start=1):
if record["rid"] == last_row_id:
rank = idx
break
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('/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('/name_reset/', name_reset, methods=['POST']),
Route('/password_reset/', password_reset, methods=['POST']),
Route('/coin_mp/', 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('/start.php', start, methods=['GET']),
Route('/sync.php', sync, methods=['GET', 'POST']),
Route('/ttag.php', ttag, methods=['GET']),
Route('/login_bonus.php', bonus, methods=['GET']),
Route('/result.php', result_request, methods=['GET']),
]
+18
View File
@@ -0,0 +1,18 @@
HOST = 192.168.0.106
PORT = 9068
ACTUAL_HOST = 192.168.0.106
ACTUAL_PORT = 9068
MODEL = 202504125800
TUNEFILE = 202504125800
SKIN = 202404191149
STAGE_PRICE = 1
AVATAR_PRICE = 1
ITEM_PRICE = 2
COIN_REWARD = 1
START_COIN = 10
AUTHORIZATION_NEEDED = False
DEBUG = True
+63
View File
@@ -0,0 +1,63 @@
from starlette.config import Config
import os
'''
Do not change the name of this file.
不要改动这个文件的名称。
'''
config = Config("config.env")
'''
IP and port of the server.
服务器的IP和端口。
'''
HOST = config("HOST", default="192.168.0.106")
PORT = int(config("PORT", default=9070))
ACTUAL_HOST = config("ACTUAL_HOST", default="192.168.0.106")
ACTUAL_PORT = int(config("ACTUAL_PORT", default=9070))
'''
Datecode of the 3 pak files.
三个pak文件的时间戳。
'''
MODEL = config("MODEL", cast=str, default="202504125800")
TUNEFILE = config("TUNEFILE", cast=str, default="202504125800")
SKIN = config("SKIN", cast=str, default="202404191149")
'''
Groove Coin-related settings.
GCoin相关设定。
'''
STAGE_PRICE = config("STAGE_PRICE", cast=int, default=1)
AVATAR_PRICE = config("AVATAR_PRICE", cast=int, default=1)
ITEM_PRICE = config("ITEM_PRICE", cast=int, default=2)
COIN_REWARD = config("COIN_REWARD", cast=int, default=1)
START_COIN = config("START_COIN", cast=int, default=10)
'''
Only the whitelisted playerID can use the service. Blacklist has priority over whitelist.
只有白名单的玩家ID才能使用服务。黑名单优先于白名单。
'''
AUTHORIZATION_NEEDED = config("AUTHORIZATION_NEEDED", cast=bool, default=False)
'''
SSL证书路径 - 留空则使用HTTP
SSL certificate path. If left blank, use HTTP.
'''
SSL_CERT = config("SSL_CERT", default=None)
SSL_KEY = config("SSL_KEY", default=None)
'''
Flask default debug
Flask内置Debug
'''
DEBUG = config("DEBUG", cast=bool, default=False)
ROOT_FOLDER = os.path.dirname(os.path.abspath(__file__))
View File
+1 -1
View File
@@ -1,4 +1,4 @@
flask
starlette
bcrypt
pycryptodome
requests