492 lines
18 KiB
Python
492 lines
18 KiB
Python
import random
|
|
from typing import Any, Dict, List, Optional
|
|
from flask import Blueprint, request, Response, abort, url_for
|
|
|
|
from bemani.backend.base import Base
|
|
from bemani.common import CardCipher, CardCipherException, ValidatedDict, GameConstants, RegionConstants
|
|
from bemani.data import Arcade, ArcadeID, Event, Machine
|
|
from bemani.frontend.app import loginrequired, jsonify, render_react, valid_pin
|
|
from bemani.frontend.gamesettings import get_game_settings
|
|
from bemani.frontend.templates import templates_location
|
|
from bemani.frontend.static import static_location
|
|
from bemani.frontend.types import g
|
|
|
|
|
|
arcade_pages = Blueprint(
|
|
'arcade_pages',
|
|
__name__,
|
|
url_prefix='/arcade',
|
|
template_folder=templates_location,
|
|
static_folder=static_location,
|
|
)
|
|
|
|
|
|
def is_user_editable(machine: Machine) -> bool:
|
|
return machine.game is None
|
|
|
|
|
|
def format_machine(machine: Machine) -> Dict[str, Any]:
|
|
if machine.game is None:
|
|
game = 'any game'
|
|
elif machine.version is None:
|
|
game = {
|
|
GameConstants.BISHI_BASHI: 'BishiBashi',
|
|
GameConstants.DDR: 'DDR',
|
|
GameConstants.IIDX: 'IIDX',
|
|
GameConstants.JUBEAT: 'Jubeat',
|
|
GameConstants.MGA: 'Metal Gear Arcade',
|
|
GameConstants.MUSECA: 'MÚSECA',
|
|
GameConstants.POPN_MUSIC: 'Pop\'n Music',
|
|
GameConstants.REFLEC_BEAT: 'Reflec Beat',
|
|
GameConstants.SDVX: 'SDVX',
|
|
}.get(machine.game)
|
|
elif machine.version > 0:
|
|
game = [
|
|
name for (game, version, name) in Base.all_games()
|
|
if game == machine.game and version == machine.version
|
|
][0]
|
|
elif machine.version < 0:
|
|
game = [
|
|
name for (game, version, name) in Base.all_games()
|
|
if game == machine.game and version == -machine.version
|
|
][0] + ' or older'
|
|
|
|
return {
|
|
'pcbid': machine.pcbid,
|
|
'name': machine.name,
|
|
'description': machine.description,
|
|
'port': machine.port,
|
|
'game': game,
|
|
'editable': is_user_editable(machine),
|
|
}
|
|
|
|
|
|
def format_arcade(arcade: Arcade) -> Dict[str, Any]:
|
|
return {
|
|
'id': arcade.id,
|
|
'name': arcade.name,
|
|
'description': arcade.description,
|
|
'pin': arcade.pin,
|
|
'region': arcade.region,
|
|
'paseli_enabled': arcade.data.get_bool('paseli_enabled'),
|
|
'paseli_infinite': arcade.data.get_bool('paseli_infinite'),
|
|
'mask_services_url': arcade.data.get_bool('mask_services_url'),
|
|
'owners': arcade.owners,
|
|
}
|
|
|
|
|
|
def format_event(event: Event) -> Dict[str, Any]:
|
|
return {
|
|
'id': event.id,
|
|
'timestamp': event.timestamp,
|
|
'userid': event.userid,
|
|
'arcadeid': event.arcadeid,
|
|
'type': event.type,
|
|
'data': event.data,
|
|
}
|
|
|
|
|
|
@arcade_pages.route('/<int:arcadeid>')
|
|
@loginrequired
|
|
def viewarcade(arcadeid: int) -> Response:
|
|
# Cast the ID for type safety.
|
|
arcadeid = ArcadeID(arcadeid)
|
|
|
|
arcade = g.data.local.machine.get_arcade(arcadeid)
|
|
if arcade is None or g.userID not in arcade.owners:
|
|
abort(403)
|
|
machines = [
|
|
format_machine(machine) for machine in g.data.local.machine.get_all_machines(arcade.id)
|
|
]
|
|
return render_react(
|
|
arcade.name,
|
|
'arcade/arcade.react.js',
|
|
{
|
|
'arcade': format_arcade(arcade),
|
|
'regions': RegionConstants.LUT,
|
|
'machines': machines,
|
|
'game_settings': get_game_settings(g.data, arcadeid),
|
|
'balances': {balance[0]: balance[1] for balance in g.data.local.machine.get_balances(arcadeid)},
|
|
'users': {user.id: user.username for user in g.data.local.user.get_all_users()},
|
|
'events': [format_event(event) for event in g.data.local.network.get_events(arcadeid=arcadeid, event='paseli_transaction')],
|
|
'enforcing': g.config.server.enforce_pcbid,
|
|
'max_pcbids': g.config.server.pcbid_self_grant_limit,
|
|
},
|
|
{
|
|
'refresh': url_for('arcade_pages.listarcade', arcadeid=arcadeid),
|
|
'paseli_enabled': url_for('arcade_pages.updatearcade', arcadeid=arcadeid, attribute='paseli_enabled'),
|
|
'paseli_infinite': url_for('arcade_pages.updatearcade', arcadeid=arcadeid, attribute='paseli_infinite'),
|
|
'mask_services_url': url_for('arcade_pages.updatearcade', arcadeid=arcadeid, attribute='mask_services_url'),
|
|
'update_settings': url_for('arcade_pages.updatesettings', arcadeid=arcadeid),
|
|
'add_balance': url_for('arcade_pages.addbalance', arcadeid=arcadeid),
|
|
'update_balance': url_for('arcade_pages.updatebalance', arcadeid=arcadeid),
|
|
'update_pin': url_for('arcade_pages.updatepin', arcadeid=arcadeid),
|
|
'update_region': url_for('arcade_pages.updateregion', arcadeid=arcadeid),
|
|
'generatepcbid': url_for('arcade_pages.generatepcbid', arcadeid=arcadeid),
|
|
'updatepcbid': url_for('arcade_pages.updatepcbid', arcadeid=arcadeid),
|
|
'removepcbid': url_for('arcade_pages.removepcbid', arcadeid=arcadeid),
|
|
},
|
|
)
|
|
|
|
|
|
@arcade_pages.route('/<int:arcadeid>/list')
|
|
@jsonify
|
|
@loginrequired
|
|
def listarcade(arcadeid: int) -> Dict[str, Any]:
|
|
# Cast the ID for type safety.
|
|
arcadeid = ArcadeID(arcadeid)
|
|
|
|
# Make sure the arcade is valid
|
|
arcade = g.data.local.machine.get_arcade(arcadeid)
|
|
if arcade is None or g.userID not in arcade.owners:
|
|
raise Exception('You don\'t own this arcade, refusing to list!')
|
|
|
|
machines = [
|
|
format_machine(machine) for machine in g.data.local.machine.get_all_machines(arcade.id)
|
|
]
|
|
return {
|
|
'machines': machines,
|
|
'balances': {balance[0]: balance[1] for balance in g.data.local.machine.get_balances(arcadeid)},
|
|
'users': {user.id: user.username for user in g.data.local.user.get_all_users()},
|
|
'events': [format_event(event) for event in g.data.local.network.get_events(arcadeid=arcadeid, event='paseli_transaction')],
|
|
}
|
|
|
|
|
|
@arcade_pages.route('/<int:arcadeid>/balance/add', methods=['POST'])
|
|
@jsonify
|
|
@loginrequired
|
|
def addbalance(arcadeid: int) -> Dict[str, Any]:
|
|
# Cast the ID for type safety.
|
|
arcadeid = ArcadeID(arcadeid)
|
|
credits = request.get_json()['credits']
|
|
card = request.get_json()['card']
|
|
|
|
# Make sure the arcade is valid
|
|
arcade = g.data.local.machine.get_arcade(arcadeid)
|
|
if arcade is None or g.userID not in arcade.owners:
|
|
raise Exception('You don\'t own this arcade, refusing to update!')
|
|
|
|
try:
|
|
cardid = CardCipher.decode(card)
|
|
userid = g.data.local.user.from_cardid(cardid)
|
|
except CardCipherException:
|
|
userid = None
|
|
|
|
if userid is None:
|
|
raise Exception('Unable to find user by this card!')
|
|
|
|
# Update balance
|
|
balance = g.data.local.user.update_balance(userid, arcadeid, credits)
|
|
if balance is not None:
|
|
g.data.local.network.put_event(
|
|
'paseli_transaction',
|
|
{
|
|
'delta': credits,
|
|
'balance': balance,
|
|
'reason': 'arcade operator adjustment',
|
|
},
|
|
userid=userid,
|
|
arcadeid=arcadeid,
|
|
)
|
|
|
|
return {
|
|
'balances': {balance[0]: balance[1] for balance in g.data.local.machine.get_balances(arcadeid)},
|
|
'users': {user.id: user.username for user in g.data.local.user.get_all_users()},
|
|
'events': [format_event(event) for event in g.data.local.network.get_events(arcadeid=arcadeid, event='paseli_transaction')],
|
|
}
|
|
|
|
|
|
@arcade_pages.route('/<int:arcadeid>/balance/update', methods=['POST'])
|
|
@jsonify
|
|
@loginrequired
|
|
def updatebalance(arcadeid: int) -> Dict[str, Any]:
|
|
# Cast the ID for type safety.
|
|
arcadeid = ArcadeID(arcadeid)
|
|
credits = request.get_json()['credits']
|
|
|
|
# Make sure the arcade is valid
|
|
arcade = g.data.local.machine.get_arcade(arcadeid)
|
|
if arcade is None or g.userID not in arcade.owners:
|
|
raise Exception('You don\'t own this arcade, refusing to update!')
|
|
|
|
# Update balances
|
|
for userid in credits:
|
|
balance = g.data.local.user.update_balance(userid, arcadeid, credits[userid])
|
|
if balance is not None:
|
|
g.data.local.network.put_event(
|
|
'paseli_transaction',
|
|
{
|
|
'delta': credits[userid],
|
|
'balance': balance,
|
|
'reason': 'arcade operator adjustment',
|
|
},
|
|
userid=userid,
|
|
arcadeid=arcadeid,
|
|
)
|
|
|
|
return {
|
|
'balances': {balance[0]: balance[1] for balance in g.data.local.machine.get_balances(arcadeid)},
|
|
'users': {user.id: user.username for user in g.data.local.user.get_all_users()},
|
|
'events': [format_event(event) for event in g.data.local.network.get_events(arcadeid=arcadeid, event='paseli_transaction')],
|
|
}
|
|
|
|
|
|
@arcade_pages.route('/<int:arcadeid>/pin/update', methods=['POST'])
|
|
@jsonify
|
|
@loginrequired
|
|
def updatepin(arcadeid: int) -> Dict[str, Any]:
|
|
# Cast the ID for type safety.
|
|
arcadeid = ArcadeID(arcadeid)
|
|
|
|
pin = request.get_json()['pin']
|
|
|
|
# Make sure the arcade is valid
|
|
arcade = g.data.local.machine.get_arcade(arcadeid)
|
|
if arcade is None or g.userID not in arcade.owners:
|
|
raise Exception('You don\'t own this arcade, refusing to update!')
|
|
|
|
if not valid_pin(pin, 'arcade'):
|
|
raise Exception('Invalid PIN, must be exactly 8 digits!')
|
|
|
|
# Update and save
|
|
arcade.pin = pin
|
|
g.data.local.machine.put_arcade(arcade)
|
|
|
|
# Return nothing
|
|
return {'pin': pin}
|
|
|
|
|
|
@arcade_pages.route('/<int:arcadeid>/region/update', methods=['POST'])
|
|
@jsonify
|
|
@loginrequired
|
|
def updateregion(arcadeid: int) -> Dict[str, Any]:
|
|
# Cast the ID for type safety.
|
|
arcadeid = ArcadeID(arcadeid)
|
|
|
|
try:
|
|
region = int(request.get_json()['region'])
|
|
except Exception:
|
|
region = 0
|
|
|
|
# Make sure the arcade is valid
|
|
arcade = g.data.local.machine.get_arcade(arcadeid)
|
|
if arcade is None or g.userID not in arcade.owners:
|
|
raise Exception('You don\'t own this arcade, refusing to update!')
|
|
|
|
if region not in {RegionConstants.EUROPE, RegionConstants.NO_MAPPING} and (region < RegionConstants.MIN or region > RegionConstants.MAX):
|
|
raise Exception('Invalid region!')
|
|
|
|
# Update and save
|
|
arcade.region = region
|
|
g.data.local.machine.put_arcade(arcade)
|
|
|
|
# Return nothing
|
|
return {'region': region}
|
|
|
|
|
|
@arcade_pages.route('/<int:arcadeid>/pcbids/generate', methods=['POST'])
|
|
@jsonify
|
|
@loginrequired
|
|
def generatepcbid(arcadeid: int) -> Dict[str, Any]:
|
|
# Cast the ID for type safety.
|
|
arcadeid = ArcadeID(arcadeid)
|
|
|
|
# Make sure that arcade owners are allowed to generate PCBIDs in the first place.
|
|
if g.config.server.pcbid_self_grant_limit <= 0:
|
|
raise Exception('You don\'t have permission to generate PCBIDs!')
|
|
|
|
# Make sure the arcade is valid and the current user has permissions to
|
|
# modify it.
|
|
arcade = g.data.local.machine.get_arcade(arcadeid)
|
|
if arcade is None or g.userID not in arcade.owners:
|
|
raise Exception('You don\'t own this arcade, refusing to update!')
|
|
|
|
# Make sure the user hasn't gone over their limit of PCBIDs.
|
|
existing_machine_count = len(
|
|
[machine for machine in g.data.local.machine.get_all_machines(arcade.id) if is_user_editable(machine)]
|
|
)
|
|
if existing_machine_count >= g.config.server.pcbid_self_grant_limit:
|
|
raise Exception('You have hit your limit of allowed PCBIDs!')
|
|
|
|
# Will be set by the game on boot.
|
|
name: str = 'なし'
|
|
pcbid: Optional[str] = None
|
|
new_machine = request.get_json()['machine']
|
|
|
|
while pcbid is None:
|
|
# Generate a new PCBID, check for uniqueness
|
|
potential_pcbid = "01201000000000" + "".join([random.choice("0123456789ABCDEF") for _ in range(6)])
|
|
if g.data.local.machine.get_machine(potential_pcbid) is None:
|
|
pcbid = potential_pcbid
|
|
|
|
# Finally, add the generated PCBID to the network.
|
|
g.data.local.machine.create_machine(pcbid, name, new_machine['description'], arcade.id)
|
|
|
|
# Just return all machines for ease of updating
|
|
return {
|
|
'machines': [format_machine(machine) for machine in g.data.local.machine.get_all_machines(arcade.id)],
|
|
}
|
|
|
|
|
|
@arcade_pages.route('/<int:arcadeid>/pcbids/update', methods=['POST'])
|
|
@jsonify
|
|
@loginrequired
|
|
def updatepcbid(arcadeid: int) -> Dict[str, Any]:
|
|
# Cast the ID for type safety.
|
|
arcadeid = ArcadeID(arcadeid)
|
|
|
|
# Make sure that arcade owners are allowed to edit PCBIDs in the first place.
|
|
if g.config.server.pcbid_self_grant_limit <= 0:
|
|
raise Exception('You don\'t have permission to edit PCBIDs!')
|
|
|
|
# Make sure the arcade is valid and the current user has permissions to
|
|
# modify it.
|
|
arcade = g.data.local.machine.get_arcade(arcadeid)
|
|
if arcade is None or g.userID not in arcade.owners:
|
|
raise Exception('You don\'t own this arcade, refusing to update!')
|
|
|
|
# Grab the new updates as well as the old values to validate editing permissions.
|
|
updated_machine = request.get_json()['machine']
|
|
current_machine = g.data.local.machine.get_machine(updated_machine['pcbid'])
|
|
|
|
# Make sure the PCBID we are trying to modify is actually owned by this arcade.
|
|
# Also, make sure that the PCBID is actually user-editable.
|
|
if current_machine is None or current_machine.arcade != arcadeid or not is_user_editable(current_machine):
|
|
raise Exception('You don\'t own this PCBID, refusing to update!')
|
|
|
|
# Make sure the port is actually valid.
|
|
try:
|
|
port = int(updated_machine['port'])
|
|
except ValueError:
|
|
port = None
|
|
if port is None:
|
|
raise Exception('The specified port is invalid!')
|
|
if port < 1 or port > 65535:
|
|
raise Exception('The specified port is out of range!')
|
|
|
|
# Make sure we don't duplicate port assignments.
|
|
other_pcbid = g.data.local.machine.from_port(port)
|
|
if other_pcbid is not None and other_pcbid != updated_machine['pcbid']:
|
|
raise Exception('The specified port is already in use!')
|
|
|
|
# Update the allowed bits of data.
|
|
current_machine.description = updated_machine['description']
|
|
current_machine.port = port
|
|
g.data.local.machine.put_machine(current_machine)
|
|
|
|
# Just return all machines for ease of updating
|
|
return {
|
|
'machines': [format_machine(machine) for machine in g.data.local.machine.get_all_machines(arcade.id)],
|
|
}
|
|
|
|
|
|
@arcade_pages.route('/<int:arcadeid>/pcbids/remove', methods=['POST'])
|
|
@jsonify
|
|
@loginrequired
|
|
def removepcbid(arcadeid: int) -> Dict[str, Any]:
|
|
# Cast the ID for type safety.
|
|
arcadeid = ArcadeID(arcadeid)
|
|
|
|
# Make sure that arcade owners are allowed to edit PCBIDs in the first place.
|
|
if g.config.server.pcbid_self_grant_limit <= 0:
|
|
raise Exception('You don\'t have permission to edit PCBIDs!')
|
|
|
|
# Make sure the arcade is valid and the current user has permissions to
|
|
# modify it.
|
|
arcade = g.data.local.machine.get_arcade(arcadeid)
|
|
if arcade is None or g.userID not in arcade.owners:
|
|
raise Exception('You don\'t own this arcade, refusing to update!')
|
|
|
|
# Attempt to look the PCBID we are deleting up to ensure it exists.
|
|
pcbid = request.get_json()['pcbid']
|
|
|
|
# Make sure the PCBID we are trying to delete is actually owned by this arcade.
|
|
# Also, make sure that the PCBID is actually user-editable.
|
|
machine = g.data.local.machine.get_machine(pcbid)
|
|
if machine is None or machine.arcade != arcadeid or not is_user_editable(machine):
|
|
raise Exception('You don\'t own this PCBID, refusing to update!')
|
|
|
|
# Actually delete it.
|
|
g.data.local.machine.destroy_machine(pcbid)
|
|
|
|
# Just return all machines for ease of updating
|
|
return {
|
|
'machines': [format_machine(machine) for machine in g.data.local.machine.get_all_machines(arcade.id)],
|
|
}
|
|
|
|
|
|
@arcade_pages.route('/<int:arcadeid>/update/<string:attribute>', methods=['POST'])
|
|
@jsonify
|
|
@loginrequired
|
|
def updatearcade(arcadeid: int, attribute: str) -> Dict[str, Any]:
|
|
# Cast the ID for type safety.
|
|
arcadeid = ArcadeID(arcadeid)
|
|
|
|
# Attempt to look this arcade up
|
|
new_value = request.get_json()['value']
|
|
arcade = g.data.local.machine.get_arcade(arcadeid)
|
|
if arcade is None or g.userID not in arcade.owners:
|
|
raise Exception('You don\'t own this arcade, refusing to update!')
|
|
|
|
if attribute == 'paseli_enabled':
|
|
arcade.data.replace_bool('paseli_enabled', new_value)
|
|
elif attribute == 'paseli_infinite':
|
|
arcade.data.replace_bool('paseli_infinite', new_value)
|
|
elif attribute == 'mask_services_url':
|
|
arcade.data.replace_bool('mask_services_url', new_value)
|
|
else:
|
|
raise Exception(f'Unknown attribute {attribute} to update!')
|
|
|
|
g.data.local.machine.put_arcade(arcade)
|
|
|
|
# Return the updated value
|
|
return {
|
|
'value': new_value,
|
|
}
|
|
|
|
|
|
@arcade_pages.route('/<int:arcadeid>/settings/update', methods=['POST'])
|
|
@jsonify
|
|
@loginrequired
|
|
def updatesettings(arcadeid: int) -> Dict[str, Any]:
|
|
# Cast the ID for type safety.
|
|
arcadeid = ArcadeID(arcadeid)
|
|
|
|
# Attempt to look this arcade up
|
|
arcade = g.data.local.machine.get_arcade(arcadeid)
|
|
if arcade is None or g.userID not in arcade.owners:
|
|
raise Exception('You don\'t own this arcade, refusing to update!')
|
|
|
|
game = GameConstants(request.get_json()['game'])
|
|
version = request.get_json()['version']
|
|
|
|
for setting_type, update_function in [
|
|
('bools', 'replace_bool'),
|
|
('ints', 'replace_int'),
|
|
('strs', 'replace_str'),
|
|
('longstrs', 'replace_str'),
|
|
]:
|
|
for game_setting in request.get_json()[setting_type]:
|
|
# Grab the value to update
|
|
category = game_setting['category']
|
|
setting = game_setting['setting']
|
|
new_value = game_setting['value']
|
|
|
|
# Update the value
|
|
current_settings = g.data.local.machine.get_settings(arcadeid, game, version, category)
|
|
if current_settings is None:
|
|
current_settings = ValidatedDict()
|
|
|
|
getattr(current_settings, update_function)(setting, new_value)
|
|
|
|
# Save it back
|
|
g.data.local.machine.put_settings(arcade.id, game, version, category, current_settings)
|
|
|
|
# Return the updated value
|
|
return {
|
|
'game_settings': [
|
|
gs for gs in get_game_settings(g.data, arcadeid)
|
|
if gs['game'] == game.value and gs['version'] == version
|
|
][0],
|
|
}
|