Astraea v2: auth+profiles+About-Me, documents, TTS, Twilio/SMTP missions, DV safety, conversation memory, FLUX landing
This commit is contained in:
205
store.py
Normal file
205
store.py
Normal file
@@ -0,0 +1,205 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Astraea data layer — no-KYC auth, profiles (avatar + About Me), and settings.
|
||||
SQLite at /opt/astraea/astraea.db. stdlib-only.
|
||||
"""
|
||||
import hashlib
|
||||
import json
|
||||
import secrets
|
||||
import sqlite3
|
||||
|
||||
DB_PATH = "/opt/astraea/astraea.db"
|
||||
|
||||
|
||||
def _conn():
|
||||
c = sqlite3.connect(DB_PATH)
|
||||
c.row_factory = sqlite3.Row
|
||||
return c
|
||||
|
||||
|
||||
def init_db():
|
||||
c = _conn()
|
||||
c.executescript("""
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
username TEXT UNIQUE NOT NULL,
|
||||
password_hash TEXT NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS sessions (
|
||||
token TEXT PRIMARY KEY,
|
||||
user_id INTEGER NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS profiles (
|
||||
user_id INTEGER PRIMARY KEY,
|
||||
display_name TEXT DEFAULT '',
|
||||
avatar INTEGER DEFAULT 0,
|
||||
about_me TEXT DEFAULT '',
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS settings (
|
||||
user_id INTEGER PRIMARY KEY,
|
||||
twilio_sid TEXT DEFAULT '',
|
||||
twilio_auth_token TEXT DEFAULT '',
|
||||
twilio_from TEXT DEFAULT '',
|
||||
smtp_host TEXT DEFAULT '',
|
||||
smtp_port INTEGER DEFAULT 587,
|
||||
smtp_user TEXT DEFAULT '',
|
||||
smtp_pass TEXT DEFAULT '',
|
||||
email_from TEXT DEFAULT '',
|
||||
no_contact_order INTEGER DEFAULT 0,
|
||||
partner_name TEXT DEFAULT '',
|
||||
domestic_violence INTEGER DEFAULT 0,
|
||||
protection_order INTEGER DEFAULT 0,
|
||||
safety_notes TEXT DEFAULT '',
|
||||
tts_voice TEXT DEFAULT 'aria'
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS messages (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL,
|
||||
agent_id TEXT NOT NULL,
|
||||
role TEXT NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
citations TEXT DEFAULT '',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
""")
|
||||
c.commit()
|
||||
# migrations for pre-existing DBs
|
||||
for col, ddl in [("domestic_violence", "INTEGER DEFAULT 0"),
|
||||
("protection_order", "INTEGER DEFAULT 0"),
|
||||
("safety_notes", "TEXT DEFAULT ''")]:
|
||||
try:
|
||||
c.execute(f"ALTER TABLE settings ADD COLUMN {col} {ddl}")
|
||||
c.commit()
|
||||
except Exception:
|
||||
pass
|
||||
c.close()
|
||||
|
||||
|
||||
def hash_password(pw):
|
||||
return hashlib.sha256(pw.encode()).hexdigest()
|
||||
|
||||
|
||||
def user_from_token(token):
|
||||
if not token:
|
||||
return None
|
||||
c = _conn()
|
||||
row = c.execute("SELECT user_id FROM sessions WHERE token=?", (token,)).fetchone()
|
||||
c.close()
|
||||
return row["user_id"] if row else None
|
||||
|
||||
|
||||
def create_user(username, password):
|
||||
c = _conn()
|
||||
c.execute("INSERT INTO users (username, password_hash) VALUES (?,?)",
|
||||
(username, hash_password(password)))
|
||||
c.commit()
|
||||
uid = c.execute("SELECT id FROM users WHERE username=?", (username,)).fetchone()["id"]
|
||||
c.execute("INSERT OR IGNORE INTO profiles (user_id) VALUES (?)", (uid,))
|
||||
c.execute("INSERT OR IGNORE INTO settings (user_id) VALUES (?)", (uid,))
|
||||
c.commit()
|
||||
c.close()
|
||||
return uid
|
||||
|
||||
|
||||
def issue_token(uid):
|
||||
token = secrets.token_hex(32)
|
||||
c = _conn()
|
||||
c.execute("INSERT INTO sessions (token, user_id) VALUES (?,?)", (token, uid))
|
||||
c.commit()
|
||||
c.close()
|
||||
return token
|
||||
|
||||
|
||||
def authenticate(username, password):
|
||||
c = _conn()
|
||||
row = c.execute("SELECT id, password_hash FROM users WHERE username=?",
|
||||
(username,)).fetchone()
|
||||
c.close()
|
||||
if row and row["password_hash"] == hash_password(password):
|
||||
return row["id"]
|
||||
return None
|
||||
|
||||
|
||||
def get_profile(uid):
|
||||
c = _conn()
|
||||
row = c.execute("SELECT * FROM profiles WHERE user_id=?", (uid,)).fetchone()
|
||||
c.close()
|
||||
if not row:
|
||||
return {"display_name": "", "avatar": 0, "about_me": ""}
|
||||
return {"display_name": row["display_name"], "avatar": row["avatar"],
|
||||
"about_me": row["about_me"]}
|
||||
|
||||
|
||||
def set_profile(uid, display_name=None, avatar=None, about_me=None):
|
||||
cur = get_profile(uid)
|
||||
dn = cur["display_name"] if display_name is None else display_name
|
||||
av = cur["avatar"] if avatar is None else avatar
|
||||
am = cur["about_me"] if about_me is None else about_me
|
||||
c = _conn()
|
||||
c.execute("""INSERT INTO profiles (user_id, display_name, avatar, about_me, updated_at)
|
||||
VALUES (?,?,?,?, CURRENT_TIMESTAMP)
|
||||
ON CONFLICT(user_id) DO UPDATE SET
|
||||
display_name=excluded.display_name, avatar=excluded.avatar,
|
||||
about_me=excluded.about_me, updated_at=CURRENT_TIMESTAMP""",
|
||||
(uid, dn, av, am))
|
||||
c.commit()
|
||||
c.close()
|
||||
return {"display_name": dn, "avatar": av, "about_me": am}
|
||||
|
||||
|
||||
def get_settings(uid):
|
||||
c = _conn()
|
||||
row = c.execute("SELECT * FROM settings WHERE user_id=?", (uid,)).fetchone()
|
||||
c.close()
|
||||
if not row:
|
||||
return {}
|
||||
# strip auth secrets from the read-back for safety, but return everything to owner
|
||||
return dict(row)
|
||||
|
||||
|
||||
def save_message(uid, agent_id, role, content, citations=None):
|
||||
c = _conn()
|
||||
c.execute("INSERT INTO messages (user_id, agent_id, role, content, citations) VALUES (?,?,?,?,?)",
|
||||
(uid, agent_id, role, content, json.dumps(citations or [])))
|
||||
c.commit()
|
||||
c.close()
|
||||
|
||||
|
||||
def get_conversation(uid, agent_id, limit=60):
|
||||
c = _conn()
|
||||
rows = c.execute(
|
||||
"SELECT role, content, citations FROM messages WHERE user_id=? AND agent_id=? ORDER BY id DESC LIMIT ?",
|
||||
(uid, agent_id, limit)).fetchall()
|
||||
c.close()
|
||||
out = []
|
||||
for r in reversed(rows):
|
||||
try:
|
||||
cit = json.loads(r["citations"]) if r["citations"] else []
|
||||
except Exception:
|
||||
cit = []
|
||||
out.append({"role": r["role"], "content": r["content"], "citations": cit})
|
||||
return out
|
||||
|
||||
|
||||
def clear_conversation(uid, agent_id):
|
||||
c = _conn()
|
||||
c.execute("DELETE FROM messages WHERE user_id=? AND agent_id=?", (uid, agent_id))
|
||||
c.commit()
|
||||
c.close()
|
||||
|
||||
|
||||
def set_settings(uid, **fields):
|
||||
allowed = {"twilio_sid", "twilio_auth_token", "twilio_from", "smtp_host", "smtp_port",
|
||||
"smtp_user", "smtp_pass", "email_from", "no_contact_order", "partner_name",
|
||||
"domestic_violence", "protection_order", "safety_notes", "tts_voice"}
|
||||
updates = {k: v for k, v in fields.items() if k in allowed}
|
||||
if not updates:
|
||||
return get_settings(uid)
|
||||
cols = ", ".join(f"{k}=?" for k in updates)
|
||||
c = _conn()
|
||||
c.execute(f"UPDATE settings SET {cols} WHERE user_id=?", (*updates.values(), uid))
|
||||
c.commit()
|
||||
c.close()
|
||||
return get_settings(uid)
|
||||
Reference in New Issue
Block a user