Compare commits

..

3 Commits

4 changed files with 287 additions and 67 deletions
+49 -16
View File
@@ -1,6 +1,18 @@
"""Appeal model for moderation actions.""" """Appeal model for moderation actions."""
from sqlalchemy import Column, Integer, String, Text, DateTime, ForeignKey, Enum from datetime import datetime
from typing import Optional
from sqlalchemy import (
Column,
Enum,
ForeignKey,
Integer,
String,
Text,
DateTime,
CheckConstraint,
)
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
from sqlalchemy.sql import func from sqlalchemy.sql import func
@@ -10,25 +22,46 @@ from app.database import Base
class Appeal(Base): class Appeal(Base):
__tablename__ = "appeals" __tablename__ = "appeals"
id = Column(Integer, primary_key=True) id = Column(Integer, primary_key=True, index=True)
moderation_action_id = Column( moderation_action_id = Column(
Integer, ForeignKey("moderation_actions.id"), nullable=False Integer,
) ForeignKey("moderation_actions.id", ondelete="CASCADE"),
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
reason = Column(Text, nullable=False)
status = Column(
Enum(
"pending", "approved", "rejected",
name="appeal_status_enum",
),
default="pending",
nullable=False, nullable=False,
) )
reviewed_by = Column(Integer, ForeignKey("users.id"), nullable=True) user_id = Column(
Integer,
ForeignKey("users.id", ondelete="CASCADE"),
nullable=False,
)
reason = Column(Text, nullable=False)
status = Column(
Enum("pending", "approved", "rejected", name="appeal_status", create_type=False),
nullable=False,
default="pending",
)
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(DateTime(timezone=True), server_default=func.now()) created_at = Column(
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")
user = relationship("User", back_populates="appeals") reviewer = relationship(
reviewer = relationship("User", foreign_keys=[reviewed_by]) "User",
foreign_keys=[reviewed_by],
back_populates="reviewed_appeals",
)
__table_args__ = (
CheckConstraint(
"status IN ('pending', 'approved', 'rejected')",
name="check_appeal_status",
),
)
+96 -13
View File
@@ -1,35 +1,118 @@
"""Moderation action model."""
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 Column, Enum, ForeignKey, Integer, String, Text, DateTime from sqlalchemy import (
Column,
Enum,
ForeignKey,
Integer,
String,
Text,
DateTime,
func,
)
from sqlalchemy.dialects.postgresql import TIMESTAMPTZ
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
from sqlalchemy.sql import func
from app.database import Base from app.database import Base
class TargetType(str, PyEnum):
"""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"
SUSPEND = "suspend"
BAN = "ban"
RESTRICT = "restrict"
REMOVE = "remove"
RESTORE = "restore"
class ModerationAction(Base): class ModerationAction(Base):
__tablename__ = "moderation_actions" __tablename__ = "moderation_actions"
id = Column(Integer, primary_key=True) id = Column(Integer, primary_key=True, index=True)
moderator_id = Column(Integer, ForeignKey("users.id"), nullable=False) moderator_id = Column(
target_user_id = Column(Integer, ForeignKey("users.id"), nullable=False) Integer,
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("user", "world", "asset", "chat_message", name="target_type_enum"), Enum(TargetType, name="target_type_enum", create_type=False),
nullable=False, nullable=False,
) )
target_id = Column(String(64), nullable=False) target_id = Column(String(64), nullable=False)
action = Column( action = Column(
Enum("warn", "suspend", "ban", "restrict", "remove", "restore", name="action_enum"), Enum(ModerationActionType, name="moderation_action_type_enum", create_type=False),
nullable=False, nullable=False,
) )
reason = Column(Text, nullable=True) reason = Column(Text, nullable=True)
report_id = Column(Integer, ForeignKey("reports.id"), nullable=True) report_id = Column(
expires_at = Column(DateTime(timezone=True), nullable=True) Integer,
revoked_at = Column(DateTime(timezone=True), nullable=True) ForeignKey("reports.id", ondelete="SET NULL"),
created_at = Column(DateTime(timezone=True), server_default=func.now()) nullable=True,
)
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("User", foreign_keys=[moderator_id]) moderator = relationship(
target_user = relationship("User", foreign_keys=[target_user_id]) "User",
foreign_keys=[moderator_id],
back_populates="moderation_actions_given",
)
target_user = relationship(
"User",
foreign_keys=[target_user_id],
back_populates="moderation_actions_received",
)
report = relationship("Report", back_populates="moderation_actions") report = relationship("Report", back_populates="moderation_actions")
appeals = relationship(
"Appeal",
back_populates="moderation_action",
cascade="all, delete-orphan",
)
+120 -34
View File
@@ -1,60 +1,146 @@
"""Report model for Trust & Safety.""" """Report model for user-generated moderation reports."""
from datetime import datetime
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,
Enum as SAEnum,
) )
from sqlalchemy.dialects.postgresql import TIMESTAMPTZ
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
from sqlalchemy.sql import func
from app.database import Base from app.database import Base
class ReportStatus:
"""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"
WORLD = "world"
ASSET = "asset"
CHAT_MESSAGE = "chat_message"
class ReasonCategory:
"""Categories for why something was reported."""
HARASSMENT = "harassment"
HATE_SPEECH = "hate_speech"
SEXUAL_CONTENT = "sexual_content"
VIOLENCE = "violence"
SPAM = "spam"
SELF_HARM = "self_harm"
OTHER = "other"
class Report(Base): class Report(Base):
__tablename__ = "reports" __tablename__ = "reports"
id = Column(Integer, primary_key=True) id = Column(Integer, primary_key=True, index=True)
reporter_id = Column(Integer, ForeignKey("users.id"), nullable=False) reporter_id = Column(
target_type = SAEnum("user", "world", "asset", "chat_message", name="target_type_enum", nullable=False) Integer,
ForeignKey("users.id", ondelete="CASCADE"),
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) target_id = Column(String(64), nullable=False)
reason_category = SAEnum( reason_category = Column(
"harassment", Enum(
"hate_speech", ReasonCategory.HARASSMENT,
"sexual_content", ReasonCategory.HATE_SPEECH,
"violence", ReasonCategory.SEXUAL_CONTENT,
"spam", ReasonCategory.VIOLENCE,
"self_harm", ReasonCategory.SPAM,
"other", ReasonCategory.SELF_HARM,
name="reason_category_enum", ReasonCategory.OTHER,
name="reason_category",
create_type=False,
),
nullable=False, nullable=False,
) )
details = Column(Text, nullable=True) details = Column(Text, nullable=True)
status = SAEnum( status = Column(
"open", Enum(
"in_review", ReportStatus.OPEN,
"resolved", ReportStatus.IN_REVIEW,
"dismissed", ReportStatus.RESOLVED,
name="report_status_enum", ReportStatus.DISMISSED,
default="open", name="report_status",
create_type=False,
),
nullable=False, nullable=False,
default=ReportStatus.OPEN,
) )
resolved_by = Column(Integer, ForeignKey("users.id"), nullable=True) resolved_by = Column(
resolution = SAEnum( Integer,
"action_taken", ForeignKey("users.id", ondelete="SET NULL"),
"action_not_needed",
"duplicate",
name="resolution_enum",
nullable=True, nullable=True,
) )
created_at = Column(DateTime(timezone=True), server_default=func.now()) resolution = Column(
updated_at = Column(DateTime(timezone=True), onupdate=func.now()) Enum(
resolved_at = Column(DateTime(timezone=True), nullable=True) Resolution.ACTION_TAKEN,
Resolution.NO_ACTION,
name="resolution",
create_type=False,
),
nullable=True,
)
created_at = Column(
TIMESTAMPTZ,
nullable=False,
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])
resolved_by_user = relationship("User", foreign_keys=[resolved_by]) 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",
)
+22 -4
View File
@@ -1,12 +1,20 @@
from datetime import date from datetime import date
from typing import Optional
from sqlalchemy import Boolean, Column, Date, DateTime, Enum, Integer, String 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
from app.database import Base from app.database import Base
class UserRole:
"""Valid user roles."""
USER = "user"
MODERATOR = "moderator"
ADMIN = "admin"
class User(Base): class User(Base):
__tablename__ = "users" __tablename__ = "users"
@@ -17,8 +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("user", "moderator", "admin", name="user_role", default="user"), Enum(UserRole.USER, UserRole.MODERATOR, UserRole.ADMIN, name="user_role", create_type=False),
default="user", nullable=False,
default=UserRole.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)
@@ -28,7 +37,16 @@ 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")
appeals = relationship("Appeal", back_populates="user") moderation_actions_given = relationship(
"ModerationAction",
foreign_keys="ModerationAction.moderator_id",
back_populates="moderator",
)
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: