from flask import Blueprint, request, jsonify from db import get_db import json from datetime import datetime api = Blueprint('api', __name__) # ------------------------------------------------------------------ # POST /api/state # Tampermonkey pushes a full town snapshot every poll cycle. # ------------------------------------------------------------------ @api.route('/api/state', methods=['POST']) def receive_state(): data = request.get_json(silent=True) if not data: return jsonify({'error': 'no data'}), 400 towns = data.get('towns', []) player = data.get('player', '') player_id = data.get('player_id', '') alliance_id = str(data.get('alliance_id', '') or '') world_id = data.get('world_id', '') conn = get_db() c = conn.cursor() for town in towns: x = town.get('x') y = town.get('y') sea = town.get('sea') c.execute(''' INSERT INTO town_state (town_id, town_name, player, player_id, alliance_id, world_id, x, y, sea, data, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT(town_id) DO UPDATE SET town_name = excluded.town_name, player = excluded.player, player_id = excluded.player_id, alliance_id = excluded.alliance_id, world_id = excluded.world_id, x = excluded.x, y = excluded.y, sea = excluded.sea, data = excluded.data, updated_at = excluded.updated_at ''', ( str(town['town_id']), town.get('town_name', ''), player, player_id, alliance_id, world_id, x, y, sea, json.dumps(town), datetime.utcnow().isoformat() )) conn.commit() conn.close() return jsonify({'ok': True, 'towns_updated': len(towns)}) # ------------------------------------------------------------------ # GET /api/commands/pending # Tampermonkey polls this to get the next command to execute. # Returns one 'build' AND one 'recruit' command independently, # so both queues are served in parallel without blocking each other. # ------------------------------------------------------------------ def _fetch_pending_of_type(c, cmd_type, player_id): row = c.execute(''' SELECT * FROM commands WHERE status = 'pending' AND type = ? AND player_id = ? ORDER BY id ASC LIMIT 1 ''', (cmd_type, player_id)).fetchone() if not row: return None c.execute(''' UPDATE commands SET status = 'executing', updated_at = ? WHERE id = ? ''', (datetime.utcnow().isoformat(), row['id'])) return { 'id': row['id'], 'town_id': row['town_id'], 'type': row['type'], 'payload': json.loads(row['payload']) } @api.route('/api/commands/pending', methods=['GET']) def get_pending_command(): player_id = request.args.get('player_id') if not player_id: return jsonify({'error': 'no player_id provided'}), 400 conn = get_db() c = conn.cursor() build_cmd = _fetch_pending_of_type(c, 'build', player_id) recruit_cmd = _fetch_pending_of_type(c, 'recruit', player_id) market_cmd = _fetch_pending_of_type(c, 'market_offer', player_id) farm_cmd = _fetch_pending_of_type(c, 'farm_loot', player_id) farm_upgrade_cmd = _fetch_pending_of_type(c, 'farm_upgrade', player_id) sync_req = _check_and_reset_sync(c, player_id) # Also return current farm settings so TM knows loot_option farm_row = c.execute( 'SELECT enabled, loot_option FROM farm_settings WHERE player_id = ?', (player_id,) ).fetchone() farm_settings = { 'enabled': bool(farm_row['enabled']) if farm_row else False, 'loot_option': farm_row['loot_option'] if farm_row else 1 } conn.commit() conn.close() return jsonify({ 'build': build_cmd, 'recruit': recruit_cmd, 'market': market_cmd, 'farm': farm_cmd, 'farm_upgrade': farm_upgrade_cmd, 'farm_settings': farm_settings, 'sync_requested': sync_req }) def _check_and_reset_sync(c, player_id): key = f'sync_request_{player_id}' row = c.execute("SELECT value FROM kv_store WHERE key = ?", (key,)).fetchone() if row and row['value'] == '1': c.execute("UPDATE kv_store SET value = '0', updated_at = ? WHERE key = ?", (datetime.utcnow().isoformat(), key)) return True return False # ------------------------------------------------------------------ # POST /api/sync-request # Dashboard requests an immediate state update from the client. # ------------------------------------------------------------------ @api.route('/api/sync-request', methods=['POST']) def sync_request(): player_id = request.args.get('player_id') if not player_id: return jsonify({'error': 'no player_id provided'}), 400 conn = get_db() conn.execute(''' INSERT INTO kv_store (key, value, updated_at) VALUES (?, '1', ?) ON CONFLICT(key) DO UPDATE SET value = '1', updated_at = excluded.updated_at ''', (f'sync_request_{player_id}', datetime.utcnow().isoformat())) conn.commit() conn.close() return jsonify({'ok': True}) # ------------------------------------------------------------------ # POST /api/commands//result # Tampermonkey reports back whether the command succeeded or failed. # ------------------------------------------------------------------ @api.route('/api/commands//result', methods=['POST']) def command_result(cmd_id): data = request.get_json(silent=True) or {} status = data.get('status', 'done') # 'done' | 'failed' msg = data.get('message', '') conn = get_db() conn.execute(''' UPDATE commands SET status = ?, result_msg = ?, updated_at = ? WHERE id = ? ''', (status, msg, datetime.utcnow().isoformat(), cmd_id)) conn.commit() conn.close() return jsonify({'ok': True}) # ------------------------------------------------------------------ # POST /api/captcha/alert # Tampermonkey reports when #hcaptcha_window appears/disappears. # Body: { detected: true | false } # ------------------------------------------------------------------ @api.route('/api/captcha/alert', methods=['POST']) def captcha_alert(): player_id = request.args.get('player_id') if not player_id: return jsonify({'error': 'no player_id provided'}), 400 data = request.get_json(silent=True) or {} detected = bool(data.get('detected', False)) kv_key = f'captcha_active_{player_id}' conn = get_db() conn.execute(''' INSERT INTO kv_store (key, value, updated_at) VALUES (?, ?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at ''', (kv_key, '1' if detected else '0', datetime.utcnow().isoformat())) conn.commit() conn.close() return jsonify({'ok': True})