Add inventory controls and enhance dashboard
This commit is contained in:
@@ -14,7 +14,7 @@ import logging
|
||||
log = logging.getLogger('werkzeug')
|
||||
log.setLevel(logging.ERROR)
|
||||
|
||||
from flask import Flask, abort, send_file
|
||||
from flask import Flask, abort, jsonify, request, send_file
|
||||
from flask_cors import CORS
|
||||
build_folder = 'web_interface/build'
|
||||
app = Flask(__name__, template_folder=build_folder, static_folder=build_folder, static_url_path='')
|
||||
@@ -62,6 +62,38 @@ _item_icon_cache = Path(__file__).resolve().parent / 'web_interface' / 'item_cac
|
||||
_item_icon_locks = {}
|
||||
_item_icon_locks_guard = threading.Lock()
|
||||
|
||||
def _api_plain(value, depth=0):
|
||||
"""Convert item component values into bounded JSON-safe structures."""
|
||||
if depth > 12:
|
||||
return '[nested data omitted]'
|
||||
if value is None or isinstance(value, (str, int, float, bool)):
|
||||
return value
|
||||
if isinstance(value, dict):
|
||||
return {str(key): _api_plain(item, depth + 1)
|
||||
for key, item in value.items()}
|
||||
if isinstance(value, (list, tuple, set)):
|
||||
return [_api_plain(item, depth + 1) for item in value]
|
||||
if (type(value).__name__ in ('Slot', 'ItemStackData') and
|
||||
all(hasattr(value, key) for key in
|
||||
('present', 'item_id', 'item_count'))):
|
||||
return {
|
||||
'present': value.present,
|
||||
'item_id': value.item_id,
|
||||
'item_count': value.item_count,
|
||||
'components': _api_plain(getattr(value, 'components', {}), depth + 1),
|
||||
'components_unknown': bool(getattr(value, 'components_unknown', False)),
|
||||
}
|
||||
if hasattr(value, 'value'):
|
||||
return _api_plain(value.value, depth + 1)
|
||||
if hasattr(value, 'items'):
|
||||
try:
|
||||
return {str(key): _api_plain(item, depth + 1)
|
||||
for key, item in value.items()}
|
||||
except Exception:
|
||||
pass
|
||||
return str(value)
|
||||
|
||||
|
||||
def _api_json_default(value):
|
||||
"""Serialize live protocol objects without invoking fragile __repr__ code."""
|
||||
from minecraft.networking.packets import Packet
|
||||
@@ -72,11 +104,10 @@ def _api_json_default(value):
|
||||
return {'__type__': type(value).__name__,
|
||||
'packet_name': getattr(value, 'packet_name', None),
|
||||
'fields': fields}
|
||||
if (type(value).__name__ == 'Slot' and
|
||||
if (type(value).__name__ in ('Slot', 'ItemStackData') and
|
||||
all(hasattr(value, key) for key in
|
||||
('present', 'item_id', 'item_count'))):
|
||||
return {'present': value.present, 'item_id': value.item_id,
|
||||
'item_count': value.item_count}
|
||||
return _api_plain(value)
|
||||
if all(hasattr(value, key) for key in ('x', 'y', 'z')):
|
||||
return {'x': value.x, 'y': value.y, 'z': value.z}
|
||||
if isinstance(value, (bytes, bytearray)):
|
||||
@@ -117,6 +148,44 @@ def api_items():
|
||||
response=json.dumps(data), status=200, mimetype='application/json')
|
||||
|
||||
|
||||
@app.route('/api/inventory/drop', methods=['POST'])
|
||||
def api_inventory_drop():
|
||||
origin = request.headers.get('Origin')
|
||||
if origin:
|
||||
from urllib.parse import urlparse
|
||||
if urlparse(origin).netloc.lower() != request.host.lower():
|
||||
return jsonify(ok=False, error='cross-origin inventory actions are blocked'), 403
|
||||
|
||||
payload = request.get_json(silent=True) or {}
|
||||
if not isinstance(payload, dict):
|
||||
return jsonify(ok=False, error='JSON body must be an object'), 400
|
||||
try:
|
||||
slot = int(payload.get('slot'))
|
||||
except (TypeError, ValueError):
|
||||
return jsonify(ok=False, error='slot must be an integer'), 400
|
||||
action = payload.get('action')
|
||||
if action not in ('one', 'stack', 'matching'):
|
||||
return jsonify(ok=False, error='unknown inventory action'), 400
|
||||
if not g.get('connected') or not g.get('game'):
|
||||
return jsonify(ok=False, error='bot is not connected'), 503
|
||||
|
||||
with g.reload_lock:
|
||||
item = g.inv.get(slot)
|
||||
if slot < 5 or slot > 45 or not item or not getattr(item, 'present', False):
|
||||
return jsonify(ok=False, error='that inventory slot is empty or protected'), 409
|
||||
if action == 'matching':
|
||||
count = g.game.start_dump(item.item_id)
|
||||
if count is None:
|
||||
return jsonify(ok=False, error='another inventory action is running'), 409
|
||||
if not count:
|
||||
return jsonify(ok=False, error='no matching stacks to drop'), 409
|
||||
return jsonify(ok=True, action=action, stacks=count)
|
||||
if not g.game.drop_inventory_slot(
|
||||
slot, whole_stack=(action == 'stack')):
|
||||
return jsonify(ok=False, error='another inventory action is running or the menu is open'), 409
|
||||
return jsonify(ok=True, action=action, slot=slot)
|
||||
|
||||
|
||||
def _item_texture_urls(name):
|
||||
root = 'https://assets.mcasset.cloud/26.3/assets/minecraft/textures'
|
||||
yield root + '/item/{}.png'.format(name)
|
||||
|
||||
Reference in New Issue
Block a user