WIP checkpoint: Trust & Safety: Moderation data model + Alembic migration

This commit is contained in:
2026-08-19 12:37:46 +00:00
parent ff610c0466
commit dd56f6a22f
6 changed files with 444 additions and 6 deletions
+10 -1
View File
@@ -2,5 +2,14 @@ from app.models.user import User
from app.models.session import Session
from app.models.world import World
from app.models.asset import Asset
from app.models.moderation import Report, ModerationAction, Appeal
__all__ = ["User", "Session", "World", "Asset"]
__all__ = [
"User",
"Session",
"World",
"Asset",
"Report",
"ModerationAction",
"Appeal",
]
+137
View File
@@ -0,0 +1,137 @@
from datetime import datetime
from typing import Optional
from sqlalchemy import Column, Integer, String, Text, ForeignKey, DateTime
from sqlalchemy.dialects.postgresql import ENUM
from sqlalchemy.orm import relationship
from sqlalchemy.sql import func
import enum
from app.database import Base
class TargetType(str, enum.Enum):
USER = "user"
WORLD = "world"
ASSET = "asset"
CHAT_MESSAGE = "chat_message"
class ReasonCategory(str, enum.Enum):
HARASSMENT = "harassment"
HATE_SPEECH = "hate_speech"
SEXUAL_CONTENT = "sexual_content"
VIOLENCE = "violence"
SPAM = "spam"
SELF_HARM = "self_harm"
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"
DISMISSED = "dismissed"
NO_VIOLATION = "no_violation"
class ActionType(str, enum.Enum):
WARN = "warn"
SUSPEND = "suspend"
BAN = "ban"
RESTRICT = "restrict"
REMOVE = "remove"
RESTORE = "restore"
class AppealStatus(str, enum.Enum):
PENDING = "pending"
APPROVED = "approved"
REJECTED = "rejected"
class Report(Base):
__tablename__ = "reports"
id = Column(Integer, primary_key=True, index=True)
reporter_id = Column(Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=False)
target_type = Column(
ENUM(TargetType, name="target_type", create_type=False),
nullable=False,
)
target_id = Column(String(64), nullable=False)
reason_category = Column(
ENUM(ReasonCategory, name="reason_category", create_type=False),
nullable=False,
)
details = Column(Text, nullable=True)
status = Column(
ENUM(ReportStatus, name="report_status", create_type=False),
default=ReportStatus.OPEN,
nullable=False,
)
resolved_by = Column(Integer, ForeignKey("users.id", ondelete="SET NULL"), nullable=True)
resolution = Column(
ENUM(Resolution, name="resolution", create_type=False),
nullable=True,
)
resolved_at = Column(DateTime(timezone=True), nullable=True)
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
reporter = relationship("User", foreign_keys=[reporter_id])
resolved_by_user = relationship("User", foreign_keys=[resolved_by])
moderation_actions = relationship("ModerationAction", back_populates="report")
class ModerationAction(Base):
__tablename__ = "moderation_actions"
id = Column(Integer, primary_key=True, index=True)
moderator_id = Column(Integer, ForeignKey("users.id", ondelete="SET NULL"), nullable=False)
target_user_id = Column(Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=False)
target_type = Column(
ENUM(TargetType, name="target_type", create_type=False),
nullable=False,
)
target_id = Column(String(64), nullable=False)
action = Column(
ENUM(ActionType, name="action_type", create_type=False),
nullable=False,
)
reason = Column(Text, nullable=True)
report_id = Column(Integer, ForeignKey("reports.id", ondelete="SET NULL"), nullable=True)
expires_at = Column(DateTime(timezone=True), nullable=True)
revoked_at = Column(DateTime(timezone=True), nullable=True)
created_at = Column(DateTime(timezone=True), server_default=func.now())
moderator = relationship("User", foreign_keys=[moderator_id])
target_user = relationship("User", foreign_keys=[target_user_id])
report = relationship("Report", back_populates="moderation_actions")
appeals = relationship("Appeal", back_populates="moderation_action")
class Appeal(Base):
__tablename__ = "appeals"
id = Column(Integer, primary_key=True, index=True)
moderation_action_id = Column(Integer, ForeignKey("moderation_actions.id", ondelete="CASCADE"), nullable=False)
user_id = Column(Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=False)
reason = Column(Text, nullable=False)
status = Column(
ENUM(AppealStatus, name="appeal_status", create_type=False),
default=AppealStatus.PENDING,
nullable=False,
)
reviewed_by = Column(Integer, ForeignKey("users.id", ondelete="SET NULL"), nullable=True)
reviewed_at = Column(DateTime(timezone=True), nullable=True)
created_at = Column(DateTime(timezone=True), server_default=func.now())
moderation_action = relationship("ModerationAction", back_populates="appeals")
user = relationship("User", foreign_keys=[user_id])
reviewed_by_user = relationship("User", foreign_keys=[reviewed_by])
+21
View File
@@ -2,12 +2,20 @@ from datetime import date
from typing import Optional
from sqlalchemy import Column, Integer, String, Boolean, DateTime, Date
from sqlalchemy.dialects.postgresql import ENUM
from sqlalchemy.orm import relationship
from sqlalchemy.sql import func
import enum
from app.database import Base
class UserRole(str, enum.Enum):
USER = "user"
MODERATOR = "moderator"
ADMIN = "admin"
class User(Base):
__tablename__ = "users"
@@ -17,12 +25,25 @@ class User(Base):
hashed_password = Column(String(255), nullable=False)
date_of_birth = Column(Date, nullable=True)
is_active = Column(Boolean, default=True)
role = Column(
ENUM(UserRole, name="user_role", create_type=False),
default=UserRole.USER,
nullable=False,
)
banned_until = Column(DateTime(timezone=True), nullable=True)
suspended_until = Column(DateTime(timezone=True), nullable=True)
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
sessions = relationship("Session", back_populates="user", cascade="all, delete-orphan")
worlds = relationship("World", back_populates="owner")
assets = relationship("Asset", back_populates="owner")
reports_made = relationship("Report", foreign_keys="Report.reporter_id", back_populates="reporter")
reports_resolved = relationship("Report", foreign_keys="Report.resolved_by", back_populates="resolved_by_user")
moderation_actions_made = relationship("ModerationAction", foreign_keys="ModerationAction.moderator_id", back_populates="moderator")
moderation_actions_targeted = relationship("ModerationAction", foreign_keys="ModerationAction.target_user_id", back_populates="target_user")
appeals_made = relationship("Appeal", foreign_keys="Appeal.user_id", back_populates="user")
appeals_reviewed = relationship("Appeal", foreign_keys="Appeal.reviewed_by", back_populates="reviewed_by_user")
@property
def is_adult(self) -> bool: