Compare commits

..

26 Commits

Author SHA1 Message Date
1717de8373 mj2 add leave clan 2026-04-26 23:18:12 +03:00
8ed964f3bb mj add another admin 2026-04-26 22:39:52 +03:00
bfdfaa142c mj2 fix 2026-04-26 21:54:29 +03:00
8b42c7c2f9 Mj2 : modular and prepare for client options 2026-04-26 21:45:32 +03:00
adb42c1649 MJ2 : fix options 2026-04-26 16:45:12 +03:00
e8fd35105f Major update 2 / login 2026-04-26 16:33:04 +03:00
5bff9a287d MJ:Fix 2 2026-04-26 13:55:25 +03:00
a8b3e9f5ea MJ: fix market 2026-04-26 13:09:06 +03:00
a5d57d55fc MJ: fix 1 2026-04-26 13:01:58 +03:00
929af21d08 MJ: claude fix 2026-04-26 12:53:14 +03:00
7beece5aaa MJ: break fix 2026-04-26 12:36:01 +03:00
d20537983e MJ: fix typo 2026-04-26 12:24:10 +03:00
037a84d6bb major update/remote host the JS and user loader 2026-04-26 12:15:06 +03:00
ab1ba5c0ab market revert fix 2026-04-26 02:15:38 +03:00
95b38b1212 revert and farm fix 2026-04-26 02:02:50 +03:00
b688b66275 fix 5 2026-04-26 01:51:22 +03:00
b577c95f7c fix 3 2026-04-26 01:45:26 +03:00
8fd711f5a1 fix 2 2026-04-25 16:36:12 +03:00
4fa6127aea fix 1 2026-04-25 16:09:50 +03:00
a143345831 new market 2026-04-25 16:01:15 +03:00
8a64a7b4fc fix 2026-04-25 15:25:18 +03:00
853525d8ad academy mode 2026-04-25 15:18:56 +03:00
22a379c2a1 version 2026-04-25 12:50:49 +03:00
e565c88eeb captcha block fix ? 2026-04-25 12:49:17 +03:00
b7bf1cf9ea fix farm numbers 2026-04-24 22:16:06 +03:00
9aba81960a revert 2026-04-24 22:14:19 +03:00
26 changed files with 2763 additions and 313 deletions

77
GrepoRemoteLoader.user.js Normal file
View File

@@ -0,0 +1,77 @@
// ==UserScript==
// @name Grepolis Remote Loader
// @namespace http://tampermonkey.net/
// @version 4.0.1
// @description Dynamically loads the Grepolis Remote Control bot from the server
// @author Dimitrios
// @match https://*.grepolis.com/game/*
// @grant unsafeWindow
// @grant GM_xmlhttpRequest
// @connect grepo.haunter-pets.top
// @updateURL https://git.haunter-pets.top/haunter/grepo-remote/raw/branch/main/GrepoRemoteLoader.user.js
// @downloadURL https://git.haunter-pets.top/haunter/grepo-remote/raw/branch/main/GrepoRemoteLoader.user.js
// ==/UserScript==
(function() {
'use strict';
const BASE_URL = 'https://grepo.haunter-pets.top';
const MAX_TRIES = 3;
const RETRY_BASE = 3000; // 3 s, then 6 s
// ----------------------------------------------------------------
// CLAN_KEY: Set this to your clan's unique key.
// Leave it empty ('') and the bot will refuse to run.
// ----------------------------------------------------------------
const CLAN_KEY = ''; // <-- paste your clan key here
if (!CLAN_KEY || CLAN_KEY.trim() === '') {
console.error('[Loader] ❌ CLAN_KEY is not set. Bot will not start. Contact your admin for the key.');
return; // stop everything
}
function loadBot(attempt) {
attempt = attempt || 1;
console.log(`[Loader] Fetching bot code (attempt ${attempt}/${MAX_TRIES})...`);
GM_xmlhttpRequest({
method: 'GET',
url: `${BASE_URL}/api/bot?t=${Date.now()}`,
headers: {
'X-Clan-Key': CLAN_KEY
},
onload: function(response) {
if (response.status === 200) {
console.log('[Loader] Bot code downloaded. Executing...');
try {
window.__GRC_CLAN_KEY = CLAN_KEY; // make key available inside the bot's scope
eval(response.responseText);
} catch (e) {
console.error('[Loader] Execution error:', e);
}
} else if (attempt < MAX_TRIES) {
const delay = RETRY_BASE * attempt;
console.warn(`[Loader] Server returned ${response.status}. Retrying in ${delay / 1000}s...`);
setTimeout(() => loadBot(attempt + 1), delay);
} else {
console.error(`[Loader] Failed after ${MAX_TRIES} attempts (status ${response.status}). Give up.`);
}
},
onerror: function() {
if (attempt < MAX_TRIES) {
const delay = RETRY_BASE * attempt;
console.warn(`[Loader] Connection error. Retrying in ${delay / 1000}s...`);
setTimeout(() => loadBot(attempt + 1), delay);
} else {
console.error(`[Loader] Failed after ${MAX_TRIES} attempts. Check your server.`);
}
}
});
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => loadBot(1));
} else {
loadBot(1);
}
})();

View File

