"""Moderation data models for Trust & Safety.""" import enum from datetime import datetime from typing import Optional from sqlalchemy import Column, Integer, String, Text, DateTime, ForeignKey from sqlalchemy.dialects.postgresql import ENUM from sqlalchemy.orm import relationship from sqlalchemy.sql import func from app.database import Base class TargetType(str, enum.Enum): """Type of entity being reported or moderated.""" USER = "user" WORLD = "world" ASSET = "asset" CHAT_MESSAGE = "chat_message" class ReasonCategory(str, enum.Enum): """Category of reason for a report.""" 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): """Status of a report.""" OPEN = "open" IN_REVIEW = "in_review" RESOLVED = "resolved" DISMISSED = "dismissed" class Resolution(str, enum.Enum): """Resolution outcome of a report.""" UPHELD = "upheld" MODIFIED = "modified" DISMISSED = "dismissed" class ModerationActionType(str, enum.Enum): """Type of moderation action taken.""" WARN = "warn" SUSPEND = "suspend" BAN = "ban" RESTRICT = "restrict" REMOVE = "remove" RESTORE = "restore" class AppealStatus(str, enum.Enum): """Status of an appeal.""" PENDING = "pending" APPROVED = "approved" REJECTED = "rejected" class Report(Base): """A report filed by a user against a target entity.""" __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, ) created_at = Column( DateTime(timezone=True), server_default=func.now(), nullable=False ) updated_at = Column( DateTime(timezone=True), server_default=func.now(), onupdate=func.now(), nullable=False, ) resolved_at = Column(DateTime(timezone=True), nullable=True) # Relationships reporter = relationship("User", foreign_keys=[reporter_id]) resolver = relationship("User", foreign_keys=[resolved_by]) moderation_actions = relationship( "ModerationAction", back_populates="report", cascade="all, delete-orphan" ) appeals = relationship( "Appeal", back_populates="report", cascade="all, delete-orphan" ) class ModerationAction(Base): """A moderation action taken by a moderator against a user or entity.""" __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(ModerationActionType, name="moderation_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(), nullable=False ) # Relationships 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", cascade="all, delete-orphan" ) class Appeal(Base): """An appeal filed by a user against a moderation action.""" __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(), nullable=False ) # Relationships moderation_action = relationship("ModerationAction", back_populates="appeals") user = relationship("User", foreign_keys=[user_id]) reviewer = relationship("User", foreign_keys=[reviewed_by])