multi account large update

This commit is contained in:
2026-04-21 21:10:14 +03:00
parent c9da46a530
commit db78568340
7 changed files with 141 additions and 32 deletions

View File

@@ -1,8 +1,8 @@
// ==UserScript==
// @name Grepolis Remote Control
// @namespace http://tampermonkey.net/
// @version 2.7
// @description Polls grepo.haunter-pets.top for remote commands and executes them in-game
// @version 2.8
// @description Polls grepo.haunter-pets.top for remote commands and executes them in-game (Multi-Player)
// @author Dimitrios
// @match https://*.grepolis.com/game/*
// @grant unsafeWindow
@@ -287,7 +287,9 @@
let captchaActive = false;
function reportCaptcha(detected) {
fetch(`${BASE_URL}/api/captcha/alert`, {
const player_id = uw.Game?.player_id;
if (!player_id) return;
fetch(`${BASE_URL}/api/captcha/alert?player_id=${player_id}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ detected })
@@ -447,10 +449,12 @@
// ----------------------------------------------------------------
async function pollAndExecute() {
if (paused) return;
const player_id = uw.Game?.player_id;
if (!player_id) return;
let cmdData;
try {
const res = await fetch(`${BASE_URL}/api/commands/pending`);
const res = await fetch(`${BASE_URL}/api/commands/pending?player_id=${player_id}`);
cmdData = await res.json();
} catch (e) {
log(`Poll failed: ${e}`);
@@ -487,7 +491,7 @@
// Boot
// ----------------------------------------------------------------
window.addEventListener('load', () => {
log('Grepolis Remote Control v2.5 loaded');
log('Grepolis Remote Control v2.8 loaded');
// Start captcha watcher immediately
detectCaptcha();

1
db.py
View File

@@ -62,6 +62,7 @@ def init_db():
'ALTER TABLE town_state ADD COLUMN x REAL',
'ALTER TABLE town_state ADD COLUMN y REAL',
'ALTER TABLE town_state ADD COLUMN sea INTEGER',
'ALTER TABLE commands ADD COLUMN player_id TEXT',
]:
try:
c.execute(_col)

View File

@@ -65,13 +65,13 @@ def receive_state():
# 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):
def _fetch_pending_of_type(c, cmd_type, player_id):
row = c.execute('''
SELECT * FROM commands
WHERE status = 'pending' AND type = ?
WHERE status = 'pending' AND type = ? AND player_id = ?
ORDER BY id ASC
LIMIT 1
''', (cmd_type,)).fetchone()
''', (cmd_type, player_id)).fetchone()
if not row:
return None
c.execute('''
@@ -88,12 +88,16 @@ def _fetch_pending_of_type(c, cmd_type):
@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')
recruit_cmd = _fetch_pending_of_type(c, 'recruit')
market_cmd = _fetch_pending_of_type(c, 'market_offer')
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)
conn.commit()
conn.close()
@@ -133,16 +137,22 @@ def command_result(cmd_id):
# ------------------------------------------------------------------
@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 ('captcha_active', ?, ?)
VALUES (?, ?, ?)
ON CONFLICT(key) DO UPDATE SET
value = excluded.value,
updated_at = excluded.updated_at
''', ('1' if detected else '0', datetime.utcnow().isoformat()))
''', (kv_key, '1' if detected else '0', datetime.utcnow().isoformat()))
conn.commit()
conn.close()
return jsonify({'ok': True})

View File

@@ -12,7 +12,14 @@ dashboard = Blueprint('dashboard', __name__)
# ------------------------------------------------------------------
@dashboard.route('/')
def index():
return render_template('dashboard.html')
conn = get_db()
players = conn.execute('SELECT DISTINCT player, player_id FROM town_state WHERE player IS NOT NULL ORDER BY player ASC').fetchall()
conn.close()
return render_template('index.html', players=players)
@dashboard.route('/player/<player_id>')
def player_dashboard(player_id):
return render_template('dashboard.html', player_id=player_id)
# ------------------------------------------------------------------
@@ -21,13 +28,15 @@ def index():
# ------------------------------------------------------------------
@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
''').fetchall()
''', (player_id, )).fetchall()
conn.close()
towns = []
@@ -74,10 +83,11 @@ def get_towns():
# ------------------------------------------------------------------
@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
''').fetchone()
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
@@ -100,9 +110,10 @@ def client_status():
# ------------------------------------------------------------------
@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 = 'captcha_active'"
"SELECT value FROM kv_store WHERE key = ?", (f'captcha_active_{player_id}', )
).fetchone()
conn.close()
active = bool(row and row['value'] == '1')
@@ -115,13 +126,15 @@ def captcha_status():
# ------------------------------------------------------------------
@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
''').fetchall()
''', (player_id, )).fetchall()
conn.close()
return jsonify([dict(r) for r in rows])
@@ -138,7 +151,7 @@ def create_command():
if not data:
return jsonify({'error': 'no data'}), 400
required = ['town_id', 'type', 'payload']
required = ['town_id', 'type', 'payload', 'player_id']
for field in required:
if field not in data:
return jsonify({'error': f'missing field: {field}'}), 400
@@ -149,7 +162,7 @@ def create_command():
# 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').fetchone()
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:
@@ -165,15 +178,16 @@ def create_command():
c = conn.cursor()
c.execute('''
INSERT INTO commands (town_id, town_name, type, payload, status, created_at, updated_at)
VALUES (?, ?, ?, ?, 'pending', ?, ?)
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()
datetime.utcnow().isoformat(),
str(data['player_id'])
))
cmd_id = c.lastrowid
conn.commit()
@@ -202,6 +216,7 @@ def cancel_command(cmd_id):
# ------------------------------------------------------------------
@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('''
@@ -209,8 +224,8 @@ def fail_stale_commands():
SET status = 'failed',
result_msg = 'Client went offline',
updated_at = ?
WHERE status IN ('pending', 'executing')
''', (datetime.utcnow().isoformat(),))
WHERE status IN ('pending', 'executing') AND player_id = ?
''', (datetime.utcnow().isoformat(), player_id))
affected = c.rowcount
conn.commit()
conn.close()

View File

@@ -4,7 +4,7 @@
window.fetchTowns = async function() {
try {
const res = await fetch('/dashboard/towns');
const res = await fetch('/dashboard/towns?player_id=' + window.PLAYER_ID);
window.towns = await res.json();
window.renderTowns();
window.updateServerStatus(true);
@@ -22,14 +22,14 @@ window.fetchTowns = async function() {
window.fetchClientStatus = async function() {
try {
const res = await fetch('/dashboard/client-status');
const res = await fetch('/dashboard/client-status?player_id=' + window.PLAYER_ID);
const data = await res.json();
const isOnline = data.online === true;
// Detect transition: online → offline
if (window.wasClientOnline === true && !isOnline) {
// Fail all queued commands immediately
await fetch('/dashboard/commands/fail-stale', { method: 'POST' });
await fetch('/dashboard/commands/fail-stale?player_id=' + window.PLAYER_ID, { method: 'POST' });
window.fetchLog();
}
@@ -43,7 +43,7 @@ window.fetchClientStatus = async function() {
window.fetchLog = async function() {
try {
const res = await fetch('/dashboard/commands');
const res = await fetch('/dashboard/commands?player_id=' + window.PLAYER_ID);
const cmds = await res.json();
window.renderLog(cmds);
} catch (e) {}
@@ -161,7 +161,8 @@ window.sendCommand = async function() {
town_id: town.town_id,
town_name: town.town_name,
type,
payload
payload,
player_id: window.PLAYER_ID
})
});
const data = await res.json();
@@ -184,7 +185,7 @@ window.cancelCommand = async function(id) {
window.fetchCaptchaStatus = async function() {
try {
const res = await fetch('/dashboard/captcha-status');
const res = await fetch('/dashboard/captcha-status?player_id=' + window.PLAYER_ID);
const data = await res.json();
const banner = document.getElementById('captcha-banner');
if (!banner) return;

View File

@@ -194,6 +194,9 @@
</div>
<script>
window.PLAYER_ID = "{{ player_id }}";
</script>
<script src="/static/js/state.js"></script>
<script src="/static/js/components/townViewer.js"></script>
<script src="/static/js/components/commandForm.js"></script>

75
templates/index.html Normal file
View File

@@ -0,0 +1,75 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Grepolis Remote Dashboard - Select Player</title>
<link rel="stylesheet" href="/static/css/styles.css">
<style>
body {
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
background-color: #1a1a24;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
.landing-container {
background: #2a2a36;
padding: 2rem;
border-radius: 8px;
box-shadow: 0 4px 6px rgba(0,0,0,0.3);
text-align: center;
max-width: 500px;
width: 100%;
}
.player-card {
background: #3a3a46;
margin: 10px 0;
padding: 15px;
border-radius: 6px;
cursor: pointer;
text-decoration: none;
color: white;
display: block;
transition: background 0.2s, transform 0.1s;
font-size: 1.1rem;
border: 1px solid #4a4a56;
}
.player-card:hover {
background: #5a5a66;
transform: translateY(-2px);
border-color: #c8a44a;
}
.player-card span {
color: #888;
font-size: 0.8rem;
margin-left: 10px;
}
h1 {
color: #c8a44a;
margin-bottom: 5px;
}
p {
color: #aaa;
margin-bottom: 20px;
}
</style>
</head>
<body>
<div class="landing-container">
<h1>⚔️ Grepolis Remote</h1>
<p>Select an active account to manage</p>
{% if not players %}
<p style="color: #ffaa55;">No players found! Install the Tampermonkey script and log into the game first.</p>
{% endif %}
{% for p in players %}
<a href="/player/{{ p.player_id }}" class="player-card">
<strong>{{ p.player }}</strong> <span>(ID: {{ p.player_id }})</span>
</a>
{% endfor %}
</div>
</body>
</html>