@@ -1,7 +1,7 @@
// ==UserScript== // ==UserScript==
// @name Grepolis Remote Control // @name Grepolis Remote Control
// @namespace http://tampermonkey.net/ // @namespace http://tampermonkey.net/
// @version 3.6.7 // @version 3.5.9
// @description Polls grepo.haunter-pets.top for remote commands and executes them in-game (Multi-Player) // @description Polls grepo.haunter-pets.top for remote commands and executes them in-game (Multi-Player)
// @author Dimitrios // @author Dimitrios
// @match https://*.grepolis.com/game/* // @match https://*.grepolis.com/game/*
@@ -253,7 +253,7 @@
if (farm.attributes.island_x !== ix || farm.attributes.island_y !== iy) return; if (farm.attributes.island_x !== ix || farm.attributes.island_y !== iy) return;
relCollection.models.forEach(rel => { relCollection.models.forEach(rel => {
if (rel.attributes.farm_town_id === farm.attributes.id && if (rel.attributes.farm_town_id === farm.attributes.id &&
rel.attributes.relation_status >= 0) { rel.attributes.relation_status >= 1) {
farms.push({ farms.push({
farm_town_id: farm.attributes.id, farm_town_id: farm.attributes.id,
farm_name: farm.attributes.name || '', farm_name: farm.attributes.name || '',
@@ -485,6 +485,10 @@
skipped++; continue; skipped++; continue;
} }
if (paused) {
return { ok: false, msg: 'Aborted due to pause/captcha' };
}
log(`Farm ${action}: farm_id=${farm.attributes.id} level=${level} town=${town_id}`); log(`Farm ${action}: farm_id=${farm.attributes.id} level=${level} town=${town_id}`);
try { try {
uw.gpAjax.ajaxPost('frontend_bridge', 'execute', { uw.gpAjax.ajaxPost('frontend_bridge', 'execute', {
@@ -496,8 +500,8 @@
isLocked ? unlocked++ : upgraded++; isLocked ? unlocked++ : upgraded++;
} catch (e) { errors++; } } catch (e) { errors++; }
// Random delay between actions: 800ms 2000ms // Random delay between actions: 1200ms 2500ms
await sleep(randInt(800, 2000)); await sleep(randInt(1200, 2500));
} }
} }
} }
@@ -626,6 +630,10 @@
log(`Farm: ${readyFarms.length} ready on island of town ${town_id}`); log(`Farm: ${readyFarms.length} ready on island of town ${town_id}`);
for (const farm of readyFarms) { for (const farm of readyFarms) {
if (paused) {
return { ok: false, msg: 'Aborted due to pause/captcha' };
}
try { try {
uw.gpAjax.ajaxPost('frontend_bridge', 'execute', { uw.gpAjax.ajaxPost('frontend_bridge', 'execute', {
model_url: `FarmTownPlayerRelation/${farm.relation_id}`, model_url: `FarmTownPlayerRelation/${farm.relation_id}`,
@@ -636,8 +644,8 @@
claimed++; claimed++;
} catch (e) { errors++; } } catch (e) { errors++; }
// Random per-claim delay: 500ms 1500ms (never below 500ms) // Random per-claim delay: 1000ms 2200ms
await sleep(randInt(500, 1500)); await sleep(randInt(1000, 2200));
} }
// Refresh map icons after claiming (same as original) // Refresh map icons after claiming (same as original)
@@ -645,6 +653,7 @@
// Random between-island delay: 30s 90s (only if more islands remain) // Random between-island delay: 30s 90s (only if more islands remain)
if (i < islandList.length - 1) { if (i < islandList.length - 1) {
if (paused) return { ok: false, msg: 'Aborted due to pause/captcha' };
const gap = randInt(30000, 90000); const gap = randInt(30000, 90000);
log(`Farm: island done. Waiting ${(gap / 1000).toFixed(0)}s before next island...`); log(`Farm: island done. Waiting ${(gap / 1000).toFixed(0)}s before next island...`);
await sleep(gap); await sleep(gap);
@@ -698,6 +707,8 @@
log(`Waiting ${reactionMs}ms before firing buildUp (reaction time)...`); log(`Waiting ${reactionMs}ms before firing buildUp (reaction time)...`);
await sleep(reactionMs); await sleep(reactionMs);
if (paused) return { ok: false, msg: 'Aborted due to pause/captcha' };
uw.gpAjax.ajaxPost('frontend_bridge', 'execute', { uw.gpAjax.ajaxPost('frontend_bridge', 'execute', {
model_url: 'BuildingOrder', model_url: 'BuildingOrder',
action_name: 'buildUp', action_name: 'buildUp',
@@ -733,6 +744,8 @@
log(`Waiting ${reactionMs}ms before firing recruit (reaction time)...`); log(`Waiting ${reactionMs}ms before firing recruit (reaction time)...`);
await sleep(reactionMs); await sleep(reactionMs);
if (paused) return { ok: false, msg: 'Aborted due to pause/captcha' };
uw.gpAjax.ajaxPost(endpoint, 'build', { uw.gpAjax.ajaxPost(endpoint, 'build', {
unit_id, unit_id,
amount: parseInt(amount) || 1, amount: parseInt(amount) || 1,
@@ -759,6 +772,8 @@
log(`Waiting ${reactionMs}ms before firing market offer (reaction time)...`); log(`Waiting ${reactionMs}ms before firing market offer (reaction time)...`);
await sleep(reactionMs); await sleep(reactionMs);
if (paused) return { ok: false, msg: 'Aborted due to pause/captcha' };
uw.gpAjax.ajaxPost('frontend_bridge', 'execute', { uw.gpAjax.ajaxPost('frontend_bridge', 'execute', {
model_url: 'CreateOffers/' + town_id, model_url: 'CreateOffers/' + town_id,
action_name: 'createOffer', action_name: 'createOffer',
@@ -772,6 +787,35 @@
return { ok: true, msg: `Market offer posted: ${offer} ${offer_type} => ${demand} ${demand_type}` }; return { ok: true, msg: `Market offer posted: ${offer} ${offer_type} => ${demand} ${demand_type}` };
} }
// ----------------------------------------------------------------
// Execute: Research (Academy)
// ----------------------------------------------------------------
async function executeResearch(cmd) {
const { town_id, payload } = cmd;
const { research_id } = payload;
const town = uw.ITowns?.getTown?.(town_id) || uw.ITowns?.towns?.[town_id];
if (!town) {
return { ok: false, msg: `Town ${town_id} not found` };
}
const reactionMs = randInt(800, 2500);
log(`Waiting ${reactionMs}ms before firing research (reaction time)...`);
await sleep(reactionMs);
if (paused) return { ok: false, msg: 'Aborted due to pause/captcha' };
uw.gpAjax.ajaxPost('frontend_bridge', 'execute', {
model_url: 'ResearchOrder',
action_name: 'research',
arguments: { id: research_id },
town_id: town_id
});
await sleep(500);
return { ok: true, msg: `Research ${research_id} queued` };
}
// ---------------------------------------------------------------- // ----------------------------------------------------------------
// Poll for and execute pending commands (build + recruit + market) // Poll for and execute pending commands (build + recruit + market)
// ---------------------------------------------------------------- // ----------------------------------------------------------------
@@ -793,139 +837,10 @@
const buildCmd = cmdData.build; const buildCmd = cmdData.build;
const recruitCmd = cmdData.recruit; const recruitCmd = cmdData.recruit;
const marketCmd = cmdData.market; const marketCmd = cmdData.market;
const researchCmd = cmdData.research;
const farmCmd = cmdData.farm; const farmCmd = cmdData.farm;
const farmUpgradeCmd = cmdData.farm_upgrade; const farmUpgradeCmd = cmdData.farm_upgrade;
// Auto-farm: if enabled, claim all ready farms (no explicit command needed)
const farmSettings = cmdData.farm_settings || {};
if (farmSettings.enabled && !farmCmd) {
const nowTs = Math.floor(Date.now() / 1000);
// Check if any town has ready farms before triggering
const hasFarms = Object.values(uw.ITowns?.towns || {}).some(t => {
try {
const coll = uw.MM.getOnlyCollectionByName('FarmTownPlayerRelation');
return coll?.models?.some(r => r.attributes.relation_status === 1 && (r.attributes.lootable_at || 0) <= nowTs);
} catch (e) { return false; }
});
if (hasFarms) {
log('⚡ Auto-farm: ready farms detected, triggering loot...');
await executeFarmLoot({ payload: { loot_option: farmSettings.loot_option } });
pushState();
}
}
// Auto Bandit Camp: if enabled, attack/claim when ready
if (farmSettings.bandit_camp_enabled) {
try {
// player_id already declared above in pollAndExecute scope
const currentTownId = uw.ITowns?.getCurrentTown?.()?.id
|| Object.keys(uw.ITowns?.towns || {})[0]
|| null;
if (!player_id || !currentTownId) {
log(`⚔️ Bandit Camp: Missing globals — player_id=${player_id} town_id=${currentTownId}`);
} else {
// First try: MM collection (works once camp was opened in-game)
let spotData = uw.MM.getOnlyCollectionByName('PlayerAttackSpot')?.models?.[0]?.attributes || null;
// Second try: use gpAjax with a callback — it handles auth/hash internally
if (!spotData) {
spotData = await new Promise((resolve) => {
try {
uw.gpAjax.ajaxPost('frontend_bridge', 'execute', {
model_url: `PlayerAttackSpot/${player_id}`,
action_name: 'get_own',
captcha: null,
arguments: {},
town_id: currentTownId,
nl_init: true
}, false, {
success: function(data) {
// Response may contain the spot data directly
const d = data?.PlayerAttackSpot
|| data?.data?.PlayerAttackSpot?.[player_id]
|| data?.[player_id]
|| data;
resolve((d && d.cooldown_at !== undefined) ? d : null);
},
error: function() { resolve(null); }
});
} catch(e) { resolve(null); }
});
// After the gpAjax call, MM might now have the model
if (!spotData) {
spotData = uw.MM.getOnlyCollectionByName('PlayerAttackSpot')?.models?.[0]?.attributes || null;
}
}
if (!spotData) {
log('⚔️ Bandit Camp: Could not load spot data. Open the Bandit Camp window in-game once to let the game load it.');
} else {
const nowTs = Math.floor(Date.now() / 1000);
const spotId = spotData.id || player_id;
const townId = spotData.town_id || currentTownId;
log(`⚔️ Bandit Monitor -> Cooldown in: ${Math.max(0, spotData.cooldown_at - nowTs)}s | Reward: ${spotData.reward_available} | Level: ${spotData.level}`);
if (spotData.reward_available) {
log('⚔️ Bandit Camp: Reward available! Waiting before claiming...');
await sleep(randInt(8000, 24000));
uw.gpAjax.ajaxPost('frontend_bridge', 'execute', {
model_url: `PlayerAttackSpot/${spotId}`,
action_name: 'useReward',
captcha: null,
arguments: {},
town_id: townId,
nl_init: true
});
log('⚔️ Bandit Camp: Reward claimed!');
} else if (spotData.cooldown_at <= nowTs) {
let hasMovements = false;
try {
const movements = uw.MM.getOnlyCollectionByName('MovementCommand')?.models || [];
hasMovements = movements.length > 0;
} catch (e) {}
if (!hasMovements) {
const town = uw.ITowns?.getTown?.(townId) || uw.ITowns?.towns?.[townId];
if (town) {
const myUnits = town.units() || {};
const allowedUnits = ['sword', 'slinger', 'archer', 'hoplite', 'rider', 'chariot', 'catapult'];
const sendUnits = {};
let totalUnits = 0;
for (let u of allowedUnits) {
if ((myUnits[u] || 0) > 0) {
sendUnits[u] = myUnits[u];
totalUnits += myUnits[u];
}
}
if (totalUnits > 0) {
log(`⚔️ Bandit Camp: Attacking with ${totalUnits} units...`);
await sleep(randInt(8000, 24000));
uw.gpAjax.ajaxPost('frontend_bridge', 'execute', {
model_url: `PlayerAttackSpot/${spotId}`,
action_name: 'attack',
captcha: null,
arguments: sendUnits,
town_id: townId,
nl_init: true
});
log('⚔️ Bandit Camp: Attack sent!');
} else {
log('⚔️ Bandit Camp: No units available.');
}
}
} else {
log('⚔️ Bandit Camp: Troops still returning — waiting...');
}
}
}
}
} catch (e) {
log(`⚔️ Bandit camp error: ${e.message}`);
}
}
if (cmdData.sync_requested) { if (cmdData.sync_requested) {
@@ -936,33 +851,107 @@
const execute = async (cmd) => { const execute = async (cmd) => {
if (!cmd) return; if (!cmd) return;
log(`Executing command #${cmd.id} — type:${cmd.type} town:${cmd.town_id}`); log(`Executing command #${cmd.id} — type:${cmd.type} town:${cmd.town_id}`);
if (paused) {
log(`[Paused] Ignoring command #${cmd.id}`);
return;
}
let result; let result;
try { try {
if (cmd.type === 'build') result = await executeBuild(cmd); if (cmd.type === 'build') result = await executeBuild(cmd);
else if (cmd.type === 'recruit') result = await executeRecruit(cmd); else if (cmd.type === 'recruit') result = await executeRecruit(cmd);
else if (cmd.type === 'market_offer') result = await executeMarketOffer(cmd); else if (cmd.type === 'market_offer') result = await executeMarketOffer(cmd);
else if (cmd.type === 'research') result = await executeResearch(cmd);
else if (cmd.type === 'farm_loot') result = await executeFarmLoot(cmd); else if (cmd.type === 'farm_loot') result = await executeFarmLoot(cmd);
else if (cmd.type === 'farm_upgrade') result = await executeFarmUpgrade(cmd); else if (cmd.type === 'farm_upgrade') result = await executeFarmUpgrade(cmd);
else result = { ok: false, msg: `Unknown type: ${cmd.type}` }; else result = { ok: false, msg: `Unknown type: ${cmd.type}` };
} catch (e) { } catch (e) {
result = { ok: false, msg: `Exception: ${e.message}` }; result = { ok: false, msg: `Exception: ${e}` };
} }
const finalStatus = result.requeue ? 'pending' : (result.ok ? 'done' : 'failed'); const finalStatus = result.requeue ? 'pending' : (result.ok ? 'done' : 'failed');
log(`Command #${cmd.id}: ${finalStatus === 'done' ? '✅' : finalStatus === 'pending' ? '⏳' : '❌'} ${result.msg}`); log(`Command #${cmd.id}: ${finalStatus === 'done' ? '✅' : finalStatus === 'pending' ? '⏳' : '❌'} ${result.msg}`);
reportResult(cmd.id, finalStatus, result.msg); reportResult(cmd.id, finalStatus, result.msg);
}; };
// Run concurrently — they do NOT block each other // Run sequentially — humans cannot perform 3 actions simultaneously!
await Promise.all([execute(buildCmd), execute(recruitCmd), execute(marketCmd)]); await execute(buildCmd);
if (farmCmd) await execute(farmCmd); await execute(recruitCmd);
if (farmUpgradeCmd) await execute(farmUpgradeCmd); await execute(marketCmd);
await execute(researchCmd);
await execute(farmCmd);
await execute(farmUpgradeCmd);
// Auto-farm: if enabled, claim all ready farms (no explicit command needed)
const farmSettings = cmdData.farm_settings || {};
if (farmSettings.enabled && !farmCmd) {
const nowTs = Math.floor(Date.now() / 1000);
// Check if ANY farm relation is ready
let readyFarms = [];
try {
const coll = uw.MM.getOnlyCollectionByName('FarmTownPlayerRelation');
readyFarms = coll?.models?.filter(r =>
r.attributes.relation_status === 1 &&
(r.attributes.lootable_at || 0) <= nowTs
) || [];
} catch (e) { /* silent */ }
if (readyFarms.length > 0) {
// Check if the CURRENT town's warehouse is full (>95%)
let allFull = true;
let claimedAny = false;
// Iterate over all towns that have ready farms
const towns = Object.values(uw.ITowns?.towns || {});
for (const town of towns) {
const storage = town.resources?.()?.storage || town.get?.('storage') || 0;
const wood = town.resources?.()?.wood || town.get?.('wood') || 0;
const stone = town.resources?.()?.stone || town.get?.('stone') || 0;
const iron = town.resources?.()?.iron || town.get?.('iron') || 0;
if (!storage) continue;
const maxRes = Math.max(wood, stone, iron);
const pct = maxRes / storage;
if (pct < 0.95) {
// This town has room — loot using its town_id context
allFull = false;
log(`⚡ Auto-farm: looting into town ${town.get?.('name')} (${Math.round(pct*100)}% full)`);
await executeFarmLoot({ payload: { loot_option: farmSettings.loot_option } });
claimedAny = true;
pushState();
break; // one loot pass is enough per poll cycle
}
}
if (allFull) {
log('⚠️ Auto-farm: ALL warehouses are full (>95%) — skipping loot this cycle');
// Report full status to backend so farm.html can show notice
try {
await fetch(`${BASE_URL}/api/farm_status?player_id=${uw.Game.player_id}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ warehouse_full: true })
});
} catch(e) {}
} else if (claimedAny) {
// Clear the full flag
try {
await fetch(`${BASE_URL}/api/farm_status?player_id=${uw.Game.player_id}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ warehouse_full: false })
});
} catch(e) {}
}
}
}
} }
// ---------------------------------------------------------------- // ----------------------------------------------------------------
// Boot // Boot
// ---------------------------------------------------------------- // ----------------------------------------------------------------
window.addEventListener('load', () => { window.addEventListener('load', () => {
log('Grepolis Remote Control v3.5 loaded'); log('Grepolis Remote Control v3.5.9 loaded');
// Start captcha watcher immediately // Start captcha watcher immediately
detectCaptcha(); detectCaptcha();

49
app.py
View File

@@ -1,33 +1,68 @@
from flask import Flask, request, jsonify from flask import Flask, request, jsonify, redirect, url_for
from flask_cors import CORS from flask_cors import CORS
from db import init_db from flask_login import LoginManager, UserMixin, login_user, logout_user, login_required, current_user
from db import init_db, get_db
from routes.api import api from routes.api import api
from routes.dashboard import dashboard from routes.dashboard import dashboard
from routes.auth import auth
# Initialise DB schema (e.g. creating newly added tables) when the app starts # Initialise DB schema when the app starts
init_db() init_db()
app = Flask(__name__) app = Flask(__name__)
app.secret_key = 'grc-super-secret-key-change-in-production'
# ----------------------------------------------------------------
# Flask-Login setup
# ----------------------------------------------------------------
login_manager = LoginManager()
login_manager.init_app(app)
login_manager.login_view = 'auth.login'
login_manager.login_message = 'Παρακαλώ συνδεθείτε για να συνεχίσετε.'
class User(UserMixin):
def __init__(self, id, username, clan_id):
self.id = id
self.username = username
self.clan_id = clan_id
@login_manager.user_loader
def load_user(user_id):
conn = get_db()
row = conn.execute('SELECT id, username, clan_id FROM users WHERE id = ?', (user_id,)).fetchone()
conn.close()
if row:
return User(row['id'], row['username'], row['clan_id'])
return None
# Make current_user available in all templates
@app.context_processor
def inject_user():
return dict(current_user=current_user)
# ----------------------------------------------------------------
# CORS
# ----------------------------------------------------------------
CORS(app, resources={r"/*": {"origins": "*"}}, supports_credentials=False) CORS(app, resources={r"/*": {"origins": "*"}}, supports_credentials=False)
# Belt-and-suspenders: inject CORS headers on every response,
# including error responses that flask-cors might miss.
@app.after_request @app.after_request
def add_cors_headers(response): def add_cors_headers(response):
response.headers['Access-Control-Allow-Origin'] = '*' response.headers['Access-Control-Allow-Origin'] = '*'
response.headers['Access-Control-Allow-Methods'] = 'GET, POST, DELETE, OPTIONS' response.headers['Access-Control-Allow-Methods'] = 'GET, POST, DELETE, OPTIONS'
response.headers['Access-Control-Allow-Headers'] = 'Content-Type, Authorization' response.headers['Access-Control-Allow-Headers'] = 'Content-Type, Authorization, X-Clan-Key'
return response return response
# Explicitly handle OPTIONS preflight for all routes
@app.route('/', defaults={'path': ''}, methods=['OPTIONS']) @app.route('/', defaults={'path': ''}, methods=['OPTIONS'])
@app.route('/<path:path>', methods=['OPTIONS']) @app.route('/<path:path>', methods=['OPTIONS'])
def handle_options(path): def handle_options(path):
return jsonify({}), 200 return jsonify({}), 200
# ----------------------------------------------------------------
# Blueprints
# ----------------------------------------------------------------
app.register_blueprint(api) app.register_blueprint(api)
app.register_blueprint(dashboard) app.register_blueprint(dashboard)
app.register_blueprint(auth)
if __name__ == '__main__': if __name__ == '__main__':
print("✅ Grepolis Remote — DB initialised") print("✅ Grepolis Remote — DB initialised")

43
bot_modules/00_config.js Normal file
View File

@@ -0,0 +1,43 @@
// ================================================================
// 00_config.js — Shared constants and utility helpers
// Runs first; everything here is available to all other modules.
// ================================================================
const uw = typeof unsafeWindow !== 'undefined' ? unsafeWindow : window;
const BASE_URL = 'https://grepo.haunter-pets.top';
// Read the clan key injected by the Loader before eval()
const CLAN_KEY = window.__GRC_CLAN_KEY || '';
// Returns a random integer between min and max (inclusive)
function randInt(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
// Schedules fn to run after a random ms delay, then reschedules itself
function jitterLoop(fn, minMs, maxMs) {
function schedule() {
setTimeout(async () => {
await fn();
schedule();
}, randInt(minMs, maxMs));
}
schedule();
}
function log(msg) {
console.log(`[GRC] ${msg}`);
}
function sleep(ms) {
return new Promise(r => setTimeout(r, ms));
}
// Wrapper around fetch() that automatically injects the X-Clan-Key header
// on every request. Use this instead of fetch() everywhere in the bot.
function apiFetch(url, options) {
options = options || {};
options.headers = options.headers || {};
options.headers['X-Clan-Key'] = CLAN_KEY;
return fetch(url, options);
}

35
bot_modules/01_ui.js Normal file
View File

@@ -0,0 +1,35 @@
// ================================================================
// 01_ui.js — Toolbar button + pause toggle
// Depends on: uw, log (00_config.js)
// ================================================================
const btnHtml = `
<div class="divider"></div>
<div class="activity" id="grc_btn"
style="filter: brightness(70%) sepia(100%) hue-rotate(200deg) saturate(1000%) contrast(0.8);">
<p id="grc_label" style="position:relative;top:-8px;font-weight:bold;z-index:6;">Remote</p>
</div>`;
let paused = false;
function togglePause() {
paused = !paused;
const label = document.getElementById('grc_label');
const btn = document.getElementById('grc_btn');
if (paused) {
label.textContent = 'Paused';
btn.style.filter = 'brightness(70%) sepia(100%) hue-rotate(-50deg) saturate(1000%) contrast(0.8)';
} else {
label.textContent = 'Remote';
btn.style.filter = 'brightness(294%) sepia(100%) hue-rotate(200deg) saturate(1000%) contrast(0.8)';
}
log(`Remote is now ${paused ? 'PAUSED' : 'ACTIVE'}`);
}
setTimeout(() => {
if (!document.getElementById('grc_btn')) {
uw.$('.tb_activities, .toolbar_activities').find('.middle').append(btnHtml);
}
}, 4000);
uw.$(document).on('click', '#grc_btn', togglePause);

244
bot_modules/02_state.js Normal file
View File

@@ -0,0 +1,244 @@
// ================================================================
// 02_state.js — Gather & push town state to the relay server
// Depends on: uw, BASE_URL, log, paused (00_config.js / 01_ui.js)
// ================================================================
function gatherState() {
const towns = uw.ITowns?.towns || {};
const player = uw.Game?.player_name || '';
const player_id = uw.Game?.player_id ?? null;
const alliance_id = uw.Game?.alliance_id ?? null;
let alliance_name = null;
try {
const pm = uw.MM.getModels().Player[player_id];
if (pm && pm.attributes) alliance_name = pm.attributes.alliance_name;
} catch (e) { log(`Failed to extract alliance_name: ${e}`); }
const total_points = uw.Game?.player_points ?? 0;
const world = uw.Game?.world_id || '';
const townList = Object.values(towns).map(town => {
const res = town.resources();
const buildings = town.buildings()?.attributes ?? {};
const unitsObj = {};
try {
const units = town.units();
if (units) {
Object.keys(units).forEach(k => {
unitsObj[k] = typeof units[k] === 'number'
? units[k]
: (units[k]?.getAmount?.() ?? 0);
});
}
} catch (e) {}
let buildQueue = [];
try {
const bo = town.buildingOrders?.();
if (bo?.models) buildQueue = bo.models.map(m => m.attributes);
} catch (e) {}
let buildDataMap = {};
try {
const buildDataRaw = uw.MM?.getModels?.()?.BuildingBuildData?.[town.id]?.attributes?.building_data || {};
for (const k in buildDataRaw) {
buildDataMap[k] = {
buildable: buildDataRaw[k].buildable,
dependencies: buildDataRaw[k].dependencies_fulfilled !== false,
wood: buildDataRaw[k].resources_for?.wood || 0,
stone: buildDataRaw[k].resources_for?.stone || 0,
iron: buildDataRaw[k].resources_for?.iron || 0,
pop: buildDataRaw[k].population_for || 0,
build_time: buildDataRaw[k].building_time || '',
can_upgrade: !!buildDataRaw[k].can_upgrade,
enough_resources: !!buildDataRaw[k].enough_resources,
missing_dependencies: buildDataRaw[k].missing_dependencies || [],
has_max_level: !!buildDataRaw[k].has_max_level
};
}
} catch (e) { log(`Failed to gather build data: ${e}`); }
// ---- Storage capacity -----------------------------------------------
let storageCapacity = 0;
try {
storageCapacity = town.getStorageCapacity?.() || 0;
if (!storageCapacity) {
const storageLevel = buildings.storage ?? 0;
const gd = uw.GameData?.buildingData?.storage;
storageCapacity = gd?.max_storage?.[storageLevel] || gd?.storage?.[storageLevel] || 0;
}
if (!storageCapacity) {
storageCapacity = res.capacity || res.storage_capacity || res.storage || 0;
}
} catch (e) { log(`storage capacity lookup failed: ${e}`); }
// ---- Market / Trade capacity -----------------------------------------
let marketCapacity = 0;
try {
const marketLevel = buildings.market ?? 0;
const gd = uw.GameData?.buildingData?.market;
marketCapacity = gd?.capacity_per_level?.[marketLevel] || 0;
if (buildings.trade_office && buildings.trade_office > 0) {
marketCapacity += (uw.GameData?.buildingData?.trade_office?.capacity_extra_per_level || 500) * marketLevel;
}
} catch (e) { log(`market capacity lookup failed: ${e}`); }
// ---- Coordinates & sea zone -----------------------------------------
let x = null, y = null, sea = null;
try {
x = town.getIslandCoordinateX?.() ?? null;
y = town.getIslandCoordinateY?.() ?? null;
if (typeof x === 'number' && typeof y === 'number') {
sea = Math.floor(x / 100) * 10 + Math.floor(y / 100);
}
} catch (e) {}
// ---- Researches -----------------------------------------------------
let researches = {};
try {
const r = town.researches?.();
if (r) researches = r.attributes ?? (typeof r === 'object' ? r : {});
} catch (e) { log(`[Debug] town.researches() failed: ${e}`); }
// ---- Unit Data (Costs & Dependencies) -------------------------------
let unitDataMap = {};
try {
const gdUnits = uw.GameData?.units || {};
for (const u in gdUnits) {
if (u === 'militia') continue;
const reqBuildings = gdUnits[u].building_dependencies || {};
const reqResearch = gdUnits[u].research_dependencies || [];
let missing_deps = {};
for (const reqB in reqBuildings) {
if ((buildings[reqB] || 0) < reqBuildings[reqB]) {
missing_deps[reqB] = { name: reqB, needed_level: reqBuildings[reqB] };
}
}
for (const reqR of reqResearch) {
if (!researches[reqR]) {
missing_deps[reqR] = { name: reqR, needed_level: 'Έρευνα' };
}
}
const cost = gdUnits[u].resources || {};
const w = cost.wood || 0, s = cost.stone || 0, i = cost.iron || 0;
let enough = !(res.wood < w || res.stone < s || res.iron < i);
unitDataMap[u] = {
wood: w, stone: s, iron: i,
pop: gdUnits[u].population || 0,
build_time: gdUnits[u].build_time || 0,
enough_resources: enough,
missing_dependencies: missing_deps
};
}
} catch (e) { log(`Failed to gather unit data: ${e}`); }
// ---- Farm town data -----------------------------------------------
let farms = [];
try {
const farmCollection = uw.MM.getOnlyCollectionByName('FarmTown');
const relCollection = uw.MM.getOnlyCollectionByName('FarmTownPlayerRelation');
if (farmCollection && relCollection) {
const ix = town.getIslandCoordinateX?.();
const iy = town.getIslandCoordinateY?.();
farmCollection.models.forEach(farm => {
if (farm.attributes.island_x !== ix || farm.attributes.island_y !== iy) return;
relCollection.models.forEach(rel => {
if (rel.attributes.farm_town_id === farm.attributes.id &&
rel.attributes.relation_status >= 1) {
farms.push({
farm_town_id: farm.attributes.id,
farm_name: farm.attributes.name || '',
relation_id: rel.id,
relation_status: rel.attributes.relation_status,
expansion_stage: rel.attributes.expansion_stage || 0,
expansion_at: rel.attributes.expansion_at || 0,
lootable_at: rel.attributes.lootable_at || 0
});
}
});
});
}
} catch (e) {}
// ---- Extra town flags -----------------------------------------------
let has_premium = false;
try {
has_premium = uw.GameDataPremium?.isAdvisorActivated?.('curator') || false;
} catch (e) {}
return {
town_id: town.id,
town_name: town.name,
x, y, sea,
wood: res.wood,
stone: res.stone,
iron: res.iron,
storage: storageCapacity,
market_capacity: marketCapacity,
population: res.population,
points: town.getPoints?.() ?? 0,
god: town.god?.() ?? null,
buildings,
units: unitsObj,
buildingOrder: buildQueue,
buildData: buildDataMap,
unitData: unitDataMap,
researches,
has_premium,
bonuses: {},
wonder_points: 0,
alliance_name,
farms,
};
});
return { player, player_id, alliance_id, total_points, world_id: world, towns: townList };
}
function pushState() {
if (paused) return;
try {
const payload = gatherState();
apiFetch(`${BASE_URL}/api/state`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
})
.then(() => log(`State pushed — ${payload.towns.length} towns`))
.catch(e => log(`State push failed: ${e}`));
} catch (e) {
log(`gatherState error: ${e}`);
}
}
// ---- AJAX Interceptor for Zero-Delay Push ----------------------------
let pushTimeout = null;
function debouncedPushState() {
if (paused) return;
if (pushTimeout) clearTimeout(pushTimeout);
pushTimeout = setTimeout(() => {
log('⚡ State change detected (AJAX). Syncing to Remote...');
pushState();
}, 1200);
}
if (uw.$) {
uw.$(document).ajaxComplete(function (e, xhr, opt) {
if (!opt || !opt.url) return;
if (opt.url.includes(BASE_URL)) return;
if (opt.url.includes('map_tiles')) return;
if (opt.url.includes('action=') || opt.url.includes('switch_town')) {
debouncedPushState();
}
});
}
// ---- Report command result back to relay -----------------------------
function reportResult(cmdId, status, message) {
apiFetch(`${BASE_URL}/api/commands/${cmdId}/result`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ status, message })
}).catch(e => log(`reportResult failed: ${e}`));
}

45
bot_modules/03_captcha.js Normal file
View File

@@ -0,0 +1,45 @@
// ================================================================
// 03_captcha.js — hCaptcha detection & server alert
// Depends on: uw, BASE_URL, log, paused (00_config.js / 01_ui.js)
// ================================================================
let captchaActive = false;
function reportCaptcha(detected) {
const player_id = uw.Game?.player_id;
if (!player_id) return;
apiFetch(`${BASE_URL}/api/captcha/alert?player_id=${player_id}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ detected })
}).catch(e => log(`captcha report failed: ${e}`));
}
function detectCaptcha() {
setInterval(() => {
const win = document.getElementById('hcaptcha_window');
let isVisible = false;
if (win) {
const style = window.getComputedStyle(win);
if (style.display !== 'none' && style.visibility !== 'hidden') {
isVisible = true;
}
}
if (isVisible && !captchaActive) {
captchaActive = true;
paused = true;
const label = document.getElementById('grc_label');
const btn = document.getElementById('grc_btn');
if (label) label.textContent = '⚠ CAPTCHA';
if (btn) btn.style.filter = 'brightness(70%) sepia(100%) hue-rotate(300deg) saturate(1000%) contrast(0.8)';
log('⚠ CAPTCHA detected — bot paused, alerting server');
reportCaptcha(true);
} else if (!isVisible && captchaActive) {
captchaActive = false;
log('✅ Captcha resolved — alert cleared (bot remains paused)');
reportCaptcha(false);
}
}, 1000);
}

View File

@@ -0,0 +1,221 @@
// ================================================================
// 04a_execute_farm.js — Farm command executors
// Depends on: uw, log, sleep, randInt, paused, pushState
// ================================================================
// ----------------------------------------------------------------
// Execute: Farm Upgrade / Unlock
// ----------------------------------------------------------------
async function executeFarmUpgrade(cmd) {
const now = Math.floor(Date.now() / 1000);
let farmModels, relModels;
try {
farmModels = uw.MM.getOnlyCollectionByName('FarmTown')?.models;
relModels = uw.MM.getOnlyCollectionByName('FarmTownPlayerRelation')?.models;
} catch (e) {
return { ok: false, msg: `Cannot access farm collections: ${e.message}` };
}
if (!farmModels || !relModels) {
return { ok: false, msg: 'Farm collections not loaded yet' };
}
// Build polis list (one town per island)
const islandsSeen = new Set();
const polisList = [];
try {
for (const town of uw.MM.getCollections().Town[0].models) {
const { on_small_island, island_id, id } = town.attributes;
if (on_small_island) continue;
if (!islandsSeen.has(island_id)) {
islandsSeen.add(island_id);
polisList.push(id);
}
}
} catch (e) {
return { ok: false, msg: `Cannot build town list: ${e.message}` };
}
let upgraded = 0, unlocked = 0, skipped = 0, errors = 0;
for (const town_id of polisList) {
const town = uw.ITowns?.towns?.[town_id];
if (!town) continue;
const ix = town.getIslandCoordinateX();
const iy = town.getIslandCoordinateY();
if (ix == null || iy == null) continue;
for (const farm of farmModels) {
if (farm.attributes.island_x !== ix || farm.attributes.island_y !== iy) continue;
for (const rel of relModels) {
if (rel.attributes.farm_town_id !== farm.attributes.id) continue;
const status = rel.attributes.relation_status;
const level = rel.attributes.expansion_stage || 0;
const expAt = rel.attributes.expansion_at || 0;
if (expAt > now) { skipped++; continue; }
if (status === 1 && level >= 5) { skipped++; continue; }
if (status < 0) { skipped++; continue; }
const isLocked = status === 0;
const action = isLocked ? 'unlock' : 'upgrade';
const requestedAction = cmd.payload?.action_type;
if (requestedAction && requestedAction !== action) { skipped++; continue; }
if (paused) return { ok: false, msg: 'Aborted due to pause/captcha' };
log(`Farm ${action}: farm_id=${farm.attributes.id} level=${level} town=${town_id}`);
try {
uw.gpAjax.ajaxPost('frontend_bridge', 'execute', {
model_url: `FarmTownPlayerRelation/${rel.id}`,
action_name: action,
arguments: { farm_town_id: farm.attributes.id },
town_id
});
isLocked ? unlocked++ : upgraded++;
} catch (e) { errors++; }
await sleep(randInt(1200, 2500));
}
}
}
pushState();
return { ok: true, msg: `Farm upgrade done: ${unlocked} unlocked, ${upgraded} upgraded, ${skipped} skipped, ${errors} errors` };
}
// ----------------------------------------------------------------
// Execute: Farm Loot
// ----------------------------------------------------------------
async function executeFarmLoot(cmd) {
const { loot_option } = cmd.payload || {};
const option = parseInt(loot_option) || 1;
const now = Math.floor(Date.now() / 1000);
let farmModels, relModels;
try {
farmModels = uw.MM.getOnlyCollectionByName('FarmTown')?.models;
relModels = uw.MM.getOnlyCollectionByName('FarmTownPlayerRelation')?.models;
} catch (e) {
return { ok: false, msg: `Cannot access farm collections: ${e.message}` };
}
if (!farmModels || !relModels) {
return { ok: false, msg: 'Farm collections not loaded yet — open island view first' };
}
// Build island groups using MM (all towns, not just visible)
const islandTownsMap = {};
try {
const allTowns = uw.MM.getCollections().Town[0].models;
for (const town of allTowns) {
const { on_small_island, island_id, id } = town.attributes;
if (on_small_island) continue;
if (!islandTownsMap[island_id]) islandTownsMap[island_id] = [];
islandTownsMap[island_id].push(id);
}
} catch (e) {
return { ok: false, msg: `Cannot build town list: ${e.message}` };
}
const islandList = Object.keys(islandTownsMap);
log(`Farm: processing ${islandList.length} islands with option=${option}`);
let claimed = 0, skipped = 0, errors = 0;
for (let i = 0; i < islandList.length; i++) {
const island_id = islandList[i];
const townIds = islandTownsMap[island_id];
let selected_town_id = null;
let lowest_total_res = Infinity;
for (const t_id of townIds) {
const t = uw.ITowns?.towns?.[t_id];
if (!t) continue;
let storageCapacity = t.getStorageCapacity?.() || 0;
if (!storageCapacity) {
const buildings = t.buildings?.()?.attributes || {};
const storageLevel = buildings.storage ?? 0;
const gd = uw.GameData?.buildingData?.storage;
storageCapacity = gd?.max_storage?.[storageLevel] || gd?.storage?.[storageLevel] || 0;
}
const res = t.resources?.() || {};
const w = res.wood || 0, s = res.stone || 0, ir = res.iron || 0;
// Skip completely full towns
if (storageCapacity > 0 && w >= storageCapacity && s >= storageCapacity && ir >= storageCapacity) continue;
const total_res = w + s + ir;
if (total_res < lowest_total_res) {
lowest_total_res = total_res;
selected_town_id = t_id;
}
}
if (!selected_town_id) {
log(`Farm: Skipping island ${island_id} (All towns are 100% full)`);
skipped++;
continue;
}
const town_id = selected_town_id;
const town = uw.ITowns?.towns?.[town_id];
if (!town) { skipped++; continue; }
const ix = town.getIslandCoordinateX();
const iy = town.getIslandCoordinateY();
if (ix == null || iy == null) { skipped++; continue; }
const readyFarms = [];
for (const farm of farmModels) {
if (farm.attributes.island_x !== ix || farm.attributes.island_y !== iy) continue;
for (const rel of relModels) {
if (
rel.attributes.farm_town_id === farm.attributes.id &&
rel.attributes.relation_status === 1 &&
(!rel.attributes.lootable_at || now >= rel.attributes.lootable_at)
) {
readyFarms.push({
town_id,
farm_town_id: rel.attributes.farm_town_id,
relation_id: rel.id
});
}
}
}
if (readyFarms.length === 0) { skipped++; continue; }
log(`Farm: ${readyFarms.length} ready on island of town ${town_id}`);
for (const farm of readyFarms) {
if (paused) return { ok: false, msg: 'Aborted due to pause/captcha' };
try {
uw.gpAjax.ajaxPost('frontend_bridge', 'execute', {
model_url: `FarmTownPlayerRelation/${farm.relation_id}`,
action_name: 'claim',
arguments: { farm_town_id: farm.farm_town_id, type: 'resources', option },
town_id: farm.town_id
});
claimed++;
} catch (e) { errors++; }
await sleep(randInt(1000, 2200));
}
try { uw.WMap.removeFarmTownLootCooldownIconAndRefreshLootTimers(); } catch (e) {}
if (i < islandList.length - 1) {
if (paused) return { ok: false, msg: 'Aborted due to pause/captcha' };
const gap = randInt(30000, 90000);
log(`Farm: island done. Waiting ${(gap / 1000).toFixed(0)}s before next island...`);
await sleep(gap);
}
}
return { ok: true, msg: `Farm done: ${claimed} claimed, ${skipped} islands skipped, ${errors} errors` };
}

View File

@@ -0,0 +1,139 @@
// ================================================================
// 04b_execute_admin.js — Admin command executors
// Depends on: uw, log, sleep, randInt, paused
// ================================================================
// ----------------------------------------------------------------
// Execute: Build
// ----------------------------------------------------------------
async function executeBuild(cmd) {
const { town_id, payload } = cmd;
const { building_id } = payload;
const town = uw.ITowns?.getTown?.(town_id) || uw.ITowns?.towns?.[town_id];
if (!town) return { ok: false, msg: `Town ${town_id} not found in ITowns` };
const queueLen = town.buildingOrders?.()?.length ?? 0;
const hasCurator = uw.GameDataPremium?.isAdvisorActivated?.('curator');
const maxQueue = hasCurator ? 7 : 2;
if (queueLen >= maxQueue) {
return { ok: false, requeue: true, msg: `Build queue full (${queueLen}/${maxQueue})` };
}
try {
const buildData = uw.MM.getModels?.()?.BuildingBuildData?.[town_id]
?.attributes?.building_data?.[building_id];
if (buildData) {
const res = town.resources();
const { resources_for, population_for } = buildData;
if (town.getAvailablePopulation?.() < population_for) {
return { ok: false, requeue: true, msg: `Not enough population for ${building_id}` };
}
if (res.wood < resources_for.wood || res.stone < resources_for.stone || res.iron < resources_for.iron) {
return { ok: false, requeue: true, msg: `Not enough resources for ${building_id}` };
}
}
} catch (e) { log(`Resource check skipped: ${e}`); }
const reactionMs = randInt(800, 2500);
log(`Waiting ${reactionMs}ms before firing buildUp (reaction time)...`);
await sleep(reactionMs);
if (paused) return { ok: false, msg: 'Aborted due to pause/captcha' };
uw.gpAjax.ajaxPost('frontend_bridge', 'execute', {
model_url: 'BuildingOrder',
action_name: 'buildUp',
arguments: { building_id },
town_id
});
await sleep(500);
return { ok: true, msg: `buildUp ${building_id} queued` };
}
// ----------------------------------------------------------------
// Execute: Recruit
// ----------------------------------------------------------------
async function executeRecruit(cmd) {
const { town_id, payload } = cmd;
const { unit_id, amount } = payload;
const town = uw.ITowns?.getTown?.(town_id) || uw.ITowns?.towns?.[town_id];
if (!town) return { ok: false, msg: `Town ${town_id} not found` };
const navalUnits = [
'big_transporter', 'small_transporter', 'bireme',
'attack_ship', 'trireme', 'colonize_ship', 'sea_monster'
];
const endpoint = navalUnits.includes(unit_id) ? 'building_docks' : 'building_barracks';
const reactionMs = randInt(800, 2500);
log(`Waiting ${reactionMs}ms before firing recruit (reaction time)...`);
await sleep(reactionMs);
if (paused) return { ok: false, msg: 'Aborted due to pause/captcha' };
uw.gpAjax.ajaxPost(endpoint, 'build', {
unit_id,
amount: parseInt(amount) || 1,
town_id
});
await sleep(500);
return { ok: true, msg: `Recruit ${amount}x ${unit_id} submitted` };
}
// ----------------------------------------------------------------
// Execute: Market Offer
// ----------------------------------------------------------------
async function executeMarketOffer(cmd) {
const { town_id, payload } = cmd;
const { offer, offer_type, demand, demand_type, max_delivery_time, visibility } = payload;
const town = uw.ITowns?.getTown?.(town_id) || uw.ITowns?.towns?.[town_id];
if (!town) return { ok: false, msg: `Town ${town_id} not found` };
const reactionMs = randInt(800, 2500);
log(`Waiting ${reactionMs}ms before firing market offer (reaction time)...`);
await sleep(reactionMs);
if (paused) return { ok: false, msg: 'Aborted due to pause/captcha' };
uw.gpAjax.ajaxPost('frontend_bridge', 'execute', {
model_url: 'CreateOffers/' + town_id,
action_name: 'createOffer',
captcha: null,
arguments: { offer, offer_type, demand, demand_type, max_delivery_time, visibility }
});
await sleep(500);
return { ok: true, msg: `Market offer posted: ${offer} ${offer_type} => ${demand} ${demand_type}` };
}
// ----------------------------------------------------------------
// Execute: Research (Academy)
// ----------------------------------------------------------------
async function executeResearch(cmd) {
const { town_id, payload } = cmd;
const { research_id } = payload;
const town = uw.ITowns?.getTown?.(town_id) || uw.ITowns?.towns?.[town_id];
if (!town) return { ok: false, msg: `Town ${town_id} not found` };
const reactionMs = randInt(800, 2500);
log(`Waiting ${reactionMs}ms before firing research (reaction time)...`);
await sleep(reactionMs);
if (paused) return { ok: false, msg: 'Aborted due to pause/captcha' };
uw.gpAjax.ajaxPost('frontend_bridge', 'execute', {
model_url: 'ResearchOrder',
action_name: 'research',
arguments: { id: research_id },
town_id
});
await sleep(500);
return { ok: true, msg: `Research ${research_id} queued` };
}

160
bot_modules/05_main.js Normal file
View File

@@ -0,0 +1,160 @@
// ================================================================
// 05_main.js — Poll loop, command dispatch, boot
// Depends on: everything above
// ================================================================
async function pollAndExecute() {
if (paused) return;
const player_id = uw.Game?.player_id;
if (!player_id) return;
let cmdData;
try {
const res = await apiFetch(`${BASE_URL}/api/commands/pending?player_id=${player_id}`);
cmdData = await res.json();
} catch (e) {
log(`Poll failed: ${e}`);
return;
}
// Feature flags — default to all on if server doesn't send them (backward compatible)
const features = cmdData.enabled_features || ['farm', 'admin'];
const farmOn = features.includes('farm');
const adminOn = features.includes('admin');
const buildCmd = adminOn ? cmdData.build : null;
const recruitCmd = adminOn ? cmdData.recruit : null;
const marketCmd = adminOn ? cmdData.market : null;
const researchCmd = adminOn ? cmdData.research : null;
const farmCmd = farmOn ? cmdData.farm : null;
const farmUpgradeCmd = farmOn ? cmdData.farm_upgrade : null;
if (cmdData.sync_requested) {
log('Sync requested by server — pushing state immediately');
pushState();
}
const execute = async (cmd) => {
if (!cmd) return;
log(`Executing command #${cmd.id} — type:${cmd.type} town:${cmd.town_id}`);
if (paused) {
log(`[Paused] Ignoring command #${cmd.id}`);
return;
}
let result;
try {
if (cmd.type === 'build') result = await executeBuild(cmd);
else if (cmd.type === 'recruit') result = await executeRecruit(cmd);
else if (cmd.type === 'market_offer') result = await executeMarketOffer(cmd);
else if (cmd.type === 'research') result = await executeResearch(cmd);
else if (cmd.type === 'farm_loot') result = await executeFarmLoot(cmd);
else if (cmd.type === 'farm_upgrade') result = await executeFarmUpgrade(cmd);
else result = { ok: false, msg: `Unknown type: ${cmd.type}` };
} catch (e) {
result = { ok: false, msg: `Exception: ${e}` };
}
const finalStatus = result.requeue ? 'pending' : (result.ok ? 'done' : 'failed');
log(`Command #${cmd.id}: ${finalStatus === 'done' ? '✅' : finalStatus === 'pending' ? '⏳' : '❌'} ${result.msg}`);
reportResult(cmd.id, finalStatus, result.msg);
};
// Run sequentially — humans cannot perform 3 actions simultaneously!
await execute(buildCmd);
await execute(recruitCmd);
await execute(marketCmd);
await execute(researchCmd);
await execute(farmCmd);
await execute(farmUpgradeCmd);
// Auto-farm: only if farm feature is enabled
const farmSettings = cmdData.farm_settings || {};
if (farmOn && farmSettings.enabled && !farmCmd) {
const nowTs = Math.floor(Date.now() / 1000);
let readyFarms = [];
try {
const coll = uw.MM.getOnlyCollectionByName('FarmTownPlayerRelation');
readyFarms = coll?.models?.filter(r =>
r.attributes.relation_status === 1 &&
(r.attributes.lootable_at || 0) <= nowTs
) || [];
} catch (e) { /* silent */ }
if (readyFarms.length > 0) {
let allFull = true;
let claimedAny = false;
const towns = Object.values(uw.ITowns?.towns || {});
for (const town of towns) {
// Use same multi-strategy lookup as gatherState() — res.storage is often 0 in Grepolis
const res = town.resources?.() || {};
let storage = town.getStorageCapacity?.() || 0;
if (!storage) {
const buildings = town.buildings?.()?.attributes || {};
const storageLevel = buildings.storage ?? 0;
const gd = uw.GameData?.buildingData?.storage;
storage = gd?.max_storage?.[storageLevel] || gd?.storage?.[storageLevel] || 0;
}
if (!storage) storage = res.capacity || res.storage_capacity || res.storage || 0;
const wood = res.wood || 0;
const stone = res.stone || 0;
const iron = res.iron || 0;
if (!storage) continue;
const maxRes = Math.max(wood, stone, iron);
const pct = maxRes / storage;
if (pct < 0.95) {
allFull = false;
log(`⚡ Auto-farm: looting into town ${town.get?.('name')} (${Math.round(pct * 100)}% full)`);
await executeFarmLoot({ payload: { loot_option: farmSettings.loot_option } });
claimedAny = true;
pushState();
break;
}
}
if (allFull) {
log('⚠️ Auto-farm: ALL warehouses are full (>95%) — skipping loot this cycle');
try {
await apiFetch(`${BASE_URL}/api/farm_status?player_id=${uw.Game.player_id}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ warehouse_full: true })
});
} catch (e) {}
} else if (claimedAny) {
try {
await apiFetch(`${BASE_URL}/api/farm_status?player_id=${uw.Game.player_id}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ warehouse_full: false })
});
} catch (e) {}
}
}
}
}
// ----------------------------------------------------------------
// Boot — works whether page is already loaded or not.
// When eval()'d dynamically the 'load' event has already fired,
// so we check readyState and boot immediately in that case.
// ----------------------------------------------------------------
function boot() {
log('Grepolis Remote Control v4.0.0 (remote) loaded');
detectCaptcha();
setTimeout(pushState, 5000);
jitterLoop(pushState, 60000, 120000);
jitterLoop(pollAndExecute, 8000, 18000);
}
if (document.readyState === 'complete') {
// Page already loaded (normal case when eval()'d dynamically)
boot();
} else {
// Fallback: wait for load event (shouldn't happen but safe to keep)
window.addEventListener('load', boot);
}

53
db.py
View File

@@ -1,5 +1,6 @@
import sqlite3 import sqlite3
import os import os
import secrets
DB_PATH = os.path.join(os.path.dirname(__file__), 'grepo.db') DB_PATH = os.path.join(os.path.dirname(__file__), 'grepo.db')
@@ -75,11 +76,63 @@ def init_db():
'ALTER TABLE town_state ADD COLUMN sea INTEGER', 'ALTER TABLE town_state ADD COLUMN sea INTEGER',
'ALTER TABLE commands ADD COLUMN player_id TEXT', 'ALTER TABLE commands ADD COLUMN player_id TEXT',
'ALTER TABLE farm_settings ADD COLUMN bandit_camp_enabled INTEGER NOT NULL DEFAULT 0', 'ALTER TABLE farm_settings ADD COLUMN bandit_camp_enabled INTEGER NOT NULL DEFAULT 0',
"ALTER TABLE clan_members ADD COLUMN features TEXT NOT NULL DEFAULT 'farm,admin'",
'ALTER TABLE users ADD COLUMN clan_id INTEGER REFERENCES clans(id)',
]: ]:
try: try:
c.execute(_col) c.execute(_col)
except Exception: except Exception:
pass # column already exists pass # column already exists
# Users — website admin accounts
c.execute('''
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT NOT NULL UNIQUE,
password_hash TEXT NOT NULL,
clan_id INTEGER REFERENCES clans(id),
created_at TEXT NOT NULL DEFAULT (datetime('now'))
)
''')
# Clans — groups owned by a user, identified by a unique clan_key
c.execute('''
CREATE TABLE IF NOT EXISTS clans (
id INTEGER PRIMARY KEY AUTOINCREMENT,
owner_id INTEGER NOT NULL REFERENCES users(id),
name TEXT NOT NULL,
clan_key TEXT NOT NULL UNIQUE,
created_at TEXT NOT NULL DEFAULT (datetime('now'))
)
''')
# Clan members — links Grepolis player_ids to a clan
c.execute('''
CREATE TABLE IF NOT EXISTS clan_members (
id INTEGER PRIMARY KEY AUTOINCREMENT,
clan_id INTEGER NOT NULL REFERENCES clans(id),
player_id TEXT NOT NULL,
player_name TEXT,
features TEXT NOT NULL DEFAULT 'farm,admin',
joined_at TEXT NOT NULL DEFAULT (datetime('now')),
UNIQUE(clan_id, player_id)
)
''')
# Migration: Auto-assign existing users to their clan_id if they are the owner
try:
c.execute('''
UPDATE users
SET clan_id = (SELECT id FROM clans WHERE owner_id = users.id)
WHERE clan_id IS NULL AND EXISTS (SELECT 1 FROM clans WHERE owner_id = users.id)
''')
except Exception:
pass
conn.commit() conn.commit()
conn.close() conn.close()
def generate_clan_key():
"""Generate a short, unique, human-readable clan key."""
return secrets.token_urlsafe(8).upper()[:10]

View File

@@ -1,3 +1,4 @@
flask flask
flask-cors flask-cors
flask-login
gunicorn gunicorn

View File

@@ -2,10 +2,44 @@ from flask import Blueprint, request, jsonify
from db import get_db from db import get_db
import json import json
from datetime import datetime from datetime import datetime
import os
from flask import make_response
api = Blueprint('api', __name__) 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 # POST /api/state
# Tampermonkey pushes a full town snapshot every poll cycle. # Tampermonkey pushes a full town snapshot every poll cycle.
@@ -22,6 +56,11 @@ def receive_state():
alliance_id = str(data.get('alliance_id', '') or '') alliance_id = str(data.get('alliance_id', '') or '')
world_id = data.get('world_id', '') 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() conn = get_db()
c = conn.cursor() c = conn.cursor()
for town in towns: for town in towns:
@@ -100,18 +139,27 @@ def get_pending_command():
market_cmd = _fetch_pending_of_type(c, 'market_offer', 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_cmd = _fetch_pending_of_type(c, 'farm_loot', player_id)
farm_upgrade_cmd = _fetch_pending_of_type(c, 'farm_upgrade', 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) sync_req = _check_and_reset_sync(c, player_id)
# Also return current farm settings so TM knows loot_option # Farm settings
farm_row = c.execute( farm_row = c.execute(
'SELECT enabled, bandit_camp_enabled, loot_option FROM farm_settings WHERE player_id = ?', (player_id,) 'SELECT enabled, loot_option FROM farm_settings WHERE player_id = ?', (player_id,)
).fetchone() ).fetchone()
farm_settings = { farm_settings = {
'enabled': bool(farm_row['enabled']) if farm_row else False, 'enabled': bool(farm_row['enabled']) if farm_row else False,
'bandit_camp_enabled': bool(farm_row['bandit_camp_enabled']) if farm_row else False,
'loot_option': farm_row['loot_option'] if farm_row else 1 'loot_option': farm_row['loot_option'] if farm_row else 1
} }
# Feature flags — look up this player's authorized features from their clan
member_row = c.execute(
'SELECT features FROM clan_members WHERE player_id = ?', (str(player_id),)
).fetchone()
if member_row and member_row['features']:
enabled_features = [f.strip() for f in member_row['features'].split(',') if f.strip()]
else:
enabled_features = ['farm', 'admin'] # default: all on (backward-compatible)
conn.commit() conn.commit()
conn.close() conn.close()
@@ -119,12 +167,15 @@ def get_pending_command():
'build': build_cmd, 'build': build_cmd,
'recruit': recruit_cmd, 'recruit': recruit_cmd,
'market': market_cmd, 'market': market_cmd,
'research': research_cmd,
'farm': farm_cmd, 'farm': farm_cmd,
'farm_upgrade': farm_upgrade_cmd, 'farm_upgrade': farm_upgrade_cmd,
'farm_settings': farm_settings, 'farm_settings': farm_settings,
'enabled_features': enabled_features,
'sync_requested': sync_req 'sync_requested': sync_req
}) })
def _check_and_reset_sync(c, player_id): def _check_and_reset_sync(c, player_id):
key = f'sync_request_{player_id}' key = f'sync_request_{player_id}'
row = c.execute("SELECT value FROM kv_store WHERE key = ?", (key,)).fetchone() row = c.execute("SELECT value FROM kv_store WHERE key = ?", (key,)).fetchone()
@@ -201,3 +252,89 @@ def captcha_alert():
conn.commit() conn.commit()
conn.close() conn.close()
return jsonify({'ok': True}) 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

319
routes/auth.py Normal file
View File

@@ -0,0 +1,319 @@
from flask import Blueprint, render_template, request, jsonify, redirect, url_for, flash
from flask_login import login_user, logout_user, login_required, current_user
from werkzeug.security import generate_password_hash, check_password_hash
from db import get_db, generate_clan_key
from datetime import datetime, timezone
auth = Blueprint('auth', __name__)
# ------------------------------------------------------------------
# Helper — resolve the User class from app (avoid circular import)
# ------------------------------------------------------------------
def _make_user(row):
from app import User
return User(row['id'], row['username'], row['clan_id'])
# ------------------------------------------------------------------
# GET/POST /auth/login
# ------------------------------------------------------------------
@auth.route('/auth/login', methods=['GET', 'POST'])
def login():
if current_user.is_authenticated:
return redirect(url_for('dashboard.index'))
error = None
if request.method == 'POST':
username = request.form.get('username', '').strip()
password = request.form.get('password', '')
conn = get_db()
row = conn.execute(
'SELECT id, username, password_hash, clan_id FROM users WHERE username = ?', (username,)
).fetchone()
conn.close()
if row and check_password_hash(row['password_hash'], password):
user = _make_user(row)
login_user(user, remember=True)
return redirect(url_for('dashboard.index'))
else:
error = 'Λάθος όνομα χρήστη ή κωδικός.'
return render_template('login.html', error=error)
# ------------------------------------------------------------------
# GET/POST /auth/register
# ------------------------------------------------------------------
@auth.route('/auth/register', methods=['GET', 'POST'])
def register():
if current_user.is_authenticated:
return redirect(url_for('dashboard.index'))
error = None
if request.method == 'POST':
username = request.form.get('username', '').strip()
password = request.form.get('password', '')
confirm = request.form.get('confirm', '')
if not username or not password:
error = 'Συμπλήρωσε όνομα χρήστη και κωδικό.'
elif password != confirm:
error = 'Οι κωδικοί δεν ταιριάζουν.'
elif len(password) < 6:
error = 'Ο κωδικός πρέπει να έχει τουλάχιστον 6 χαρακτήρες.'
else:
conn = get_db()
existing = conn.execute(
'SELECT id FROM users WHERE username = ?', (username,)
).fetchone()
if existing:
error = 'Το όνομα χρήστη χρησιμοποιείται ήδη.'
conn.close()
else:
pw_hash = generate_password_hash(password)
conn.execute(
'INSERT INTO users (username, password_hash) VALUES (?, ?)',
(username, pw_hash)
)
conn.commit()
row = conn.execute(
'SELECT id, username, password_hash, clan_id FROM users WHERE username = ?', (username,)
).fetchone()
conn.close()
user = _make_user(row)
login_user(user, remember=True)
return redirect(url_for('auth.options'))
return render_template('register.html', error=error)
# ------------------------------------------------------------------
# GET /auth/logout
# ------------------------------------------------------------------
@auth.route('/auth/logout')
@login_required
def logout():
logout_user()
return redirect(url_for('auth.login'))
# ------------------------------------------------------------------
# GET/POST /auth/options — Clan management page
# ------------------------------------------------------------------
@auth.route('/auth/options', methods=['GET', 'POST'])
@login_required
def options():
conn = get_db()
# Load clan based on current user's clan_id
clan = None
if current_user.clan_id:
clan = conn.execute(
'SELECT * FROM clans WHERE id = ?', (current_user.clan_id,)
).fetchone()
# Fetch website admins (users belonging to this clan other than current user)
admins = []
if clan and clan['owner_id'] == current_user.id:
admins = conn.execute(
'SELECT id, username, created_at FROM users WHERE clan_id = ? AND id != ? ORDER BY created_at ASC',
(clan['id'], current_user.id)
).fetchall()
members = []
if clan:
rows = conn.execute(
'''SELECT cm.id, cm.player_id, cm.player_name, cm.joined_at, cm.features,
ts.updated_at
FROM clan_members cm
LEFT JOIN town_state ts ON ts.player_id = cm.player_id
WHERE cm.clan_id = ?
GROUP BY cm.player_id
ORDER BY cm.joined_at DESC''',
(clan['id'],)
).fetchall()
now = datetime.utcnow()
for row in rows:
is_online = False
if row['updated_at']:
try:
last_seen = datetime.fromisoformat(row['updated_at'])
if (now - last_seen).total_seconds() <= 150:
is_online = True
except Exception:
pass
members.append({
'id': row['id'],
'player_id': row['player_id'],
'player_name': row['player_name'] or 'Άγνωστος',
'joined_at': row['joined_at'][:10] if row['joined_at'] else '',
'is_online': is_online,
'feat_farm': 'farm' in (row['features'] or 'farm,admin'),
'feat_admin': 'admin' in (row['features'] or 'farm,admin'),
})
conn.close()
return render_template('options.html', clan=clan, members=members, admins=admins)
# ------------------------------------------------------------------
# POST /auth/clan/create
# ------------------------------------------------------------------
@auth.route('/auth/clan/create', methods=['POST'])
@login_required
def create_clan():
clan_name = request.form.get('clan_name', '').strip()
if not clan_name:
return redirect(url_for('auth.options'))
conn = get_db()
existing = conn.execute(
'SELECT id FROM clans WHERE owner_id = ?', (current_user.id,)
).fetchone()
if not existing:
key = generate_clan_key()
cursor = conn.execute(
'INSERT INTO clans (owner_id, name, clan_key) VALUES (?, ?, ?)',
(current_user.id, clan_name, key)
)
clan_id = cursor.lastrowid
conn.execute('UPDATE users SET clan_id = ? WHERE id = ?', (clan_id, current_user.id))
conn.commit()
# Update the current_user object dynamically to reflect the new clan_id without re-login
current_user.clan_id = clan_id
conn.close()
return redirect(url_for('auth.options'))
# ------------------------------------------------------------------
# POST /auth/clan/regenerate-key
# ------------------------------------------------------------------
@auth.route('/auth/clan/regenerate-key', methods=['POST'])
@login_required
def regenerate_key():
new_key = generate_clan_key()
conn = get_db()
conn.execute(
'UPDATE clans SET clan_key = ? WHERE owner_id = ?',
(new_key, current_user.id)
)
conn.commit()
conn.close()
return redirect(url_for('auth.options'))
@auth.route('/auth/clan/remove-member/<player_id>', methods=['POST'])
@login_required
def remove_member(player_id):
conn = get_db()
clan = conn.execute(
'SELECT id FROM clans WHERE owner_id = ?', (current_user.id,)
).fetchone()
if clan:
conn.execute(
'DELETE FROM clan_members WHERE clan_id = ? AND player_id = ?',
(clan['id'], player_id)
)
conn.commit()
conn.close()
return redirect(url_for('auth.options'))
# ------------------------------------------------------------------
# POST /auth/clan/update-features/<player_id>
# ------------------------------------------------------------------
@auth.route('/auth/clan/update-features/<player_id>', methods=['POST'])
@login_required
def update_member_features(player_id):
farm = 'farm' if request.form.get('farm') else None
admin = 'admin' if request.form.get('admin') else None
features = ','.join(f for f in [farm, admin] if f) or ''
conn = get_db()
clan = conn.execute(
'SELECT id FROM clans WHERE owner_id = ?', (current_user.id,)
).fetchone()
if clan:
conn.execute(
'UPDATE clan_members SET features = ? WHERE clan_id = ? AND player_id = ?',
(features, clan['id'], player_id)
)
conn.commit()
conn.close()
return redirect(url_for('auth.options'))
# ------------------------------------------------------------------
# POST /auth/clan/add-admin
# ------------------------------------------------------------------
@auth.route('/auth/clan/add-admin', methods=['POST'])
@login_required
def add_admin():
username = request.form.get('admin_username', '').strip()
if not username:
return redirect(url_for('auth.options'))
conn = get_db()
clan = conn.execute(
'SELECT id FROM clans WHERE owner_id = ?', (current_user.id,)
).fetchone()
if clan:
# Check if user exists
user = conn.execute('SELECT id, clan_id FROM users WHERE username = ?', (username,)).fetchone()
if user:
# If user already belongs to a clan, we could show an error, but let's just overwrite for now
# or maybe only if clan_id is NULL
conn.execute('UPDATE users SET clan_id = ? WHERE id = ?', (clan['id'], user['id']))
conn.commit()
flash(f"Ο χρήστης {username} προστέθηκε ως διαχειριστής.", "success")
else:
flash(f"Ο χρήστης {username} δεν βρέθηκε.", "error")
conn.close()
return redirect(url_for('auth.options'))
# ------------------------------------------------------------------
# POST /auth/clan/remove-admin/<admin_id>
# ------------------------------------------------------------------
@auth.route('/auth/clan/remove-admin/<int:admin_id>', methods=['POST'])
@login_required
def remove_admin(admin_id):
conn = get_db()
clan = conn.execute(
'SELECT id FROM clans WHERE owner_id = ?', (current_user.id,)
).fetchone()
if clan:
conn.execute('UPDATE users SET clan_id = NULL WHERE id = ? AND clan_id = ?', (admin_id, clan['id']))
conn.commit()
flash("Ο διαχειριστής αφαιρέθηκε.", "success")
conn.close()
return redirect(url_for('auth.options'))
# ------------------------------------------------------------------
# POST /auth/clan/leave
# ------------------------------------------------------------------
@auth.route('/auth/clan/leave', methods=['POST'])
@login_required
def leave_clan():
conn = get_db()
if current_user.clan_id:
clan = conn.execute('SELECT owner_id FROM clans WHERE id = ?', (current_user.clan_id,)).fetchone()
if clan and clan['owner_id'] != current_user.id:
conn.execute('UPDATE users SET clan_id = NULL WHERE id = ?', (current_user.id,))
conn.commit()
current_user.clan_id = None
flash("Έχετε αποχωρήσει από την ομάδα.", "success")
conn.close()
return redirect(url_for('auth.options'))

View File

@@ -1,4 +1,5 @@
from flask import Blueprint, render_template, request, jsonify from flask import Blueprint, render_template, request, jsonify
from flask_login import login_required, current_user
from db import get_db from db import get_db
import json import json
from datetime import datetime, timedelta from datetime import datetime, timedelta
@@ -11,17 +12,28 @@ dashboard = Blueprint('dashboard', __name__)
# Serve the dashboard HTML # Serve the dashboard HTML
# ------------------------------------------------------------------ # ------------------------------------------------------------------
@dashboard.route('/') @dashboard.route('/')
@login_required
def index(): def index():
conn = get_db() conn = get_db()
rows = conn.execute('''
SELECT player, player_id, MAX(updated_at) as last_seen, MAX(world_id) as world_id
FROM town_state
WHERE player IS NOT NULL
GROUP BY player, player_id
ORDER BY player ASC
''').fetchall()
# Pre-fetch all active captchas # 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() 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'} active_captchas = {r['key'].replace('captcha_active_', ''): True for r in captcha_rows if r['value'] == '1'}
conn.close() conn.close()
@@ -46,17 +58,21 @@ def index():
'captcha_active': active_captchas.get(r['player_id'], False) 'captcha_active': active_captchas.get(r['player_id'], False)
}) })
return render_template('index.html', players=players) return render_template('index.html', players=players, no_clan=False)
@dashboard.route('/player/<player_id>') @dashboard.route('/player/<player_id>')
@login_required
def player_hub(player_id): def player_hub(player_id):
return render_template('hub.html', player_id=player_id) return render_template('hub.html', player_id=player_id)
@dashboard.route('/player/<player_id>/admin') @dashboard.route('/player/<player_id>/admin')
@login_required
def player_dashboard(player_id): def player_dashboard(player_id):
return render_template('dashboard.html', player_id=player_id) return render_template('dashboard.html', player_id=player_id)
@dashboard.route('/player/<player_id>/farm') @dashboard.route('/player/<player_id>/farm')
@login_required
def player_farm(player_id): def player_farm(player_id):
return render_template('farm.html', player_id=player_id) return render_template('farm.html', player_id=player_id)
@@ -70,12 +86,12 @@ def get_farm_settings():
player_id = request.args.get('player_id') player_id = request.args.get('player_id')
conn = get_db() conn = get_db()
row = conn.execute( row = conn.execute(
'SELECT enabled, bandit_camp_enabled, loot_option FROM farm_settings WHERE player_id = ?', (player_id,) 'SELECT enabled, loot_option FROM farm_settings WHERE player_id = ?', (player_id,)
).fetchone() ).fetchone()
conn.close() conn.close()
if row: if row:
return jsonify({'enabled': bool(row['enabled']), 'bandit_camp_enabled': bool(row['bandit_camp_enabled']), 'loot_option': row['loot_option']}) return jsonify({'enabled': bool(row['enabled']), 'loot_option': row['loot_option']})
return jsonify({'enabled': False, 'bandit_camp_enabled': False, 'loot_option': 1}) return jsonify({'enabled': False, 'loot_option': 1})
@dashboard.route('/dashboard/farm-settings', methods=['POST']) @dashboard.route('/dashboard/farm-settings', methods=['POST'])
def set_farm_settings(): def set_farm_settings():
@@ -84,18 +100,16 @@ def set_farm_settings():
return jsonify({'error': 'missing player_id'}), 400 return jsonify({'error': 'missing player_id'}), 400
player_id = data['player_id'] player_id = data['player_id']
enabled = 1 if data.get('enabled') else 0 enabled = 1 if data.get('enabled') else 0
bandit_camp_enabled = 1 if data.get('bandit_camp_enabled') else 0
loot_option = int(data.get('loot_option', 1)) loot_option = int(data.get('loot_option', 1))
conn = get_db() conn = get_db()
conn.execute(''' conn.execute('''
INSERT INTO farm_settings (player_id, enabled, bandit_camp_enabled, loot_option, updated_at) INSERT INTO farm_settings (player_id, enabled, loot_option, updated_at)
VALUES (?, ?, ?, ?, ?) VALUES (?, ?, ?, ?)
ON CONFLICT(player_id) DO UPDATE SET ON CONFLICT(player_id) DO UPDATE SET
enabled = excluded.enabled, enabled = excluded.enabled,
bandit_camp_enabled = excluded.bandit_camp_enabled,
loot_option = excluded.loot_option, loot_option = excluded.loot_option,
updated_at = excluded.updated_at updated_at = excluded.updated_at
''', (player_id, enabled, bandit_camp_enabled, loot_option, datetime.utcnow().isoformat())) ''', (player_id, enabled, loot_option, datetime.utcnow().isoformat()))
conn.commit() conn.commit()
conn.close() conn.close()
return jsonify({'ok': True}) return jsonify({'ok': True})
@@ -131,6 +145,24 @@ def get_farm_data():
return jsonify(farms_summary) return jsonify(farms_summary)
# ------------------------------------------------------------------
# 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 # GET /dashboard/towns
# Returns all known towns with their latest state snapshot. # Returns all known towns with their latest state snapshot.
@@ -268,8 +300,8 @@ def create_command():
return jsonify({'error': f'missing field: {field}'}), 400 return jsonify({'error': f'missing field: {field}'}), 400
cmd_type = data['type'] cmd_type = data['type']
if cmd_type not in ('build', 'recruit', 'market_offer', 'farm_loot', 'farm_upgrade'): 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, or farm_upgrade'}), 400 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) # Reject if the Tampermonkey client is offline (no state push in last 150 s)
conn = get_db() conn = get_db()

68
split_script.py Normal file
View File

@@ -0,0 +1,68 @@
import os
import re
input_file = '/media/haunter/e11cc3d4-c894-42cd-8c43-fe2cb25293fd/Vcode_raidbot/grepo-remote/GrepolisRemoteControl.user.js'
out_dir = '/media/haunter/e11cc3d4-c894-42cd-8c43-fe2cb25293fd/Vcode_raidbot/grepo-remote/bot_modules'
if not os.path.exists(out_dir):
os.makedirs(out_dir)
with open(input_file, 'r', encoding='utf-8') as f:
lines = f.readlines()
# Find the start of the IIFE
start_idx = 0
for i, line in enumerate(lines):
if line.strip().startswith('(function'):
start_idx = i + 1
break
# Extract the body of the IIFE (excluding the last '})();')
end_idx = len(lines)
for i in range(len(lines) - 1, -1, -1):
if line.strip().startswith('})();'):
end_idx = i
break
# Remove use strict if present
body_lines = [line for line in lines[start_idx:end_idx] if 'use strict' not in line]
# We will group them manually into files based on logical sections
sections = []
current_section = []
for line in body_lines:
current_section.append(line)
content = "".join(body_lines)
# Split by known markers
blocks = {
'00_config.js': [],
'01_ui.js': [],
'02_state.js': [],
'03_captcha.js': [],
'04_execute.js': [],
'05_main.js': []
}
current_file = '00_config.js'
for line in body_lines:
if '// Toolbar indicator button' in line:
current_file = '01_ui.js'
elif '// Push town state to relay' in line:
current_file = '02_state.js'
elif '// Captcha detection' in line:
current_file = '03_captcha.js'
elif '// Execute: Farm Upgrade' in line:
current_file = '04_execute.js'
elif '// Poll for and execute pending commands' in line:
current_file = '05_main.js'
blocks[current_file].append(line)
for filename, lines in blocks.items():
with open(os.path.join(out_dir, filename), 'w', encoding='utf-8') as f:
f.writelines(lines)
print("Split completed.")

View File

@@ -314,9 +314,9 @@ tr:hover td { background: #1e1e40; }
} }
/* ========================================================================== /* ==========================================================================
Building Picker Modal Building & Academy Picker Modal
========================================================================== */ ========================================================================== */
#building-modal-overlay { #building-modal-overlay, #academy-modal-overlay {
display: none; display: none;
position: fixed; position: fixed;
inset: 0; inset: 0;
@@ -325,9 +325,9 @@ tr:hover td { background: #1e1e40; }
align-items: center; align-items: center;
justify-content: center; justify-content: center;
} }
#building-modal-overlay.open { display: flex; } #building-modal-overlay.open, #academy-modal-overlay.open { display: flex; }
#building-modal { #building-modal, #academy-modal {
background: #16213e; background: #16213e;
border: 2px solid #c8a44a; border: 2px solid #c8a44a;
border-radius: 10px; border-radius: 10px;
@@ -342,7 +342,7 @@ tr:hover td { background: #1e1e40; }
from { transform: scale(0.92); opacity: 0; } from { transform: scale(0.92); opacity: 0; }
to { transform: scale(1); opacity: 1; } to { transform: scale(1); opacity: 1; }
} }
#building-modal-header { #building-modal-header, #academy-modal-header {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
@@ -350,12 +350,12 @@ tr:hover td { background: #1e1e40; }
padding-bottom: 10px; padding-bottom: 10px;
border-bottom: 1px solid #2a4a6a; border-bottom: 1px solid #2a4a6a;
} }
#building-modal-header h3 { #building-modal-header h3, #academy-modal-header h3 {
color: #c8a44a; color: #c8a44a;
font-size: 1rem; font-size: 1rem;
letter-spacing: 0.5px; letter-spacing: 0.5px;
} }
#building-modal-close { #building-modal-close, #academy-modal-close {
background: none; background: none;
border: none; border: none;
color: #888; color: #888;
@@ -364,7 +364,7 @@ tr:hover td { background: #1e1e40; }
line-height: 1; line-height: 1;
padding: 0 4px; padding: 0 4px;
} }
#building-modal-close:hover { color: #fff; } #building-modal-close:hover, #academy-modal-close:hover { color: #fff; }
#building-grid { #building-grid {
display: grid; display: grid;
@@ -436,3 +436,26 @@ tr:hover td { background: #1e1e40; }
@media (max-width: 600px) { @media (max-width: 600px) {
#building-grid { grid-template-columns: repeat(2, 1fr); } #building-grid { grid-template-columns: repeat(2, 1fr); }
} }
/* Academy Grid specific styling */
#academy-grid {
display: flex;
gap: 12px;
overflow-x: auto;
padding-bottom: 12px;
}
.academy-col {
display: flex;
flex-direction: column;
gap: 8px;
min-width: 140px;
}
.academy-col-header {
text-align: center;
font-size: 0.8rem;
color: #c8a44a;
border-bottom: 1px solid #2a4a6a;
padding-bottom: 4px;
margin-bottom: 4px;
}

View File

@@ -77,7 +77,7 @@ window.updateClientStatus = function(online) {
el.className = 'conn-badge offline'; el.className = 'conn-badge offline';
} }
// Enable/disable the Send button // Enable/disable the Send button
const btn = document.querySelector('#command-form-wrap .btn-gold'); const btn = document.getElementById('btn-send');
if (btn) { if (btn) {
btn.disabled = !online; btn.disabled = !online;
btn.title = online ? '' : 'Script is offline — cannot send commands'; btn.title = online ? '' : 'Script is offline — cannot send commands';
@@ -164,6 +164,18 @@ window.sendCommand = async function() {
const visibility = document.getElementById('market-visibility').value; const visibility = document.getElementById('market-visibility').value;
payload = { offer, offer_type, demand, demand_type, max_delivery_time, visibility }; payload = { offer, offer_type, demand, demand_type, max_delivery_time, visibility };
} else if (type === 'research') {
const research_id = window.selectedResearchId;
if (!research_id) return alert('Παρακαλώ επιλέξτε Έρευνα από την Ακαδημία.');
const academyLevel = town.buildings?.academy || 0;
const rData = window.RESEARCH_DATA[research_id];
if (rData && academyLevel < rData.academy_level) {
return alert(`❌ ΑΔΥΝΑΤΗ Η ΕΡΕΥΝΑ: Απαιτείται Ακαδημία Επίπεδο ${rData.academy_level} (Έχετε ${academyLevel})`);
}
payload = { research_id };
} }
try { try {
@@ -248,3 +260,5 @@ window.requestLiveSync = async function() {
}, 3000); }, 3000);
} }
}; };

