Files
romm/backend/models/user.py
T
nendo 75302ed59a Add play session ingest for game time tracking
Backend API for collecting and querying play sessions, modeled after
the Argosy session data format. Clients submit batches per device,
recording both the session window and screen-on time.
2026-03-22 20:22:55 +09:00

134 lines
4.2 KiB
Python

from __future__ import annotations
import enum
from datetime import datetime, timezone
from typing import TYPE_CHECKING, Any
from sqlalchemy import TIMESTAMP, Enum, String
from sqlalchemy.orm import Mapped, mapped_column, relationship
from starlette.authentication import SimpleUser
from config import KIOSK_MODE
from handler.auth.constants import (
EDIT_SCOPES,
FULL_SCOPES,
READ_SCOPES,
WRITE_SCOPES,
Scope,
)
from models.base import BaseModel
from utils.database import CustomJSON
if TYPE_CHECKING:
from models.assets import Save, Screenshot, State
from models.client_token import ClientToken
from models.collection import Collection, SmartCollection
from models.device import Device
from models.play_session import PlaySession
from models.rom import RomNote, RomUser
class Role(enum.Enum):
VIEWER = "viewer"
EDITOR = "editor"
ADMIN = "admin"
TEXT_FIELD_LENGTH = 255
class User(BaseModel, SimpleUser):
__tablename__ = "users"
__table_args__ = {"extend_existing": True}
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
username: Mapped[str] = mapped_column(
String(length=TEXT_FIELD_LENGTH), unique=True, index=True
)
hashed_password: Mapped[str | None] = mapped_column(
String(length=TEXT_FIELD_LENGTH)
)
email: Mapped[str | None] = mapped_column(
String(length=TEXT_FIELD_LENGTH), unique=True, index=True
)
enabled: Mapped[bool] = mapped_column(default=True)
role: Mapped[Role] = mapped_column(Enum(Role), default=Role.VIEWER)
avatar_path: Mapped[str] = mapped_column(
String(length=TEXT_FIELD_LENGTH), default=""
)
last_login: Mapped[datetime | None] = mapped_column(TIMESTAMP(timezone=True))
last_active: Mapped[datetime | None] = mapped_column(TIMESTAMP(timezone=True))
ra_username: Mapped[str | None] = mapped_column(
String(length=TEXT_FIELD_LENGTH), default=""
)
ra_progression: Mapped[dict[str, Any] | None] = mapped_column(
CustomJSON(), default=dict
)
ui_settings: Mapped[dict[str, Any] | None] = mapped_column(
CustomJSON(), default=dict
)
saves: Mapped[list[Save]] = relationship(lazy="raise", back_populates="user")
states: Mapped[list[State]] = relationship(lazy="raise", back_populates="user")
screenshots: Mapped[list[Screenshot]] = relationship(
lazy="raise", back_populates="user"
)
rom_users: Mapped[list[RomUser]] = relationship(lazy="raise", back_populates="user")
notes: Mapped[list[RomNote]] = relationship(lazy="raise", back_populates="user")
collections: Mapped[list[Collection]] = relationship(
lazy="raise", back_populates="user"
)
smart_collections: Mapped[list["SmartCollection"]] = relationship(
lazy="raise", back_populates="user"
)
devices: Mapped[list["Device"]] = relationship(
lazy="raise", back_populates="user", cascade="all, delete-orphan"
)
client_tokens: Mapped[list["ClientToken"]] = relationship(
lazy="raise", back_populates="user", cascade="all, delete-orphan"
)
play_sessions: Mapped[list["PlaySession"]] = relationship(
lazy="raise", back_populates="user", cascade="all, delete-orphan"
)
@classmethod
def kiosk_mode_user(cls) -> User:
now = datetime.now(timezone.utc)
return cls(
id=-1,
username="kiosk",
role=Role.VIEWER,
enabled=True,
avatar_path="",
last_active=now,
last_login=now,
created_at=now,
updated_at=now,
)
@property
def oauth_scopes(self) -> list[Scope]:
if self.role == Role.ADMIN:
return FULL_SCOPES
if self.role == Role.EDITOR:
return EDIT_SCOPES
if KIOSK_MODE:
return READ_SCOPES
return WRITE_SCOPES
@property
def fs_safe_folder_name(self):
# Uses the ID to avoid issues with username changes
return f"User:{self.id}".encode().hex()
def set_last_active(self):
from handler.database import db_user_handler
db_user_handler.update_user(
self.id, {"last_active": datetime.now(timezone.utc)}
)