File size: 5,649 Bytes
9a69412
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
# app/store_sqlite.py
import json
import os
import sqlite3
import threading
import time

WINDOW = 6          # เก็บกี่เทิร์นล่าสุด
TURN_TTL = 7200     # 2 ชม.
SESS_TTL = 86400    # 24 ชม.


def _pick_db_path() -> str:
    """หาโฟลเดอร์ที่เขียนได้จริง — Space ฟรีไม่มี /data"""
    cands = []
    if os.environ.get("DB_PATH"):
        cands.append(os.environ["DB_PATH"])
    cands += ["/data/npc.db", "/home/user/app/data/npc.db", "/tmp/npc.db"]
    for c in cands:
        try:
            d = os.path.dirname(c) or "."
            os.makedirs(d, exist_ok=True)
            probe = os.path.join(d, ".wtest")
            with open(probe, "w") as f:
                f.write("1")
            os.remove(probe)
            return c
        except Exception:
            continue
    return ":memory:"


DB_PATH = _pick_db_path()
print(f"[store] db path = {DB_PATH}", flush=True)

_conn = sqlite3.connect(DB_PATH, check_same_thread=False, timeout=5.0)
_conn.execute("PRAGMA journal_mode=WAL")      # อ่าน/เขียนพร้อมกันได้
_conn.execute("PRAGMA synchronous=NORMAL")    # เร็วขึ้น ~3x
_conn.execute("PRAGMA busy_timeout=5000")
_lock = threading.Lock()

_conn.executescript("""
CREATE TABLE IF NOT EXISTS turns (
  sid TEXT, seq INTEGER, u TEXT, a TEXT, beat TEXT, ts INTEGER,
  PRIMARY KEY (sid, seq));
CREATE TABLE IF NOT EXISTS sessions (
  sid TEXT PRIMARY KEY, pid TEXT, npc TEXT,
  summary TEXT DEFAULT '', n INTEGER DEFAULT 0, ts INTEGER);
CREATE TABLE IF NOT EXISTS players (
  pid TEXT PRIMARY KEY, name TEXT, facts TEXT DEFAULT '{}',
  rel TEXT DEFAULT '{}', quests TEXT DEFAULT '{}',
  v INTEGER DEFAULT 0, last_seen INTEGER);
CREATE TABLE IF NOT EXISTS rl (
  pid TEXT, minute INTEGER, n INTEGER, PRIMARY KEY (pid, minute));
CREATE INDEX IF NOT EXISTS ix_turns ON turns(sid, seq DESC);
""")
_conn.commit()


# ─────────── window ───────────
def push_turn(sid, pid, npc, u, a):
    now = int(time.time())
    with _lock, _conn:
        _conn.execute(
            "INSERT INTO sessions(sid,pid,npc,n,ts) VALUES(?,?,?,1,?) "
            "ON CONFLICT(sid) DO UPDATE SET n=n+1, ts=excluded.ts",
            (sid, pid, npc, now))
        n = _conn.execute("SELECT n FROM sessions WHERE sid=?", (sid,)).fetchone()[0]
        _conn.execute("INSERT OR REPLACE INTO turns VALUES(?,?,?,?,?,?)",
                      (sid, n, u, a, None, now))
        _conn.execute("DELETE FROM turns WHERE sid=? AND seq<=?", (sid, n - WINDOW))
    return n


def get_window(sid):
    rows = _conn.execute(
        "SELECT u,a,beat FROM turns WHERE sid=? ORDER BY seq ASC", (sid,)).fetchall()
    return [{"u": r[0], "a": r[1], "beat": r[2]} for r in rows]


def get_turn_count(sid):
    r = _conn.execute("SELECT n FROM sessions WHERE sid=?", (sid,)).fetchone()
    return r[0] if r else 0


def reset_session(sid):
    with _lock, _conn:
        _conn.execute("DELETE FROM turns WHERE sid=?", (sid,))
        _conn.execute("DELETE FROM sessions WHERE sid=?", (sid,))


# ─────────── summary ───────────
def get_summary(sid):
    r = _conn.execute("SELECT summary FROM sessions WHERE sid=?", (sid,)).fetchone()
    return (r[0] if r else "") or ""


def set_summary(sid, text):
    with _lock, _conn:
        _conn.execute("UPDATE sessions SET summary=? WHERE sid=?", (text, sid))


# ─────────── profile ───────────
def get_profile(pid):
    r = _conn.execute(
        "SELECT name,facts,rel,quests,v FROM players WHERE pid=?", (pid,)).fetchone()
    if not r:
        return {"pid": pid, "name": None, "facts": {}, "rel": {}, "quests": {}, "v": 0}
    return {"pid": pid, "name": r[0], "facts": json.loads(r[1]),
            "rel": json.loads(r[2]), "quests": json.loads(r[3]), "v": r[4]}


def save_profile(p):
    with _lock, _conn:
        _conn.execute(
            "INSERT INTO players(pid,name,facts,rel,quests,v,last_seen) "
            "VALUES(?,?,?,?,?,?,?) ON CONFLICT(pid) DO UPDATE SET "
            "name=excluded.name, facts=excluded.facts, rel=excluded.rel, "
            "quests=excluded.quests, v=players.v+1, last_seen=excluded.last_seen",
            (p["pid"], p.get("name"), json.dumps(p.get("facts", {})),
             json.dumps(p.get("rel", {})), json.dumps(p.get("quests", {})),
             p.get("v", 0) + 1, int(time.time())))


# ─────────── rate limit ───────────
def rate_limit(pid, limit=12):
    m = int(time.time() // 60)
    with _lock, _conn:
        _conn.execute("INSERT INTO rl(pid,minute,n) VALUES(?,?,1) "
                      "ON CONFLICT(pid,minute) DO UPDATE SET n=n+1", (pid, m))
        n = _conn.execute("SELECT n FROM rl WHERE pid=? AND minute=?",
                          (pid, m)).fetchone()[0]
    return n <= limit


# ─────────── janitor (แทน TTL ของ Redis) ───────────
def janitor():
    now = int(time.time())
    with _lock, _conn:
        _conn.execute("DELETE FROM rl WHERE minute < ?", (now // 60 - 2,))
        _conn.execute("DELETE FROM turns WHERE ts < ?", (now - TURN_TTL,))
        _conn.execute("DELETE FROM sessions WHERE ts < ?", (now - SESS_TTL,))


def stats():
    q = lambda s: _conn.execute(s).fetchone()[0]
    return {"db": DB_PATH, "turns": q("SELECT COUNT(*) FROM turns"),
            "sessions": q("SELECT COUNT(*) FROM sessions"),
            "players": q("SELECT COUNT(*) FROM players")}