mirror of
https://github.com/DragonMinded/bemaniutils.git
synced 2026-04-17 06:25:51 -05:00
* Pop'n Music 27 Unilab support Known issues: I don't know how to trigger KAC Lab. This seems to be something that should be able to be accessed on appropriate versions of the dll but I can't seem to figure it out. Rare softlock on pop'n quest Lively II event if you mess with the phase flags and put the game in an invalid state. In theory (and according to bemaniwiki) the entire event should be clearable on earlier Unilab builds. Not an issue/will not fix: 狼弦暴威 does not appear in Awakening Elem when the event flag is set. The solution to this (for some reason) is to clear the other 10 events. This is not a bemaniutils issue.
348 lines
11 KiB
Python
348 lines
11 KiB
Python
import copy
|
|
import json
|
|
import traceback
|
|
from typing import Any, Callable, Dict, List
|
|
from flask import Flask, abort, request, Response
|
|
from functools import wraps
|
|
|
|
from bemani.api.exceptions import APIException
|
|
from bemani.api.objects import (
|
|
RecordsObject,
|
|
ProfileObject,
|
|
StatisticsObject,
|
|
CatalogObject,
|
|
)
|
|
from bemani.api.types import g
|
|
from bemani.common import GameConstants, APIConstants, VersionConstants
|
|
from bemani.data import Config, Data
|
|
|
|
app = Flask(__name__)
|
|
config = Config()
|
|
|
|
SUPPORTED_VERSIONS: List[str] = ["v1"]
|
|
|
|
|
|
def jsonify_response(data: Dict[str, Any], code: int = 200) -> Response:
|
|
return Response(
|
|
json.dumps(data).encode("utf8"),
|
|
content_type="application/json; charset=utf-8",
|
|
status=code,
|
|
)
|
|
|
|
|
|
@app.before_request
|
|
def before_request() -> None:
|
|
global config
|
|
|
|
g.config = config
|
|
g.data = Data(config)
|
|
g.authorized = False
|
|
|
|
authkey = request.headers.get("Authorization")
|
|
if authkey is not None:
|
|
try:
|
|
authtype, authtoken = authkey.split(" ", 1)
|
|
except ValueError:
|
|
authtype = None
|
|
authtoken = None
|
|
|
|
if authtype is not None and authtoken is not None and authtype.lower() == "token":
|
|
g.authorized = g.data.local.api.validate_client(authtoken)
|
|
|
|
|
|
@app.after_request
|
|
def after_request(response: Response) -> Response:
|
|
# Make sure our REST responses don't get cached, so that remote
|
|
# servers which respect cache headers don't get confused.
|
|
response.cache_control.no_cache = True
|
|
response.cache_control.must_revalidate = True
|
|
response.cache_control.private = True
|
|
return response
|
|
|
|
|
|
@app.teardown_request
|
|
def teardown_request(exception: Any) -> None:
|
|
data = getattr(g, "data", None)
|
|
if data is not None:
|
|
data.close()
|
|
|
|
|
|
def authrequired(func: Callable) -> Callable:
|
|
@wraps(func)
|
|
def decoratedfunction(*args: Any, **kwargs: Any) -> Response:
|
|
if not g.authorized:
|
|
return jsonify_response(
|
|
{"error": "Unauthorized client!"},
|
|
401,
|
|
)
|
|
else:
|
|
return func(*args, **kwargs)
|
|
|
|
return decoratedfunction
|
|
|
|
|
|
def jsonify(func: Callable) -> Callable:
|
|
@wraps(func)
|
|
def decoratedfunction(*args: Any, **kwargs: Any) -> Response:
|
|
return jsonify_response(func(*args, **kwargs))
|
|
|
|
return decoratedfunction
|
|
|
|
|
|
@app.errorhandler(Exception)
|
|
def server_exception(exception: Any) -> Response:
|
|
stack = "".join(traceback.format_exception(type(exception), exception, exception.__traceback__))
|
|
print(stack)
|
|
try:
|
|
g.data.local.network.put_event(
|
|
"exception",
|
|
{
|
|
"service": "api",
|
|
"request": request.url,
|
|
"traceback": stack,
|
|
},
|
|
)
|
|
except Exception:
|
|
pass
|
|
|
|
return jsonify_response(
|
|
{"error": "Exception occured while processing request."},
|
|
500,
|
|
)
|
|
|
|
|
|
@app.errorhandler(APIException)
|
|
def api_exception(exception: Any) -> Response:
|
|
return jsonify_response(
|
|
{"error": exception.message},
|
|
exception.code,
|
|
)
|
|
|
|
|
|
@app.errorhandler(500)
|
|
def server_error(error: Any) -> Response:
|
|
return jsonify_response(
|
|
{"error": "Exception occured while processing request."},
|
|
500,
|
|
)
|
|
|
|
|
|
@app.errorhandler(501)
|
|
def protocol_error(error: Any) -> Response:
|
|
return jsonify_response(
|
|
{"error": "Unsupported protocol version in request."},
|
|
501,
|
|
)
|
|
|
|
|
|
@app.errorhandler(400)
|
|
def bad_json(error: Any) -> Response:
|
|
return jsonify_response(
|
|
{"error": "Request JSON could not be decoded."},
|
|
500,
|
|
)
|
|
|
|
|
|
@app.errorhandler(404)
|
|
def unrecognized_object(error: Any) -> Response:
|
|
return jsonify_response(
|
|
{"error": "Unrecognized request game/version or object."},
|
|
404,
|
|
)
|
|
|
|
|
|
@app.errorhandler(405)
|
|
def invalid_request(error: Any) -> Response:
|
|
return jsonify_response(
|
|
{"error": "Invalid request URI or method."},
|
|
405,
|
|
)
|
|
|
|
|
|
@app.route("/<path:path>", methods=["GET", "POST"])
|
|
@authrequired
|
|
def catch_all(path: str) -> Response:
|
|
abort(405)
|
|
|
|
|
|
@app.route("/", methods=["GET", "POST"])
|
|
@authrequired
|
|
@jsonify
|
|
def info() -> Dict[str, Any]:
|
|
requestdata = request.get_json()
|
|
if requestdata is None:
|
|
raise APIException("Request JSON could not be decoded.")
|
|
if requestdata:
|
|
raise APIException("Unrecognized parameters for request.")
|
|
|
|
return {
|
|
"versions": SUPPORTED_VERSIONS,
|
|
"name": g.config.name,
|
|
"email": g.config.email,
|
|
}
|
|
|
|
|
|
@app.route("/<protoversion>/<requestgame>/<requestversion>", methods=["GET", "POST"])
|
|
@authrequired
|
|
@jsonify
|
|
def lookup(protoversion: str, requestgame: str, requestversion: str) -> Dict[str, Any]:
|
|
requestdata = request.get_json()
|
|
for expected in ["type", "ids", "objects"]:
|
|
if expected not in requestdata:
|
|
raise APIException("Missing parameters for request.")
|
|
for param in requestdata:
|
|
if param not in ["type", "ids", "objects", "since", "until"]:
|
|
raise APIException("Unrecognized parameters for request.")
|
|
|
|
args = copy.deepcopy(requestdata)
|
|
del args["type"]
|
|
del args["ids"]
|
|
del args["objects"]
|
|
|
|
if protoversion not in SUPPORTED_VERSIONS:
|
|
# Don't know about this protocol version
|
|
abort(501)
|
|
|
|
# Figure out what games we support based on config, and map those.
|
|
gamemapping = {}
|
|
for gameid, constant in [
|
|
("ddr", GameConstants.DDR),
|
|
("iidx", GameConstants.IIDX),
|
|
("jubeat", GameConstants.JUBEAT),
|
|
("museca", GameConstants.MUSECA),
|
|
("popnmusic", GameConstants.POPN_MUSIC),
|
|
("reflecbeat", GameConstants.REFLEC_BEAT),
|
|
("soundvoltex", GameConstants.SDVX),
|
|
]:
|
|
if constant in g.config.support:
|
|
gamemapping[gameid] = constant
|
|
game = gamemapping.get(requestgame)
|
|
if game is None:
|
|
# Don't support this game!
|
|
abort(404)
|
|
|
|
if requestversion[0] == "o":
|
|
omnimix = True
|
|
requestversion = requestversion[1:]
|
|
else:
|
|
omnimix = False
|
|
|
|
version = (
|
|
{
|
|
GameConstants.DDR: {
|
|
"12": VersionConstants.DDR_X2,
|
|
"13": VersionConstants.DDR_X3_VS_2NDMIX,
|
|
"14": VersionConstants.DDR_2013,
|
|
"15": VersionConstants.DDR_2014,
|
|
"16": VersionConstants.DDR_ACE,
|
|
"17": VersionConstants.DDR_A20,
|
|
},
|
|
GameConstants.IIDX: {
|
|
"20": VersionConstants.IIDX_TRICORO,
|
|
"21": VersionConstants.IIDX_SPADA,
|
|
"22": VersionConstants.IIDX_PENDUAL,
|
|
"23": VersionConstants.IIDX_COPULA,
|
|
"24": VersionConstants.IIDX_SINOBUZ,
|
|
"25": VersionConstants.IIDX_CANNON_BALLERS,
|
|
"26": VersionConstants.IIDX_ROOTAGE,
|
|
"27": VersionConstants.IIDX_HEROIC_VERSE,
|
|
"28": VersionConstants.IIDX_BISTROVER,
|
|
},
|
|
GameConstants.JUBEAT: {
|
|
"5": VersionConstants.JUBEAT_SAUCER,
|
|
"5a": VersionConstants.JUBEAT_SAUCER_FULFILL,
|
|
"6": VersionConstants.JUBEAT_PROP,
|
|
"7": VersionConstants.JUBEAT_QUBELL,
|
|
"8": VersionConstants.JUBEAT_CLAN,
|
|
"9": VersionConstants.JUBEAT_FESTO,
|
|
"10": VersionConstants.JUBEAT_AVENUE,
|
|
},
|
|
GameConstants.MUSECA: {
|
|
"1": VersionConstants.MUSECA,
|
|
"1p": VersionConstants.MUSECA_1_PLUS,
|
|
},
|
|
GameConstants.POPN_MUSIC: {
|
|
"19": VersionConstants.POPN_MUSIC_TUNE_STREET,
|
|
"20": VersionConstants.POPN_MUSIC_FANTASIA,
|
|
"21": VersionConstants.POPN_MUSIC_SUNNY_PARK,
|
|
"22": VersionConstants.POPN_MUSIC_LAPISTORIA,
|
|
"23": VersionConstants.POPN_MUSIC_ECLALE,
|
|
"24": VersionConstants.POPN_MUSIC_USANEKO,
|
|
"25": VersionConstants.POPN_MUSIC_PEACE,
|
|
"26": VersionConstants.POPN_MUSIC_KAIMEI_RIDDLES,
|
|
"27": VersionConstants.POPN_MUSIC_UNILAB,
|
|
},
|
|
GameConstants.REFLEC_BEAT: {
|
|
"1": VersionConstants.REFLEC_BEAT,
|
|
"2": VersionConstants.REFLEC_BEAT_LIMELIGHT,
|
|
# We don't support non-final COLETTE, so just return scores for
|
|
# final colette to any network that asks.
|
|
"3w": VersionConstants.REFLEC_BEAT_COLETTE,
|
|
"3sp": VersionConstants.REFLEC_BEAT_COLETTE,
|
|
"3su": VersionConstants.REFLEC_BEAT_COLETTE,
|
|
"3a": VersionConstants.REFLEC_BEAT_COLETTE,
|
|
"3as": VersionConstants.REFLEC_BEAT_COLETTE,
|
|
# We don't support groovin'!!, so just return upper scores.
|
|
"4": VersionConstants.REFLEC_BEAT_GROOVIN,
|
|
"4u": VersionConstants.REFLEC_BEAT_GROOVIN,
|
|
"5": VersionConstants.REFLEC_BEAT_VOLZZA,
|
|
"5a": VersionConstants.REFLEC_BEAT_VOLZZA_2,
|
|
"6": VersionConstants.REFLEC_BEAT_REFLESIA,
|
|
},
|
|
GameConstants.SDVX: {
|
|
"1": VersionConstants.SDVX_BOOTH,
|
|
"2": VersionConstants.SDVX_INFINITE_INFECTION,
|
|
"3": VersionConstants.SDVX_GRAVITY_WARS,
|
|
"4": VersionConstants.SDVX_HEAVENLY_HAVEN,
|
|
},
|
|
}
|
|
.get(game, {})
|
|
.get(requestversion)
|
|
)
|
|
if version is None:
|
|
# Don't support this version!
|
|
abort(404)
|
|
|
|
# Attempt to coerce ID type. If we fail, provide the correct failure message.
|
|
idtype = None
|
|
try:
|
|
idtype = APIConstants(requestdata["type"])
|
|
except ValueError:
|
|
pass
|
|
if idtype is None:
|
|
raise APIException("Invalid ID type provided!")
|
|
|
|
# Validate the provided IDs given the ID type above.
|
|
ids = requestdata["ids"]
|
|
if idtype == APIConstants.ID_TYPE_CARD and len(ids) == 0:
|
|
raise APIException("Invalid number of IDs given!")
|
|
if idtype == APIConstants.ID_TYPE_SONG and len(ids) not in [1, 2]:
|
|
raise APIException("Invalid number of IDs given!")
|
|
if idtype == APIConstants.ID_TYPE_INSTANCE and len(ids) != 3:
|
|
raise APIException("Invalid number of IDs given!")
|
|
if idtype == APIConstants.ID_TYPE_SERVER and len(ids) != 0:
|
|
raise APIException("Invalid number of IDs given!")
|
|
|
|
responsedata = {}
|
|
for obj in requestdata["objects"]:
|
|
handler = {
|
|
"records": RecordsObject,
|
|
"profile": ProfileObject,
|
|
"statistics": StatisticsObject,
|
|
"catalog": CatalogObject,
|
|
}.get(obj)
|
|
if handler is None:
|
|
# Don't support this object type
|
|
abort(404)
|
|
|
|
inst = handler(g.data, game, version, omnimix)
|
|
try:
|
|
fetchmethod = getattr(inst, f"fetch_{protoversion}")
|
|
except AttributeError:
|
|
# Don't know how to handle this object for this version
|
|
abort(501)
|
|
|
|
responsedata[obj] = fetchmethod(idtype, ids, args)
|
|
|
|
return responsedata
|