382 lines
14 KiB
Python
382 lines
14 KiB
Python
from flask import Blueprint, render_template, request, jsonify
|
||
from flask_login import login_required, current_user
|
||
from db import get_db
|
||
import json
|
||
from datetime import datetime, timedelta
|
||
|
||
dashboard = Blueprint('dashboard', __name__)
|
||
|
||
|
||
# ------------------------------------------------------------------
|
||
# GET /
|
||
# Serve the dashboard HTML
|
||
# ------------------------------------------------------------------
|
||
@dashboard.route('/')
|
||
@login_required
|
||
def index():
|
||
conn = get_db()
|
||
|
||
# Get the clan the logged-in user belongs to
|
||
clan_id = current_user.clan_id
|
||
|
||
if not clan_id:
|
||
# User has no clan yet — send them to options to create/join one
|
||
conn.close()
|
||
return render_template('index.html', players=[], no_clan=True)
|
||
|
||
# Only fetch players that are members of this clan
|
||
rows = conn.execute('''
|
||
SELECT ts.player, ts.player_id, MAX(ts.updated_at) as last_seen, MAX(ts.world_id) as world_id
|
||
FROM town_state ts
|
||
INNER JOIN clan_members cm ON cm.player_id = ts.player_id AND cm.clan_id = ?
|
||
WHERE ts.player IS NOT NULL
|
||
GROUP BY ts.player, ts.player_id
|
||
ORDER BY ts.player ASC
|
||
''', (clan_id,)).fetchall()
|
||
|
||
captcha_rows = conn.execute("SELECT key, value FROM kv_store WHERE key LIKE 'captcha_active_%'").fetchall()
|
||
active_captchas = {r['key'].replace('captcha_active_', ''): True for r in captcha_rows if r['value'] == '1'}
|
||
conn.close()
|
||
|
||
players = []
|
||
now = datetime.utcnow()
|
||
for r in rows:
|
||
is_online = False
|
||
if r['last_seen']:
|
||
try:
|
||
last_seen = datetime.fromisoformat(r['last_seen'])
|
||
if (now - last_seen).total_seconds() <= 150:
|
||
is_online = True
|
||
except Exception:
|
||
pass
|
||
|
||
players.append({
|
||
'player': r['player'],
|
||
'player_id': r['player_id'],
|
||
'world_id': r['world_id'] or 'Unknown',
|
||
'is_online': is_online,
|
||
'captcha_active': active_captchas.get(r['player_id'], False)
|
||
})
|
||
|
||
return render_template('index.html', players=players, no_clan=False)
|
||
|
||
|
||
@dashboard.route('/player/<player_id>')
|
||
@login_required
|
||
def player_hub(player_id):
|
||
return render_template('hub.html', player_id=player_id)
|
||
|
||
@dashboard.route('/player/<player_id>/admin')
|
||
@login_required
|
||
def player_dashboard(player_id):
|
||
return render_template('dashboard.html', player_id=player_id)
|
||
|
||
@dashboard.route('/player/<player_id>/farm')
|
||
@login_required
|
||
def player_farm(player_id):
|
||
return render_template('farm.html', player_id=player_id)
|
||
|
||
|
||
# ------------------------------------------------------------------
|
||
# GET /dashboard/farm-settings — returns current farm config
|
||
# POST /dashboard/farm-settings — updates farm config
|
||
# ------------------------------------------------------------------
|
||
@dashboard.route('/dashboard/farm-settings', methods=['GET'])
|
||
def get_farm_settings():
|
||
player_id = request.args.get('player_id')
|
||
conn = get_db()
|
||
row = conn.execute(
|
||
'SELECT enabled, loot_option FROM farm_settings WHERE player_id = ?', (player_id,)
|
||
).fetchone()
|
||
conn.close()
|
||
if row:
|
||
return jsonify({'enabled': bool(row['enabled']), 'loot_option': row['loot_option']})
|
||
return jsonify({'enabled': False, 'loot_option': 1})
|
||
|
||
@dashboard.route('/dashboard/farm-settings', methods=['POST'])
|
||
def set_farm_settings():
|
||
data = request.get_json(silent=True)
|
||
if not data or 'player_id' not in data:
|
||
return jsonify({'error': 'missing player_id'}), 400
|
||
player_id = data['player_id']
|
||
enabled = 1 if data.get('enabled') else 0
|
||
loot_option = int(data.get('loot_option', 1))
|
||
conn = get_db()
|
||
conn.execute('''
|
||
INSERT INTO farm_settings (player_id, enabled, loot_option, updated_at)
|
||
VALUES (?, ?, ?, ?)
|
||
ON CONFLICT(player_id) DO UPDATE SET
|
||
enabled = excluded.enabled,
|
||
loot_option = excluded.loot_option,
|
||
updated_at = excluded.updated_at
|
||
''', (player_id, enabled, loot_option, datetime.utcnow().isoformat()))
|
||
conn.commit()
|
||
conn.close()
|
||
return jsonify({'ok': True})
|
||
|
||
|
||
# ------------------------------------------------------------------
|
||
# GET /dashboard/farm-data
|
||
# Returns ready-to-loot farm towns for a player across all towns
|
||
# ------------------------------------------------------------------
|
||
@dashboard.route('/dashboard/farm-data', methods=['GET'])
|
||
def get_farm_data():
|
||
player_id = request.args.get('player_id')
|
||
conn = get_db()
|
||
rows = conn.execute(
|
||
'SELECT town_id, town_name, data FROM town_state WHERE player_id = ?', (player_id,)
|
||
).fetchall()
|
||
|
||
# Also fetch when the bot last farmed
|
||
lf_row = conn.execute(
|
||
"SELECT value FROM kv_store WHERE key = ?", (f'last_farmed_{player_id}',)
|
||
).fetchone()
|
||
last_farmed_at = lf_row['value'] if lf_row else None
|
||
conn.close()
|
||
|
||
now_ts = int(datetime.utcnow().timestamp())
|
||
farms_summary = []
|
||
for row in rows:
|
||
d = json.loads(row['data'])
|
||
farm_data = d.get('farms', [])
|
||
ready = [f for f in farm_data if f.get('lootable_at', 0) <= now_ts and f.get('relation_status', 0) == 1]
|
||
if farm_data:
|
||
farms_summary.append({
|
||
'town_id': row['town_id'],
|
||
'town_name': row['town_name'],
|
||
'total_farms': len(farm_data),
|
||
'ready_farms': len(ready),
|
||
'next_ready_at': min((f['lootable_at'] for f in farm_data if f.get('lootable_at', 0) > now_ts and f.get('relation_status', 0) == 1), default=None)
|
||
})
|
||
return jsonify({'towns': farms_summary, 'last_farmed_at': last_farmed_at})
|
||
|
||
|
||
# ------------------------------------------------------------------
|
||
# GET /dashboard/market-data
|
||
# Returns the latest market scan data for a player.
|
||
# ------------------------------------------------------------------
|
||
@dashboard.route('/dashboard/market-data', methods=['GET'])
|
||
def get_market_data():
|
||
player_id = request.args.get('player_id')
|
||
conn = get_db()
|
||
row = conn.execute(
|
||
"SELECT value, updated_at FROM kv_store WHERE key = ?", (f'market_data_{player_id}', )
|
||
).fetchone()
|
||
conn.close()
|
||
|
||
if row:
|
||
return jsonify({'data': json.loads(row['value']), 'updated_at': row['updated_at']})
|
||
return jsonify({'data': None, 'updated_at': None})
|
||
|
||
|
||
# ------------------------------------------------------------------
|
||
# GET /dashboard/towns
|
||
# Returns all known towns with their latest state snapshot.
|
||
# ------------------------------------------------------------------
|
||
@dashboard.route('/dashboard/towns', methods=['GET'])
|
||
def get_towns():
|
||
player_id = request.args.get('player_id')
|
||
conn = get_db()
|
||
rows = conn.execute('''
|
||
SELECT town_id, town_name, player, player_id, alliance_id,
|
||
world_id, x, y, sea, data, updated_at
|
||
FROM town_state
|
||
WHERE player_id = ?
|
||
ORDER BY town_name ASC
|
||
''', (player_id, )).fetchall()
|
||
conn.close()
|
||
|
||
towns = []
|
||
for row in rows:
|
||
d = json.loads(row['data'])
|
||
towns.append({
|
||
'town_id': row['town_id'],
|
||
'town_name': row['town_name'],
|
||
'player': row['player'],
|
||
'player_id': row['player_id'],
|
||
'alliance_id': row['alliance_id'],
|
||
'world_id': row['world_id'],
|
||
'x': row['x'],
|
||
'y': row['y'],
|
||
'sea': row['sea'],
|
||
'updated_at': row['updated_at'],
|
||
'resources': {
|
||
'wood': d.get('wood', 0),
|
||
'stone': d.get('stone', 0),
|
||
'iron': d.get('iron', 0),
|
||
'storage': d.get('storage', 0),
|
||
'market_capacity': d.get('market_capacity', 0),
|
||
'population': d.get('population', 0),
|
||
},
|
||
'buildings': d.get('buildings', {}),
|
||
'units': d.get('units', {}),
|
||
'points': d.get('points', 0),
|
||
'god': d.get('god', None),
|
||
'build_queue': d.get('buildingOrder', []),
|
||
'build_data': d.get('buildData', {}),
|
||
'unit_data': d.get('unitData', {}),
|
||
'researches': d.get('researches', {}),
|
||
'has_premium': d.get('has_premium', False),
|
||
'bonuses': d.get('bonuses', {}),
|
||
'wonder_points': d.get('wonder_points', 0),
|
||
'total_points': d.get('total_points', 0),
|
||
'alliance_name': d.get('alliance_name', None)
|
||
})
|
||
return jsonify(towns)
|
||
|
||
|
||
# ------------------------------------------------------------------
|
||
# GET /dashboard/client-status
|
||
# Returns whether the Tampermonkey client is considered online.
|
||
# Online = at least one town_state row updated within the last 150 s.
|
||
# ------------------------------------------------------------------
|
||
@dashboard.route('/dashboard/client-status', methods=['GET'])
|
||
def client_status():
|
||
player_id = request.args.get('player_id')
|
||
conn = get_db()
|
||
row = conn.execute('''
|
||
SELECT MAX(updated_at) AS last_seen FROM town_state WHERE player_id = ?
|
||
''', (player_id, )).fetchone()
|
||
conn.close()
|
||
|
||
last_seen = row['last_seen'] if row else None
|
||
if last_seen:
|
||
try:
|
||
dt = datetime.fromisoformat(last_seen)
|
||
age_s = (datetime.utcnow() - dt).total_seconds()
|
||
online = age_s <= 150
|
||
except Exception:
|
||
online = False
|
||
else:
|
||
online = False
|
||
|
||
return jsonify({'online': online, 'last_seen': last_seen})
|
||
|
||
|
||
# ------------------------------------------------------------------
|
||
# GET /dashboard/captcha-status
|
||
# Returns whether a captcha is currently active in the game client.
|
||
# ------------------------------------------------------------------
|
||
@dashboard.route('/dashboard/captcha-status', methods=['GET'])
|
||
def captcha_status():
|
||
player_id = request.args.get('player_id')
|
||
conn = get_db()
|
||
row = conn.execute(
|
||
"SELECT value FROM kv_store WHERE key = ?", (f'captcha_active_{player_id}', )
|
||
).fetchone()
|
||
conn.close()
|
||
active = bool(row and row['value'] == '1')
|
||
return jsonify({'captcha_active': active})
|
||
|
||
|
||
# ------------------------------------------------------------------
|
||
# GET /dashboard/commands
|
||
# Returns command history (last 50) for the log panel.
|
||
# ------------------------------------------------------------------
|
||
@dashboard.route('/dashboard/commands', methods=['GET'])
|
||
def get_commands():
|
||
player_id = request.args.get('player_id')
|
||
conn = get_db()
|
||
rows = conn.execute('''
|
||
SELECT id, town_id, town_name, type, payload, status, result_msg, created_at, updated_at
|
||
FROM commands
|
||
WHERE player_id = ?
|
||
ORDER BY id DESC
|
||
LIMIT 50
|
||
''', (player_id, )).fetchall()
|
||
conn.close()
|
||
|
||
return jsonify([dict(r) for r in rows])
|
||
|
||
|
||
# ------------------------------------------------------------------
|
||
# POST /dashboard/commands
|
||
# Dashboard sends a new command.
|
||
# Body: { town_id, town_name, type: 'build'|'recruit', payload: {...} }
|
||
# ------------------------------------------------------------------
|
||
@dashboard.route('/dashboard/commands', methods=['POST'])
|
||
def create_command():
|
||
data = request.get_json(silent=True)
|
||
if not data:
|
||
return jsonify({'error': 'no data'}), 400
|
||
|
||
required = ['town_id', 'type', 'payload', 'player_id']
|
||
for field in required:
|
||
if field not in data:
|
||
return jsonify({'error': f'missing field: {field}'}), 400
|
||
|
||
cmd_type = data['type']
|
||
if cmd_type not in ('build', 'recruit', 'market_offer', 'farm_loot', 'farm_upgrade', 'research'):
|
||
return jsonify({'error': 'type must be build, recruit, market_offer, farm_loot, farm_upgrade, or research'}), 400
|
||
|
||
# Reject if the Tampermonkey client is offline (no state push in last 150 s)
|
||
conn = get_db()
|
||
row = conn.execute('SELECT MAX(updated_at) AS last_seen FROM town_state WHERE player_id = ?', (data['player_id'], )).fetchone()
|
||
last_seen = row['last_seen'] if row else None
|
||
client_online = False
|
||
if last_seen:
|
||
try:
|
||
dt = datetime.fromisoformat(last_seen)
|
||
client_online = (datetime.utcnow() - dt).total_seconds() <= 150
|
||
except Exception:
|
||
pass
|
||
|
||
if not client_online:
|
||
conn.close()
|
||
return jsonify({'error': 'client_offline', 'message': 'Το script είναι offline — δεν μπορείτε να στείλετε εντολές.'}), 503
|
||
|
||
c = conn.cursor()
|
||
c.execute('''
|
||
INSERT INTO commands (town_id, town_name, type, payload, status, created_at, updated_at, player_id)
|
||
VALUES (?, ?, ?, ?, 'pending', ?, ?, ?)
|
||
''', (
|
||
str(data['town_id']),
|
||
data.get('town_name', ''),
|
||
cmd_type,
|
||
json.dumps(data['payload']),
|
||
datetime.utcnow().isoformat(),
|
||
datetime.utcnow().isoformat(),
|
||
str(data['player_id'])
|
||
))
|
||
cmd_id = c.lastrowid
|
||
conn.commit()
|
||
conn.close()
|
||
|
||
return jsonify({'ok': True, 'id': cmd_id})
|
||
|
||
|
||
# ------------------------------------------------------------------
|
||
# DELETE /dashboard/commands/<id>
|
||
# Fully delete a command from the dashboard history.
|
||
# ------------------------------------------------------------------
|
||
@dashboard.route('/dashboard/commands/<int:cmd_id>', methods=['DELETE'])
|
||
def cancel_command(cmd_id):
|
||
conn = get_db()
|
||
conn.execute('DELETE FROM commands WHERE id = ?', (cmd_id,))
|
||
conn.commit()
|
||
conn.close()
|
||
return jsonify({'ok': True})
|
||
|
||
|
||
# ------------------------------------------------------------------
|
||
# POST /dashboard/commands/fail-stale
|
||
# Mark all pending/executing commands as failed (called by dashboard
|
||
# when it detects the client has gone offline).
|
||
# ------------------------------------------------------------------
|
||
@dashboard.route('/dashboard/commands/fail-stale', methods=['POST'])
|
||
def fail_stale_commands():
|
||
player_id = request.args.get('player_id')
|
||
conn = get_db()
|
||
c = conn.cursor()
|
||
c.execute('''
|
||
UPDATE commands
|
||
SET status = 'failed',
|
||
result_msg = 'Client went offline',
|
||
updated_at = ?
|
||
WHERE status IN ('pending', 'executing') AND player_id = ?
|
||
''', (datetime.utcnow().isoformat(), player_id))
|
||
affected = c.rowcount
|
||
conn.commit()
|
||
conn.close()
|
||
return jsonify({'ok': True, 'failed': affected})
|