"""Trust & Safety models: reports, moderation actions, and appeals.""" from datetime import datetime from typing import Optional from sqlalchemy import ( CheckConstraint, Column, DateTime, Enum, ForeignKey, Integer, String, Text, UniqueConstraint, func, ) from sqlalchemy.dialects.postgresql import ENUM as PG_ENUM from sqlalchemy.orm import relationship from app.database import Base # --------------------------------------------------------------------------- # ENUM helpers — created explicitly in Alembic, not via SQLAlchemy create_type # --------------------------------------------------------------------------- _role_enum = PG_ENUM( "user", "moderator", "admin", name="user_role", create_type=False, ) _target_type_enum = PG_ENUM( "user", "world", "asset", "chat_message", name="target_type", create_type=False, ) _reason_category_enum = PG_ENUM( "harassment", "hate_speech", "sexual_content", "violence", "spam", "self_harm", "other", name="reason_category", create_type=False, ) _report_status_enum = PG_ENUM( "open", "in_review", "resolved", "dismissed", name="report_status", create_type=False, ) _resolution_enum = PG_ENUM( "action_taken", "action_not_needed", "duplicate", "escalated", name="resolution", create_type=False, ) _moderation_action_enum = PG_ENUM( "warn", "suspend", "ban", "restrict", "remove", "restore", name="moderation_action", create_type=False, ) _appeal_status_enum = PG_ENUM( "pending", "approved", "rejected", name="appeal_status", create_type=False, ) class Report(Base): """A user-submitted report about another entity.""" __tablename__ = "reports" id = Column(Integer, primary_key=True, autoincrement=True) reporter_id = Column( Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=False, ) target_type = Column(_target_type_enum, nullable=False) target_id = Column(String(64), nullable=False) reason_category = Column(_reason_category_enum, nullable=False) details = Column(Text, nullable=True) status = Column( _report_status_enum, nullable=False, server_default="open", ) resolved_by = Column( Integer, ForeignKey("users.id", ondelete="SET NULL"), nullable=True, ) resolution = Column(_resolution_enum, nullable=True) created_at = Column( DateTime(timezone=True), server_default=func.now(), ) updated_at = Column( DateTime(timezone=True), onupdate=func.now(), ) resolved_at = Column(DateTime(timezone=True), nullable=True) # Coalesce duplicate reports from the same reporter about the same target __table_args__ = ( UniqueConstraint("reporter_id", "target_type", "target_id", name="uq_reporter_target"), ) # Relationships reporter = relationship("User", back_populates="reports", foreign_keys=[reporter_id]) resolved_by_user = relationship("User", back_populates="resolved_reports", 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 against a target.""" __tablename__ = "moderation_actions" id = Column(Integer, primary_key=True, autoincrement=True) moderator_id = Column( Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=False, ) target_user_id = Column( Integer, ForeignKey("users.id", ondelete="SET NULL"), nullable=True, ) target_type = Column(_target_type_enum, nullable=False) target_id = Column(String(64), nullable=False) action = Column(_moderation_action_enum, nullable=False) reason = Column(Text, nullable=False) 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(), ) # Relationships moderator = relationship("User", back_populates="moderation_actions", foreign_keys=[moderator_id]) target_user = relationship("User", back_populates="moderation_actions_target", 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 against a moderation action.""" __tablename__ = "appeals" id = Column(Integer, primary_key=True, autoincrement=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( _appeal_status_enum, nullable=False, server_default="pending", ) 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(), ) # Prevent duplicate open appeals per sanction __table_args__ = ( UniqueConstraint("moderation_action_id", "user_id", name="uq_action_user_appeal"), ) # Relationships moderation_action = relationship( "ModerationAction", back_populates="appeals", ) user = relationship("User", back_populates="appeals") reviewed_by_user = relationship("User", back_populates="reviewed_appeals", foreign_keys=[reviewed_by])