Compare commits

..

4 Commits

6 changed files with 422 additions and 231 deletions
+1 -1
View File
@@ -8,7 +8,7 @@ from sqlalchemy.ext.asyncio import create_async_engine
from app.config import settings from app.config import settings
from app.database import Base from app.database import Base
from app.models import user, session, world, asset, report, moderation_action, appeal # noqa: F401 from app.models import user, session, world, asset # noqa: F401
# this is the Alembic Config object, which provides # this is the Alembic Config object, which provides
# access to the values within the .ini file in use. # access to the values within the .ini file in use.
@@ -0,0 +1,329 @@
"""trust & safety: reports, moderation_actions, appeals + user role/ban/suspend
Revision ID: 0003_trust_safety
Revises: 0002_add_date_of_birth_to_users
Create Date: 2025-01-01 00:00:00.000000
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision: str = "0003_trust_safety"
down_revision: Union[str, None] = "0002_add_date_of_birth_to_users"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
"""Create trust & safety tables and extend users table."""
bind = op.get_bind()
# ── ENUM types ──────────────────────────────────────────────────────
# user_role
user_role = postgresql.ENUM("user", "moderator", "admin", name="user_role")
user_role.create(bind, checkfirst=True)
# report_target_type
report_target_type = postgresql.ENUM(
"user", "world", "asset", "chat_message", name="report_target_type"
)
report_target_type.create(bind, checkfirst=True)
# reason_category
reason_category = postgresql.ENUM(
"harassment",
"hate_speech",
"sexual_content",
"violence",
"spam",
"self_harm",
"other",
name="reason_category",
)
reason_category.create(bind, checkfirst=True)
# report_status
report_status = postgresql.ENUM(
"open", "in_review", "resolved", "dismissed", name="report_status"
)
report_status.create(bind, checkfirst=True)
# resolution
resolution_enum = postgresql.ENUM(
"action_taken", "no_action", "false_positive", name="resolution"
)
resolution_enum.create(bind, checkfirst=True)
# moderation_action_type
moderation_action_type = postgresql.ENUM(
"warn", "suspend", "ban", "restrict", "remove", "restore",
name="moderation_action_type",
)
moderation_action_type.create(bind, checkfirst=True)
# moderation_target_type
moderation_target_type = postgresql.ENUM(
"user", "world", "asset", "chat_message", name="moderation_target_type"
)
moderation_target_type.create(bind, checkfirst=True)
# appeal_status
appeal_status = postgresql.ENUM(
"pending", "approved", "rejected", name="appeal_status"
)
appeal_status.create(bind, checkfirst=True)
# ── Extend users table ──────────────────────────────────────────────
op.add_column(
"users",
sa.Column(
"role",
postgresql.ENUM("user", "moderator", "admin", name="user_role"),
nullable=False,
server_default="user",
),
)
op.add_column(
"users",
sa.Column("banned_until", sa.DateTime(timezone=True), nullable=True),
)
op.add_column(
"users",
sa.Column("suspended_until", sa.DateTime(timezone=True), nullable=True),
)
# ── reports table ───────────────────────────────────────────────────
op.create_table(
"reports",
sa.Column("id", sa.BigInteger(), primary_key=True, autoincrement=True),
sa.Column("reporter_id", sa.Integer(), nullable=False),
sa.Column(
"target_type",
postgresql.ENUM(
"user", "world", "asset", "chat_message", name="report_target_type"
),
nullable=False,
),
sa.Column("target_id", sa.String(length=64), nullable=False),
sa.Column(
"reason_category",
postgresql.ENUM(
"harassment",
"hate_speech",
"sexual_content",
"violence",
"spam",
"self_harm",
"other",
name="reason_category",
),
nullable=False,
),
sa.Column(
"details", sa.Text(), nullable=True,
),
sa.Column(
"status",
postgresql.ENUM(
"open", "in_review", "resolved", "dismissed", name="report_status"
),
nullable=False,
server_default="open",
),
sa.Column("resolved_by", sa.Integer(), nullable=True),
sa.Column(
"resolution",
postgresql.ENUM(
"action_taken", "no_action", "false_positive", name="resolution"
),
nullable=True,
),
sa.Column("resolved_at", sa.DateTime(timezone=True), nullable=True),
sa.Column(
"created_at",
sa.DateTime(timezone=True),
server_default=sa.func.now(),
nullable=False,
),
sa.Column(
"updated_at",
sa.DateTime(timezone=True),
onupdate=sa.func.now(),
nullable=True,
),
sa.ForeignKeyConstraint(["reporter_id"], ["users.id"], ondelete="CASCADE"),
sa.ForeignKeyConstraint(["resolved_by"], ["users.id"], ondelete="SET NULL"),
sa.UniqueConstraint(
"reporter_id", "target_type", "target_id",
name="uq_reporter_target",
),
)
op.create_index(op.f("ix_reports_id"), "reports", ["id"], unique=False)
op.create_index(
op.f("ix_reports_reporter_id"), "reports", ["reporter_id"], unique=False
)
op.create_index(
op.f("ix_reports_status"), "reports", ["status"], unique=False
)
# ── moderation_actions table ────────────────────────────────────────
op.create_table(
"moderation_actions",
sa.Column("id", sa.BigInteger(), primary_key=True, autoincrement=True),
sa.Column("moderator_id", sa.Integer(), nullable=False),
sa.Column("target_user_id", sa.Integer(), nullable=True),
sa.Column(
"target_type",
postgresql.ENUM(
"user", "world", "asset", "chat_message",
name="moderation_target_type",
),
nullable=False,
),
sa.Column("target_id", sa.String(length=64), nullable=True),
sa.Column(
"action",
postgresql.ENUM(
"warn", "suspend", "ban", "restrict", "remove", "restore",
name="moderation_action_type",
),
nullable=False,
),
sa.Column("reason", sa.Text(), nullable=True),
sa.Column("report_id", sa.Integer(), nullable=True),
sa.Column("expires_at", sa.DateTime(timezone=True), nullable=True),
sa.Column("revoked_at", sa.DateTime(timezone=True), nullable=True),
sa.Column(
"created_at",
sa.DateTime(timezone=True),
server_default=sa.func.now(),
nullable=False,
),
sa.ForeignKeyConstraint(["moderator_id"], ["users.id"], ondelete="CASCADE"),
sa.ForeignKeyConstraint(
["target_user_id"], ["users.id"], ondelete="CASCADE"
),
sa.ForeignKeyConstraint(["report_id"], ["reports.id"], ondelete="SET NULL"),
)
op.create_index(
op.f("ix_moderation_actions_id"),
"moderation_actions",
["id"],
unique=False,
)
op.create_index(
op.f("ix_moderation_actions_moderator_id"),
"moderation_actions",
["moderator_id"],
unique=False,
)
op.create_index(
op.f("ix_moderation_actions_target_user_id"),
"moderation_actions",
["target_user_id"],
unique=False,
)
op.create_index(
op.f("ix_moderation_actions_report_id"),
"moderation_actions",
["report_id"],
unique=False,
)
# ── appeals table ───────────────────────────────────────────────────
op.create_table(
"appeals",
sa.Column("id", sa.BigInteger(), primary_key=True, autoincrement=True),
sa.Column(
"moderation_action_id", sa.Integer(), nullable=False
),
sa.Column("user_id", sa.Integer(), nullable=False),
sa.Column("reason", sa.Text(), nullable=False),
sa.Column(
"status",
postgresql.ENUM(
"pending", "approved", "rejected", name="appeal_status"
),
nullable=False,
server_default="pending",
),
sa.Column("reviewed_by", sa.Integer(), nullable=True),
sa.Column("reviewed_at", sa.DateTime(timezone=True), nullable=True),
sa.Column(
"created_at",
sa.DateTime(timezone=True),
server_default=sa.func.now(),
nullable=False,
),
sa.ForeignKeyConstraint(
["moderation_action_id"],
["moderation_actions.id"],
ondelete="CASCADE",
),
sa.ForeignKeyConstraint(["user_id"], ["users.id"], ondelete="CASCADE"),
sa.ForeignKeyConstraint(
["reviewed_by"], ["users.id"], ondelete="SET NULL"
),
)
op.create_index(op.f("ix_appeals_id"), "appeals", ["id"], unique=False)
op.create_index(
op.f("ix_appeals_moderation_action_id"),
"appeals",
["moderation_action_id"],
unique=False,
)
op.create_index(
op.f("ix_appeals_user_id"),
"appeals",
["user_id"],
unique=False,
)
op.create_index(
op.f("ix_appeals_status"),
"appeals",
["status"],
unique=False,
)
def downgrade() -> None:
"""Remove trust & safety tables and revert users table."""
op.drop_index(op.f("ix_appeals_status"), table_name="appeals")
op.drop_index(op.f("ix_appeals_user_id"), table_name="appeals")
op.drop_index(op.f("ix_appeals_moderation_action_id"), table_name="appeals")
op.drop_index(op.f("ix_appeals_id"), table_name="appeals")
op.drop_table("appeals")
op.drop_index(op.f("ix_moderation_actions_report_id"), table_name="moderation_actions")
op.drop_index(op.f("ix_moderation_actions_target_user_id"), table_name="moderation_actions")
op.drop_index(op.f("ix_moderation_actions_moderator_id"), table_name="moderation_actions")
op.drop_index(op.f("ix_moderation_actions_id"), table_name="moderation_actions")
op.drop_table("moderation_actions")
op.drop_index(op.f("ix_reports_status"), table_name="reports")
op.drop_index(op.f("ix_reports_reporter_id"), table_name="reports")
op.drop_index(op.f("ix_reports_id"), table_name="reports")
op.drop_table("reports")
op.drop_column("users", "suspended_until")
op.drop_column("users", "banned_until")
op.drop_column("users", "role")
# Drop ENUM types
postgresql.ENUM(name="appeal_status").drop(bind, checkfirst=True)
postgresql.ENUM(name="moderation_target_type").drop(bind, checkfirst=True)
postgresql.ENUM(name="moderation_action_type").drop(bind, checkfirst=True)
postgresql.ENUM(name="resolution").drop(bind, checkfirst=True)
postgresql.ENUM(name="report_status").drop(bind, checkfirst=True)
postgresql.ENUM(name="reason_category").drop(bind, checkfirst=True)
postgresql.ENUM(name="report_target_type").drop(bind, checkfirst=True)
postgresql.ENUM(name="user_role").drop(bind, checkfirst=True)
+19 -39
View File
@@ -1,67 +1,47 @@
"""Appeal model for moderation actions.""" """Appeal model for Trust & Safety."""
from datetime import datetime from datetime import datetime
from typing import Optional from typing import Optional
from sqlalchemy import ( from sqlalchemy import (
Column, Column,
Enum,
ForeignKey,
Integer, Integer,
String,
Text, Text,
DateTime, DateTime,
CheckConstraint, ForeignKey,
) )
from sqlalchemy.dialects.postgresql import ENUM
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
from sqlalchemy.sql import func from sqlalchemy.sql import func
import enum
from app.database import Base from app.database import Base
class AppealStatus(str, enum.Enum):
PENDING = "pending"
APPROVED = "approved"
REJECTED = "rejected"
class Appeal(Base): class Appeal(Base):
__tablename__ = "appeals" __tablename__ = "appeals"
id = Column(Integer, primary_key=True, index=True) id = Column(Integer, primary_key=True, autoincrement=True)
moderation_action_id = Column( moderation_action_id = Column(
Integer, Integer, ForeignKey("moderation_actions.id", ondelete="CASCADE"), nullable=False
ForeignKey("moderation_actions.id", ondelete="CASCADE"),
nullable=False,
)
user_id = Column(
Integer,
ForeignKey("users.id", ondelete="CASCADE"),
nullable=False,
) )
user_id = Column(Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=False)
reason = Column(Text, nullable=False) reason = Column(Text, nullable=False)
status = Column( status = Column(
Enum("pending", "approved", "rejected", name="appeal_status", create_type=False), ENUM(AppelStatus, name="appeal_status", create_type=False),
nullable=False, default=AppealStatus.PENDING,
default="pending",
)
reviewed_by = Column(
Integer,
ForeignKey("users.id", ondelete="SET NULL"),
nullable=True,
) )
reviewed_by = Column(Integer, ForeignKey("users.id", ondelete="SET NULL"), nullable=True)
reviewed_at = Column(DateTime(timezone=True), nullable=True) reviewed_at = Column(DateTime(timezone=True), nullable=True)
created_at = Column( created_at = Column(DateTime(timezone=True), server_default=func.now(), nullable=False)
DateTime(timezone=True),
server_default=func.now(),
nullable=False,
)
# Relationships # Relationships
moderation_action = relationship("ModerationAction", back_populates="appeals") moderation_action = relationship("ModerationAction", back_populates="appeals")
reviewer = relationship( user = relationship("User", foreign_keys=[user_id])
"User", reviewer = relationship("User", foreign_keys=[reviewed_by])
foreign_keys=[reviewed_by],
back_populates="reviewed_appeals",
)
__table_args__ = (
CheckConstraint(
"status IN ('pending', 'approved', 'rejected')",
name="check_appeal_status",
),
)
+27 -82
View File
@@ -1,60 +1,25 @@
"""Moderation action model.""" """ModerationAction model for Trust & Safety."""
from datetime import datetime from datetime import datetime
from enum import Enum as PyEnum
from typing import Optional from typing import Optional
from sqlalchemy import ( from sqlalchemy import (
Column, Column,
Enum,
ForeignKey,
Integer, Integer,
String, String,
Text, Text,
DateTime, DateTime,
func, ForeignKey,
) )
from sqlalchemy.dialects.postgresql import TIMESTAMPTZ from sqlalchemy.dialects.postgresql import ENUM
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
from sqlalchemy.sql import func
import enum
from app.database import Base from app.database import Base
class TargetType(str, PyEnum): class ModerationActionType(str, enum.Enum):
"""Type of entity being reported or moderated."""
USER = "user"
WORLD = "world"
ASSET = "asset"
CHAT_MESSAGE = "chat_message"
class ReasonCategory(str, PyEnum):
"""Category of report reason."""
HARASSMENT = "harassment"
HATE_SPEECH = "hate_speech"
SEXUAL_CONTENT = "sexual_content"
VIOLENCE = "violence"
SPAM = "spam"
SELF_HARM = "self_harm"
OTHER = "other"
class ReportStatus(str, PyEnum):
"""Status of a report."""
OPEN = "open"
IN_REVIEW = "in_review"
RESOLVED = "resolved"
DISMISSED = "dismissed"
class Resolution(str, PyEnum):
"""Resolution outcome of a report."""
ACTION_TAKEN = "action_taken"
NO_ACTION = "no_action"
class ModerationActionType(str, PyEnum):
"""Type of moderation action."""
WARN = "warn" WARN = "warn"
SUSPEND = "suspend" SUSPEND = "suspend"
BAN = "ban" BAN = "ban"
@@ -63,56 +28,36 @@ class ModerationActionType(str, PyEnum):
RESTORE = "restore" RESTORE = "restore"
class ModerationTargetType(str, enum.Enum):
USER = "user"
WORLD = "world"
ASSET = "asset"
CHAT_MESSAGE = "chat_message"
class ModerationAction(Base): class ModerationAction(Base):
__tablename__ = "moderation_actions" __tablename__ = "moderation_actions"
id = Column(Integer, primary_key=True, index=True) id = Column(Integer, primary_key=True, autoincrement=True)
moderator_id = Column( moderator_id = Column(Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=False)
Integer, target_user_id = Column(Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=True)
ForeignKey("users.id", ondelete="SET NULL"),
nullable=True,
)
target_user_id = Column(
Integer,
ForeignKey("users.id", ondelete="SET NULL"),
nullable=True,
)
target_type = Column( target_type = Column(
Enum(TargetType, name="target_type_enum", create_type=False), ENUM(ModerationTargetType, name="moderation_target_type", create_type=False),
nullable=False, nullable=False,
) )
target_id = Column(String(64), nullable=False) target_id = Column(String(64), nullable=True)
action = Column( action = Column(
Enum(ModerationActionType, name="moderation_action_type_enum", create_type=False), ENUM(ModerationActionType, name="moderation_action_type", create_type=False),
nullable=False, nullable=False,
) )
reason = Column(Text, nullable=True) reason = Column(Text, nullable=True)
report_id = Column( report_id = Column(Integer, ForeignKey("reports.id", ondelete="SET NULL"), nullable=True)
Integer, expires_at = Column(DateTime(timezone=True), nullable=True)
ForeignKey("reports.id", ondelete="SET NULL"), revoked_at = Column(DateTime(timezone=True), nullable=True)
nullable=True, created_at = Column(DateTime(timezone=True), server_default=func.now(), nullable=False)
)
expires_at = Column(TIMESTAMPTZ, nullable=True)
revoked_at = Column(TIMESTAMPTZ, nullable=True)
created_at = Column(
DateTime(timezone=True),
server_default=func.now(),
)
# Relationships # Relationships
moderator = relationship( moderator = relationship("User", foreign_keys=[moderator_id])
"User", target_user = relationship("User", foreign_keys=[target_user_id])
foreign_keys=[moderator_id], report = relationship("Report", foreign_keys=[report_id])
back_populates="moderation_actions_given", appeals = relationship("Appeal", back_populates="moderation_action", cascade="all, delete-orphan")
)
target_user = relationship(
"User",
foreign_keys=[target_user_id],
back_populates="moderation_actions_received",
)
report = relationship("Report", back_populates="moderation_actions")
appeals = relationship(
"Appeal",
back_populates="moderation_action",
cascade="all, delete-orphan",
)
+37 -95
View File
@@ -1,52 +1,33 @@
"""Report model for user-generated moderation reports.""" """Report model for Trust & Safety."""
from datetime import datetime from datetime import datetime
from typing import Optional from typing import Optional
import sqlalchemy as sa
from sqlalchemy import ( from sqlalchemy import (
Column, Column,
DateTime,
Enum,
ForeignKey,
Integer, Integer,
String, String,
Text, Text,
DateTime,
ForeignKey,
UniqueConstraint,
) )
from sqlalchemy.dialects.postgresql import TIMESTAMPTZ from sqlalchemy.dialects.postgresql import ENUM
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
from sqlalchemy.sql import func
import enum
from app.database import Base from app.database import Base
class ReportStatus: class ReportTargetType(str, enum.Enum):
"""Valid statuses for a Report."""
OPEN = "open"
IN_REVIEW = "in_review"
RESOLVED = "resolved"
DISMISSED = "dismissed"
class Resolution:
"""Possible resolution outcomes for a Report."""
ACTION_TAKEN = "action_taken"
NO_ACTION = "no_action"
class TargetType:
"""Types of entities that can be reported."""
USER = "user" USER = "user"
WORLD = "world" WORLD = "world"
ASSET = "asset" ASSET = "asset"
CHAT_MESSAGE = "chat_message" CHAT_MESSAGE = "chat_message"
class ReasonCategory: class ReasonCategory(str, enum.Enum):
"""Categories for why something was reported."""
HARASSMENT = "harassment" HARASSMENT = "harassment"
HATE_SPEECH = "hate_speech" HATE_SPEECH = "hate_speech"
SEXUAL_CONTENT = "sexual_content" SEXUAL_CONTENT = "sexual_content"
@@ -56,91 +37,52 @@ class ReasonCategory:
OTHER = "other" OTHER = "other"
class ReportStatus(str, enum.Enum):
OPEN = "open"
IN_REVIEW = "in_review"
RESOLVED = "resolved"
DISMISSED = "dismissed"
class Resolution(str, enum.Enum):
ACTION_TAKEN = "action_taken"
NO_ACTION = "no_action"
FALSE_POSITIVE = "false_positive"
class Report(Base): class Report(Base):
__tablename__ = "reports" __tablename__ = "reports"
id = Column(Integer, primary_key=True, index=True) id = Column(Integer, primary_key=True, autoincrement=True)
reporter_id = Column( reporter_id = Column(Integer, ForeignKey("users.id"), nullable=False)
Integer,
ForeignKey("users.id", ondelete="CASCADE"),
nullable=False,
)
target_type = Column( target_type = Column(
Enum( ENUM(ReportTargetType, name="report_target_type", create_type=False),
TargetType.USER,
TargetType.WORLD,
TargetType.ASSET,
TargetType.CHAT_MESSAGE,
name="target_type",
create_type=False,
),
nullable=False, nullable=False,
) )
target_id = Column(String(64), nullable=False) target_id = Column(String(64), nullable=False)
reason_category = Column( reason_category = Column(
Enum( ENUM(ReasonCategory, name="reason_category", create_type=False),
ReasonCategory.HARASSMENT,
ReasonCategory.HATE_SPEECH,
ReasonCategory.SEXUAL_CONTENT,
ReasonCategory.VIOLENCE,
ReasonCategory.SPAM,
ReasonCategory.SELF_HARM,
ReasonCategory.OTHER,
name="reason_category",
create_type=False,
),
nullable=False, nullable=False,
) )
details = Column(Text, nullable=True) details = Column(Text, nullable=True)
status = Column( status = Column(
Enum( ENUM(ReportStatus, name="report_status", create_type=False),
ReportStatus.OPEN,
ReportStatus.IN_REVIEW,
ReportStatus.RESOLVED,
ReportStatus.DISMISSED,
name="report_status",
create_type=False,
),
nullable=False,
default=ReportStatus.OPEN, default=ReportStatus.OPEN,
) )
resolved_by = Column( resolved_by = Column(Integer, ForeignKey("users.id"), nullable=True)
Integer,
ForeignKey("users.id", ondelete="SET NULL"),
nullable=True,
)
resolution = Column( resolution = Column(
Enum( ENUM(Resolution, name="resolution", create_type=False),
Resolution.ACTION_TAKEN,
Resolution.NO_ACTION,
name="resolution",
create_type=False,
),
nullable=True, nullable=True,
) )
created_at = Column( resolved_at = Column(DateTime(timezone=True), nullable=True)
TIMESTAMPTZ, created_at = Column(DateTime(timezone=True), server_default=func.now())
nullable=False, updated_at = Column(DateTime(timezone=True), onupdate=func.now())
server_default=sa.func.now(),
)
updated_at = Column(
TIMESTAMPTZ,
nullable=False,
server_default=sa.func.now(),
onupdate=sa.func.now(),
)
resolved_at = Column(TIMESTAMPTZ, nullable=True)
# Relationships # Relationships
reporter = relationship("User", foreign_keys=[reporter_id]) reporter = relationship("User", foreign_keys=[reporter_id])
resolver = relationship("User", foreign_keys=[resolved_by]) resolved_by_user = relationship("User", foreign_keys=[resolved_by])
moderation_actions = relationship(
"ModerationAction", __table_args__ = (
back_populates="report", UniqueConstraint("reporter_id", "target_type", "target_id",
cascade="all, delete-orphan", name="uq_reporter_target"),
)
appeals = relationship(
"Appeal",
back_populates="report",
cascade="all, delete-orphan",
) )
+9 -14
View File
@@ -4,12 +4,12 @@ from typing import Optional
from sqlalchemy import Column, Integer, String, Boolean, DateTime, Date, Enum from sqlalchemy import Column, Integer, String, Boolean, DateTime, Date, Enum
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
from sqlalchemy.sql import func from sqlalchemy.sql import func
import enum
from app.database import Base from app.database import Base
class UserRole: class UserRole(str, enum.Enum):
"""Valid user roles."""
USER = "user" USER = "user"
MODERATOR = "moderator" MODERATOR = "moderator"
ADMIN = "admin" ADMIN = "admin"
@@ -25,9 +25,9 @@ class User(Base):
date_of_birth = Column(Date, nullable=True) date_of_birth = Column(Date, nullable=True)
is_active = Column(Boolean, default=True) is_active = Column(Boolean, default=True)
role = Column( role = Column(
Enum(UserRole.USER, UserRole.MODERATOR, UserRole.ADMIN, name="user_role", create_type=False), Enum(UserRole, name="user_role"),
nullable=False, nullable=False,
default=UserRole.USER, server_default="user",
) )
banned_until = Column(DateTime(timezone=True), nullable=True) banned_until = Column(DateTime(timezone=True), nullable=True)
suspended_until = Column(DateTime(timezone=True), nullable=True) suspended_until = Column(DateTime(timezone=True), nullable=True)
@@ -37,16 +37,11 @@ class User(Base):
sessions = relationship("Session", back_populates="user", cascade="all, delete-orphan") sessions = relationship("Session", back_populates="user", cascade="all, delete-orphan")
worlds = relationship("World", back_populates="owner") worlds = relationship("World", back_populates="owner")
assets = relationship("Asset", back_populates="owner") assets = relationship("Asset", back_populates="owner")
moderation_actions_given = relationship( reports_made = relationship("Report", foreign_keys="Report.reporter_id", back_populates="reporter")
"ModerationAction", reports_resolved = relationship("Report", foreign_keys="Report.resolved_by", back_populates="resolved_by_user")
foreign_keys="ModerationAction.moderator_id", moderation_actions_as_moderator = relationship("ModerationAction", foreign_keys="ModerationAction.moderator_id", back_populates="moderator")
back_populates="moderator", moderation_actions_as_target = relationship("ModerationAction", foreign_keys="ModerationAction.target_user_id", back_populates="target_user")
) appeals = relationship("Appeal", foreign_keys="Appeal.user_id", back_populates="user")
moderation_actions_received = relationship(
"ModerationAction",
foreign_keys="ModerationAction.target_user_id",
back_populates="target_user",
)
@property @property
def is_adult(self) -> bool: def is_adult(self) -> bool: