Files
grepo-remote/db.py
2026-05-03 14:19:34 +03:00

285 lines
12 KiB
Python

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,
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)')
# 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)',
]:
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]