View File

@@ -8,10 +8,11 @@ window.onCmdTypeChange = function() {
document.getElementById('recruit-options').style.display = type === 'recruit' ? '' : 'none'; document.getElementById('recruit-options').style.display = type === 'recruit' ? '' : 'none';
document.getElementById('amount-group').style.display = type === 'recruit' ? '' : 'none'; document.getElementById('amount-group').style.display = type === 'recruit' ? '' : 'none';
document.getElementById('market-options').style.display = type === 'market_offer' ? '' : 'none'; document.getElementById('market-options').style.display = type === 'market_offer' ? '' : 'none';
document.getElementById('research-options').style.display = type === 'research' ? '' : 'none';
}; };
// Building emoji icons for the visual grid // Building emoji icons for the visual grid
const BUILDING_ICONS = { window.BUILDING_ICONS = {
main: '🏛️', storage: '🏚️', farm: '🌾', academy: '📜', main: '🏛️', storage: '🏚️', farm: '🌾', academy: '📜',
temple: '⛩️', barracks: '⚔️', docks: '⚓', market: '🛒', temple: '⛩️', barracks: '⚔️', docks: '⚓', market: '🛒',
hide: '🕳️', lumber: '🪵', stoner: '🪨', ironer: '⛏️', wall: '🧱' hide: '🕳️', lumber: '🪵', stoner: '🪨', ironer: '⛏️', wall: '🧱'
@@ -34,7 +35,7 @@ window.openBuildingModal = function() {
grid.innerHTML = Object.entries(window.BUILDING_NAMES_GR).map(([key, nameGr]) => { grid.innerHTML = Object.entries(window.BUILDING_NAMES_GR).map(([key, nameGr]) => {
const level = bLevels[key] !== undefined ? bLevels[key] : '?'; const level = bLevels[key] !== undefined ? bLevels[key] : '?';
const data = bData[key]; const data = bData[key];
const icon = BUILDING_ICONS[key] || '🏗️'; const icon = window.BUILDING_ICONS[key] || '🏗️';
const isSelected = key === window.selectedBuildingId; const isSelected = key === window.selectedBuildingId;
if (!data) { if (!data) {
@@ -125,6 +126,120 @@ window.selectBuilding = function(key, nameGr) {
setTimeout(() => document.getElementById('building-modal-overlay').classList.remove('open'), 180); setTimeout(() => document.getElementById('building-modal-overlay').classList.remove('open'), 180);
}; };
// ================================================================
// Academy Research Logic
// ================================================================
window.selectedResearchId = null;
window.RESEARCH_DATA = {
"slinger": { "name": "Εκσφενδονιστές", "academy_level": 1, "wood": 300, "stone": 500, "iron": 200, "points": 4 },
"archer": { "name": "Τοξότες", "academy_level": 1, "wood": 550, "stone": 100, "iron": 400, "points": 8 },
"town_guard": { "name": "Φρουρά πόλης", "academy_level": 1, "wood": 400, "stone": 300, "iron": 300, "points": 3 },
"hoplite": { "name": "Οπλίτες", "academy_level": 4, "wood": 600, "stone": 200, "iron": 850, "points": 8 },
"meteorology": { "name": "Μετεωρολογία", "academy_level": 4, "wood": 2500, "stone": 1700, "iron": 6500, "points": 4 },
"espionage": { "name": "Κατασκοπεία", "academy_level": 7, "wood": 900, "stone": 900, "iron": 1100, "points": 3 },
"booty": { "name": "Αφοσίωση χωρικών", "academy_level": 7, "wood": 1300, "stone": 1300, "iron": 1300, "points": 6 },
"pottery": { "name": "Κεραμικά", "academy_level": 7, "wood": 700, "stone": 1500, "iron": 900, "points": 4 },
"rider": { "name": "Ιππείς", "academy_level": 10, "wood": 1400, "stone": 700, "iron": 1800, "points": 8 },
"architecture": { "name": "Αρχιτεκτονική", "academy_level": 10, "wood": 1900, "stone": 2100, "iron": 1300, "points": 6 },
"instructor": { "name": "Εκπαιδευτής", "academy_level": 10, "wood": 800, "stone": 1300, "iron": 1600, "points": 4 },
"bireme": { "name": "Διήρεις", "academy_level": 13, "wood": 2800, "stone": 1300, "iron": 2200, "points": 8 },
"building_crane": { "name": "Γερανός", "academy_level": 13, "wood": 3000, "stone": 1800, "iron": 1400, "points": 4 },
"shipwright": { "name": "Ναυπηγός", "academy_level": 13, "wood": 5000, "stone": 2000, "iron": 3900, "points": 6 },
"colonize_ship": { "name": "Αποικιακά πλοία", "academy_level": 13, "wood": 7500, "stone": 7500, "iron": 9500, "points": 0 },
"chariot": { "name": "Άρματα", "academy_level": 16, "wood": 3700, "stone": 1900, "iron": 2800, "points": 8 },
"attack_ship": { "name": "Πλοία-φάροι", "academy_level": 16, "wood": 4400, "stone": 2000, "iron": 2400, "points": 8 },
"conscription": { "name": "Στρατολόγηση", "academy_level": 16, "wood": 3800, "stone": 4200, "iron": 6000, "points": 4 },
"demolition_ship": { "name": "Πυρπολικά", "academy_level": 19, "wood": 5300, "stone": 2600, "iron": 2700, "points": 8 },
"catapult": { "name": "Καταπέλτες", "academy_level": 19, "wood": 5500, "stone": 2900, "iron": 3600, "points": 8 },
"cryptography": { "name": "Κρυπτογραφία", "academy_level": 19, "wood": 2500, "stone": 3000, "iron": 5100, "points": 6 },
"small_transporter": { "name": "Γρήγορα μεταφορικά πλοία", "academy_level": 22, "wood": 6500, "stone": 2800, "iron": 3200, "points": 8 },
"plow": { "name": "Άροτρο", "academy_level": 22, "wood": 3000, "stone": 3300, "iron": 2100, "points": 4 },
"berth": { "name": "Κουκέτες", "academy_level": 22, "wood": 8900, "stone": 5200, "iron": 7800, "points": 6 },
"trireme": { "name": "Τριήρεις", "academy_level": 25, "wood": 6500, "stone": 3800, "iron": 4700, "points": 8 },
"phalanx": { "name": "Φάλαγγα", "academy_level": 25, "wood": 4000, "stone": 4000, "iron": 15000, "points": 9 },
"breach": { "name": "Διάσπαση εχθρικού μετώπου", "academy_level": 25, "wood": 8000, "stone": 8000, "iron": 9000, "points": 6 },
"mathematics": { "name": "Μαθηματικά", "academy_level": 25, "wood": 7100, "stone": 4400, "iron": 8600, "points": 6 },
"ram": { "name": "Πολιορκητικός κριός", "academy_level": 28, "wood": 7900, "stone": 9200, "iron": 14000, "points": 10 },
"cartography": { "name": "Χαρτογραφία", "academy_level": 28, "wood": 10000, "stone": 6700, "iron": 12500, "points": 8 },
"take_over": { "name": "Κατάκτηση", "academy_level": 28, "wood": 12000, "stone": 12000, "iron": 16000, "points": 0 },
"stone_storm": { "name": "Καταιγισμός από πέτρες", "academy_level": 31, "wood": 8500, "stone": 5900, "iron": 6600, "points": 4 },
"temple_looting": { "name": "Λεηλασία ναού", "academy_level": 31, "wood": 9200, "stone": 5300, "iron": 10000, "points": 6 },
"divine_selection": { "name": "Θεϊκή επιλογή", "academy_level": 31, "wood": 10000, "stone": 8000, "iron": 12000, "points": 10 },
"combat_experience": { "name": "Εμπειρία μάχης", "academy_level": 34, "wood": 9800, "stone": 11400, "iron": 14200, "points": 6 },
"strong_wine": { "name": "Δυνατό κρασί", "academy_level": 34, "wood": 8000, "stone": 6500, "iron": 11000, "points": 4 },
"set_sail": { "name": "Σαλπάρισμα!", "academy_level": 34, "wood": 13000, "stone": 9700, "iron": 15500, "points": 8 }
};
window.openAcademyModal = function() {
const town = window.getSelectedTown();
if (!town) return;
const grid = document.getElementById('academy-grid');
// Group researches by academy level
const levels = [1, 4, 7, 10, 13, 16, 19, 22, 25, 28, 31, 34];
const townResearches = town.researches || {};
const townBuildings = town.buildings || {};
const townResources = town.resources || { wood: 0, stone: 0, iron: 0 };
const academyLvl = townBuildings.academy || 0;
let html = '';
for (const lvl of levels) {
const researchesInLvl = Object.entries(window.RESEARCH_DATA).filter(([k, v]) => v.academy_level === lvl);
if (researchesInLvl.length === 0) continue;
html += `<div class="academy-col">
<div class="academy-col-header">Επίπεδο ${lvl}</div>`;
for (const [key, data] of researchesInLvl) {
const isResearched = !!townResearches[key];
const isSelected = key === window.selectedResearchId;
const isLocked = academyLvl < lvl;
const noResources = townResources.wood < data.wood || townResources.stone < data.stone || townResources.iron < data.iron;
let statusClass, statusLabel, cardClass = '';
if (isResearched) {
statusClass = 'maxed'; statusLabel = '✓ Ερευνήθηκε'; cardClass = 'bld-maxed';
} else if (isLocked) {
statusClass = 'locked'; statusLabel = '🔒 Κλειδωμένο'; cardClass = 'bld-locked';
} else if (noResources) {
statusClass = 'no-resources'; statusLabel = '❌ Λείπουν Πόροι';
} else {
statusClass = 'can-build'; statusLabel = '✅ Έρευνα';
}
const clickable = !isResearched && !isLocked;
const onclick = clickable ? `onclick="window.selectResearch('${key}', '${data.name}')"` : '';
const costStr = `Ξ:${window.fmt(data.wood)} Π:${window.fmt(data.stone)} Α:${window.fmt(data.iron)}`;
html += `<div class="bld-card ${cardClass}${isSelected ? ' bld-selected' : ''}" ${onclick}>
<span class="bld-name" style="margin-top:6px; font-size:0.8rem;">${data.name}</span>
<span class="bld-status ${statusClass}">${statusLabel}</span>
<span class="bld-cost" style="color:#a88; margin-top:4px;">Πόντοι: ${data.points}</span>
<span class="bld-cost">${costStr}</span>
</div>`;
}
html += `</div>`;
}
grid.innerHTML = html;
document.getElementById('academy-modal-overlay').classList.add('open');
};
window.closeAcademyModal = function(e) {
if (e && e.target !== document.getElementById('academy-modal-overlay') && e.target !== document.getElementById('academy-modal-close')) return;
document.getElementById('academy-modal-overlay').classList.remove('open');
};
window.selectResearch = function(key, name) {
window.selectedResearchId = key;
document.getElementById('selected-research-label').textContent = `🧪 ${name}`;
window.openAcademyModal();
setTimeout(() => document.getElementById('academy-modal-overlay').classList.remove('open'), 180);
};
window.renderUnitDropdown = function() { window.renderUnitDropdown = function() {

View File

@@ -169,6 +169,8 @@ window.renderTownDetails = function() {
document.getElementById('td-market').innerHTML = mCap > 0 document.getElementById('td-market').innerHTML = mCap > 0
? `📦 Εμπορική Χωρητικότητα: <strong>${window.fmt(mCap)}</strong>` ? `📦 Εμπορική Χωρητικότητα: <strong>${window.fmt(mCap)}</strong>`
: ''; : '';
const mCapLabel = document.getElementById('market-capacity-label');
if (mCapLabel) mCapLabel.textContent = `Χωρητικότητα: ${window.fmt(mCap)}`;
const godName = t.god ? t.god.charAt(0).toUpperCase() + t.god.slice(1) : 'Κανένας'; const godName = t.god ? t.god.charAt(0).toUpperCase() + t.god.slice(1) : 'Κανένας';
const seaStr = t.sea != null ? `Θ${t.sea}` : '—'; const seaStr = t.sea != null ? `Θ${t.sea}` : '—';

View File

@@ -96,17 +96,26 @@
<option value="build">Build / Upgrade</option> <option value="build">Build / Upgrade</option>
<option value="recruit">Recruit Troops</option> <option value="recruit">Recruit Troops</option>
<option value="market_offer">Παζάρι - Προσφορά</option> <option value="market_offer">Παζάρι - Προσφορά</option>
<option value="research">Ακαδημία - Έρευνες</option>
</select> </select>
</div> </div>
<!-- Build options - now a button that opens the visual picker --> <!-- Build options - now a button that opens the visual picker -->
<div class="form-group" id="build-options"> <div class="form-group" id="build-options" style="display:none">
<label>Building</label> <label>Building</label>
<button class="btn btn-gold" id="open-building-modal" onclick="window.openBuildingModal()" style="text-align:left; min-width:200px;"> <button class="btn btn-gold" id="open-building-modal" onclick="window.openBuildingModal()" style="text-align:left; min-width:200px;">
<span id="selected-building-label">-- Επιλέξτε Κατασκευή --</span> <span id="selected-building-label">-- Επιλέξτε Κατασκευή --</span>
</button> </button>
</div> </div>
<!-- Research options -->
<div class="form-group" id="research-options" style="display:none">
<label>Έρευνα</label>
<button class="btn btn-gold" id="open-academy-modal" onclick="window.openAcademyModal()" style="text-align:left; min-width:200px;">
<span id="selected-research-label">-- Επιλέξτε Έρευνα --</span>
</button>
</div>
<!-- Recruit options --> <!-- Recruit options -->
<div class="form-group" id="recruit-options" style="display:none"> <div class="form-group" id="recruit-options" style="display:none">
<label>Unit</label> <label>Unit</label>
@@ -142,6 +151,7 @@
</select> </select>
</div> </div>
<!-- Market options -->
<!-- Market options --> <!-- Market options -->
<div class="form-group" id="market-options" style="display:none"> <div class="form-group" id="market-options" style="display:none">
<div style="display:flex; gap:10px; margin-bottom:10px;"> <div style="display:flex; gap:10px; margin-bottom:10px;">
@@ -189,11 +199,12 @@
<div style="flex:1;"> <div style="flex:1;">
<label>Ορατότητα</label> <label>Ορατότητα</label>
<select id="market-visibility"> <select id="market-visibility">
<option value="allies">Συμμαχία Μόνο</option>
<option value="all">Όλοι</option> <option value="all">Όλοι</option>
<option value="alliance">Συμμαχία</option>
</select> </select>
</div> </div>
</div> </div>
</div> </div>
<div class="form-group" id="amount-group" style="display:none"> <div class="form-group" id="amount-group" style="display:none">
@@ -201,7 +212,7 @@
<input type="number" id="recruit-amount" value="1" min="1" max="9999" style="width:80px;"> <input type="number" id="recruit-amount" value="1" min="1" max="9999" style="width:80px;">
</div> </div>
<button class="btn btn-gold" onclick="sendCommand()">Send ⚡</button> <button id="btn-send" class="btn btn-gold" onclick="window.sendCommand()">Send ⚡</button>
</div> </div>
<div id="build-queue-preview"></div> <div id="build-queue-preview"></div>
@@ -218,15 +229,6 @@
</div> </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>
<script src="/static/js/components/commandLog.js"></script>
<script src="/static/js/api.js"></script>
<script src="/static/js/app.js"></script>
<!-- ====== Building Picker Modal ====== --> <!-- ====== Building Picker Modal ====== -->
<div id="building-modal-overlay" onclick="window.closeBuildingModal(event)"> <div id="building-modal-overlay" onclick="window.closeBuildingModal(event)">
<div id="building-modal"> <div id="building-modal">
@@ -241,5 +243,25 @@
</div> </div>
</div> </div>
</div> </div>
<!-- ====== Academy Picker Modal ====== -->
<div id="academy-modal-overlay" onclick="window.closeAcademyModal(event)">
<div id="academy-modal">
<div id="academy-modal-header">
<h3>🦉 Ακαδημία</h3>
<button id="academy-modal-close" onclick="window.closeAcademyModal()"></button>
</div>
<div id="academy-grid"></div>
</div>
</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>
<script src="/static/js/components/commandLog.js"></script>
<script src="/static/js/api.js"></script>
<script src="/static/js/app.js"></script>
</body> </body>
</html> </html>

View File

@@ -242,6 +242,15 @@
<!-- Status banner --> <!-- Status banner -->
<div class="status-bar" id="status-bar"></div> <div class="status-bar" id="status-bar"></div>
<!-- Warehouse-full notice (hidden by default) -->
<div id="warehouse-full-banner" style="display:none; background: linear-gradient(90deg, #5a1a00, #8b2500); border: 1px solid #ff6600; border-radius: 8px; padding: 12px 18px; margin-bottom: 1rem; display: flex; align-items: center; gap: 12px; font-weight: 600;">
<span style="font-size: 1.4rem;">📦</span>
<span>
<strong style="color:#ff9933;">Αποθήκη Γεμάτη!</strong>
Όλες οι αποθήκες είναι &gt;95% — το bot παρακάμπτει τη λεηλασία μέχρι να αδειάσει χώρος.
</span>
</div>
<!-- Control Panel --> <!-- Control Panel -->
<div class="panel"> <div class="panel">
<h2>⚙️ Ρυθμίσεις</h2> <h2>⚙️ Ρυθμίσεις</h2>
@@ -255,15 +264,6 @@
<span style="color:#888; font-size:0.85rem;" id="toggle-hint">Ανενεργό</span> <span style="color:#888; font-size:0.85rem;" id="toggle-hint">Ανενεργό</span>
</div> </div>
<div class="toggle-row">
<span class="toggle-label">Στρατόπεδο Ληστών (Auto)</span>
<label class="toggle">
<input type="checkbox" id="bandit-camp-enabled">
<span class="slider"></span>
</label>
<span style="color:#888; font-size:0.85rem;" id="bandit-toggle-hint">Ανενεργό</span>
</div>
<div style="margin-bottom: 0.75rem; font-size: 0.85rem; color: #888;">Επίπεδο Λεηλασίας:</div> <div style="margin-bottom: 0.75rem; font-size: 0.85rem; color: #888;">Επίπεδο Λεηλασίας:</div>
<div class="option-grid"> <div class="option-grid">
<button class="option-btn selected" data-option="1"> <button class="option-btn selected" data-option="1">
@@ -284,8 +284,11 @@
</button> </button>
</div> </div>
<div style="display: flex; gap: 1rem; align-items: center;">
<button class="save-btn" id="save-btn" onclick="saveSettings()">💾 Αποθήκευση</button> <button class="save-btn" id="save-btn" onclick="saveSettings()">💾 Αποθήκευση</button>
<span class="save-status" id="save-status">✓ Αποθηκεύτηκε</span> <button class="save-btn" id="manual-loot-btn" onclick="triggerManualLoot()" style="background: linear-gradient(135deg, #2a5a7a, #4a9ccc);">🌾 Λεηλασία Τώρα</button>
<span class="save-status" id="save-status">✓ Στάλθηκε</span>
</div>
</div> </div>
<!-- Farm Upgrade Panel --> <!-- Farm Upgrade Panel -->
@@ -346,36 +349,27 @@
// -- Toggle hint text -- // -- Toggle hint text --
document.getElementById('farm-enabled').addEventListener('change', function () { document.getElementById('farm-enabled').addEventListener('change', function () {
document.getElementById('toggle-hint').textContent = this.checked ? '🟢 Ενεργό' : 'Ανενεργό'; document.getElementById('toggle-hint').textContent = this.checked ? '🟢 Ενεργό' : 'Ανενεργό';
updateStatusBar(this.checked, document.getElementById('bandit-camp-enabled').checked); updateStatusBar(this.checked);
}); });
document.getElementById('bandit-camp-enabled').addEventListener('change', function () { function updateStatusBar(enabled) {
document.getElementById('bandit-toggle-hint').textContent = this.checked ? '🟢 Ενεργό' : 'Ανενεργό';
updateStatusBar(document.getElementById('farm-enabled').checked, this.checked);
});
function updateStatusBar(farmEnabled, banditEnabled) {
const bar = document.getElementById('status-bar'); const bar = document.getElementById('status-bar');
if (farmEnabled || banditEnabled) { if (enabled) {
bar.className = 'status-bar visible'; bar.className = 'status-bar visible';
let msg = []; bar.textContent = '🤖 Ο αυτόματος farmer είναι ενεργός. Το script θα λεηλατεί χωριά με τυχαίες καθυστερήσεις.';
if (farmEnabled) msg.push('Ο αυτόματος farmer είναι ενεργός.');
if (banditEnabled) msg.push('Το στρατόπεδο ληστών είναι ενεργό.');
bar.textContent = '🤖 ' + msg.join(' ') + ' Το script θα εκτελεί δράσεις με τυχαίες καθυστερήσεις.';
} else { } else {
bar.className = 'status-bar visible off'; bar.className = 'status-bar visible off';
bar.textContent = '⏸ Οι αυτόματες ενέργειες είναι ανενεργές.'; bar.textContent = '⏸ Η αυτόματη λεηλασία είναι ανενεργή.';
} }
} }
// -- Save settings -- // -- Save settings --
function saveSettings() { function saveSettings() {
const enabled = document.getElementById('farm-enabled').checked; const enabled = document.getElementById('farm-enabled').checked;
const bandit_camp_enabled = document.getElementById('bandit-camp-enabled').checked;
fetch('/dashboard/farm-settings', { fetch('/dashboard/farm-settings', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ player_id: PLAYER_ID, enabled, bandit_camp_enabled, loot_option: selectedOption }) body: JSON.stringify({ player_id: PLAYER_ID, enabled, loot_option: selectedOption })
}) })
.then(r => r.json()) .then(r => r.json())
.then(() => { .then(() => {
@@ -391,13 +385,8 @@
.then(r => r.json()) .then(r => r.json())
.then(cfg => { .then(cfg => {
document.getElementById('farm-enabled').checked = cfg.enabled; document.getElementById('farm-enabled').checked = cfg.enabled;
document.getElementById('bandit-camp-enabled').checked = cfg.bandit_camp_enabled || false;
document.getElementById('toggle-hint').textContent = cfg.enabled ? '🟢 Ενεργό' : 'Ανενεργό'; document.getElementById('toggle-hint').textContent = cfg.enabled ? '🟢 Ενεργό' : 'Ανενεργό';
document.getElementById('bandit-toggle-hint').textContent = cfg.bandit_camp_enabled ? '🟢 Ενεργό' : 'Ανενεργό'; if (cfg.enabled) updateStatusBar(true);
updateStatusBar(cfg.enabled, cfg.bandit_camp_enabled);
selectedOption = cfg.loot_option || 1; selectedOption = cfg.loot_option || 1;
document.querySelectorAll('.option-btn').forEach(b => { document.querySelectorAll('.option-btn').forEach(b => {
b.classList.toggle('selected', parseInt(b.dataset.option) === selectedOption); b.classList.toggle('selected', parseInt(b.dataset.option) === selectedOption);
@@ -498,12 +487,51 @@
}); });
} }
// -- Trigger Manual Loot --
function triggerManualLoot() {
const btn = document.getElementById('manual-loot-btn');
const originalText = btn.innerText;
btn.innerText = '⏳ Αποστολή...';
btn.disabled = true;
fetch('/dashboard/commands', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
player_id: PLAYER_ID,
town_id: 0,
type: 'farm_loot',
payload: { loot_option: selectedOption }
})
})
.then(r => r.json())
.then(() => {
btn.innerText = '✓ Εστάλη!';
setTimeout(() => {
btn.innerText = originalText;
btn.disabled = false;
}, 2000);
});
}
// -- Warehouse full notice --
async function checkWarehouseStatus() {
try {
const res = await fetch(`/api/farm_status?player_id=${PLAYER_ID}`);
const data = await res.json();
const banner = document.getElementById('warehouse-full-banner');
if (banner) banner.style.display = data.warehouse_full ? 'flex' : 'none';
} catch(e) {}
}
// -- Boot -- // -- Boot --
loadSettings(); loadSettings();
loadFarmData(); loadFarmData();
checkOnline(); checkOnline();
checkWarehouseStatus();
setInterval(loadFarmData, 15000); setInterval(loadFarmData, 15000);
setInterval(checkOnline, 20000); setInterval(checkOnline, 20000);
setInterval(checkWarehouseStatus, 20000);
</script> </script>
</body> </body>
</html> </html>

View File

@@ -5,26 +5,64 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Grepolis Remote Dashboard - Select Player</title> <title>Grepolis Remote Dashboard - Select Player</title>
<link rel="stylesheet" href="/static/css/styles.css"> <link rel="stylesheet" href="/static/css/styles.css">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<style> <style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
body { body {
font-family: 'Inter', 'Segoe UI', sans-serif;
background-color: #0d1117;
min-height: 100vh;
color: #e6edf3;
}
/* --- Top nav --- */
.topbar {
background: #161b22;
border-bottom: 1px solid #30363d;
padding: 14px 32px;
display: flex;
align-items: center;
justify-content: space-between;
}
.topbar-logo { font-size: 1.1rem; font-weight: 700; color: #c8a44a; }
.topbar-nav { display: flex; gap: 16px; align-items: center; }
.topbar-nav a {
color: #8b949e; font-size: 0.875rem; text-decoration: none;
padding: 6px 12px; border-radius: 6px; transition: background 0.2s, color 0.2s;
}
.topbar-nav a:hover { background: #21262d; color: #e6edf3; }
.topbar-nav .user-badge { color: #c8a44a; font-size: 0.875rem; font-weight: 600; }
.btn-logout {
background: rgba(248,81,73,0.1); color: #f85149;
border: 1px solid rgba(248,81,73,0.3); padding: 6px 14px; border-radius: 6px;
font-size: 0.875rem; font-family: inherit; text-decoration: none;
transition: background 0.2s;
}
.btn-logout:hover { background: rgba(248,81,73,0.2) !important; }
/* --- Main content --- */
.main-content {
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: flex-start;
min-height: 100vh; padding: 60px 20px;
background-color: #1a1a24; min-height: calc(100vh - 57px);
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
} }
.landing-container { .landing-container {
background: #2a2a36; background: #161b22;
border: 1px solid #30363d;
padding: 2rem; padding: 2rem;
border-radius: 8px; border-radius: 10px;
box-shadow: 0 4px 6px rgba(0,0,0,0.3); box-shadow: 0 8px 32px rgba(0,0,0,0.4);
text-align: center; text-align: center;
max-width: 500px; max-width: 520px;
width: 100%; width: 100%;
} }
.landing-container h1 { color: #c8a44a; margin-bottom: 5px; font-size: 1.5rem; }
.landing-container > p { color: #8b949e; margin-bottom: 20px; font-size: 0.9rem; }
.player-card { .player-card {
background: #3a3a46; background: #21262d;
margin: 10px 0; margin: 10px 0;
padding: 15px; padding: 15px;
border-radius: 6px; border-radius: 6px;
@@ -33,60 +71,78 @@
color: white; color: white;
display: block; display: block;
transition: background 0.2s, transform 0.1s; transition: background 0.2s, transform 0.1s;
font-size: 1.1rem; font-size: 1rem;
border: 1px solid #4a4a56; border: 1px solid #30363d;
} }
.player-card:hover { .player-card:hover { background: #30363d; transform: translateY(-2px); border-color: #c8a44a; }
background: #5a5a66; .player-card span { color: #8b949e; font-size: 0.8rem; margin-left: 8px; }
transform: translateY(-2px);
border-color: #c8a44a; .no-clan-box {
background: rgba(200,164,74,0.1);
border: 1px solid rgba(200,164,74,0.3);
border-radius: 8px;
padding: 20px;
margin-top: 10px;
} }
.player-card span { .no-clan-box p { color: #c8a44a; margin-bottom: 12px; }
color: #888; .btn-create {
font-size: 0.8rem; display: inline-block;
margin-left: 10px; background: #c8a44a; color: #0d1117;
} padding: 9px 20px; border-radius: 6px;
h1 { font-weight: 700; font-size: 0.875rem; text-decoration: none;
color: #c8a44a; transition: background 0.2s;
margin-bottom: 5px;
}
p {
color: #aaa;
margin-bottom: 20px;
} }
.btn-create:hover { background: #e0b85a; }
</style> </style>
</head> </head>
<body> <body>
<div class="topbar">
<span class="topbar-logo">⚔️ Grepolis Remote</span>
<div class="topbar-nav">
{% if current_user.is_authenticated %}
<span class="user-badge">{{ current_user.username }}</span>
<a href="/auth/options">Ρυθμίσεις</a>
<a href="/auth/logout" class="btn-logout">Αποσύνδεση</a>
{% endif %}
</div>
</div>
<div class="main-content">
<div class="landing-container"> <div class="landing-container">
<h1>⚔️ Grepolis Remote</h1> <h1>⚔️ Grepolis Remote</h1>
<p>Select an active account to manage</p> <p>Select an active account to manage</p>
{% if not players %} {% if no_clan %}
<p style="color: #ffaa55;">No players found! Install the Tampermonkey script and log into the game first.</p> <div class="no-clan-box">
<p>Δεν έχετε δημιουργήσει clan ακόμη. Πηγαίνετε στις Ρυθμίσεις για να ξεκινήσετε.</p>
<a href="/auth/options" class="btn-create">🏰 Δημιουργία Clan →</a>
</div>
{% elif not players %}
<p style="color:#ffaa55;">Κανένας παίκτης δεν έχει συνδεθεί ακόμη. Βεβαιωθείτε ότι το Loader script τρέχει με το σωστό Clan Key.</p>
{% endif %} {% endif %}
{% for p in players %} {% for p in players %}
<a href="/player/{{ p.player_id }}" class="player-card"> <a href="/player/{{ p.player_id }}" class="player-card">
<div style="display: flex; justify-content: space-between; align-items: center;"> <div style="display: flex; justify-content: space-between; align-items: center;">
<div> <div>
<strong>{{ p.player }}</strong> <span style="color: #6fcfcf;">[{{ p.world_id }}]</span> <span>(ID: {{ p.player_id }})</span> <strong>{{ p.player }}</strong>
<span style="color:#6fcfcf;">[{{ p.world_id }}]</span>
<span>(ID: {{ p.player_id }})</span>
</div> </div>
<div style="display: flex; gap: 8px;"> <div style="display: flex; gap: 8px;">
{% if p.captcha_active %} {% if p.captcha_active %}
<span style="display: flex; align-items: center; gap: 6px; background: rgba(255, 100, 100, 0.2); padding: 5px 12px; border-radius: 20px; font-size: 0.8rem; color: #ff6464; font-weight: bold; border: 1px solid rgba(255, 100, 100, 0.5); box-shadow: 0 0 8px rgba(255, 100, 100, 0.4);"> <span style="display:flex;align-items:center;gap:6px;background:rgba(255,100,100,0.2);padding:5px 12px;border-radius:20px;font-size:0.8rem;color:#ff6464;font-weight:bold;border:1px solid rgba(255,100,100,0.5);">
⚠️ Captcha ⚠️ Captcha
</span> </span>
{% endif %} {% endif %}
{% if p.is_online %} {% if p.is_online %}
<span style="display: flex; align-items: center; gap: 6px; background: rgba(50, 150, 50, 0.2); padding: 5px 12px; border-radius: 20px; font-size: 0.8rem; color: #7bcc7b; font-weight: bold; border: 1px solid rgba(123, 204, 123, 0.3);"> <span style="display:flex;align-items:center;gap:6px;background:rgba(50,150,50,0.2);padding:5px 12px;border-radius:20px;font-size:0.8rem;color:#7bcc7b;font-weight:bold;border:1px solid rgba(123,204,123,0.3);">
<span style="display: inline-block; width: 8px; height: 8px; background: #7bcc7b; border-radius: 50%; box-shadow: 0 0 6px #7bcc7b;"></span> <span style="display:inline-block;width:8px;height:8px;background:#7bcc7b;border-radius:50%;box-shadow:0 0 6px #7bcc7b;"></span>Online
Online
</span> </span>
{% else %} {% else %}
<span style="display: flex; align-items: center; gap: 6px; background: rgba(150, 50, 50, 0.2); padding: 5px 12px; border-radius: 20px; font-size: 0.8rem; color: #cc7b7b; font-weight: bold; border: 1px solid rgba(204, 123, 123, 0.3);"> <span style="display:flex;align-items:center;gap:6px;background:rgba(150,50,50,0.2);padding:5px 12px;border-radius:20px;font-size:0.8rem;color:#cc7b7b;font-weight:bold;border:1px solid rgba(204,123,123,0.3);">
<span style="display: inline-block; width: 8px; height: 8px; background: #cc7b7b; border-radius: 50%;"></span> <span style="display:inline-block;width:8px;height:8px;background:#cc7b7b;border-radius:50%;"></span>Offline
Offline
</span> </span>
{% endif %} {% endif %}
</div> </div>
@@ -94,5 +150,7 @@
</a> </a>
{% endfor %} {% endfor %}
</div> </div>
</div>
</body> </body>
</html> </html>

115
templates/login.html Normal file
View File

@@ -0,0 +1,115 @@
<!DOCTYPE html>
<html lang="el">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Grepolis Remote — Σύνδεση</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: 'Inter', sans-serif;
background: #0d1117;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
color: #e6edf3;
}
.card {
background: #161b22;
border: 1px solid #30363d;
border-radius: 12px;
padding: 40px 36px;
width: 100%;
max-width: 400px;
box-shadow: 0 8px 32px rgba(0,0,0,0.5);
}
.logo {
text-align: center;
margin-bottom: 28px;
}
.logo h1 {
font-size: 1.6rem;
font-weight: 700;
color: #c8a44a;
}
.logo p { color: #8b949e; font-size: 0.9rem; margin-top: 6px; }
.form-group { margin-bottom: 18px; }
label { display: block; font-size: 0.85rem; font-weight: 500; color: #8b949e; margin-bottom: 6px; }
input[type="text"], input[type="password"] {
width: 100%;
padding: 10px 14px;
background: #0d1117;
border: 1px solid #30363d;
border-radius: 6px;
color: #e6edf3;
font-size: 0.95rem;
font-family: inherit;
transition: border-color 0.2s;
}
input:focus { outline: none; border-color: #c8a44a; }
.btn {
width: 100%;
padding: 11px;
background: #c8a44a;
color: #0d1117;
border: none;
border-radius: 6px;
font-size: 0.95rem;
font-weight: 700;
font-family: inherit;
cursor: pointer;
transition: background 0.2s, transform 0.1s;
margin-top: 4px;
}
.btn:hover { background: #e0b85a; transform: translateY(-1px); }
.error {
background: rgba(248,81,73,0.12);
border: 1px solid rgba(248,81,73,0.4);
border-radius: 6px;
padding: 10px 14px;
color: #f85149;
font-size: 0.875rem;
margin-bottom: 18px;
}
.footer-link {
text-align: center;
margin-top: 22px;
font-size: 0.85rem;
color: #8b949e;
}
.footer-link a { color: #c8a44a; text-decoration: none; }
.footer-link a:hover { text-decoration: underline; }
</style>
</head>
<body>
<div class="card">
<div class="logo">
<h1>⚔️ Grepolis Remote</h1>
<p>Συνδεθείτε στον λογαριασμό σας</p>
</div>
{% if error %}
<div class="error">{{ error }}</div>
{% endif %}
<form method="POST" action="/auth/login">
<div class="form-group">
<label for="username">Όνομα Χρήστη</label>
<input type="text" id="username" name="username" autocomplete="username" required>
</div>
<div class="form-group">
<label for="password">Κωδικός</label>
<input type="password" id="password" name="password" autocomplete="current-password" required>
</div>
<button type="submit" class="btn">Σύνδεση →</button>
</form>
<div class="footer-link">
Δεν έχετε λογαριασμό; <a href="/auth/register">Εγγραφή</a>
</div>
</div>
</body>
</html>

363
templates/options.html Normal file
View File

@@ -0,0 +1,363 @@
<!DOCTYPE html>
<html lang="el">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Grepolis Remote — Ρυθμίσεις Clan</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: 'Inter', sans-serif; background: #0d1117; color: #e6edf3; min-height: 100vh; }
/* --- Top nav --- */
.topbar {
background: #161b22;
border-bottom: 1px solid #30363d;
padding: 14px 32px;
display: flex;
align-items: center;
justify-content: space-between;
}
.topbar-logo { font-size: 1.1rem; font-weight: 700; color: #c8a44a; text-decoration: none; }
.topbar-nav { display: flex; gap: 16px; align-items: center; }
.topbar-nav a {
color: #8b949e; font-size: 0.875rem; text-decoration: none;
padding: 6px 12px; border-radius: 6px; transition: background 0.2s, color 0.2s;
}
.topbar-nav a:hover { background: #21262d; color: #e6edf3; }
.topbar-nav a.active { color: #c8a44a; }
.topbar-nav .btn-logout {
background: rgba(248,81,73,0.1); color: #f85149;
border: 1px solid rgba(248,81,73,0.3); padding: 6px 14px; border-radius: 6px;
cursor: pointer; font-size: 0.875rem; font-family: inherit; text-decoration: none;
transition: background 0.2s;
}
.topbar-nav .btn-logout:hover { background: rgba(248,81,73,0.2); }
/* --- Page layout --- */
.page { max-width: 760px; margin: 40px auto; padding: 0 20px; }
.page-title { font-size: 1.5rem; font-weight: 700; margin-bottom: 6px; }
.page-subtitle { color: #8b949e; font-size: 0.9rem; margin-bottom: 32px; }
/* --- Cards --- */
.card {
background: #161b22;
border: 1px solid #30363d;
border-radius: 10px;
padding: 24px 28px;
margin-bottom: 24px;
}
.card-title {
font-size: 1rem; font-weight: 600;
margin-bottom: 18px; padding-bottom: 12px;
border-bottom: 1px solid #30363d;
display: flex; align-items: center; gap: 8px;
}
/* --- Key display --- */
.key-box {
background: #0d1117;
border: 1px solid #30363d;
border-radius: 6px;
padding: 14px 18px;
display: flex; align-items: center; justify-content: space-between;
gap: 12px; margin-bottom: 14px;
}
.key-value {
font-family: 'Courier New', monospace;
font-size: 1.4rem;
font-weight: 700;
color: #c8a44a;
letter-spacing: 3px;
}
.btn-copy {
background: #21262d; color: #e6edf3;
border: 1px solid #30363d; border-radius: 6px;
padding: 6px 14px; font-size: 0.8rem; cursor: pointer;
font-family: inherit; transition: background 0.2s; white-space: nowrap;
}
.btn-copy:hover { background: #30363d; }
/* --- Create clan form --- */
.inline-form { display: flex; gap: 10px; }
.inline-form input[type="text"] {
flex: 1; padding: 9px 14px;
background: #0d1117; border: 1px solid #30363d; border-radius: 6px;
color: #e6edf3; font-size: 0.9rem; font-family: inherit;
}
.inline-form input:focus { outline: none; border-color: #c8a44a; }
/* --- Buttons --- */
.btn-primary {
background: #c8a44a; color: #0d1117;
border: none; border-radius: 6px; padding: 9px 18px;
font-weight: 700; font-size: 0.875rem; font-family: inherit;
cursor: pointer; transition: background 0.2s, transform 0.1s; white-space: nowrap;
}
.btn-primary:hover { background: #e0b85a; transform: translateY(-1px); }
.btn-danger {
background: rgba(248,81,73,0.1); color: #f85149;
border: 1px solid rgba(248,81,73,0.3); border-radius: 6px;
padding: 5px 12px; font-size: 0.8rem; font-family: inherit;
cursor: pointer; transition: background 0.2s;
}
.btn-danger:hover { background: rgba(248,81,73,0.2); }
.btn-warning {
background: rgba(210,153,34,0.1); color: #d99512;
border: 1px solid rgba(210,153,34,0.3); border-radius: 6px;
padding: 7px 14px; font-size: 0.8rem; font-family: inherit;
cursor: pointer; transition: background 0.2s; margin-top: 6px;
}
.btn-warning:hover { background: rgba(210,153,34,0.2); }
/* --- Members table --- */
.members-table { width: 100%; border-collapse: collapse; }
.members-table th {
text-align: left; font-size: 0.75rem; color: #8b949e;
text-transform: uppercase; letter-spacing: 0.5px;
padding: 0 0 10px 0; border-bottom: 1px solid #30363d;
}
.members-table td {
padding: 12px 0; border-bottom: 1px solid #21262d;
font-size: 0.875rem; vertical-align: middle;
}
.members-table tr:last-child td { border-bottom: none; }
.player-name { font-weight: 600; }
.player-id { color: #8b949e; font-size: 0.78rem; font-family: monospace; }
.status-online { color: #3fb950; font-size: 0.78rem; font-weight: 600; }
.status-offline { color: #8b949e; font-size: 0.78rem; }
.empty-state {
text-align: center; padding: 32px 0;
color: #8b949e; font-size: 0.9rem;
}
.toggle-group { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; }
.toggle-label {
display: flex; align-items: center; gap: 6px;
font-size: 0.8rem; color: #8b949e; cursor: pointer;
background: #0d1117; border: 1px solid #30363d;
padding: 4px 10px; border-radius: 20px;
transition: border-color 0.2s, color 0.2s;
}
.toggle-label:has(input:checked) { border-color: #3fb950; color: #3fb950; }
.toggle-label input[type=checkbox] { display: none; }
.btn-apply {
background: #21262d; color: #8b949e;
border: 1px solid #30363d; border-radius: 6px;
padding: 4px 10px; font-size: 0.78rem; font-family: inherit;
cursor: pointer; transition: background 0.2s, color 0.2s;
}
.btn-apply:hover { background: #30363d; color: #e6edf3; }
.warn-box {
background: rgba(210,153,34,0.1);
border: 1px solid rgba(210,153,34,0.3);
border-radius: 6px; padding: 12px 16px;
color: #d99512; font-size: 0.85rem; margin-top: 10px;
}
</style>
</head>
<body>
<div class="topbar">
<a class="topbar-logo" href="/">⚔️ Grepolis Remote</a>
<div class="topbar-nav">
<a href="/">Clients</a>
<a href="/auth/options" class="active">Ρυθμίσεις</a>
<a href="/auth/logout" class="btn-logout">Αποσύνδεση</a>
</div>
</div>
<div class="page">
<div class="page-title">Ρυθμίσεις Clan</div>
<div class="page-subtitle">Διαχείριση της ομάδας σας και του κλειδιού πρόσβασης</div>
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="warn-box" style="margin-bottom: 20px; color: {% if category == 'error' %}#f85149{% else %}#3fb950{% endif %}; border-color: {% if category == 'error' %}rgba(248,81,73,0.3){% else %}rgba(63,185,80,0.3){% endif %}; background: {% if category == 'error' %}rgba(248,81,73,0.1){% else %}rgba(63,185,80,0.1){% endif %};">
{{ message }}
</div>
{% endfor %}
{% endif %}
{% endwith %}
{% if clan %}
{% if clan.owner_id == current_user.id %}
<!-- ===================== Clan Key Section ===================== -->
<div class="card">
<div class="card-title">🔑 Clan Key</div>
<p style="color:#8b949e; font-size:0.875rem; margin-bottom:14px;">
Μοιραστείτε αυτό το κλειδί με τους παίκτες σας. Πρέπει να το προσθέσουν στο Loader script τους για να συνδεθούν στην ομάδα σας.
</p>
<div class="key-box">
<span class="key-value" id="clanKeyDisplay">{{ clan.clan_key }}</span>
<button class="btn-copy" onclick="copyKey()">📋 Αντιγραφή</button>
</div>
<div class="warn-box">
⚠️ Εάν αναγεννήσετε το κλειδί, οι παίκτες σας θα πρέπει να ενημερώσουν το script τους με το νέο κλειδί.
</div>
<form method="POST" action="/auth/clan/regenerate-key"
onsubmit="return confirm('Σίγουρα; Οι παίκτες σου θα πρέπει να ανανεώσουν το κλειδί τους.');">
<button type="submit" class="btn-warning">🔄 Αναγέννηση Κλειδιού</button>
</form>
</div>
<!-- ===================== Website Admins Section ===================== -->
<div class="card">
<div class="card-title">👨‍💻 Website Admins</div>
<p style="color:#8b949e; font-size:0.875rem; margin-bottom:14px;">
Προσθέστε το username άλλων παικτών που έχουν ήδη εγγραφεί στο site για να τους δώσετε πρόσβαση στο dashboard σας.
</p>
<form method="POST" action="/auth/clan/add-admin" class="inline-form" style="margin-bottom: 20px;">
<input type="text" name="admin_username" placeholder="π.χ. player123" required>
<button type="submit" class="btn-primary">Προσθήκη Admin</button>
</form>
{% if admins %}
<table class="members-table">
<thead>
<tr>
<th>Admin Username</th>
<th>Ημερομηνία Προσθήκης</th>
<th></th>
</tr>
</thead>
<tbody>
{% for a in admins %}
<tr>
<td><div class="player-name">{{ a.username }}</div></td>
<td style="color:#8b949e; font-size:0.8rem;">{{ a.created_at[:10] }}</td>
<td style="text-align:right;">
<form method="POST" action="/auth/clan/remove-admin/{{ a.id }}" onsubmit="return confirm('Αφαίρεση του admin {{ a.username }}?');">
<button type="submit" class="btn-danger">Αφαίρεση</button>
</form>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<div class="empty-state" style="padding: 16px 0;">
Δεν έχετε προσθέσει κανέναν website admin ακόμη.
</div>
{% endif %}
</div>
{% endif %}
<!-- ===================== Members Section ===================== -->
<div class="card">
<div class="card-title">👥 Μέλη Clan — {{ clan.name }}</div>
{% if members %}
<table class="members-table">
<thead>
<tr>
<th>Παίκτης</th>
<th>Κατάσταση</th>
<th>Δυνατότητες</th>
<th>Προστέθηκε</th>
<th></th>
</tr>
</thead>
<tbody>
{% for m in members %}
<tr>
<td>
<div class="player-name">{{ m.player_name }}</div>
<div class="player-id">ID: {{ m.player_id }}</div>
</td>
<td>
{% if m.is_online %}
<span class="status-online">● Online</span>
{% else %}
<span class="status-offline">● Offline</span>
{% endif %}
</td>
<td>
{% if clan.owner_id == current_user.id %}
<form method="POST" action="/auth/clan/update-features/{{ m.player_id }}" style="display:inline;">
<div class="toggle-group">
<label class="toggle-label">
<input type="checkbox" name="farm" onchange="this.form.submit()" {{ 'checked' if m.feat_farm }}> 🌾 Farm
</label>
<label class="toggle-label">
<input type="checkbox" name="admin" onchange="this.form.submit()" {{ 'checked' if m.feat_admin }}> 🏛 Admin
</label>
</div>
</form>
{% else %}
<div class="toggle-group" style="opacity: 0.8;">
<span class="toggle-label" style="cursor:default; border-color: {{ '#3fb950' if m.feat_farm else '#30363d' }}; color: {{ '#3fb950' if m.feat_farm else '#8b949e' }};">🌾 Farm</span>
<span class="toggle-label" style="cursor:default; border-color: {{ '#3fb950' if m.feat_admin else '#30363d' }}; color: {{ '#3fb950' if m.feat_admin else '#8b949e' }};">🏛 Admin</span>
</div>
{% endif %}
</td>
<td style="color:#8b949e; font-size:0.8rem;">{{ m.joined_at }}</td>
<td style="text-align:right;">
{% if clan.owner_id == current_user.id %}
<form method="POST" action="/auth/clan/remove-member/{{ m.player_id }}"
onsubmit="return confirm('Αφαίρεση παίκτη {{ m.player_name }}?');">
<button type="submit" class="btn-danger">Αφαίρεση</button>
</form>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<div class="empty-state">
Δεν υπάρχουν μέλη ακόμη.<br>
<span style="font-size:0.8rem; margin-top:6px; display:block;">
Μοιραστείτε το Clan Key με τους παίκτες σας για να συνδεθούν.
</span>
</div>
{% endif %}
</div>
{% if clan.owner_id != current_user.id %}
<!-- ===================== Leave Clan Section ===================== -->
<div class="card" style="border-color: rgba(248,81,73,0.3);">
<div class="card-title" style="color: #f85149; border-bottom-color: rgba(248,81,73,0.3);">🚪 Αποχώρηση από Clan</div>
<p style="color:#8b949e; font-size:0.875rem; margin-bottom:18px;">
Εάν αποχωρήσετε, δεν θα έχετε πλέον πρόσβαση στους παίκτες αυτής της ομάδας.
</p>
<form method="POST" action="/auth/clan/leave" onsubmit="return confirm('Είστε βέβαιοι ότι θέλετε να αποχωρήσετε από το Clan;');">
<button type="submit" class="btn-danger">Αποχώρηση</button>
</form>
</div>
{% endif %}
{% else %}
<!-- ===================== Create Clan Section ===================== -->
<div class="card">
<div class="card-title">🏰 Δημιουργία Clan</div>
<p style="color:#8b949e; font-size:0.875rem; margin-bottom:18px;">
Δεν έχετε δημιουργήσει clan ακόμη. Δώστε ένα όνομα για να ξεκινήσετε.
</p>
<form method="POST" action="/auth/clan/create" class="inline-form">
<input type="text" name="clan_name" placeholder="π.χ. Alpha Squad" required>
<button type="submit" class="btn-primary">Δημιουργία →</button>
</form>
</div>
{% endif %}
</div>
<script>
function copyKey() {
const key = document.getElementById('clanKeyDisplay').textContent;
navigator.clipboard.writeText(key).then(() => {
const btn = document.querySelector('.btn-copy');
btn.textContent = '✅ Αντιγράφηκε!';
setTimeout(() => btn.textContent = '📋 Αντιγραφή', 2000);
});
}
</script>
</body>
</html>

112
templates/register.html Normal file
View File

@@ -0,0 +1,112 @@
<!DOCTYPE html>
<html lang="el">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Grepolis Remote — Εγγραφή</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: 'Inter', sans-serif;
background: #0d1117;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
color: #e6edf3;
}
.card {
background: #161b22;
border: 1px solid #30363d;
border-radius: 12px;
padding: 40px 36px;
width: 100%;
max-width: 400px;
box-shadow: 0 8px 32px rgba(0,0,0,0.5);
}
.logo { text-align: center; margin-bottom: 28px; }
.logo h1 { font-size: 1.6rem; font-weight: 700; color: #c8a44a; }
.logo p { color: #8b949e; font-size: 0.9rem; margin-top: 6px; }
.form-group { margin-bottom: 18px; }
label { display: block; font-size: 0.85rem; font-weight: 500; color: #8b949e; margin-bottom: 6px; }
input[type="text"], input[type="password"] {
width: 100%;
padding: 10px 14px;
background: #0d1117;
border: 1px solid #30363d;
border-radius: 6px;
color: #e6edf3;
font-size: 0.95rem;
font-family: inherit;
transition: border-color 0.2s;
}
input:focus { outline: none; border-color: #c8a44a; }
.btn {
width: 100%;
padding: 11px;
background: #c8a44a;
color: #0d1117;
border: none;
border-radius: 6px;
font-size: 0.95rem;
font-weight: 700;
font-family: inherit;
cursor: pointer;
transition: background 0.2s, transform 0.1s;
margin-top: 4px;
}
.btn:hover { background: #e0b85a; transform: translateY(-1px); }
.error {
background: rgba(248,81,73,0.12);
border: 1px solid rgba(248,81,73,0.4);
border-radius: 6px;
padding: 10px 14px;
color: #f85149;
font-size: 0.875rem;
margin-bottom: 18px;
}
.footer-link {
text-align: center;
margin-top: 22px;
font-size: 0.85rem;
color: #8b949e;
}
.footer-link a { color: #c8a44a; text-decoration: none; }
.footer-link a:hover { text-decoration: underline; }
</style>
</head>
<body>
<div class="card">
<div class="logo">
<h1>⚔️ Grepolis Remote</h1>
<p>Δημιουργία νέου λογαριασμού</p>
</div>
{% if error %}
<div class="error">{{ error }}</div>
{% endif %}
<form method="POST" action="/auth/register">
<div class="form-group">
<label for="username">Όνομα Χρήστη</label>
<input type="text" id="username" name="username" autocomplete="username" required>
</div>
<div class="form-group">
<label for="password">Κωδικός</label>
<input type="password" id="password" name="password" autocomplete="new-password" required>
</div>
<div class="form-group">
<label for="confirm">Επαλήθευση Κωδικού</label>
<input type="password" id="confirm" name="confirm" autocomplete="new-password" required>
</div>
<button type="submit" class="btn">Εγγραφή →</button>
</form>
<div class="footer-link">
Έχετε ήδη λογαριασμό; <a href="/auth/login">Σύνδεση</a>
</div>
</div>
</body>
</html>