diff --git a/foxnouns/db/__init__.py b/foxnouns/db/__init__.py index 2a6fe1b..d40cf30 100644 --- a/foxnouns/db/__init__.py +++ b/foxnouns/db/__init__.py @@ -1,2 +1,3 @@ from .base import Base from .user import User, Token, AuthMethod, FediverseApp +from .member import Member diff --git a/foxnouns/db/base.py b/foxnouns/db/base.py index f75ec44..fa2b68a 100644 --- a/foxnouns/db/base.py +++ b/foxnouns/db/base.py @@ -1,4 +1,5 @@ from sqlalchemy.orm import DeclarativeBase + class Base(DeclarativeBase): pass diff --git a/foxnouns/db/member.py b/foxnouns/db/member.py new file mode 100644 index 0000000..164157f --- /dev/null +++ b/foxnouns/db/member.py @@ -0,0 +1,28 @@ +from typing import Any + +from sqlalchemy import Text, BigInteger, ForeignKey +from sqlalchemy.dialects.postgresql import JSONB +from sqlalchemy.orm import Mapped, mapped_column, relationship + +from .base import Base +from .snowflake import Snowflake +from .user import User + + +class Member(Base): + __tablename__ = "members" + + id: Mapped[int] = mapped_column( + BigInteger(), primary_key=True, default=Snowflake.generate_int + ) + name: Mapped[str] = mapped_column(Text(), nullable=False) + + display_name: Mapped[str | None] = mapped_column(Text(), nullable=True) + bio: Mapped[str | None] = mapped_column(Text(), nullable=True) + + names: Mapped[list[Any]] = mapped_column(JSONB(), nullable=False, default=[]) + pronouns: Mapped[list[Any]] = mapped_column(JSONB(), nullable=False, default=[]) + fields: Mapped[list[Any]] = mapped_column(JSONB(), nullable=False, default=[]) + + user_id: Mapped[int] = mapped_column(ForeignKey("users.id")) + user: Mapped[User] = relationship(back_populates="members", lazy="immediate") diff --git a/foxnouns/db/user.py b/foxnouns/db/user.py index 5ef9591..0649b9a 100644 --- a/foxnouns/db/user.py +++ b/foxnouns/db/user.py @@ -1,8 +1,9 @@ from datetime import datetime import enum +from typing import Any from sqlalchemy import Text, Integer, BigInteger, ForeignKey, DateTime -from sqlalchemy.dialects.postgresql import ARRAY +from sqlalchemy.dialects.postgresql import ARRAY, JSONB from sqlalchemy.orm import Mapped, mapped_column, relationship from .base import Base @@ -19,17 +20,27 @@ class User(Base): display_name: Mapped[str | None] = mapped_column(Text(), nullable=True) bio: Mapped[str | None] = mapped_column(Text(), nullable=True) + names: Mapped[list[Any]] = mapped_column(JSONB(), nullable=False, default=[]) + pronouns: Mapped[list[Any]] = mapped_column(JSONB(), nullable=False, default=[]) + fields: Mapped[list[Any]] = mapped_column(JSONB(), nullable=False, default=[]) + tokens: Mapped[list["Token"]] = relationship( back_populates="user", cascade="all, delete-orphan" ) auth_methods: Mapped[list["AuthMethod"]] = relationship( back_populates="user", cascade="all, delete-orphan" ) + members: Mapped[list["Member"]] = relationship( + back_populates="user", cascade="all, delete-orphan" + ) def __repr__(self): return f"User(id={self.id!r}, username={self.username!r})" +from .member import Member + + class Token(Base): __tablename__ = "tokens" @@ -40,7 +51,7 @@ class Token(Base): scopes: Mapped[list[str]] = mapped_column(ARRAY(Text), nullable=False) user_id: Mapped[int] = mapped_column(ForeignKey("users.id")) - user: Mapped[User] = relationship(back_populates="tokens") + user: Mapped[User] = relationship(back_populates="tokens", lazy="immediate") def __repr__(self): return f"Token(id={self.id!r}, user={self.user_id!r})" @@ -78,12 +89,12 @@ class AuthMethod(Base): remote_username: Mapped[str | None] = mapped_column(Text(), nullable=True) user_id: Mapped[int] = mapped_column(ForeignKey("users.id")) - user: Mapped[User] = relationship(back_populates="auth_methods") + user: Mapped[User] = relationship(back_populates="auth_methods", lazy="immediate") fediverse_app_id: Mapped[int] = mapped_column( ForeignKey("fediverse_apps.id"), nullable=True ) - fediverse_app: Mapped["FediverseApp"] = relationship() + fediverse_app: Mapped["FediverseApp"] = relationship(lazy="immediate") class FediverseInstanceType(enum.IntEnum): diff --git a/foxnouns/db/util.py b/foxnouns/db/util.py index 617db48..152902f 100644 --- a/foxnouns/db/util.py +++ b/foxnouns/db/util.py @@ -4,9 +4,11 @@ from itsdangerous import BadSignature from itsdangerous.url_safe import URLSafeTimedSerializer from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select, insert +from sqlalchemy.orm import selectinload from quart import g from .user import User, Token +from .member import Member from foxnouns.exceptions import ForbiddenError, ErrorCode from foxnouns.settings import SECRET_KEY @@ -15,7 +17,7 @@ async def user_from_ref(session: AsyncSession, user_ref: str): """Returns a user from a `user_ref` value. If `user_ref` is `@me`, returns the current user. Otherwise, tries to convert the user to a snowflake ID and queries that. Otherwise, returns a user with that username. """ - query = select(User) + query = select(User).options(selectinload(User.members)) if user_ref == "@me": if "user" in g: @@ -37,6 +39,13 @@ async def user_from_ref(session: AsyncSession, user_ref: str): return await session.scalar(query) +async def user_members(session: AsyncSession, user: User): + query = select(Member).where(Member.user_id == user.id) + + res = await session.scalars(query) + return res.all() + + serializer = URLSafeTimedSerializer(SECRET_KEY) diff --git a/foxnouns/models/__init__.py b/foxnouns/models/__init__.py index 4a59cbf..3891698 100644 --- a/foxnouns/models/__init__.py +++ b/foxnouns/models/__init__.py @@ -1,7 +1,6 @@ -from typing import Any - from pydantic import BaseModel, field_validator + class BasePatchModel(BaseModel): model_config = {"from_attributes": True} diff --git a/foxnouns/models/fields.py b/foxnouns/models/fields.py new file mode 100644 index 0000000..a548429 --- /dev/null +++ b/foxnouns/models/fields.py @@ -0,0 +1,17 @@ +from pydantic import BaseModel, Field + + +class FieldEntry(BaseModel): + value: str = Field(max_length=128) + status: str + + +class ProfileField(BaseModel): + name: str = Field(max_length=128) + entries: list[FieldEntry] + + +class PronounEntry(BaseModel): + value: str = Field(max_length=128) + status: str + display: str | None = Field(max_length=128, default=None) diff --git a/foxnouns/models/member.py b/foxnouns/models/member.py new file mode 100644 index 0000000..cfb0354 --- /dev/null +++ b/foxnouns/models/member.py @@ -0,0 +1,7 @@ +from pydantic import Field + +from .user import BaseMemberModel, BaseUserModel + + +class FullMemberModel(BaseMemberModel): + user: BaseUserModel diff --git a/foxnouns/models/user.py b/foxnouns/models/user.py index 2ba7ecf..f5ad505 100644 --- a/foxnouns/models/user.py +++ b/foxnouns/models/user.py @@ -3,12 +3,22 @@ from pydantic import Field from . import BaseSnowflakeModel -class UserModel(BaseSnowflakeModel): +class BaseUserModel(BaseSnowflakeModel): name: str = Field(alias="username") display_name: str | None bio: str | None +class UserModel(BaseUserModel): + members: list["BaseMemberModel"] = Field(default=[]) + + +class BaseMemberModel(BaseSnowflakeModel): + name: str + display_name: str | None + bio: str | None + + class SelfUserModel(UserModel): pass