1
0
mirror of synced 2024-12-03 01:57:18 +01:00
bemaniutils/bemani/frontend/admin/admin.py
2023-02-17 03:40:07 +00:00

1285 lines
38 KiB
Python

import random
from typing import Dict, Tuple, Any, Optional
from flask import Blueprint, request, Response, render_template, url_for
from bemani.backend.base import Base
from bemani.common import (
CardCipher,
CardCipherException,
GameConstants,
RegionConstants,
ValidatedDict,
)
from bemani.data import Arcade, Machine, User, UserID, News, Event, Server, Client
from bemani.data.api.client import APIClient, NotAuthorizedAPIException, APIException
from bemani.frontend.app import (
adminrequired,
jsonify,
valid_email,
valid_username,
valid_pin,
render_react,
)
from bemani.frontend.gamesettings import get_game_settings
from bemani.frontend.iidx.iidx import IIDXFrontend
from bemani.frontend.jubeat.jubeat import JubeatFrontend
from bemani.frontend.popn.popn import PopnMusicFrontend
from bemani.frontend.templates import templates_location
from bemani.frontend.static import static_location
from bemani.frontend.types import g
admin_pages = Blueprint(
"admin_pages",
__name__,
url_prefix="/admin",
template_folder=templates_location,
static_folder=static_location,
)
def format_arcade(arcade: Arcade) -> Dict[str, Any]:
owners = []
for owner in arcade.owners:
user = g.data.local.user.get_user(owner)
if user is not None:
owners.append(user.username)
return {
"id": arcade.id,
"name": arcade.name,
"description": arcade.description,
"region": arcade.region,
"area": arcade.area or "",
"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": owners,
}
def format_machine(machine: Machine) -> Dict[str, Any]:
return {
"id": machine.id,
"pcbid": machine.pcbid,
"name": machine.name,
"description": machine.description,
"arcade": machine.arcade,
"port": machine.port,
"game": machine.game.value if machine.game else "any",
"version": machine.version,
}
def format_card(card: Tuple[str, Optional[UserID]]) -> Dict[str, Any]:
owner = None
if card[1] is not None:
user = g.data.local.user.get_user(card[1])
if user is not None:
owner = user.username
try:
return {
"number": CardCipher.encode(card[0]),
"owner": owner,
"id": card[1],
}
except CardCipherException:
return {
"number": "????????????????",
"owner": owner,
"id": card[1],
}
def format_user(user: User) -> Dict[str, Any]:
return {
"id": user.id,
"username": user.username,
"email": user.email,
"admin": user.admin,
}
def format_news(news: News) -> Dict[str, Any]:
return {
"id": news.id,
"timestamp": news.timestamp,
"title": news.title,
"body": news.body,
}
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,
}
def format_client(client: Client) -> Dict[str, Any]:
return {
"id": client.id,
"name": client.name,
"token": client.token,
}
def format_server(server: Server) -> Dict[str, Any]:
return {
"id": server.id,
"uri": server.uri,
"token": server.token,
"allow_stats": server.allow_stats,
"allow_scores": server.allow_scores,
}
@admin_pages.route("/")
@adminrequired
def viewsettings() -> Response:
return Response(
render_template(
"admin/settings.html",
**{
"title": "Network Settings",
"config": g.config,
"region": RegionConstants.LUT,
},
)
)
@admin_pages.route("/events")
@adminrequired
def viewevents() -> Response:
iidx = IIDXFrontend(g.data, g.config, g.cache)
jubeat = JubeatFrontend(g.data, g.config, g.cache)
pnm = PopnMusicFrontend(g.data, g.config, g.cache)
return render_react(
"Events",
"admin/events.react.js",
{
"events": [
format_event(event)
for event in g.data.local.network.get_events(limit=100)
],
"users": {
user.id: user.username for user in g.data.local.user.get_all_users()
},
"arcades": {
arcade.id: arcade.name
for arcade in g.data.local.machine.get_all_arcades()
},
"iidxsongs": iidx.get_all_songs(),
"jubeatsongs": jubeat.get_all_songs(),
"pnmsongs": pnm.get_all_songs(),
"iidxversions": {
version: name for (game, version, name) in iidx.all_games()
},
"jubeatversions": {
version: name for (game, version, name) in jubeat.all_games()
},
"pnmversions": {version: name for (game, version, name) in pnm.all_games()},
},
{
"refresh": url_for("admin_pages.listevents", since=-1),
"backfill": url_for("admin_pages.backfillevents", until=-1),
"viewuser": url_for("admin_pages.viewuser", userid=-1),
"jubeatsong": url_for("jubeat_pages.viewtopscores", musicid=-1)
if GameConstants.JUBEAT in g.config.support
else None,
"iidxsong": url_for("iidx_pages.viewtopscores", musicid=-1)
if GameConstants.IIDX in g.config.support
else None,
"pnmsong": url_for("popn_pages.viewtopscores", musicid=-1)
if GameConstants.POPN_MUSIC in g.config.support
else None,
},
)
@admin_pages.route("/events/backfill/<int:until>")
@jsonify
@adminrequired
def backfillevents(until: int) -> Dict[str, Any]:
return {
"events": [
format_event(event)
for event in g.data.local.network.get_events(until_id=until, limit=1000)
],
}
@admin_pages.route("/events/list/<int:since>")
@jsonify
@adminrequired
def listevents(since: int) -> Dict[str, Any]:
return {
"events": [
format_event(event)
for event in g.data.local.network.get_events(since_id=since)
],
"users": {user.id: user.username for user in g.data.local.user.get_all_users()},
"arcades": {
arcade.id: arcade.name for arcade in g.data.local.machine.get_all_arcades()
},
}
@admin_pages.route("/api")
@adminrequired
def viewapi() -> Response:
return render_react(
"Data API",
"admin/api.react.js",
{
"clients": [
format_client(client) for client in g.data.local.api.get_all_clients()
],
"servers": [
format_server(server) for server in g.data.local.api.get_all_servers()
],
},
{
"addclient": url_for("admin_pages.addclient"),
"updateclient": url_for("admin_pages.updateclient"),
"removeclient": url_for("admin_pages.removeclient"),
"addserver": url_for("admin_pages.addserver"),
"updateserver": url_for("admin_pages.updateserver"),
"removeserver": url_for("admin_pages.removeserver"),
"queryserver": url_for("admin_pages.queryserver", serverid=-1),
},
)
@admin_pages.route("/arcades")
@adminrequired
def viewarcades() -> Response:
return render_react(
"Arcades",
"admin/arcades.react.js",
{
"arcades": [
format_arcade(arcade)
for arcade in g.data.local.machine.get_all_arcades()
],
"regions": RegionConstants.LUT,
"usernames": g.data.local.user.get_all_usernames(),
"paseli_enabled": g.config.paseli.enabled,
"paseli_infinite": g.config.paseli.infinite,
"default_region": g.config.server.region,
"default_area": g.config.server.area,
"mask_services_url": False,
},
{
"addarcade": url_for("admin_pages.addarcade"),
"updatearcade": url_for("admin_pages.updatearcade"),
"removearcade": url_for("admin_pages.removearcade"),
},
)
@admin_pages.route("/pcbids")
@adminrequired
def viewmachines() -> Response:
games: Dict[str, Dict[int, str]] = {}
for game, version, name in Base.all_games():
if game.value not in games:
games[game.value] = {}
games[game.value][version] = name
return render_react(
"Machines",
"admin/machines.react.js",
{
"machines": [
format_machine(machine)
for machine in g.data.local.machine.get_all_machines()
],
"arcades": {
arcade.id: arcade.name
for arcade in g.data.local.machine.get_all_arcades()
},
"series": {
GameConstants.BISHI_BASHI.value: "BishiBashi",
GameConstants.DDR.value: "DDR",
GameConstants.IIDX.value: "IIDX",
GameConstants.JUBEAT.value: "Jubeat",
GameConstants.MGA.value: "Metal Gear Arcade",
GameConstants.MUSECA.value: "MÚSECA",
GameConstants.POPN_MUSIC.value: "Pop'n Music",
GameConstants.REFLEC_BEAT.value: "Reflec Beat",
GameConstants.SDVX.value: "SDVX",
},
"games": games,
"enforcing": g.config.server.enforce_pcbid,
},
{
"refresh": url_for("admin_pages.listmachines"),
"generatepcbid": url_for("admin_pages.generatepcbid"),
"addpcbid": url_for("admin_pages.addpcbid"),
"updatepcbid": url_for("admin_pages.updatepcbid"),
"removepcbid": url_for("admin_pages.removepcbid"),
},
)
@admin_pages.route("/cards")
@adminrequired
def viewcards() -> Response:
return render_react(
"Cards",
"admin/cards.react.js",
{
"cards": [format_card(card) for card in g.data.local.user.get_all_cards()],
"usernames": g.data.local.user.get_all_usernames(),
},
{
"addcard": url_for("admin_pages.addcard"),
"removecard": url_for("admin_pages.removecard"),
"viewuser": url_for("admin_pages.viewuser", userid=-1),
},
)
@admin_pages.route("/users")
@adminrequired
def viewusers() -> Response:
return render_react(
"Users",
"admin/users.react.js",
{
"users": [format_user(user) for user in g.data.local.user.get_all_users()],
},
{
"searchusers": url_for("admin_pages.searchusers"),
"viewuser": url_for("admin_pages.viewuser", userid=-1),
},
)
@admin_pages.route("/news")
@adminrequired
def viewnews() -> Response:
return render_react(
"News",
"admin/news.react.js",
{
"news": [format_news(news) for news in g.data.local.network.get_all_news()],
},
{
"removenews": url_for("admin_pages.removenews"),
"addnews": url_for("admin_pages.addnews"),
"updatenews": url_for("admin_pages.updatenews"),
},
)
@admin_pages.route("/gamesettings")
@adminrequired
def viewgamesettings() -> Response:
return render_react(
"Game Settings",
"admin/gamesettings.react.js",
{
"game_settings": get_game_settings(
g.data, g.data.local.machine.DEFAULT_SETTINGS_ARCADE
),
},
{
"update_settings": url_for("admin_pages.updatesettings"),
},
)
@admin_pages.route("/users/<int:userid>")
@adminrequired
def viewuser(userid: int) -> Response:
# Cast the userID.
userid = UserID(userid)
user = g.data.local.user.get_user(userid)
def __format_card(card: str) -> str:
try:
return CardCipher.encode(card)
except CardCipherException:
return "????????????????"
cards = [__format_card(card) for card in g.data.local.user.get_cards(userid)]
arcades = g.data.local.machine.get_all_arcades()
return render_react(
"User",
"admin/user.react.js",
{
"user": {
"email": user.email,
"username": user.username,
},
"cards": cards,
"arcades": {arcade.id: arcade.name for arcade in arcades},
"balances": {
arcade.id: g.data.local.user.get_balance(userid, arcade.id)
for arcade in arcades
},
"events": [
format_event(event)
for event in g.data.local.network.get_events(
userid=userid, event="paseli_transaction"
)
],
},
{
"refresh": url_for("admin_pages.listuser", userid=userid),
"removeusercard": url_for("admin_pages.removeusercard", userid=userid),
"addusercard": url_for("admin_pages.addusercard", userid=userid),
"updatebalance": url_for("admin_pages.updatebalance", userid=userid),
"updateusername": url_for("admin_pages.updateusername", userid=userid),
"updateemail": url_for("admin_pages.updateemail", userid=userid),
"updatepin": url_for("admin_pages.updatepin", userid=userid),
"updatepassword": url_for("admin_pages.updatepassword", userid=userid),
},
)
@admin_pages.route("/users/<int:userid>/list")
@jsonify
@adminrequired
def listuser(userid: int) -> Dict[str, Any]:
# Cast the userID.
userid = UserID(userid)
def __format_card(card: str) -> str:
try:
return CardCipher.encode(card)
except CardCipherException:
return "????????????????"
cards = [__format_card(card) for card in g.data.local.user.get_cards(userid)]
arcades = g.data.local.machine.get_all_arcades()
return {
"cards": cards,
"arcades": {arcade.id: arcade.name for arcade in arcades},
"balances": {
arcade.id: g.data.local.user.get_balance(userid, arcade.id)
for arcade in arcades
},
"events": [
format_event(event)
for event in g.data.local.network.get_events(
userid=userid, event="paseli_transaction"
)
],
}
@admin_pages.route("/arcades/list")
@jsonify
@adminrequired
def listmachines() -> Dict[str, Any]:
return {
"machines": [
format_machine(machine)
for machine in g.data.local.machine.get_all_machines()
],
"arcades": {
arcade.id: arcade.name for arcade in g.data.local.machine.get_all_arcades()
},
}
@admin_pages.route("/arcades/update", methods=["POST"])
@jsonify
@adminrequired
def updatearcade() -> Dict[str, Any]:
# Attempt to look this arcade up
new_values = request.get_json()["arcade"]
arcade = g.data.local.machine.get_arcade(new_values["id"])
if arcade is None:
raise Exception("Unable to find arcade to update!")
arcade.name = new_values["name"]
arcade.description = new_values["description"]
arcade.region = new_values["region"]
arcade.area = new_values["area"] or None
arcade.data.replace_bool("paseli_enabled", new_values["paseli_enabled"])
arcade.data.replace_bool("paseli_infinite", new_values["paseli_infinite"])
arcade.data.replace_bool("mask_services_url", new_values["mask_services_url"])
owners = []
for owner in new_values["owners"]:
ownerid = g.data.local.user.from_username(owner)
if ownerid is not None:
owners.append(ownerid)
owners = list(set(owners))
arcade.owners = owners
g.data.local.machine.put_arcade(arcade)
# Just return all arcades for ease of updating
return {
"arcades": [
format_arcade(arcade) for arcade in g.data.local.machine.get_all_arcades()
],
}
@admin_pages.route("/arcades/add", methods=["POST"])
@jsonify
@adminrequired
def addarcade() -> Dict[str, Any]:
# Attempt to look this arcade up
new_values = request.get_json()["arcade"]
if len(new_values["name"]) == 0:
raise Exception("Please name your new arcade!")
if len(new_values["description"]) == 0:
raise Exception("Please describe your new arcade!")
owners = []
for owner in new_values["owners"]:
ownerid = g.data.local.user.from_username(owner)
if ownerid is not None:
owners.append(ownerid)
owners = list(set(owners))
g.data.local.machine.create_arcade(
new_values["name"],
new_values["description"],
new_values["region"],
new_values["area"] or None,
{
"paseli_enabled": new_values["paseli_enabled"],
"paseli_infinite": new_values["paseli_infinite"],
"mask_services_url": new_values["mask_services_url"],
},
owners,
)
# Just return all arcades for ease of updating
return {
"arcades": [
format_arcade(arcade) for arcade in g.data.local.machine.get_all_arcades()
],
}
@admin_pages.route("/arcades/remove", methods=["POST"])
@jsonify
@adminrequired
def removearcade() -> Dict[str, Any]:
# Attempt to look this arcade up
arcadeid = request.get_json()["arcadeid"]
arcade = g.data.local.machine.get_arcade(arcadeid)
if arcade is None:
raise Exception("Unable to find arcade to delete!")
g.data.local.machine.destroy_arcade(arcadeid)
# Just return all arcades for ease of updating
return {
"arcades": [
format_arcade(arcade) for arcade in g.data.local.machine.get_all_arcades()
],
}
@admin_pages.route("/clients/update", methods=["POST"])
@jsonify
@adminrequired
def updateclient() -> Dict[str, Any]:
# Attempt to look this client up
new_values = request.get_json()["client"]
client = g.data.local.api.get_client(new_values["id"])
if client is None:
raise Exception("Unable to find client to update!")
if len(new_values["name"]) == 0:
raise Exception("Client names must be at least one character long!")
client.name = new_values["name"]
g.data.local.api.put_client(client)
# Just return all clients for ease of updating
return {
"clients": [
format_client(client) for client in g.data.local.api.get_all_clients()
],
}
@admin_pages.route("/clients/add", methods=["POST"])
@jsonify
@adminrequired
def addclient() -> Dict[str, Any]:
# Attempt to look this client up
new_values = request.get_json()["client"]
if len(new_values["name"]) == 0:
raise Exception("Please name your new client!")
g.data.local.api.create_client(
new_values["name"],
)
# Just return all clientss for ease of updating
return {
"clients": [
format_client(client) for client in g.data.local.api.get_all_clients()
],
}
@admin_pages.route("/clients/remove", methods=["POST"])
@jsonify
@adminrequired
def removeclient() -> Dict[str, Any]:
# Attempt to look this client up
clientid = request.get_json()["clientid"]
client = g.data.local.api.get_client(clientid)
if client is None:
raise Exception("Unable to find client to delete!")
g.data.local.api.destroy_client(clientid)
# Just return all clients for ease of updating
return {
"clients": [
format_client(client) for client in g.data.local.api.get_all_clients()
],
}
@admin_pages.route("/server/<int:serverid>/info")
@jsonify
@adminrequired
def queryserver(serverid: int) -> Dict[str, Any]:
# Attempt to look this server up
server = g.data.local.api.get_server(serverid)
if server is None:
raise Exception("Unable to find server to query!")
client = APIClient(server.uri, server.token, False, False)
try:
serverinfo = client.get_server_info()
info = {
"name": serverinfo["name"],
"email": serverinfo["email"],
}
info["status"] = (
"ok" if APIClient.API_VERSION in serverinfo["versions"] else "badversion"
)
except NotAuthorizedAPIException:
info = {
"name": "unknown",
"email": "unknown",
"status": "badauth",
}
except APIException:
info = {
"name": "unknown",
"email": "unknown",
"status": "error",
}
return info
@admin_pages.route("/servers/update", methods=["POST"])
@jsonify
@adminrequired
def updateserver() -> Dict[str, Any]:
# Attempt to look this server up
new_values = request.get_json()["server"]
server = g.data.local.api.get_server(new_values["id"])
if server is None:
raise Exception("Unable to find server to update!")
if len(new_values["uri"]) == 0:
raise Exception("Please provide a valid connection URI for this server!")
if len(new_values["token"]) == 0 or len(new_values["token"]) > 64:
raise Exception("Please provide a valid connection token for this server!")
server.uri = new_values["uri"]
server.token = new_values["token"]
server.allow_stats = new_values["allow_stats"]
server.allow_scores = new_values["allow_scores"]
g.data.local.api.put_server(server)
# Just return all servers for ease of updating
return {
"servers": [
format_server(server) for server in g.data.local.api.get_all_servers()
],
}
@admin_pages.route("/servers/add", methods=["POST"])
@jsonify
@adminrequired
def addserver() -> Dict[str, Any]:
# Attempt to look this server up
new_values = request.get_json()["server"]
if len(new_values["uri"]) == 0:
raise Exception("Please provide a connection URI for the new server!")
if len(new_values["token"]) == 0 or len(new_values["token"]) > 64:
raise Exception("Please provide a valid connection token for the new server!")
g.data.local.api.create_server(
new_values["uri"],
new_values["token"],
)
# Just return all serverss for ease of updating
return {
"servers": [
format_server(server) for server in g.data.local.api.get_all_servers()
],
}
@admin_pages.route("/servers/remove", methods=["POST"])
@jsonify
@adminrequired
def removeserver() -> Dict[str, Any]:
# Attempt to look this server up
serverid = request.get_json()["serverid"]
server = g.data.local.api.get_server(serverid)
if server is None:
raise Exception("Unable to find server to delete!")
g.data.local.api.destroy_server(serverid)
# Just return all servers for ease of updating
return {
"servers": [
format_server(server) for server in g.data.local.api.get_all_servers()
],
}
@admin_pages.route("/pcbids/generate", methods=["POST"])
@jsonify
@adminrequired
def generatepcbid() -> Dict[str, Any]:
# Attempt to look this arcade up
new_pcbid = request.get_json()["machine"]
if new_pcbid["arcade"] is not None:
arcade = g.data.local.machine.get_arcade(new_pcbid["arcade"])
if arcade is None:
raise Exception("Unable to find arcade to link PCBID to!")
# Will be set by the game on boot.
name: str = "なし"
pcbid: Optional[str] = None
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
g.data.local.machine.create_machine(
pcbid, name, new_pcbid["description"], new_pcbid["arcade"]
)
# Just return all machines for ease of updating
return {
"machines": [
format_machine(machine)
for machine in g.data.local.machine.get_all_machines()
],
}
@admin_pages.route("/pcbids/add", methods=["POST"])
@jsonify
@adminrequired
def addpcbid() -> Dict[str, Any]:
# Attempt to look this arcade up
new_pcbid = request.get_json()["machine"]
if new_pcbid["arcade"] is not None:
arcade = g.data.local.machine.get_arcade(new_pcbid["arcade"])
if arcade is None:
raise Exception("Unable to find arcade to link PCBID to!")
# Verify that the PCBID is valid
potential_pcbid = "".join(
[c for c in new_pcbid["pcbid"].upper() if c in "0123456789ABCDEF"]
)
if len(potential_pcbid) != len(new_pcbid["pcbid"]):
raise Exception("Invalid characters in PCBID!")
if len(potential_pcbid) != 20:
raise Exception("PCBID has invalid length!")
if g.data.local.machine.get_machine(potential_pcbid) is not None:
raise Exception("PCBID already exists!")
# Will be set by the game on boot.
name = "なし"
g.data.local.machine.create_machine(
potential_pcbid, name, new_pcbid["description"], new_pcbid["arcade"]
)
# Just return all machines for ease of updating
return {
"machines": [
format_machine(machine)
for machine in g.data.local.machine.get_all_machines()
],
}
@admin_pages.route("/pcbids/update", methods=["POST"])
@jsonify
@adminrequired
def updatepcbid() -> Dict[str, Any]:
# Attempt to look this machine up
machine = request.get_json()["machine"]
if machine["arcade"] is not None:
arcade = g.data.local.machine.get_arcade(machine["arcade"])
if arcade is None:
raise Exception("Unable to find arcade to link PCBID to!")
# Make sure we don't duplicate port assignments
other_pcbid = g.data.local.machine.from_port(machine["port"])
if other_pcbid is not None and other_pcbid != machine["pcbid"]:
raise Exception(f"The specified port is already in use by '{other_pcbid}'!")
if machine["port"] < 1 or machine["port"] > 65535:
raise Exception("The specified port is out of range!")
current_machine = g.data.local.machine.get_machine(machine["pcbid"])
current_machine.description = machine["description"]
current_machine.arcade = machine["arcade"]
current_machine.port = machine["port"]
current_machine.game = (
None if machine["game"] == "any" else GameConstants(machine["game"])
)
current_machine.version = None if machine["game"] == "any" else machine["version"]
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()
],
}
@admin_pages.route("/pcbids/remove", methods=["POST"])
@jsonify
@adminrequired
def removepcbid() -> Dict[str, Any]:
# Attempt to look this machine up
pcbid = request.get_json()["pcbid"]
if g.data.local.machine.get_machine(pcbid) is None:
raise Exception("Unable to find PCBID to delete!")
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()
],
}
@admin_pages.route("/cards/remove", methods=["POST"])
@jsonify
@adminrequired
def removecard() -> Dict[str, Any]:
# Grab card, convert it
card = request.get_json()["card"]
try:
cardid = CardCipher.decode(card)
except CardCipherException:
raise Exception("Invalid card number!")
# Make sure it is our card
userid = g.data.local.user.from_cardid(cardid)
# Remove it from the user's account
g.data.local.user.destroy_card(userid, cardid)
# Return new card list
return {
"cards": [format_card(card) for card in g.data.local.user.get_all_cards()],
}
@admin_pages.route("/cards/add", methods=["POST"])
@jsonify
@adminrequired
def addcard() -> Dict[str, Any]:
# Grab card, convert it
card = request.get_json()["card"]
try:
cardid = CardCipher.decode(card["number"])
except CardCipherException:
raise Exception("Invalid card number!")
# Make sure it is our card
userid = g.data.local.user.from_username(card["owner"])
if userid is None:
raise Exception("Cannot find user to add card to!")
# See if it is already claimed
curuserid = g.data.local.user.from_cardid(cardid)
if curuserid is not None:
raise Exception("This card is already in use!")
# Add it to the user's account
g.data.local.user.add_card(userid, cardid)
# Return new card list
return {
"cards": [format_card(card) for card in g.data.local.user.get_all_cards()],
}
@admin_pages.route("/users/search", methods=["POST"])
@jsonify
@adminrequired
def searchusers() -> Dict[str, Any]:
# Grab card, convert it
searchdetails = request.get_json()["user_search"]
if len(searchdetails["card"]) > 0:
try:
cardid = CardCipher.decode(searchdetails["card"])
actual_userid = g.data.local.user.from_cardid(cardid)
if actual_userid is None:
# Force a non-match below
actual_userid = UserID(-1)
except CardCipherException:
actual_userid = UserID(-1)
else:
actual_userid = None
def match(user: User) -> bool:
if actual_userid is not None:
return user.id == actual_userid
else:
return True
return {
"users": [
format_user(user)
for user in g.data.local.user.get_all_users()
if match(user)
],
}
@admin_pages.route("/users/<int:userid>/balance/update", methods=["POST"])
@jsonify
@adminrequired
def updatebalance(userid: int) -> Dict[str, Any]:
# Cast the userID.
userid = UserID(userid)
credits = request.get_json()["credits"]
user = g.data.local.user.get_user(userid)
arcades = g.data.local.machine.get_all_arcades()
# Make sure the user ID is valid
if user is None:
raise Exception("Cannot find user to update!")
# Update balances
for arcadeid in credits:
balance = g.data.local.user.update_balance(userid, arcadeid, credits[arcadeid])
if balance is not None:
g.data.local.network.put_event(
"paseli_transaction",
{
"delta": credits[arcadeid],
"balance": balance,
"reason": "admin adjustment",
},
userid=userid,
arcadeid=arcadeid,
)
return {
"arcades": {arcade.id: arcade.name for arcade in arcades},
"balances": {
arcade.id: g.data.local.user.get_balance(userid, arcade.id)
for arcade in arcades
},
"events": [
format_event(event)
for event in g.data.local.network.get_events(
userid=userid, event="paseli_transaction"
)
],
}
@admin_pages.route("/users/<int:userid>/username/update", methods=["POST"])
@jsonify
@adminrequired
def updateusername(userid: int) -> Dict[str, Any]:
# Cast the userID.
userid = UserID(userid)
username = request.get_json()["username"]
user = g.data.local.user.get_user(userid)
# Make sure the user ID is valid
if user is None:
raise Exception("Cannot find user to update!")
if not valid_username(username):
raise Exception("Invalid username!")
# Make sure this user ID isn't taken
potential_userid = g.data.local.user.from_username(username)
if potential_userid is not None and potential_userid != userid:
raise Exception("That username is already taken!")
# Update the user
user.username = username
g.data.local.user.put_user(user)
return {
"username": username,
}
@admin_pages.route("/users/<int:userid>/email/update", methods=["POST"])
@jsonify
@adminrequired
def updateemail(userid: int) -> Dict[str, Any]:
# Cast the userID.
userid = UserID(userid)
email = request.get_json()["email"]
user = g.data.local.user.get_user(userid)
# Make sure the user ID is valid
if user is None:
raise Exception("Cannot find user to update!")
if not valid_email(email):
raise Exception("Invalid email!")
# Update the user
user.email = email
g.data.local.user.put_user(user)
return {
"email": email,
}
@admin_pages.route("/users/<int:userid>/pin/update", methods=["POST"])
@jsonify
@adminrequired
def updatepin(userid: int) -> Dict[str, Any]:
# Cast the userID.
userid = UserID(userid)
pin = request.get_json()["pin"]
user = g.data.local.user.get_user(userid)
# Make sure the user ID is valid
if user is None:
raise Exception("Cannot find user to update!")
if not valid_pin(pin, "card"):
raise Exception("Invalid pin, must be exactly 4 digits!")
# Update the user
g.data.local.user.update_pin(userid, pin)
return {}
@admin_pages.route("/users/<int:userid>/password/update", methods=["POST"])
@jsonify
@adminrequired
def updatepassword(userid: int) -> Dict[str, Any]:
# Cast the userID.
userid = UserID(userid)
new1 = request.get_json()["new1"]
new2 = request.get_json()["new2"]
user = g.data.local.user.get_user(userid)
# Make sure the user ID is valid
if user is None:
raise Exception("Cannot find user to update!")
# Now, make sure that the passwords match
if new1 != new2:
raise Exception("Passwords do not match each other!")
# Now, make sure passwords are long enough
if len(new1) < 6:
raise Exception("Password is not long enough!")
# Update the user
g.data.local.user.update_password(userid, new1)
return {}
@admin_pages.route("/users/<int:userid>/cards/remove", methods=["POST"])
@jsonify
@adminrequired
def removeusercard(userid: int) -> Dict[str, Any]:
# Cast the userID.
userid = UserID(userid)
# Grab card, convert it
card = request.get_json()["card"]
try:
cardid = CardCipher.decode(card)
except CardCipherException:
raise Exception("Invalid card number!")
user = g.data.local.user.get_user(userid)
# Make sure the user ID is valid
if user is None:
raise Exception("Cannot find user to update!")
# Remove it from the user's account
g.data.local.user.destroy_card(userid, cardid)
# Return new card list
return {
"cards": [
CardCipher.encode(card) for card in g.data.local.user.get_cards(userid)
],
}
@admin_pages.route("/users/<int:userid>/cards/add", methods=["POST"])
@jsonify
@adminrequired
def addusercard(userid: int) -> Dict[str, Any]:
# Cast the userID.
userid = UserID(userid)
# Grab card, convert it
card = request.get_json()["card"]
try:
cardid = CardCipher.decode(card)
except CardCipherException:
raise Exception("Invalid card number!")
user = g.data.local.user.get_user(userid)
# Make sure the user ID is valid
if user is None:
raise Exception("Cannot find user to update!")
# See if it is already claimed
curuserid = g.data.local.user.from_cardid(cardid)
if curuserid is not None:
raise Exception("This card is already in use!")
# Add it to the user's account
g.data.local.user.add_card(userid, cardid)
# Return new card list
return {
"cards": [
CardCipher.encode(card) for card in g.data.local.user.get_cards(userid)
],
}
@admin_pages.route("/news/add", methods=["POST"])
@jsonify
@adminrequired
def addnews() -> Dict[str, Any]:
news = request.get_json()["news"]
if len(news["title"]) == 0:
raise Exception("Please provide a title!")
if len(news["body"]) == 0:
raise Exception("Please provide a body!")
g.data.local.network.create_news(news["title"], news["body"])
return {
"news": [format_news(news) for news in g.data.local.network.get_all_news()],
}
@admin_pages.route("/news/remove", methods=["POST"])
@jsonify
@adminrequired
def removenews() -> Dict[str, Any]:
newsid = request.get_json()["newsid"]
if g.data.local.network.get_news(newsid) is None:
raise Exception("Unable to find entry to delete!")
g.data.local.network.destroy_news(newsid)
return {
"news": [format_news(news) for news in g.data.local.network.get_all_news()],
}
@admin_pages.route("/news/update", methods=["POST"])
@jsonify
@adminrequired
def updatenews() -> Dict[str, Any]:
new_news = request.get_json()["news"]
if g.data.local.network.get_news(new_news["id"]) is None:
raise Exception("Unable to find entry to update!")
if len(new_news["title"]) == 0:
raise Exception("Please provide a title!")
if len(new_news["body"]) == 0:
raise Exception("Please provide a body!")
news = g.data.local.network.get_news(new_news["id"])
news.title = new_news["title"]
news.body = new_news["body"]
g.data.local.network.put_news(news)
return {
"news": [format_news(news) for news in g.data.local.network.get_all_news()],
}
@admin_pages.route("/gamesettings/update", methods=["POST"])
@jsonify
@adminrequired
def updatesettings() -> Dict[str, Any]:
# Cast the ID for type safety.
arcadeid = g.data.local.machine.DEFAULT_SETTINGS_ARCADE
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(
arcadeid, 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],
}