diff --git a/bemani/data/config.py b/bemani/data/config.py index b7067cc..89b0021 100644 --- a/bemani/data/config.py +++ b/bemani/data/config.py @@ -75,6 +75,10 @@ class Server: def enforce_pcbid(self) -> bool: return bool(self.__config.get('server', {}).get('enforce_pcbid', False)) + @property + def pcbid_self_grant_limit(self) -> int: + return int(self.__config.get('server', {}).get('pcbid_self_grant_limit', 0)) + class Client: def __init__(self, parent_config: "Config") -> None: diff --git a/bemani/frontend/admin/admin.py b/bemani/frontend/admin/admin.py index 84535bd..3d20a4c 100644 --- a/bemani/frontend/admin/admin.py +++ b/bemani/frontend/admin/admin.py @@ -226,7 +226,7 @@ def viewarcades() -> Response: ) -@admin_pages.route('/machines') +@admin_pages.route('/pcbids') @adminrequired def viewmachines() -> Response: games: Dict[str, Dict[int, str]] = {} @@ -255,6 +255,7 @@ def viewmachines() -> Response: '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'), @@ -378,6 +379,16 @@ def listuser(userid: int) -> Dict[str, Any]: } +@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 @@ -619,7 +630,7 @@ def removeserver() -> Dict[str, Any]: } -@admin_pages.route('/machines/generate', methods=['POST']) +@admin_pages.route('/pcbids/generate', methods=['POST']) @jsonify @adminrequired def generatepcbid() -> Dict[str, Any]: @@ -647,7 +658,7 @@ def generatepcbid() -> Dict[str, Any]: } -@admin_pages.route('/machines/add', methods=['POST']) +@admin_pages.route('/pcbids/add', methods=['POST']) @jsonify @adminrequired def addpcbid() -> Dict[str, Any]: @@ -678,11 +689,11 @@ def addpcbid() -> Dict[str, Any]: } -@admin_pages.route('/machines/update', methods=['POST']) +@admin_pages.route('/pcbids/update', methods=['POST']) @jsonify @adminrequired def updatepcbid() -> Dict[str, Any]: - # Attempt to look this arcade up + # 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']) @@ -692,7 +703,7 @@ def updatepcbid() -> Dict[str, Any]: # 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'This port is already in use by \'{other_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!') @@ -711,14 +722,14 @@ def updatepcbid() -> Dict[str, Any]: } -@admin_pages.route('/machines/remove', methods=['POST']) +@admin_pages.route('/pcbids/remove', methods=['POST']) @jsonify @adminrequired def removepcbid() -> Dict[str, Any]: - # Attempt to look this arcade up + # 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 machine to delete!') + raise Exception('Unable to find PCBID to delete!') g.data.local.machine.destroy_machine(pcbid) diff --git a/bemani/frontend/app.py b/bemani/frontend/app.py index a515419..aeff493 100644 --- a/bemani/frontend/app.py +++ b/bemani/frontend/app.py @@ -670,7 +670,7 @@ def navigation() -> Dict[str, Any]: 'uri': url_for('admin_pages.viewarcades'), }, { - 'label': 'Machines', + 'label': 'PCBIDs', 'uri': url_for('admin_pages.viewmachines'), }, { diff --git a/bemani/frontend/arcade/arcade.py b/bemani/frontend/arcade/arcade.py index 9d81e25..1dfc60b 100644 --- a/bemani/frontend/arcade/arcade.py +++ b/bemani/frontend/arcade/arcade.py @@ -1,4 +1,5 @@ -from typing import Any, Dict, List +import random +from typing import Any, Dict, List, Optional from flask import Blueprint, request, Response, abort, url_for from bemani.backend.base import Base @@ -19,6 +20,10 @@ arcade_pages = Blueprint( ) +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' @@ -50,6 +55,7 @@ def format_machine(machine: Machine) -> Dict[str, Any]: 'description': machine.description, 'port': machine.port, 'game': game, + 'editable': is_user_editable(machine), } @@ -154,10 +160,10 @@ def viewarcade(arcadeid: int) -> Response: '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), - 'viewuser': url_for('admin_pages.viewuser', userid=-1), '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'), @@ -165,6 +171,9 @@ def viewarcade(arcadeid: int) -> Response: '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), + 'generatepcbid': url_for('arcade_pages.generatepcbid', arcadeid=arcadeid), + 'updatepcbid': url_for('arcade_pages.updatepcbid', arcadeid=arcadeid), + 'removepcbid': url_for('arcade_pages.removepcbid', arcadeid=arcadeid), }, ) @@ -296,6 +305,137 @@ def updatepin(arcadeid: int) -> Dict[str, Any]: return {'pin': pin} +@arcade_pages.route('//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('//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('//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('//update/', methods=['POST']) @jsonify @loginrequired diff --git a/bemani/frontend/static/controllers/admin/api.react.js b/bemani/frontend/static/controllers/admin/api.react.js index 3b97706..9e744e4 100644 --- a/bemani/frontend/static/controllers/admin/api.react.js +++ b/bemani/frontend/static/controllers/admin/api.react.js @@ -510,7 +510,7 @@ var api_management = React.createClass({ @@ -593,7 +593,7 @@ var api_management = React.createClass({ diff --git a/bemani/frontend/static/controllers/admin/arcades.react.js b/bemani/frontend/static/controllers/admin/arcades.react.js index bf10885..254d971 100644 --- a/bemani/frontend/static/controllers/admin/arcades.react.js +++ b/bemani/frontend/static/controllers/admin/arcades.react.js @@ -457,7 +457,7 @@ var card_management = React.createClass({ diff --git a/bemani/frontend/static/controllers/admin/machines.react.js b/bemani/frontend/static/controllers/admin/machines.react.js index 31880f4..8bf98a4 100644 --- a/bemani/frontend/static/controllers/admin/machines.react.js +++ b/bemani/frontend/static/controllers/admin/machines.react.js @@ -20,6 +20,10 @@ var machine_management = React.createClass({ }; }, + componentDidMount: function() { + this.refreshMachines(); + }, + componentDidUpdate: function() { if (this.focus_element && this.focus_element != this.already_focused) { this.focus_element.focus(); @@ -27,6 +31,20 @@ var machine_management = React.createClass({ } }, + refreshMachines: function() { + AJAX.get( + Link.get('refresh'), + function(response) { + this.setState({ + machines: response.machines, + arcade: response.arcades, + }); + // Refresh every 5 seconds + setTimeout(this.refreshMachines, 5000); + }.bind(this) + ); + }, + generateNewMachine: function(event) { AJAX.post( Link.get('generatepcbid'), @@ -65,6 +83,11 @@ var machine_management = React.createClass({ }, saveMachine: function(event) { + machine = this.state.editing_machine; + if (machine.port == '') { + machine.port = 0; + } + AJAX.post( Link.get('updatepcbid'), {machine: this.state.editing_machine}, @@ -83,8 +106,8 @@ var machine_management = React.createClass({ escapeKey: 'Cancel', animation: 'none', closeAnimation: 'none', - title: 'Delete Arcade', - content: 'Are you sure you want to delete this arcade from the network?', + title: 'Delete PCBID', + content: 'Are you sure you want to delete this PCBID from the network?', buttons: { Delete: { btnClass: 'delete', @@ -348,7 +371,11 @@ var machine_management = React.createClass({ var machine = this.state.editing_machine; var intRegex = /^\d*$/; if (intRegex.test(event.target.value)) { - machine.port = parseInt(event.target.value); + if (event.target.value.length > 0) { + machine.port = parseInt(event.target.value); + } else { + machine.port = ''; + } this.setState({ editing_machine: machine, }); @@ -457,6 +484,7 @@ var machine_management = React.createClass({ { name: '', render: this.renderEditButton, + action: true, }, ]} rows={this.state.machines} @@ -519,7 +547,7 @@ var machine_management = React.createClass({ @@ -569,7 +597,7 @@ var machine_management = React.createClass({ diff --git a/bemani/frontend/static/controllers/admin/news.react.js b/bemani/frontend/static/controllers/admin/news.react.js index b43830c..53f2a65 100644 --- a/bemani/frontend/static/controllers/admin/news.react.js +++ b/bemani/frontend/static/controllers/admin/news.react.js @@ -280,7 +280,7 @@ var news_management = React.createClass({ /> diff --git a/bemani/frontend/static/controllers/arcade/arcade.react.js b/bemani/frontend/static/controllers/arcade/arcade.react.js index 2a7e5c9..db59d99 100644 --- a/bemani/frontend/static/controllers/arcade/arcade.react.js +++ b/bemani/frontend/static/controllers/arcade/arcade.react.js @@ -8,6 +8,14 @@ var valid_settings = window.game_settings.map(function(setting) { }); var pagenav = new History(valid_settings); +function count_pcbids(machines) { + var count = 0; + machines.map(function(machine) { + count += (machine.editable ? 1 : 0); + }); + return count; +} + var arcade_management = React.createClass({ getInitialState: function(props) { var credits = {}; @@ -27,8 +35,13 @@ var arcade_management = React.createClass({ paseli_enabled_saving: false, paseli_infinite_saving: false, mask_services_url_saving: false, + editing_machine: null, machines: window.machines, settings: window.game_settings, + pcbcount: count_pcbids(window.machines), + random_pcbid: { + description: '', + }, current_setting: pagenav.getInitialState(makeSettingName(window.game_settings[0])), settings_changed: {}, settings_saving: {}, @@ -66,9 +79,10 @@ var arcade_management = React.createClass({ users: response.users, balances: response.balances, machines: response.machines, + pcbcount: count_pcbids(response.machines), events: response.events, }); - // Refresh every 15 seconds + // Refresh every 5 seconds setTimeout(this.refreshArcade, 5000); }.bind(this) ); @@ -282,6 +296,177 @@ var arcade_management = React.createClass({ ); }, + generateNewMachine: function(event) { + AJAX.post( + Link.get('generatepcbid'), + {machine: this.state.random_pcbid}, + function(response) { + this.setState({ + machines: response.machines, + pcbcount: count_pcbids(response.machines), + random_pcbid: { + description: '', + }, + }); + }.bind(this) + ); + event.preventDefault(); + }, + + deleteExistingMachine: function(event, pcbid) { + $.confirm({ + escapeKey: 'Cancel', + animation: 'none', + closeAnimation: 'none', + title: 'Delete PCBID', + content: 'Are you sure you want to delete this PCBID from the network?', + buttons: { + Delete: { + btnClass: 'delete', + action: function() { + AJAX.post( + Link.get('removepcbid'), + {pcbid: pcbid}, + function(response) { + this.setState({ + machines: response.machines, + pcbcount: count_pcbids(response.machines), + }); + }.bind(this) + ); + }.bind(this), + }, + Cancel: function() { + }, + } + }); + event.preventDefault(); + }, + + saveMachine: function(event) { + machine = this.state.editing_machine; + if (machine.port == '') { + machine.port = 0; + } + + AJAX.post( + Link.get('updatepcbid'), + {machine: this.state.editing_machine}, + function(response) { + this.setState({ + machines: response.machines, + pcbcount: count_pcbids(response.machines), + editing_machine: null, + }); + }.bind(this) + ); + event.preventDefault(); + }, + + renderDescription: function(machine) { + if (this.state.editing_machine && machine.pcbid == this.state.editing_machine.pcbid) { + return (this.focus_element = c)} + value={ this.state.editing_machine.description } + onChange={function(event) { + var machine = this.state.editing_machine; + machine.description = event.target.value; + this.setState({ + editing_machine: machine, + }); + }.bind(this)} + />; + } else { + return ( + { machine.description } + ); + } + }, + + renderPort: function(machine) { + if (this.state.editing_machine && machine.pcbid == this.state.editing_machine.pcbid) { + return 0) { + machine.port = parseInt(event.target.value); + } else { + machine.port = ''; + } + this.setState({ + editing_machine: machine, + }); + } + }.bind(this)} + />; + } else { + return ( + { machine.port } + ); + } + }, + + renderEditButton: function(machine) { + if (this.state.editing_machine) { + if (this.state.editing_machine.pcbid == machine.pcbid) { + return ( + + + + + ); + } else { + return ; + } + } else { + if (window.max_pcbids > 0 && machine.editable) { + return ( + + + + + ); + } else { + return ; + } + } + }, + render: function() { return (
@@ -320,40 +505,86 @@ var arcade_management = React.createClass({

PCBIDs Assigned to This Arcade

- + +
+ + { window.enforcing && this.state.pcbcount < window.max_pcbids ? +
+

Generate PCBID

+
+
+ + + + + + + + + + + + +
Description
+ + + +
+ +
+ : null + }

Game Settings For This Arcade

{ this.state.settings.map(function(game_settings) { diff --git a/bemani/frontend/templates/admin/settings.html b/bemani/frontend/templates/admin/settings.html index c5d71d0..b52ecfc 100644 --- a/bemani/frontend/templates/admin/settings.html +++ b/bemani/frontend/templates/admin/settings.html @@ -59,6 +59,8 @@
PCBID Enforcement
{{ 'active' if config.server.enforce_pcbid else 'inactive' }}
+
Self-Generated PCBID Limit
+
{{ 'disabled' if (config.server.pcbid_self_grant_limit <= 0 or not config.server.enforce_pcbid) else config.server.pcbid_self_grant_limit }}
PASELI Enabled
{{ 'yes' if config.paseli.enabled else 'no' }} (can be overridden by arcade settings)
Infinite PASELI Enabled
diff --git a/config/server.yaml b/config/server.yaml index 51ed02a..898f653 100644 --- a/config/server.yaml +++ b/config/server.yaml @@ -29,6 +29,10 @@ server: redirect: "https://eagate.573.jp" # Whether PCBIDs must be added to the network before games will work. enforce_pcbid: False + # How many PCBIDs an arcade owner can grant to themselves on the arcade + # page. Note that this setting is irrelevant if PCBID enforcing is off. + # Set to 0 or delete this setting to disable self-granting PCBIDs. + pcbid_self_grant_limit: 0 # Webhook URLs. These allow for game scores from games with scorecard support to be broadcasted to outside services. # Delete this to disable this support.