Files
horror/backend/app/users.py
2026-09-30 15:15:00 +02:00

275 lines
9.2 KiB
Python

"""Die Runde: wer mitmacht, und wer gerade wer ist.
Der Host pflegt die Namensliste. Alle anderen wählen sich einen davon aus.
Wer nicht will, dass ein anderer in seinem Namen bewertet, hinterlegt in
der Verwaltung einen Anmeldefilm. Dann muss man ihn beim Wechsel erst in
der Suche finden und anklicken. Kein Passwort im Klartext, nichts zu
merken außer einem Film — und für einen Filmabend unter Freunden genau die
richtige Hürde. Wer keinen Film hinterlegt, ist weiter offen für alle.
Die Zuordnung hängt an derselben Cookie-Sitzung wie Vorschläge und
Wertungen. Damit steht der Name serverseitig fest und kommt nicht mehr aus
einem Textfeld, in das jeder schreiben kann, was er will.
"""
import sqlite3
from datetime import datetime, timezone
from . import db
from .db import connect
SCHEMA = """
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE,
created_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS user_sessions (
session_id TEXT PRIMARY KEY,
user_id INTEGER NOT NULL,
seit TEXT NOT NULL
);
-- Wer den Hostmodus eingeschaltet hat. Der Host führt durch den Abend und
-- wechselt die Namen frei, ohne jedes Mal den Anmeldefilm zu suchen.
CREATE TABLE IF NOT EXISTS host_sessions (
session_id TEXT PRIMARY KEY,
seit TEXT NOT NULL
);
"""
def init():
with connect() as conn:
conn.executescript(SCHEMA)
# Der Anmeldefilm kam später dazu
spalten = {r[1] for r in conn.execute("PRAGMA table_info(users)")}
if "auth_movie_id" not in spalten:
conn.execute("ALTER TABLE users ADD COLUMN auth_movie_id INTEGER")
def _oeffentlich(row):
"""Der hinterlegte Film verlässt den Server nie — sonst stünde das
Passwort im Netzwerk-Tab. Nach draußen geht nur, ob einer gesetzt ist."""
benutzer = dict(row)
benutzer["geschuetzt"] = benutzer.pop("auth_movie_id", None) is not None
return benutzer
def alle():
with connect() as conn:
rows = conn.execute("SELECT * FROM users ORDER BY name COLLATE NOCASE").fetchall()
return [_oeffentlich(r) for r in rows]
def anlegen(name):
"""Gibt None zurück, wenn der Name schon vergeben ist."""
name = (name or "").strip()[:40]
if not name:
return None
try:
with connect() as conn:
conn.execute(
"INSERT INTO users (name, created_at) VALUES (?, ?)",
(name, datetime.now(timezone.utc).isoformat(timespec="seconds")),
)
except sqlite3.IntegrityError:
return None
return von_name(name)
def von_name(name):
with connect() as conn:
row = conn.execute("SELECT * FROM users WHERE name = ?", (name,)).fetchone()
return _oeffentlich(row) if row else None
def schutz_setzen(user_id, movie_id):
"""Anmeldefilm hinterlegen oder mit None wieder freigeben."""
with connect() as conn:
return conn.execute(
"UPDATE users SET auth_movie_id = ? WHERE id = ?", (movie_id, user_id)
).rowcount
def schutz_von(user_id):
"""Die hinterlegte Film-Kennung, oder None bei offenem Namen."""
with connect() as conn:
row = conn.execute(
"SELECT auth_movie_id FROM users WHERE id = ?", (user_id,)
).fetchone()
return row["auth_movie_id"] if row else None
def entfernen(user_id):
with connect() as conn:
conn.execute("DELETE FROM user_sessions WHERE user_id = ?", (user_id,))
return conn.execute("DELETE FROM users WHERE id = ?", (user_id,)).rowcount
def waehlen(session_id, user_id):
"""Gibt None zurück, wenn es den Namen nicht gibt."""
with connect() as conn:
da = conn.execute("SELECT * FROM users WHERE id = ?", (user_id,)).fetchone()
if not da:
return None
conn.execute(
"INSERT INTO user_sessions (session_id, user_id, seit) VALUES (?, ?, ?) "
"ON CONFLICT (session_id) DO UPDATE SET "
"user_id = excluded.user_id, seit = excluded.seit",
(
session_id,
user_id,
datetime.now(timezone.utc).isoformat(timespec="seconds"),
),
)
return _oeffentlich(da)
def abwaehlen(session_id):
with connect() as conn:
return conn.execute(
"DELETE FROM user_sessions WHERE session_id = ?", (session_id,)
).rowcount
def aktuell(session_id):
"""Der Benutzer dieser Sitzung, oder None."""
with connect() as conn:
row = conn.execute(
"SELECT u.* FROM users u "
"JOIN user_sessions s ON s.user_id = u.id "
"WHERE s.session_id = ?",
(session_id,),
).fetchone()
return _oeffentlich(row) if row else None
# Der Host-Film: das Passwort für den Hostmodus, wie der Anmeldefilm
# für einen Namen. Steht in der meta-Tabelle, gesetzt aus der Verwaltung.
def host_film_setzen(movie_id):
with connect() as conn:
db.set_meta(conn, "host_film", str(movie_id) if movie_id else "")
def host_film():
wert = db.get_meta("host_film")
return int(wert) if wert else None
def ist_host(session_id):
with connect() as conn:
return bool(
conn.execute(
"SELECT 1 FROM host_sessions WHERE session_id = ?", (session_id,)
).fetchone()
)
def host_setzen(session_id, an):
"""Den Hostmodus merkt sich auch der Server — sonst könnte er den
Anmeldefilm nicht überspringen."""
with connect() as conn:
if an:
conn.execute(
"INSERT OR REPLACE INTO host_sessions (session_id, seit) VALUES (?, ?)",
(session_id, datetime.now(timezone.utc).isoformat(timespec="seconds")),
)
else:
conn.execute(
"DELETE FROM host_sessions WHERE session_id = ?", (session_id,)
)
return an
def online_ids(session_ids):
"""Die Namen hinter offenen Verbindungen — wer gerade da ist."""
return {besitzer(sid) for sid in session_ids} - {None}
def besitzer(session_id):
"""Die user_id dieser Sitzung, oder None ohne gewählten Namen.
Sammlungen, Vorschläge und Wertungen werden damit beschriftet. Ein
Namenswechsel im selben Browser lässt die alten Einträge dann in Ruhe —
sie gehören weiter dem Namen, unter dem sie entstanden sind.
"""
with connect() as conn:
row = conn.execute(
"SELECT user_id FROM user_sessions WHERE session_id = ?",
(session_id,),
).fetchone()
return row["user_id"] if row else None
def gehoert_mir(zeile, session_id, user_id):
"""Ein Eintrag gehört mir, wenn der Name passt — sonst die Sitzung."""
if zeile["user_id"] is not None or user_id is not None:
return zeile["user_id"] == user_id
return zeile["session_id"] == session_id
# Dasselbe als WHERE-Klausel. Parameter: (user_id, user_id, session_id)
WEM = "(user_id = ? OR (? IS NULL AND user_id IS NULL AND session_id = ?))"
def user_id_nachruesten(conn, tabelle):
"""Spalte ergänzen und aus der Sitzung füllen — einmalig je Datenbank."""
spalten = {r[1] for r in conn.execute(f"PRAGMA table_info({tabelle})")}
if "user_id" in spalten:
return
conn.execute(f"ALTER TABLE {tabelle} ADD COLUMN user_id INTEGER")
conn.execute(
f"UPDATE {tabelle} SET user_id = ("
" SELECT user_id FROM user_sessions s WHERE s.session_id = "
f" {tabelle}.session_id)"
)
def je_person(conn, tabelle, schema, *schluessel):
"""Eindeutig je Person statt je Sitzung.
Früher stand das UNIQUE auf der Sitzung — dann blockierte der Eintrag
des einen Namens den des nächsten, sobald beide denselben Browser
benutzten. Mit Namen zählt jetzt der Name, ohne Namen die Sitzung.
Das alte UNIQUE steckt in der Spaltendefinition und lässt sich nicht
ändern — die Tabelle wird dafür nach `schema` neu gebaut. Hat ein Name
aus mehreren Browsern doppelt eingetragen, bleibt der jüngste Eintrag.
"""
user_id_nachruesten(conn, tabelle)
alt = conn.execute(
"SELECT sql FROM sqlite_master WHERE type='table' AND name=?", (tabelle,)
).fetchone()
if alt and "UNIQUE" in alt["sql"]:
spalten = ", ".join(
r[1] for r in conn.execute(f"PRAGMA table_info({tabelle})")
)
conn.executescript(
f"ALTER TABLE {tabelle} RENAME TO {tabelle}_alt;"
f"{schema}"
f"INSERT INTO {tabelle} ({spalten}) "
f"SELECT {spalten} FROM {tabelle}_alt;"
f"DROP TABLE {tabelle}_alt;"
)
mit_name = ", ".join((*schluessel, "user_id"))
ohne_name = ", ".join((*schluessel, "session_id"))
conn.execute(
f"DELETE FROM {tabelle} WHERE user_id IS NOT NULL AND id NOT IN "
f"(SELECT MAX(id) FROM {tabelle} GROUP BY {mit_name})"
)
conn.executescript(
f"CREATE UNIQUE INDEX IF NOT EXISTS {tabelle}_je_benutzer "
f"ON {tabelle} ({mit_name}) WHERE user_id IS NOT NULL;"
f"CREATE UNIQUE INDEX IF NOT EXISTS {tabelle}_je_sitzung "
f"ON {tabelle} ({ohne_name}) WHERE user_id IS NULL;"
)
def name_von(session_id):
"""Nur der Name, für Vorschläge und Wertungen."""
benutzer = aktuell(session_id)
return benutzer["name"] if benutzer else None