Compare commits

..

3 Commits

9 changed files with 349 additions and 522 deletions
+1 -1
View File
@@ -8,7 +8,7 @@ from sqlalchemy.ext.asyncio import create_async_engine
from app.config import settings
from app.database import Base
from app.models import user, session, world, asset, moderation # noqa: F401
from app.models import user, session, world, asset, report, moderation_action, appeal # noqa: F401
# this is the Alembic Config object, which provides
# access to the values within the .ini file in use.
+2 -2
View File
@@ -69,7 +69,7 @@ def upgrade() -> None:
sa.Column(
"visibility",
postgresql.ENUM(
"public", "private", "friends_only", name="world_visibility", create_type=False
"public", "private", "friends_only", name="world_visibility"
),
nullable=True,
),
@@ -89,7 +89,7 @@ def upgrade() -> None:
"asset_type",
postgresql.ENUM(
"image", "audio", "video", "document", "model_3d", "other",
name="asset_type", create_type=False,
name="asset_type",
),
nullable=False,
),
-211
View File
@@ -1,211 +0,0 @@
"""add moderation tables and extend users table
Revision ID: 0003_moderation
Revises: 0002_add_date_of_birth_to_users
Create Date: 2025-01-01 00:00:00.000000
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision: str = "0003_moderation"
down_revision: Union[str, None] = "0002_add_date_of_birth_to_users"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def _create_enum_if_not_exists(enum_name: str, values: list[str]) -> None:
"""Create a PostgreSQL ENUM type if it does not already exist."""
bind = op.get_bind()
# Check if the type already exists in pg_type
result = bind.execute(
sa.text(
"SELECT 1 FROM pg_type WHERE typname = :name"
).bindparams(name=enum_name)
).fetchone()
if result is None:
values_str = ", ".join(f"'{v}'" for v in values)
op.execute(f"CREATE TYPE {enum_name} AS ENUM ({values_str})")
def upgrade() -> None:
"""Add role, banned_until, suspended_until to users table
and create reports, moderation_actions, appeals tables."""
# --- Create ENUM types if they don't exist ---
_create_enum_if_not_exists("user_role", ["user", "moderator", "admin"])
_create_enum_if_not_exists("report_target_type", ["user", "world", "asset", "chat_message"])
_create_enum_if_not_exists("report_reason_category", [
"harassment", "hate_speech", "sexual_content",
"violence", "spam", "self_harm", "other"
])
_create_enum_if_not_exists("report_status", ["open", "in_review", "resolved", "dismissed"])
_create_enum_if_not_exists("report_resolution", ["warn", "suspend", "ban", "restrict", "remove"])
_create_enum_if_not_exists("moderation_target_type", ["user", "world", "asset", "chat_message"])
_create_enum_if_not_exists("moderation_action_type", ["warn", "suspend", "ban", "restrict", "remove", "restore"])
_create_enum_if_not_exists("appeal_status", ["pending", "approved", "rejected"])
# --- Extend users table ---
# Add role column
op.add_column(
"users",
sa.Column(
"role",
postgresql.ENUM("user", "moderator", "admin", name="user_role", create_type=False),
nullable=False,
server_default="user",
),
)
# Add banned_until column
op.add_column(
"users",
sa.Column("banned_until", sa.DateTime(timezone=True), nullable=True),
)
# Add suspended_until column
op.add_column(
"users",
sa.Column("suspended_until", sa.DateTime(timezone=True), nullable=True),
)
# --- Create reports table ---
op.create_table(
"reports",
sa.Column("id", sa.Integer(), primary_key=True),
sa.Column("reporter_id", sa.Integer(), sa.ForeignKey("users.id"), nullable=False),
sa.Column(
"target_type",
postgresql.ENUM("user", "world", "asset", "chat_message", name="report_target_type", create_type=False),
nullable=False,
),
sa.Column("target_id", sa.String(64), nullable=False),
sa.Column(
"reason_category",
postgresql.ENUM(
"harassment", "hate_speech", "sexual_content",
"violence", "spam", "self_harm", "other",
name="report_reason_category", create_type=False,
),
nullable=False,
),
sa.Column("details", sa.Text(), nullable=True),
sa.Column(
"status",
postgresql.ENUM("open", "in_review", "resolved", "dismissed", name="report_status", create_type=False),
nullable=False,
server_default="open",
),
sa.Column("resolved_by", sa.Integer(), sa.ForeignKey("users.id"), nullable=True),
sa.Column(
"resolution",
postgresql.ENUM("warn", "suspend", "ban", "restrict", "remove", name="report_resolution", create_type=False),
nullable=True,
),
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False),
sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False),
sa.Column("resolved_at", sa.DateTime(timezone=True), nullable=True),
)
# Indexes for reports
op.create_index("ix_reports_reporter_id", "reports", ["reporter_id"])
op.create_index("ix_reports_target", "reports", ["target_type", "target_id"])
op.create_index("ix_reports_status", "reports", ["status"])
# --- Create moderation_actions table ---
op.create_table(
"moderation_actions",
sa.Column("id", sa.Integer(), primary_key=True),
sa.Column("moderator_id", sa.Integer(), sa.ForeignKey("users.id"), nullable=False),
sa.Column("target_user_id", sa.Integer(), sa.ForeignKey("users.id"), nullable=True),
sa.Column(
"target_type",
postgresql.ENUM("user", "world", "asset", "chat_message", name="moderation_target_type", create_type=False),
nullable=False,
),
sa.Column("target_id", sa.String(64), nullable=False),
sa.Column(
"action",
postgresql.ENUM("warn", "suspend", "ban", "restrict", "remove", "restore", name="moderation_action_type", create_type=False),
nullable=False,
),
sa.Column("reason", sa.Text(), nullable=True),
sa.Column("report_id", sa.Integer(), sa.ForeignKey("reports.id"), nullable=True),
sa.Column("expires_at", sa.DateTime(timezone=True), nullable=True),
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False),
)
# Indexes for moderation_actions
op.create_index("ix_moderation_actions_moderator_id", "moderation_actions", ["moderator_id"])
op.create_index("ix_moderation_actions_target_user", "moderation_actions", ["target_user_id"])
op.create_index("ix_moderation_actions_target", "moderation_actions", ["target_type", "target_id"])
op.create_index("ix_moderation_actions_report_id", "moderation_actions", ["report_id"])
# --- Create appeals table ---
op.create_table(
"appeals",
sa.Column("id", sa.Integer(), primary_key=True),
sa.Column("moderation_action_id", sa.Integer(), sa.ForeignKey("moderation_actions.id"), nullable=False),
sa.Column("user_id", sa.Integer(), sa.ForeignKey("users.id"), nullable=False),
sa.Column("reason", sa.Text(), nullable=False),
sa.Column(
"status",
postgresql.ENUM("pending", "approved", "rejected", name="appeal_status", create_type=False),
nullable=False,
server_default="pending",
),
sa.Column("reviewed_by", sa.Integer(), sa.ForeignKey("users.id"), nullable=True),
sa.Column("reviewed_at", sa.DateTime(timezone=True), nullable=True),
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False),
)
# Indexes for appeals
op.create_index("ix_appeals_moderation_action_id", "appeals", ["moderation_action_id"])
op.create_index("ix_appeals_user_id", "appeals", ["user_id"])
op.create_index("ix_appeals_status", "appeals", ["status"])
def downgrade() -> None:
"""Remove moderation tables and revert users table changes."""
# Drop appeals table
op.drop_index("ix_appeals_status", table_name="appeals")
op.drop_index("ix_appeals_user_id", table_name="appeals")
op.drop_index("ix_appeals_moderation_action_id", table_name="appeals")
op.drop_table("appeals")
# Drop moderation_actions table
op.drop_index("ix_moderation_actions_report_id", table_name="moderation_actions")
op.drop_index("ix_moderation_actions_target", table_name="moderation_actions")
op.drop_index("ix_moderation_actions_target_user", table_name="moderation_actions")
op.drop_index("ix_moderation_actions_moderator_id", table_name="moderation_actions")
op.drop_table("moderation_actions")
# Drop reports table
op.drop_index("ix_reports_status", table_name="reports")
op.drop_index("ix_reports_target", table_name="reports")
op.drop_index("ix_reports_reporter_id", table_name="reports")
op.drop_table("reports")
# Remove columns from users table
op.drop_column("users", "suspended_until")
op.drop_column("users", "banned_until")
op.drop_column("users", "role")
# Drop the enum types
op.execute("DROP TYPE IF EXISTS user_role")
op.execute("DROP TYPE IF EXISTS report_target_type")
op.execute("DROP TYPE IF EXISTS report_reason_category")
op.execute("DROP TYPE IF EXISTS report_status")
op.execute("DROP TYPE IF EXISTS report_resolution")
op.execute("DROP TYPE IF EXISTS moderation_target_type")
op.execute("DROP TYPE IF EXISTS moderation_action_type")
op.execute("DROP TYPE IF EXISTS appeal_status")
+3 -1
View File
@@ -2,6 +2,8 @@ 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
from app.models.report import Report
from app.models.moderation_action import ModerationAction
from app.models.appeal import Appeal
__all__ = ["User", "Session", "World", "Asset", "Report", "ModerationAction", "Appeal"]
+67
View File
@@ -0,0 +1,67 @@
"""Appeal model for moderation actions."""
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.sql import func
from app.database import Base
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("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)
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],
back_populates="reviewed_appeals",
)
__table_args__ = (
CheckConstraint(
"status IN ('pending', 'approved', 'rejected')",
name="check_appeal_status",
),
)
-286
View File
@@ -1,286 +0,0 @@
"""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"),
)
+118
View File
@@ -0,0 +1,118 @@
"""Moderation action model."""
from datetime import datetime
from enum import Enum as PyEnum
from typing import Optional
from sqlalchemy import (
Column,
Enum,
ForeignKey,
Integer,
String,
Text,
DateTime,
func,
)
from sqlalchemy.dialects.postgresql import TIMESTAMPTZ
from sqlalchemy.orm import relationship
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):
__tablename__ = "moderation_actions"
id = Column(Integer, primary_key=True, index=True)
moderator_id = Column(
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(
Enum(TargetType, name="target_type_enum", create_type=False),
nullable=False,
)
target_id = Column(String(64), nullable=False)
action = Column(
Enum(ModerationActionType, name="moderation_action_type_enum", 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(TIMESTAMPTZ, nullable=True)
revoked_at = Column(TIMESTAMPTZ, nullable=True)
created_at = Column(
DateTime(timezone=True),
server_default=func.now(),
)
# Relationships
moderator = relationship(
"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")
appeals = relationship(
"Appeal",
back_populates="moderation_action",
cascade="all, delete-orphan",
)
+146
View File
@@ -0,0 +1,146 @@
"""Report model for user-generated moderation reports."""
from datetime import datetime
from typing import Optional
import sqlalchemy as sa
from sqlalchemy import (
Column,
DateTime,
Enum,
ForeignKey,
Integer,
String,
Text,
)
from sqlalchemy.dialects.postgresql import TIMESTAMPTZ
from sqlalchemy.orm import relationship
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):
__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.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,
),
nullable=False,
default=ReportStatus.OPEN,
)
resolved_by = Column(
Integer,
ForeignKey("users.id", ondelete="SET NULL"),
nullable=True,
)
resolution = Column(
Enum(
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
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",
)
+12 -21
View File
@@ -1,4 +1,4 @@
from datetime import date, datetime
from datetime import date
from typing import Optional
from sqlalchemy import Column, Integer, String, Boolean, DateTime, Date, Enum
@@ -8,6 +8,13 @@ from sqlalchemy.sql import func
from app.database import Base
class UserRole:
"""Valid user roles."""
USER = "user"
MODERATOR = "moderator"
ADMIN = "admin"
class User(Base):
__tablename__ = "users"
@@ -18,9 +25,9 @@ class User(Base):
date_of_birth = Column(Date, nullable=True)
is_active = Column(Boolean, default=True)
role = Column(
Enum("user", "moderator", "admin", name="user_role", create_type=False),
default="user",
Enum(UserRole.USER, UserRole.MODERATOR, UserRole.ADMIN, name="user_role", create_type=False),
nullable=False,
default=UserRole.USER,
)
banned_until = Column(DateTime(timezone=True), nullable=True)
suspended_until = Column(DateTime(timezone=True), nullable=True)
@@ -30,32 +37,16 @@ class User(Base):
sessions = relationship("Session", back_populates="user", cascade="all, delete-orphan")
worlds = relationship("World", back_populates="owner")
assets = relationship("Asset", back_populates="owner")
reported_by = relationship(
"Report",
foreign_keys="Report.reporter_id",
back_populates="reporter",
)
resolved_reports = relationship(
"Report",
foreign_keys="Report.resolved_by",
back_populates="resolver",
)
moderation_actions = relationship(
moderation_actions_given = relationship(
"ModerationAction",
foreign_keys="ModerationAction.moderator_id",
back_populates="moderator",
)
target_moderation_actions = relationship(
moderation_actions_received = relationship(
"ModerationAction",
foreign_keys="ModerationAction.target_user_id",
back_populates="target_user",
)
appeals = relationship("Appeal", back_populates="user")
reviewed_appeals = relationship(
"Appeal",
foreign_keys="Appeal.reviewed_by",
back_populates="reviewer",
)
@property
def is_adult(self) -> bool: