1
0
mirror of synced 2024-12-18 17:25:54 +01:00
bemaniutils/bemani/api/app.py

349 lines
11 KiB
Python
Raw Normal View History

import copy
import json
import traceback
2021-09-07 19:56:31 +02:00
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.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,
},
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