Files
Groove_Coaster_2_Server/api/shop.py
2025-08-18 19:47:32 +08:00

467 lines
22 KiB
Python

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, FMAX_PRICE, EX_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 = 615
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') and not spawn_prev_page:
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: 110px; margin-top: -100px;" />
</a><br>
"""
fmax_inc = 1
elif 700 in my_stage and os.path.isfile('./files/dlc_4max.html') and not spawn_prev_page:
buttons_html += """
<a href="wwic://web_shop_detail?&cnt_type=1&cnt_id=-3">
<img src="/files/web/dlc_4max.jpg" style="width: 84%; margin-bottom: 110px; margin-top: -100px;" />
</a><br>
"""
if 980 not in my_stage and os.path.isfile('./files/dlc_extra.html') and not spawn_prev_page:
buttons_html += """
<a href="wwic://web_shop_detail?&cnt_type=1&cnt_id=-2">
<img src="/files/web/dlc_extra.jpg" style="width: 84%; margin-bottom: 20px; margin-top: -100px;" />
</a><br>
"""
fmax_inc = 1
elif 980 in my_stage and os.path.isfile('./files/dlc_4max.html') and not spawn_prev_page:
buttons_html += """
<a href="wwic://web_shop_detail?&cnt_type=1&cnt_id=-4">
<img src="/files/web/dlc_extra.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}&return_page={page}'">
</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}&return_page={page}'">
</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()
if b'return_page' in decrypted_fields:
return_page = decrypted_fields[b'return_page'][0].decode()
else:
return_page = "1"
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 == -3:
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 == -4:
html = f"""
<div class="text-content">
<p>You have unlocked the EXTRA Challenge!</p>
<p>Please report bugs/missing tracks to Discord: #AnTcfgss, or QQ 3421587952.</p>
<button class="quit-button-extra" onclick="window.location.href='wwic://web_shop?&cnt_type=1'">
Go Back
</button>
</div>
"""
elif cnt_id == -2:
html = f"""
<div class="text-content">
<p>Are you looking for a bad time?</p>
<p>If so, this is the Ultimate - Extra - Challenge.</p>
<p>180+ Arcade Extra difficulty charts await you.</p>
<p>You have been warned.</p>
</div>
<button class="buy-button-extra" onclick="window.location.href='wwic://web_purchase_coin?&cnt_type=1&cnt_id=-2&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;"> {EX_PRICE}</span>
</div>
</button>
<br><br>
<button class="quit-button-extra" onclick="window.location.href='wwic://web_shop?&cnt_type=1'">
Go Back
</button>
"""
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;"> {FMAX_PRICE}</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 == -1 or cnt_id == -3):
source_html = f"files/dlc_4max.html"
elif cnt_type == "1" and (cnt_id == -2 or cnt_id == -4):
source_html = f"files/dlc_extra.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}&page={return_page}" 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 = FMAX_PRICE
if coin < song_stage_price:
return Response(fail_url, media_type="application/xml")
for i in range(615, 926):
my_stage.add(i)
coin -= song_stage_price
elif cnt_id == -2:
song_stage_price = EX_PRICE
if coin < song_stage_price:
return Response(fail_url, media_type="application/xml")
for i in range(926, 985):
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']),
]