server notice hot update

This commit is contained in:
UnitedAirforce
2025-12-03 10:06:08 +08:00
parent cf409d78a1
commit 1b2086b6d0
9 changed files with 214 additions and 40 deletions

View File

@@ -189,7 +189,7 @@ async def register(request: Request):
if not decrypted_fields:
return inform_page("FAILED:<br>Invalid request data.", 0)
user_info, _ = await decrypt_fields_to_user_info(decrypted_fields)
user_info = await user_name_to_user_info(username)
if user_info:
return inform_page("FAILED:<br>Another user already has this name.", 0)

View File

@@ -4,6 +4,8 @@ from starlette.responses import HTMLResponse, JSONResponse, RedirectResponse
import sqlalchemy
import json
from datetime import datetime
import os
import xml.etree.ElementTree as ET
from api.database import player_database, accounts, results, devices, whitelists, blacklists, batch_tokens, binds, webs, logs, is_admin, read_user_save_file, write_user_save_file
from api.misc import crc32_decimal
@@ -11,11 +13,11 @@ from api.misc import crc32_decimal
TABLE_MAP = {
"accounts": (accounts, ["id", "username", "password_hash", "save_crc", "save_timestamp", "save_id", "coin_mp", "title", "avatar", "mobile_delta", "arcade_delta", "total_delta", "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"]),
"devices": (devices, ["device_id", "user_id", "my_stage", "my_avatar", "item", "daily_day", "coin", "lvl", "title", "avatar", "created_at", "updated_at", "last_login_at"]),
"devices": (devices, ["device_id", "user_id", "my_stage", "my_avatar", "item", "daily_day", "coin", "lvl", "title", "avatar", "created_at", "updated_at", "bind_token", "last_login_at"]),
"whitelist": (whitelists, ["id", "device_id"]),
"blacklist": (blacklists, ["id", "ban_terms", "reason"]),
"batch_tokens": (batch_tokens, ["id", "batch_token", "expire_at", "uses_left", "auth_id", "created_at", "updated_at"]),
"binds": (binds, ["id", "user_id", "bind_account", "bind_code", "is_verified", "auth_token", "bind_date"]),
"binds": (binds, ["id", "user_id", "bind_account", "bind_code", "is_verified", "bind_date"]),
"webs": (webs, ["id", "user_id", "permission", "web_token", "last_save_export", "created_at", "updated_at"]),
"logs": (logs, ["id", "user_id", "filename", "filesize", "timestamp"]),
}
@@ -346,6 +348,40 @@ async def web_admin_data_save(request: Request):
return JSONResponse({"status": "success", "message": "Data saved successfully."})
async def web_admin_update_maintenance(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()
status = params.get("status")
message_en = params.get("message_en")
message_ja = params.get("message_ja")
message_fr = params.get("message_fr")
message_it = params.get("message_it")
# Create the XML structure directly
notice_xml = f"""<?xml version="1.0" encoding="UTF-8"?>
<response>
<code>{status}</code>
<message>
<en>{message_en}</en>
<ja>{message_ja}</ja>
<fr>{message_fr}</fr>
<it>{message_it}</it>
</message>
</response>
"""
# Save the XML to the file
notice_file_path = os.path.join('files/notice.xml')
try:
with open(notice_file_path, 'w', encoding='utf-8') as f:
f.write(notice_xml)
return JSONResponse({"status": "success", "message": "Maintenance settings updated successfully."})
except Exception as e:
return JSONResponse({"status": "failed", "message": f"An error occurred: {str(e)}"}, status_code=500)
routes = [
Route("/admin", web_admin_page, methods=["GET"]),
Route("/admin/", web_admin_page, methods=["GET"]),
@@ -355,4 +391,5 @@ routes = [
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"]),
Route("/admin/update_maintenance", web_admin_update_maintenance, methods=["POST"])
]

View File

@@ -64,7 +64,8 @@ devices = Table(
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)
Column("last_login_at", DateTime, default=None),
Column("bind_token", String(64), unique=True, nullable=True)
)
results = Table(
@@ -141,7 +142,6 @@ binds = Table(
Column("bind_account", String(128), unique=True, nullable=False),
Column("bind_code", String(6), nullable=False),
Column("is_verified", Integer, default=0),
Column("auth_token", String(64), unique=True),
Column("bind_date", DateTime, default=datetime.utcnow)
)
@@ -191,13 +191,13 @@ 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:
async with db.execute("PRAGMA table_info(devices);") 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 "bind_token" not in columns:
await db.execute("ALTER TABLE devices ADD COLUMN bind_token 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
@@ -212,14 +212,16 @@ async def get_bind(user_id):
result = await player_database.fetch_one(query)
return dict(result) if result else None
async def refresh_bind(user_id):
async def refresh_bind(user_id, device_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(
auth_token=new_auth_token
update_query = update(devices).where(devices.c.device_id == device_id).values(
bind_token=new_auth_token
)
await player_database.execute(update_query)
return new_auth_token
return ""
async def log_download(user_id, filename, filesize):
query = logs.insert().values(
@@ -255,7 +257,7 @@ async def verify_user_code(code, user_id):
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")
bind_date=datetime.utcnow()
)
await player_database.execute(update_query)
return "Verified and account successfully bound."
@@ -276,6 +278,12 @@ async def decrypt_fields_to_user_info(decrypted_fields):
return None, None
async def get_device_info(device_id):
query = devices.select().where(devices.c.device_id == device_id)
device_record = await player_database.fetch_one(query)
device_record = dict(device_record) if device_record else None
return device_record
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)
@@ -317,7 +325,7 @@ async def check_blacklist(decrypted_fields):
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_query = select(devices.c.my_stage, devices.c.my_avatar).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 []

View File

@@ -34,14 +34,19 @@ async def serve_file(request: Request):
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)
existing_device_query = select(devices).where((devices.c.bind_token == auth_token))
existing_device = await player_database.fetch_one(existing_device_query)
if not existing_device:
return Response("Unauthorized - device not found", status_code=403)
existing_bind_query = select(binds).where((binds.c.user_id == existing_device['user_id']) & (binds.c.is_verified == 1))
bind_result = await player_database.fetch_one(existing_bind_query)
if not bind_result:
return Response("Unauthorized - bind not verified", status_code=403)
daily_bytes = await get_downloaded_bytes(bind_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))
@@ -55,7 +60,7 @@ async def serve_file(request: Request):
# get size of file
if AUTHORIZATION_MODE != 0:
file_size = os.path.getsize(file_path)
await log_download(result['user_id'], filename, file_size)
await log_download(bind_result['user_id'], filename, file_size)
return FileResponse(file_path)
else:
return Response("File not found", status_code=404)

View File

@@ -7,8 +7,11 @@ import bcrypt
import hashlib
import re
import xml.etree.ElementTree as ET
import copy
import os
from config import MODEL, TUNEFILE, SKIN, AUTHORIZATION_NEEDED, AUTHORIZATION_MODE, GRANDFATHERED_ACCOUNT_LIMIT, BIND_SALT
from api.database import get_bind, check_whitelist, check_blacklist, decrypt_fields_to_user_info, user_id_to_user_info_simple
from api.database import get_bind, check_whitelist, check_blacklist, decrypt_fields_to_user_info, user_id_to_user_info_simple, get_device_info, refresh_bind
from api.template import START_XML
FMAX_VER = None
FMAX_RES = None
@@ -75,18 +78,22 @@ async def get_model_pak(decrypted_fields, user_id):
mid = ET.Element("model_pak")
rid = ET.Element("date")
uid = ET.Element("url")
device_id = decrypted_fields[b'vid'][0].decode()
host = await get_host_string()
if AUTHORIZATION_MODE == 0:
auth_token = decrypted_fields[b'vid'][0].decode()
auth_token = device_id
rid.text = MODEL
uid.text = host + "files/gc2/" + auth_token + "/pak/model" + MODEL + ".pak"
else:
if user_id:
device_info = await get_device_info(device_id)
bind_info = await get_bind(user_id)
if bind_info and bind_info['is_verified'] == 1:
auth_token = bind_info['auth_token']
auth_token = device_info['bind_token']
if not auth_token:
auth_token = await refresh_bind(user_id, device_id)
rid.text = MODEL
uid.text = host + "files/gc2/" + auth_token + "/pak/model" + MODEL + ".pak"
else:
@@ -104,18 +111,20 @@ async def get_tune_pak(decrypted_fields, user_id):
mid = ET.Element("tuneFile_pak")
rid = ET.Element("date")
uid = ET.Element("url")
device_id = decrypted_fields[b'vid'][0].decode()
host = await get_host_string()
if AUTHORIZATION_MODE == 0:
auth_token = decrypted_fields[b'vid'][0].decode()
auth_token = device_id
rid.text = TUNEFILE
uid.text = host + "files/gc2/" + auth_token + "/pak/tuneFile" + TUNEFILE + ".pak"
else:
if user_id:
device_info = await get_device_info(device_id)
bind_info = await get_bind(user_id)
if bind_info and bind_info['is_verified'] == 1:
auth_token = bind_info['auth_token']
auth_token = device_info['bind_token']
rid.text = TUNEFILE
uid.text = host + "files/gc2/" + auth_token + "/pak/tuneFile" + TUNEFILE + ".pak"
else:
@@ -133,18 +142,20 @@ async def get_skin_pak(decrypted_fields, user_id):
mid = ET.Element("skin_pak")
rid = ET.Element("date")
uid = ET.Element("url")
device_id = decrypted_fields[b'vid'][0].decode()
host = await get_host_string()
if AUTHORIZATION_MODE == 0:
auth_token = decrypted_fields[b'vid'][0].decode()
auth_token = device_id
rid.text = SKIN
uid.text = host + "files/gc2/" + auth_token + "/pak/skin" + SKIN + ".pak"
else:
if user_id:
device_info = await get_device_info(device_id)
bind_info = await get_bind(user_id)
if bind_info and bind_info['is_verified'] == 1:
auth_token = bind_info['auth_token']
auth_token = device_info['bind_token']
rid.text = SKIN
uid.text = host + "files/gc2/" + auth_token + "/pak/skin" + SKIN + ".pak"
else:
@@ -160,16 +171,18 @@ async def get_skin_pak(decrypted_fields, user_id):
async def get_m4a_path(decrypted_fields, user_id):
host = await get_host_string()
device_id = decrypted_fields[b'vid'][0].decode()
if AUTHORIZATION_MODE == 0:
auth_token = decrypted_fields[b'vid'][0].decode()
auth_token = device_id
mid = ET.Element("m4a_path")
mid.text = host + "files/gc2/" + auth_token + "/audio/"
else:
if user_id:
device_info = await get_device_info(device_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/"
mid.text = host + "files/gc2/" + device_info['bind_token'] + "/audio/"
else:
mid = ET.Element("m4a_path")
mid.text = host
@@ -181,16 +194,18 @@ async def get_m4a_path(decrypted_fields, user_id):
async def get_stage_path(decrypted_data, user_id):
host = await get_host_string()
device_id = decrypted_data[b'vid'][0].decode()
if AUTHORIZATION_MODE == 0:
auth_token = decrypted_data[b'vid'][0].decode()
auth_token = device_id
mid = ET.Element("stage_path")
mid.text = host + "files/gc2/" + auth_token + "/stage/"
else:
if user_id:
device_info = await get_device_info(device_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/"
mid.text = host + "files/gc2/" + device_info['bind_token'] + "/stage/"
else:
mid = ET.Element("stage_path")
mid.text = host
@@ -293,3 +308,14 @@ 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
async def get_start_xml():
root = copy.deepcopy(START_XML.getroot())
with open(os.path.join('files/notice.xml'), 'r', encoding='utf-8') as f:
response_xml = ET.parse(f)
response_root = response_xml.getroot()
for child in response_root:
root.append(child)
return root

View File

@@ -1,14 +1,13 @@
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.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, get_start_xml
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
@@ -62,7 +61,7 @@ async def start(request: Request):
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())
root = await get_start_xml()
user_info, device_info = await decrypt_fields_to_user_info(decrypted_fields)
username = user_info['username'] if user_info else None
@@ -74,7 +73,7 @@ async def start(request: Request):
return Response("""<response><code>403</code><message>Access denied.</message></response>""", media_type="application/xml")
if user_id:
await refresh_bind(user_id)
_ = await refresh_bind(user_id, device_id)
root.append(await get_model_pak(decrypted_fields, user_id))
root.append(await get_tune_pak(decrypted_fields, user_id))
@@ -265,7 +264,7 @@ async def bonus(request: Request):
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())
root = await get_start_xml()
daily_reward_elem = root.find(".//login_bonus")
last_count_elem = daily_reward_elem.find("last_count")

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<response>
<code>1</code>
<message>
<en>testtest</en>
<ja>Welcome to the private server!</ja>
<fr>Welcome to the private server!</fr>
<it>Welcome to the private server!</it>
</message>
</response>

View File

@@ -1,5 +1,4 @@
<?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>

View File

@@ -82,6 +82,7 @@
<li class="nav-item"><a class="nav-link" href="#" id="bindTab">Bind</a></li>
<li class="nav-item"><a class="nav-link" href="#" id="websTab">Webs</a></li>
<li class="nav-item"><a class="nav-link" href="#" id="logsTab">Logs</a></li>
<li class="nav-item"><a class="nav-link" href="#" id="maintenanceTab">Maintenance</a></li>
</ul>
<div class="d-flex ms-auto">
<button class="btn btn-primary me-2" id="searchBtn">
@@ -173,6 +174,38 @@
</div>
</div>
</div>
<!-- Maintenance Settings Modal -->
<div class="modal fade" id="maintenanceModal" tabindex="-1" aria-labelledby="maintenanceModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg modal-dialog-scrollable">
<div class="modal-content">
<form id="maintenanceForm">
<div class="modal-header">
<h5 class="modal-title" id="maintenanceModalLabel">Maintenance Settings</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body" style="max-height: 60vh; overflow-y: auto;">
<div class="row mb-3">
<div class="col-md-2">
<label class="form-label">Status</label>
<input type="number" class="form-control" id="ServerMaintenanceStatus" placeholder="Enter status code">
</div>
</div>
<div class="mb-3">
<label class="form-label">Message</label>
<input type="text" class="form-control mb-1" id="ServerTipsEn" placeholder="English">
<input type="text" class="form-control mb-1" id="ServerTipsJa" placeholder="Japanese">
<input type="text" class="form-control mb-1" id="ServerTipsFr" placeholder="French">
<input type="text" class="form-control mb-1" id="ServerTipsIt" placeholder="Italian">
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-primary" onclick="updateMaintenance()">Save Changes</button>
</div>
</form>
</div>
</div>
</div>
<script>
window.currentTableName = null;
let dataUserId = -1;
@@ -483,6 +516,62 @@ document.getElementById('batchTokensTab').onclick = () => showTable("batch_token
document.getElementById('bindTab').onclick = () => showTable("binds");
document.getElementById('websTab').onclick = () => showTable("webs");
document.getElementById('logsTab').onclick = () => showTable("logs");
document.getElementById('maintenanceTab').onclick = () => editMaintenance();
function load_maintenance() {
fetch('/files/notice.xml?timestamp=' + new Date().getTime())
.then(response => response.text())
.then(xmlData => {
const parser = new DOMParser();
const xmlDoc = parser.parseFromString(xmlData, "application/xml");
const statusCode = xmlDoc.querySelector("response > code").textContent;
const messageEn = xmlDoc.querySelector("response > message > en").textContent;
const messageJa = xmlDoc.querySelector("response > message > ja").textContent;
const messageFr = xmlDoc.querySelector("response > message > fr").textContent;
const messageIt = xmlDoc.querySelector("response > message > it").textContent;
document.getElementById("ServerMaintenanceStatus").value = parseInt(statusCode, 10);
document.getElementById("ServerTipsEn").value = messageEn;
document.getElementById("ServerTipsJa").value = messageJa;
document.getElementById("ServerTipsFr").value = messageFr;
document.getElementById("ServerTipsIt").value = messageIt;
console.log('Maintenance data loaded successfully.');
})
.catch(error => console.error('Error loading notice.xml:', error));
}
load_maintenance();
function editMaintenance() {
const maintenanceModal = new bootstrap.Modal(document.getElementById('maintenanceModal'));
maintenanceModal.show();
}
async function updateMaintenance() {
const status = parseInt(document.getElementById('ServerMaintenanceStatus').value, 10) || 0;
const message_en = document.getElementById('ServerTipsEn').value || "";
const message_ja = document.getElementById('ServerTipsJa').value || "";
const message_fr = document.getElementById('ServerTipsFr').value || "";
const message_it = document.getElementById('ServerTipsIt').value || "";
const response = await fetch('/admin/update_maintenance', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ status: status, message_en: message_en, message_ja: message_ja, message_fr: message_fr, message_it: message_it })
});
const result = await response.json();
if (result.status === "success") {
const modal = bootstrap.Modal.getOrCreateInstance(document.getElementById('maintenanceModal'));
modal.hide();
load_maintenance();
} else {
alert(result.message || "Update failed.");
}
}
document.getElementById('logoutBtn').addEventListener('click', function() {
document.cookie = "token=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;";