import sqlite3 import os import secrets DB_PATH = os.path.join(os.path.dirname(__file__), 'grepo.db') def get_db(): conn = sqlite3.connect(DB_PATH) conn.row_factory = sqlite3.Row return conn def init_db(): conn = get_db() c = conn.cursor() # Commands queue — sent from dashboard, consumed by Tampermonkey c.execute(''' CREATE TABLE IF NOT EXISTS commands ( id INTEGER PRIMARY KEY AUTOINCREMENT, town_id TEXT NOT NULL, town_name TEXT, type TEXT NOT NULL, -- 'build' | 'recruit' payload TEXT NOT NULL, -- JSON string status TEXT NOT NULL DEFAULT 'pending', -- pending | executing | done | failed result_msg TEXT, position INTEGER, -- manual sort order for build queue (lower = first) created_at TEXT NOT NULL DEFAULT (datetime('now')), updated_at TEXT NOT NULL DEFAULT (datetime('now')) ) ''') # Town state — pushed by Tampermonkey every poll cycle c.execute(''' CREATE TABLE IF NOT EXISTS town_state ( town_id TEXT PRIMARY KEY, town_name TEXT, player TEXT, player_id TEXT, alliance_id TEXT, world_id TEXT, x REAL, y REAL, sea INTEGER, data TEXT NOT NULL, -- full JSON snapshot updated_at TEXT NOT NULL DEFAULT (datetime('now')) ) ''') # Key-value store — generic flags (e.g. captcha_active) c.execute(''' CREATE TABLE IF NOT EXISTS kv_store ( key TEXT PRIMARY KEY, value TEXT, updated_at TEXT NOT NULL DEFAULT (datetime('now')) ) ''') # Farm settings — per-player auto-farm configuration c.execute(''' CREATE TABLE IF NOT EXISTS farm_settings ( player_id TEXT PRIMARY KEY, enabled INTEGER NOT NULL DEFAULT 0, bandit_camp_enabled INTEGER NOT NULL DEFAULT 0, loot_option INTEGER NOT NULL DEFAULT 1, -- 1=5min, 2=20min, 3=90min, 4=4h updated_at TEXT NOT NULL DEFAULT (datetime('now')) ) ''') # Bot settings — per-player config for bootcamp & rural-trade auto-loops c.execute(''' CREATE TABLE IF NOT EXISTS bot_settings ( player_id TEXT PRIMARY KEY, bootcamp_enabled INTEGER NOT NULL DEFAULT 0, bootcamp_use_def INTEGER NOT NULL DEFAULT 0, rural_trade_enabled INTEGER NOT NULL DEFAULT 0, rural_trade_ratio INTEGER NOT NULL DEFAULT 3, -- 1=0.25 2=0.5 3=0.75 4=1.0 5=1.25 updated_at TEXT NOT NULL DEFAULT (datetime('now')) ) ''') # Bot logs — ring buffer of last 50 entries per player per feature c.execute(''' CREATE TABLE IF NOT EXISTS bot_logs ( id INTEGER PRIMARY KEY AUTOINCREMENT, player_id TEXT NOT NULL, world_id TEXT NOT NULL DEFAULT '', feature TEXT NOT NULL, -- 'bootcamp' | 'rural_trade' message TEXT NOT NULL, created_at TEXT NOT NULL DEFAULT (datetime('now')) ) ''') c.execute('CREATE INDEX IF NOT EXISTS idx_bot_logs_player_feature ON bot_logs(player_id, feature)') # Celebrations — active celebration state per town, pushed by the bot # One row per town_id + celebration_type. Upserted on every state push. # finished_at = 0 means no active celebration of that type. c.execute(''' CREATE TABLE IF NOT EXISTS celebrations ( id INTEGER PRIMARY KEY AUTOINCREMENT, player_id TEXT NOT NULL, world_id TEXT NOT NULL, town_id TEXT NOT NULL, town_name TEXT, celebration_type TEXT NOT NULL, -- 'party' | 'triumph' finished_at INTEGER NOT NULL DEFAULT 0, -- unix timestamp (0 = not running) updated_at TEXT NOT NULL DEFAULT (datetime('now')), UNIQUE(player_id, world_id, town_id, celebration_type) ) ''') c.execute('CREATE INDEX IF NOT EXISTS idx_celebrations_player_world ON celebrations(player_id, world_id)') # Culture log — immutable audit log of every Αγορά command fired from the dashboard # Records what was fired, what it cost, and whether the bot confirmed success. c.execute(''' CREATE TABLE IF NOT EXISTS culture_log ( id INTEGER PRIMARY KEY AUTOINCREMENT, player_id TEXT NOT NULL, world_id TEXT NOT NULL, town_id TEXT NOT NULL, town_name TEXT, celebration_type TEXT NOT NULL, -- 'party' | 'triumph' cost_wood INTEGER NOT NULL DEFAULT 0, cost_stone INTEGER NOT NULL DEFAULT 0, cost_iron INTEGER NOT NULL DEFAULT 0, cost_battle_pts INTEGER NOT NULL DEFAULT 0, status TEXT NOT NULL DEFAULT 'pending', -- pending | success | failed source TEXT NOT NULL DEFAULT 'manual', -- 'manual' | 'auto' result_msg TEXT, fired_at TEXT NOT NULL DEFAULT (datetime('now')), confirmed_at TEXT ) ''') c.execute('CREATE INDEX IF NOT EXISTS idx_culture_log_player_world ON culture_log(player_id, world_id)') # Culture queue — dedicated queue for celebration commands (separate from commands table). # Max 1 pending/executing per player+world+celebration_type enforced at app level. # source: 'manual' (dashboard button) | 'auto' (server-side auto-fire from state push) c.execute(''' CREATE TABLE IF NOT EXISTS culture_queue ( id INTEGER PRIMARY KEY AUTOINCREMENT, player_id TEXT NOT NULL, world_id TEXT NOT NULL, town_id TEXT NOT NULL, town_name TEXT NOT NULL DEFAULT '', celebration_type TEXT NOT NULL, -- 'party' | 'triumph' status TEXT NOT NULL DEFAULT 'pending', -- pending | executing | done | failed source TEXT NOT NULL DEFAULT 'manual', -- 'manual' | 'auto' result_msg TEXT, created_at TEXT NOT NULL DEFAULT (datetime('now')), executed_at TEXT ) ''') c.execute('CREATE INDEX IF NOT EXISTS idx_culture_queue_player_world ON culture_queue(player_id, world_id, status)') # Culture settings — per-town auto-mode toggle (party / triumph). # One row per player+world+town. Updated from Αγορά dashboard toggles. c.execute(''' CREATE TABLE IF NOT EXISTS culture_settings ( id INTEGER PRIMARY KEY AUTOINCREMENT, player_id TEXT NOT NULL, world_id TEXT NOT NULL, town_id TEXT NOT NULL, auto_party INTEGER NOT NULL DEFAULT 0, auto_triumph INTEGER NOT NULL DEFAULT 0, updated_at TEXT NOT NULL DEFAULT (datetime('now')), UNIQUE(player_id, world_id, town_id) ) ''') c.execute('CREATE INDEX IF NOT EXISTS idx_culture_settings_player_world ON culture_settings(player_id, world_id)') # Blueprints - assigns a blueprint to a specific town c.execute(''' CREATE TABLE IF NOT EXISTS town_blueprints ( town_id TEXT PRIMARY KEY, blueprint_name TEXT NOT NULL, is_active INTEGER NOT NULL DEFAULT 1, updated_at TEXT NOT NULL DEFAULT (datetime('now')) ) ''') # Troop movements — pushed by Tampermonkey from game events # Fully isolated per player_id + world_id. c.execute(''' CREATE TABLE IF NOT EXISTS movements ( id INTEGER PRIMARY KEY AUTOINCREMENT, player_id TEXT NOT NULL, world_id TEXT NOT NULL, command_id TEXT NOT NULL, cmd_type TEXT NOT NULL, origin_town TEXT, origin_player TEXT, target_town TEXT, target_player TEXT, arrival_at INTEGER, raw_data TEXT, updated_at TEXT NOT NULL DEFAULT (datetime('now')), UNIQUE(player_id, world_id, command_id) ) ''') c.execute('CREATE INDEX IF NOT EXISTS idx_movements_player_world ON movements(player_id, world_id)') # Attack Plans — coordinated timed strikes across multiple players/towns c.execute(''' CREATE TABLE IF NOT EXISTS attack_plans ( id INTEGER PRIMARY KEY AUTOINCREMENT, world_id TEXT NOT NULL, plan_name TEXT NOT NULL, created_by_player_id TEXT NOT NULL, target_town_id TEXT, target_town_name TEXT, target_x REAL, target_y REAL, target_arrival_time INTEGER NOT NULL, -- unix epoch (UTC) status TEXT NOT NULL DEFAULT 'draft', -- draft | active | completed | cancelled created_at TEXT NOT NULL DEFAULT (datetime('now')), updated_at TEXT NOT NULL DEFAULT (datetime('now')) ) ''') c.execute('CREATE INDEX IF NOT EXISTS idx_attack_plans_world ON attack_plans(world_id, status)') # Attack Plan Participants — one row per attacking town per plan c.execute(''' CREATE TABLE IF NOT EXISTS attack_plan_participants ( id INTEGER PRIMARY KEY AUTOINCREMENT, plan_id INTEGER NOT NULL REFERENCES attack_plans(id) ON DELETE CASCADE, player_id TEXT NOT NULL, world_id TEXT NOT NULL, origin_town_id TEXT NOT NULL, origin_town_name TEXT, units TEXT NOT NULL DEFAULT '{}', -- JSON attack_type TEXT, -- 'attack_land' | 'attack_sea' transport_needed INTEGER NOT NULL DEFAULT 0, transport_count INTEGER NOT NULL DEFAULT 0, travel_time_secs INTEGER, send_time INTEGER, -- unix epoch (UTC), calculated return_time INTEGER, -- unix epoch (UTC), calculated is_feasible INTEGER NOT NULL DEFAULT 1, error_msg TEXT, status TEXT NOT NULL DEFAULT 'pending', -- pending | armed | sent | missed | cancelled updated_at TEXT NOT NULL DEFAULT (datetime('now')), UNIQUE(plan_id, origin_town_id) ) ''') c.execute('CREATE INDEX IF NOT EXISTS idx_app_plan ON attack_plan_participants(plan_id)') c.execute('CREATE INDEX IF NOT EXISTS idx_app_player ON attack_plan_participants(player_id, world_id)') # Migration: add new columns if upgrading an existing database for _col in [ 'ALTER TABLE town_state ADD COLUMN player_id TEXT', 'ALTER TABLE town_state ADD COLUMN alliance_id TEXT', 'ALTER TABLE town_state ADD COLUMN x REAL', 'ALTER TABLE town_state ADD COLUMN y REAL', 'ALTER TABLE town_state ADD COLUMN sea INTEGER', 'ALTER TABLE commands ADD COLUMN player_id TEXT', 'ALTER TABLE commands ADD COLUMN position INTEGER', '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 clan_members ADD COLUMN world_id TEXT', 'ALTER TABLE users ADD COLUMN clan_id INTEGER REFERENCES clans(id)', # bot_logs gained world_id for per-world isolation "ALTER TABLE bot_logs ADD COLUMN world_id TEXT NOT NULL DEFAULT ''", ]: try: c.execute(_col) except Exception: pass # column already exists # Back-fill position for existing rows that have NULL position try: c.execute('UPDATE commands SET position = id WHERE position IS NULL') except Exception: pass # 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. # UNIQUE on (clan_id, player_id, world_id) so the same player # appearing in multiple worlds creates separate rows. 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, world_id TEXT NOT NULL DEFAULT '', features TEXT NOT NULL DEFAULT 'farm,admin', joined_at TEXT NOT NULL DEFAULT (datetime('now')), UNIQUE(clan_id, player_id, world_id) ) ''') # Migration: if clan_members still has the old UNIQUE(clan_id, player_id) constraint # (without world_id), recreate the table with the correct 3-column constraint. try: tbl_sql = c.execute( "SELECT sql FROM sqlite_master WHERE type='table' AND name='clan_members'" ).fetchone() if tbl_sql and 'player_id, world_id' not in (tbl_sql['sql'] or ''): c.execute(''' CREATE TABLE IF NOT EXISTS _clan_members_new ( id INTEGER PRIMARY KEY AUTOINCREMENT, clan_id INTEGER NOT NULL REFERENCES clans(id), player_id TEXT NOT NULL, player_name TEXT, world_id TEXT NOT NULL DEFAULT '', features TEXT NOT NULL DEFAULT 'farm,admin', joined_at TEXT NOT NULL DEFAULT (datetime('now')), UNIQUE(clan_id, player_id, world_id) ) ''') c.execute(''' INSERT OR IGNORE INTO _clan_members_new (id, clan_id, player_id, player_name, world_id, features, joined_at) SELECT id, clan_id, player_id, player_name, COALESCE(world_id, ''), features, joined_at FROM clan_members ''') c.execute('DROP TABLE clan_members') c.execute('ALTER TABLE _clan_members_new RENAME TO clan_members') except Exception as _e: print(f'clan_members migration skipped: {_e}') # 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.close() def generate_clan_key(): """Generate a short, unique, human-readable clan key.""" return secrets.token_urlsafe(8).upper()[:10]