Add the trust and safety subsystem and the App Store compliance work

Implements the moderation and consent obligations a social platform carries,
so the web version and any client that speaks to it enforce the same rules.

Moderation core (services/moderation/, database/moderation.py): a reportable
target registry, the content filter and its choke points, the report queue with
atomic resolution, enforcement actions, consent tracking, maturity gating, and
account deletion with a grace window.

Surfaces: POST /reports plus the member report list, /admin/moderation and the
per-report admin view, /workspaces, terms acceptance at /auth/terms, consent and
account deletion under /profile, the report button and dialog partials, the
maturity gate, and the moderation stylesheet and ReportDialog client.

Every user-generated surface stays reportable by construction: new content tables
are registered in REPORTABLE_TARGETS or listed in UNREPORTABLE_TABLES with a
reason, and the registry test fails the suite on anything left unclassified.

Docs: community guidelines, content moderation, intellectual property, privacy,
terms, contact, and the admin-only moderation operations page, plus the
moderation API group and the Devii moderation actions.

Compliance record: applecomp.md is the requirement register, applechanges.md the
gap analysis against this codebase, and appleimpl.md the implementation design
they resolve to.

Tests cover the report flow, admin moderation, consent, account deletion, terms
acceptance, workspaces, and the registry invariant across the unit, api, and e2e
tiers.
This commit is contained in:
2026-08-09 00:18:20 +02:00
parent 68c2bbe387
commit 8e9d3fad98
348 changed files with 10633 additions and 238 deletions
+1
View File
@@ -29,6 +29,7 @@ def _seed_user(prefix):
{
"uid": uid,
"username": f"{prefix}_{uid[:8]}",
"terms_version": "1",
"email": f"{uid[:8]}@seed.devplace",
"password_hash": "x",
"role": "Member",
+49 -102
View File
@@ -1,120 +1,67 @@
# retoor <retoor@molodetz.nl>
import re
from tests.conftest import BASE_URL
FOLLOW = "form[action='/follow/bob_test'] button"
UNFOLLOW = "form[action='/follow/unfollow/bob_test'] button"
from playwright.sync_api import expect
from tests.e2e.post import create_post
from tests.conftest import BASE_URL
CONSENT = "ai_third_party"
def test_profile_shows_online_presence(alice):
page, user = alice
def _privacy_tab(page, username):
page.goto(
f"{BASE_URL}/profile/{user['username']}", wait_until="domcontentloaded"
f"{BASE_URL}/profile/{username}?tab=privacy", wait_until="domcontentloaded"
)
presence = page.locator(".profile-presence")
expect(presence).to_be_visible()
expect(presence).to_have_class(re.compile(r"\bonline\b"))
expect(presence).to_have_text("online")
page.locator(".privacy-panel").wait_for(state="visible")
def test_nav_avatar_shows_online_presence_dot(alice):
page, _ = alice
page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
nav_dot = page.locator(".topnav-user .presence-dot")
expect(nav_dot).to_be_visible()
expect(nav_dot).to_have_class(re.compile(r"\bonline\b"))
def test_the_owner_toggles_a_consent_from_the_privacy_tab(bob):
page, user = bob
_privacy_tab(page, user["username"])
row = page.locator(f".privacy-panel tr:has(input[value='{CONSENT}'])")
button = row.locator("button[type='submit']")
original = (button.inner_text() or "").strip()
flipped = "Withdraw" if original == "Grant" else "Grant"
try:
button.click()
expect(row.locator("button[type='submit']")).to_have_text(
flipped, timeout=10000
)
finally:
_privacy_tab(page, user["username"])
if (row.locator("button[type='submit']").inner_text() or "").strip() != original:
row.locator("button[type='submit']").click()
expect(row.locator("button[type='submit']")).to_have_text(
original, timeout=10000
)
def _post_comment(page, body):
textarea = page.locator(".comment-form textarea[name='content']")
textarea.fill(body)
page.locator(".comment-form button:has-text('Post')").click()
expect(page.locator(f".comment-text:has-text('{body}')")).to_be_visible()
def test_activity_comment_card_links_to_parent_post(alice):
page, user = alice
create_post(page, "devlog", "Post for activity comment link")
post_url = page.url
_post_comment(page, "Activity clickable comment body")
page.goto(
f"{BASE_URL}/profile/{user['username']}?tab=activity",
wait_until="domcontentloaded",
)
card_link = page.locator(
".activity-card.card-link-host a.card-link[href*='#comment-']"
).first
expect(card_link).to_have_count(1)
href = card_link.get_attribute("href")
assert "/posts/" in href and "#comment-" in href
card_link.click()
page.wait_for_url("**/posts/**", wait_until="domcontentloaded")
def test_the_owner_sees_every_privacy_control(bob):
page, user = bob
_privacy_tab(page, user["username"])
panel = page.locator(".privacy-panel")
expect(panel.locator(f"form[action='/profile/{user['username']}/consent']").first).to_be_attached()
expect(
page.locator(".comment-text:has-text('Activity clickable comment body')")
panel.locator(f"form[action='/profile/{user['username']}/mature-content']")
).to_be_visible()
expect(panel.locator("a:has-text('Delete account')")).to_be_visible()
def test_activity_post_card_links_to_post(alice):
page, user = alice
create_post(page, "showcase", "Post for activity post link", title="Activity Post Title")
def test_an_admin_reads_the_state_but_gets_no_privacy_controls(alice, seeded_db):
admin_page, _ = alice
member = seeded_db["bob"]
_privacy_tab(admin_page, member["username"])
panel = admin_page.locator(".privacy-panel")
expect(panel).to_contain_text("Only the account holder can change this.")
expect(panel).to_contain_text("Only the account holder can change this preference.")
expect(panel.locator("form[action$='/consent']")).to_have_count(0)
expect(panel.locator("form[action$='/mature-content']")).to_have_count(0)
expect(panel.locator("a:has-text('Delete account')")).to_have_count(0)
def test_a_stranger_never_reaches_the_privacy_panel(bob):
page, _ = bob
page.goto(
f"{BASE_URL}/profile/{user['username']}?tab=activity",
wait_until="domcontentloaded",
f"{BASE_URL}/profile/alice_test?tab=privacy", wait_until="domcontentloaded"
)
post_link = page.locator(
".activity-card.card-link-host a.card-link[href^='/posts/']:not([href*='#comment-'])"
).first
expect(post_link).to_have_count(1)
post_link.click()
page.wait_for_url("**/posts/**", wait_until="domcontentloaded")
expect(page.locator(".post-detail-title:has-text('Activity Post Title')")).to_be_visible()
def test_activity_inner_content_link_stays_clickable(alice, bob):
page, user = alice
create_post(page, "random", "Post for activity mention comment")
_post_comment(page, "Ping @bob_test in activity")
page.goto(
f"{BASE_URL}/profile/{user['username']}?tab=activity",
wait_until="domcontentloaded",
)
mention = page.locator(
".activity-card.card-link-host .activity-content a[href='/profile/bob_test']"
).first
expect(mention).to_have_count(1)
mention.click()
page.wait_for_url("**/profile/bob_test", wait_until="domcontentloaded")
def test_follow_then_unfollow(alice):
page, _ = alice
page.goto(f"{BASE_URL}/profile/bob_test", wait_until="domcontentloaded")
if page.is_visible(UNFOLLOW):
page.click(UNFOLLOW)
page.wait_for_url("**/profile/bob_test", wait_until="domcontentloaded")
assert page.is_visible(FOLLOW)
page.click(FOLLOW)
page.wait_for_url("**/profile/bob_test", wait_until="domcontentloaded")
assert page.is_visible(UNFOLLOW)
page.click(UNFOLLOW)
page.wait_for_url("**/profile/bob_test", wait_until="domcontentloaded")
assert page.is_visible(FOLLOW)
def test_profile_viewing_other_user(bob, alice):
page_b, user_b = bob
page_b.goto(f"{BASE_URL}/profile/alice_test", wait_until="domcontentloaded")
assert page_b.is_visible("text=alice_test")
def test_profile_message_button(bob, alice):
page_b, _ = bob
page_b.goto(f"{BASE_URL}/profile/alice_test", wait_until="domcontentloaded")
message_btn = page_b.locator("a:has-text('Send Message')")
assert message_btn.is_visible()
expect(page.locator(".privacy-panel")).to_have_count(0)
expect(page.locator("form[action='/profile/alice_test/consent']")).to_have_count(0)
+9
View File
@@ -18,6 +18,8 @@ def _signup_api_auth(password="secret123"):
"email": email,
"password": password,
"confirm_password": password,
"birth_date": "1990-01-01",
"accept_terms": "1",
},
allow_redirects=True,
)
@@ -67,6 +69,8 @@ def _register_session(prefix):
"email": f"{name}@t.dev",
"password": "secret123",
"confirm_password": "secret123",
"birth_date": "1990-01-01",
"accept_terms": "1",
},
allow_redirects=True,
)
@@ -77,6 +81,7 @@ def _seed_gists(count):
{
"uid": owner,
"username": f"pag_{owner[:8]}",
"terms_version": "1",
"email": f"{owner[:8]}@test.devplace",
"password_hash": "x",
"role": "Member",
@@ -142,6 +147,7 @@ def _seed_searchable_gists(language="python"):
{
"uid": owner,
"username": f"gsrch_{owner[:8]}",
"terms_version": "1",
"email": f"{owner[:8]}@test.devplace",
"password_hash": "x",
"role": "Member",
@@ -189,6 +195,8 @@ def _signup_media(prefix="media"):
"email": f"{name}@test.devplace",
"password": "secret123",
"confirm_password": "secret123",
"birth_date": "1990-01-01",
"accept_terms": "1",
},
allow_redirects=True,
)
@@ -320,6 +328,7 @@ def _seed_owner():
{
"uid": uid,
"username": f"seo_{uid[:8]}",
"terms_version": "1",
"email": f"{uid[:8]}@seo.test",
"password_hash": "x",
"role": "Member",