Compare commits

..

2 Commits

7 changed files with 35 additions and 361 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, report, moderation_action, appeal # noqa: F401
from app.models import user, session, world, asset # noqa: F401
# this is the Alembic Config object, which provides
# access to the values within the .ini file in use.
+1 -4
View File
@@ -2,8 +2,5 @@ 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.report import Report
from app.models.moderation_action import ModerationAction
from app.models.appeal import Appeal
__all__ = ["User", "Session", "World", "Asset", "Report", "ModerationAction", "Appeal"]
__all__ = ["User", "Session", "World", "Asset"]
-67
View File
@@ -1,67 +0,0 @@
"""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",
),
)
+32
View File
@@ -3,6 +3,10 @@ from sqlalchemy.dialects.postgresql import ENUM
from sqlalchemy.orm import relationship
from sqlalchemy.sql import func
import enum
import json
from typing import Optional, List
from pydantic import BaseModel as PydanticBaseModel
from app.database import Base
@@ -38,3 +42,31 @@ class Asset(Base):
owner = relationship("User", back_populates="assets")
world = relationship("World", back_populates="assets")
class GLTFMetadata(PydanticBaseModel):
"""Pydantic schema for glTF asset metadata."""
mesh_count: Optional[int] = None
vertex_count: Optional[int] = None
has_animation: bool = False
texture_refs: Optional[List[str]] = None
tags: Optional[List[str]] = None
model_config = {"from_attributes": True}
def parse_metadata_json(metadata_text: Optional[str]) -> dict:
"""Parse the metadata_json text field into a dict, handling None gracefully."""
if not metadata_text:
return {}
try:
return json.loads(metadata_text)
except (json.JSONDecodeError, TypeError):
return {}
def serialize_metadata(metadata: dict) -> Optional[str]:
"""Serialize metadata dict to JSON string for storage."""
if not metadata:
return None
return json.dumps(metadata)
-118
View File
@@ -1,118 +0,0 @@
"""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
@@ -1,146 +0,0 @@
"""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",
)
+1 -25
View File
@@ -1,20 +1,13 @@
from datetime import date
from typing import Optional
from sqlalchemy import Column, Integer, String, Boolean, DateTime, Date, Enum
from sqlalchemy import Column, Integer, String, Boolean, DateTime, Date
from sqlalchemy.orm import relationship
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"
@@ -24,29 +17,12 @@ class User(Base):
hashed_password = Column(String(255), nullable=False)
date_of_birth = Column(Date, nullable=True)
is_active = Column(Boolean, default=True)
role = Column(
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)
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
sessions = relationship("Session", back_populates="user", cascade="all, delete-orphan")
worlds = relationship("World", back_populates="owner")
assets = relationship("Asset", back_populates="owner")
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
def is_adult(self) -> bool: