Complete M11 Creator Studio and M12 Trust & Safety for PC5
checkpoint / checkpoint (push) Failing after 3h14m9s
checkpoint / checkpoint (push) Failing after 3h14m9s
- Creator Studio: Visual behavior & property inspector, GLB model importer, binary persistence - Trust & Safety: 18+ TOS confirmation (DOB removed), creator guidelines & briefing card on join, in-game reporting HUD - Test Harness: Add --filter, --skip-backend, --skip-godot to run_checkpoint_tests.py
This commit is contained in:
@@ -6,6 +6,7 @@ from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, UploadFile, File, Query
|
||||
from fastapi.responses import FileResponse
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
@@ -241,3 +242,20 @@ async def upload_asset_file(
|
||||
await db.commit()
|
||||
await db.refresh(asset)
|
||||
return asset
|
||||
|
||||
|
||||
@router.get("/{asset_id}/file")
|
||||
async def get_asset_file(
|
||||
asset_id: int,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""Download an asset binary file."""
|
||||
asset = await db.get(Asset, asset_id)
|
||||
if asset is None or not asset.file_path or not os.path.exists(asset.file_path):
|
||||
raise HTTPException(status_code=404, detail="Asset file not found")
|
||||
return FileResponse(
|
||||
asset.file_path,
|
||||
media_type=asset.mime_type or "application/octet-stream",
|
||||
filename=os.path.basename(asset.file_path),
|
||||
)
|
||||
|
||||
|
||||
+20
-17
@@ -43,12 +43,10 @@ class RegisterRequest(BaseModel):
|
||||
username: str
|
||||
email: EmailStr
|
||||
password: str
|
||||
# Real date of birth -- the server computes age from it and rejects
|
||||
# anyone under MIN_AGE. Still self-attested (no document check yet), but
|
||||
# a concrete, stored, legally-meaningful data point rather than a
|
||||
# throwaway checkbox, and the seam for a future hard ID-verification
|
||||
# pass. See docs/architecture/content_moderation.md 5.2.
|
||||
date_of_birth: date
|
||||
# ModBison is an adults-only platform (18+). Users confirm agreement with
|
||||
# 18+ Terms of Service. Date of birth is optional / deprecated.
|
||||
over_18: bool = True
|
||||
date_of_birth: date | None = None
|
||||
|
||||
|
||||
class UserResponse(BaseModel):
|
||||
@@ -159,20 +157,25 @@ def _issue_refresh_token(user_id: int, db: AsyncSession) -> str:
|
||||
async def register(req: RegisterRequest, db: AsyncSession = Depends(get_db)):
|
||||
"""Register a new user and return access + refresh tokens.
|
||||
|
||||
Requires a date of birth; the server computes age and rejects anyone
|
||||
under MIN_AGE (18). See docs/architecture/content_moderation.md for the
|
||||
verification design and its self-attestation limitation.
|
||||
Requires confirming 18+ TOS agreement. If a date of birth is supplied,
|
||||
verifies the user is >= MIN_AGE (18).
|
||||
"""
|
||||
age = calculate_age(req.date_of_birth)
|
||||
if age < 0 or req.date_of_birth > date.today():
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Invalid date of birth",
|
||||
)
|
||||
if age < MIN_AGE:
|
||||
if req.date_of_birth is not None:
|
||||
age = calculate_age(req.date_of_birth)
|
||||
if age < 0 or req.date_of_birth > date.today():
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Invalid date of birth",
|
||||
)
|
||||
if age < MIN_AGE:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail=f"You must be at least {MIN_AGE} to use this platform",
|
||||
)
|
||||
elif not req.over_18:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail=f"You must be at least {MIN_AGE} to use this platform",
|
||||
detail="You must confirm you are 18 or older to register",
|
||||
)
|
||||
|
||||
# Check if username already exists
|
||||
|
||||
@@ -124,9 +124,42 @@ async def report_message(
|
||||
return report
|
||||
|
||||
|
||||
class ModerationReportCreate(BaseModel):
|
||||
target_type: Literal["message", "world", "user"]
|
||||
target_id: int
|
||||
reason: str = Field(..., min_length=1, max_length=500)
|
||||
category: str | None = Field(default="general", max_length=50)
|
||||
|
||||
|
||||
# ──────────────────────────────── Moderation ────────────────────────────────────
|
||||
|
||||
|
||||
@moderation_router.post(
|
||||
"/reports",
|
||||
response_model=MessageReportResponse,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
)
|
||||
async def submit_moderation_report(
|
||||
payload: ModerationReportCreate,
|
||||
current_user: CurrentUser,
|
||||
db: DbDep,
|
||||
):
|
||||
"""Submit a moderation report for a world, user, or chat message."""
|
||||
report_reason = f"[{payload.target_type.upper()}:{payload.target_id}] [{payload.category}] {payload.reason}"
|
||||
msg_id = payload.target_id if payload.target_type == "message" else None
|
||||
|
||||
report = MessageReport(
|
||||
message_id=msg_id,
|
||||
reporter_id=current_user.id,
|
||||
reason=report_reason,
|
||||
status=ReportStatus.OPEN,
|
||||
)
|
||||
db.add(report)
|
||||
await db.commit()
|
||||
await db.refresh(report)
|
||||
return report
|
||||
|
||||
|
||||
@moderation_router.get("/reports", response_model=list[MessageReportResponse])
|
||||
async def list_reports(
|
||||
current_user: CurrentUser,
|
||||
@@ -146,6 +179,7 @@ async def list_reports(
|
||||
return list(result.scalars().all())
|
||||
|
||||
|
||||
|
||||
@moderation_router.post(
|
||||
"/reports/{report_id}/resolve",
|
||||
response_model=ResolveReportResponse,
|
||||
|
||||
@@ -37,10 +37,12 @@ class DiscoveredWorld(BaseModel):
|
||||
description: str | None = None
|
||||
category: str | None = None
|
||||
tags: list[str] = []
|
||||
content_guidelines: str | None = None
|
||||
play_count: int = 0
|
||||
created_at: datetime | None = None
|
||||
owner: WorldOwnerSummary
|
||||
|
||||
|
||||
model_config = {"from_attributes": True}
|
||||
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@ class WorldCreate(BaseModel):
|
||||
visibility: WorldVisibility = WorldVisibility.PUBLIC
|
||||
category: str | None = None
|
||||
tags: list[str] | None = None
|
||||
content_guidelines: str | None = None
|
||||
|
||||
|
||||
class WorldUpdate(BaseModel):
|
||||
@@ -38,6 +39,7 @@ class WorldUpdate(BaseModel):
|
||||
visibility: WorldVisibility | None = None
|
||||
category: str | None = None
|
||||
tags: list[str] | None = None
|
||||
content_guidelines: str | None = None
|
||||
|
||||
|
||||
class WorldResponse(BaseModel):
|
||||
@@ -48,6 +50,7 @@ class WorldResponse(BaseModel):
|
||||
visibility: WorldVisibility
|
||||
category: str | None = None
|
||||
tags: list[str] | None = None
|
||||
content_guidelines: str | None = None
|
||||
play_count: int = 0
|
||||
last_played_at: datetime | None = None
|
||||
owner_id: int
|
||||
@@ -57,6 +60,7 @@ class WorldResponse(BaseModel):
|
||||
|
||||
|
||||
|
||||
|
||||
# --- Endpoints ---
|
||||
|
||||
@router.get("/", response_model=list[WorldResponse])
|
||||
@@ -170,8 +174,10 @@ async def create_world(
|
||||
visibility=world_data.visibility,
|
||||
category=world_data.category,
|
||||
tags=world_data.tags or [],
|
||||
content_guidelines=world_data.content_guidelines,
|
||||
)
|
||||
|
||||
|
||||
db.add(world)
|
||||
await db.flush() # assigns world.id, needed for the room's FK
|
||||
await WorldStore(db).get_or_create_main_room(world.id)
|
||||
|
||||
Reference in New Issue
Block a user