330 lines
12 KiB
Python
330 lines
12 KiB
Python
from flask import Blueprint, request, jsonify
|
|
from db import get_db
|
|
import json
|
|
from datetime import datetime
|
|
import os
|
|
from flask import make_response
|
|
|
|
api = Blueprint('api', __name__)
|
|
|
|
|
|
# ------------------------------------------------------------------
|
|
# Helper — look up clan by the X-Clan-Key header.
|
|
# Returns the clan row dict, or None if key is missing / invalid.
|
|
# ------------------------------------------------------------------
|
|
def _get_clan_from_request():
|
|
key = request.headers.get('X-Clan-Key', '').strip()
|
|
if not key:
|
|
return None
|
|
conn = get_db()
|
|
clan = conn.execute('SELECT * FROM clans WHERE clan_key = ?', (key,)).fetchone()
|
|
conn.close()
|
|
return clan
|
|
|
|
|
|
# ------------------------------------------------------------------
|
|
# Helper — auto-register a player_id under a clan on first push.
|
|
# ------------------------------------------------------------------
|
|
def _auto_register_member(clan_id, player_id, player_name):
|
|
conn = get_db()
|
|
conn.execute('''
|
|
INSERT OR IGNORE INTO clan_members (clan_id, player_id, player_name)
|
|
VALUES (?, ?, ?)
|
|
''', (clan_id, str(player_id), player_name or ''))
|
|
# Update name in case it changed
|
|
conn.execute('''
|
|
UPDATE clan_members SET player_name = ?
|
|
WHERE clan_id = ? AND player_id = ?
|
|
''', (player_name or '', clan_id, str(player_id)))
|
|
conn.commit()
|
|
conn.close()
|
|
|
|
|
|
# ------------------------------------------------------------------
|
|
# 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', '')
|
|
|
|
# Auto-register this player to the clan that matches the key (if any)
|
|
clan = _get_clan_from_request()
|
|
if clan:
|
|
_auto_register_member(clan['id'], player_id, player)
|
|
|
|
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)
|
|
research_cmd = _fetch_pending_of_type(c, 'research', 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,
|
|
'research': research_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/<id>/result
|
|
# Tampermonkey reports back whether the command succeeded or failed.
|
|
# ------------------------------------------------------------------
|
|
@api.route('/api/commands/<int:cmd_id>/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})
|
|
|
|
# ------------------------------------------------------------------
|
|
# POST /api/market_data
|
|
# Tampermonkey uploads the market scan data.
|
|
# ------------------------------------------------------------------
|
|
@api.route('/api/market_data', methods=['POST'])
|
|
def upload_market_data():
|
|
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 {}
|
|
kv_key = f'market_data_{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, json.dumps(data), datetime.utcnow().isoformat()))
|
|
conn.commit()
|
|
conn.close()
|
|
return jsonify({'ok': True})
|
|
|
|
# ------------------------------------------------------------------
|
|
# POST/GET /api/farm_status — TM reports warehouse_full; dashboard reads it
|
|
# ------------------------------------------------------------------
|
|
@api.route('/api/farm_status', methods=['POST', 'GET'])
|
|
def farm_status():
|
|
player_id = request.args.get('player_id')
|
|
if not player_id:
|
|
return jsonify({'error': 'no player_id'}), 400
|
|
kv_key = f'farm_status_{player_id}'
|
|
conn = get_db()
|
|
if request.method == 'POST':
|
|
data = request.get_json(silent=True) or {}
|
|
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, json.dumps(data), datetime.utcnow().isoformat()))
|
|
conn.commit()
|
|
conn.close()
|
|
return jsonify({'ok': True})
|
|
else:
|
|
row = conn.execute('SELECT value FROM kv_store WHERE key=?', (kv_key,)).fetchone()
|
|
conn.close()
|
|
return jsonify(json.loads(row['value']) if row else {'warehouse_full': False})
|
|
|
|
# ------------------------------------------------------------------
|
|
# GET /api/bot
|
|
# Serves the modular bot code concatenated into a single response
|
|
# ------------------------------------------------------------------
|
|
@api.route('/api/bot', methods=['GET'])
|
|
def serve_bot():
|
|
# Require a valid clan key — reject unknown clients
|
|
clan = _get_clan_from_request()
|
|
if not clan:
|
|
return make_response('Unauthorized: invalid or missing clan key', 403)
|
|
|
|
bot_dir = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'bot_modules')
|
|
if not os.path.exists(bot_dir):
|
|
return make_response("Bot modules directory not found", 404)
|
|
|
|
|
|
modules = sorted([f for f in os.listdir(bot_dir) if f.endswith('.js')])
|
|
|
|
combined_code = []
|
|
combined_code.append("(function() {")
|
|
combined_code.append(" 'use strict';\n")
|
|
|
|
for module in modules:
|
|
with open(os.path.join(bot_dir, module), 'r', encoding='utf-8') as f:
|
|
combined_code.append(f" // --- BEGIN {module} ---")
|
|
combined_code.append(f.read())
|
|
combined_code.append(f" // --- END {module} ---\n")
|
|
|
|
combined_code.append("})();")
|
|
|
|
response = make_response("\n".join(combined_code))
|
|
response.headers['Content-Type'] = 'application/javascript'
|
|
# Prevent caching so updates are instant
|
|
response.headers['Cache-Control'] = 'no-cache, no-store, must-revalidate'
|
|
return response
|
|
|