"""Moderation-related models: Report, ModerationAction, Appeal.""" from datetime import datetime from typing import Optional from sqlalchemy import ( Column, Enum, ForeignKey, Integer, String, Text, DateTime, Index, ) from sqlalchemy.orm import relationship from sqlalchemy.sql import func from app.database import Base # --- Enums --- class TargetType(str): USER = "user" WORLD = "world" ASSET = "asset" CHAT_MESSAGE = "chat_message" class ReasonCategory(str): HARASSMENT = "harassment" HATE_SPEECH = "hate_speech" SEXUAL_CONTENT = "sexual_content" VIOLENCE = "violence" SPAM = "spam" SELF_HARM = "self_harm" OTHER = "other" class ReportStatus(str): OPEN = "open" IN_REVIEW = "in_review" RESOLVED = "resolved" DISMISSED = "dismissed" class ResolutionAction(str): WARN = "warn" SUSPEND = "suspend" BAN = "ban" RESTRICT = "restrict" REMOVE = "remove" RESTORE = "restore" class ModerationActionType(str): WARN = "warn" SUSPEND = "suspend" BAN = "ban" RESTRICT = "restrict" REMOVE = "remove" RESTORE = "restore" class AppealStatus(str): PENDING = "pending" APPROVED = "approved" REJECTED = "rejected" # --- Models --- class Report(Base): """A moderation report filed by a user or system.""" __tablename__ = "reports" id = Column(Integer, primary_key=True) reporter_id = Column( Integer, ForeignKey("users.id", ondelete="SET NULL"), nullable=False, ) target_type = Column( Enum( TargetType.USER, TargetType.WORLD, TargetType.ASSET, TargetType.CHAT_MESSAGE, name="target_type", create_type=False, ), nullable=False, ) target_id = Column(String(64), nullable=False) reason_category = Column( Enum( 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, ) details = Column(Text, nullable=True) status = Column( Enum( ReportStatus.OPEN, ReportStatus.IN_REVIEW, ReportStatus.RESOLVED, ReportStatus.DISMISSED, 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( ResolutionAction.WARN, ResolutionAction.SUSPEND, ResolutionAction.BAN, ResolutionAction.RESTRICT, ResolutionAction.REMOVE, ResolutionAction.RESTORE, name="resolution_action", 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]) actions = relationship( "ModerationAction", back_populates="report", cascade="all, delete-orphan", ) __table_args__ = ( Index("ix_reports_target", "target_type", "target_id"), Index("ix_reports_status", "status"), Index("ix_reports_created_at", "created_at"), ) class ModerationAction(Base): """A moderation action taken by a moderator/admin.""" __tablename__ = "moderation_actions" id = Column(Integer, primary_key=True) moderator_id = Column( Integer, ForeignKey("users.id", ondelete="SET NULL"), nullable=False, ) target_user_id = Column( Integer, ForeignKey("users.id", ondelete="SET NULL"), nullable=True, ) target_type = Column( Enum( TargetType.USER, TargetType.WORLD, TargetType.ASSET, TargetType.CHAT_MESSAGE, name="mod_target_type", create_type=False, ), nullable=False, ) target_id = Column(String(64), nullable=False) action = Column( Enum( ModerationActionType.WARN, ModerationActionType.SUSPEND, ModerationActionType.BAN, ModerationActionType.RESTRICT, ModerationActionType.REMOVE, ModerationActionType.RESTORE, 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) 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="actions") appeals = relationship("Appeal", back_populates="moderation_action") __table_args__ = ( Index("ix_moderation_actions_target", "target_type", "target_id"), Index("ix_moderation_actions_created_at", "created_at"), ) class Appeal(Base): """An appeal filed by a user against a moderation action.""" __tablename__ = "appeals" id = Column(Integer, primary_key=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.PENDING, AppealStatus.APPROVED, AppealStatus.REJECTED, 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" ) reviewer = relationship("User", foreign_keys=[reviewed_by]) __table_args__ = ( Index("ix_appeals_status", "status"), Index("ix_appeals_created_at", "created_at"), )