Skip to content

Honor group-admin moderation deletes in timeline ingest#874

Merged
mubarakcoded merged 3 commits into
masterfrom
fix/admin-delete-timeline-ingest
Jul 17, 2026
Merged

Honor group-admin moderation deletes in timeline ingest#874
mubarakcoded merged 3 commits into
masterfrom
fix/admin-delete-timeline-ingest

Conversation

@mubarakcoded

@mubarakcoded mubarakcoded commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Timeline ingest previously honored a delete only when its author matched the target message's author, so a group admin deleting another member's message tombstoned it locally while every other member silently ignored it — and the deleter's own store resurrected the message on reprojection.

Design

The moderation verdict is evaluated once, at ingest, against the signed MLS group state, and persisted with the event:

  • app_events.moderation_grant (migration 0027) is stamped true only for a delete whose authenticated sender is in the group's admin set (admin_pubkeys, mirrored from signed group state) and whose group is not a direct (two-member, unnamed) conversation. Lookup failures fail closed to self-retraction semantics.
  • The timeline acceptance rule becomes: honor a delete when delete.sender == row.sender OR the delete carries the persisted grant. Reaction retraction keeps its sender-equality forged-delete guard — moderation authority applies to messages, not reaction removal.
  • The verdict is frozen at the event's first record (the upsert keeps the stored value), so reprojection and relay echoes arriving after an admin-set change can never flip an honored tombstone.
  • The grant is stamped on both chokepoints: inbound sync ingest and the local-send projection, so the moderator's own store keeps the tombstone across reprojection too.

Verification

  • just fast-ci green (fmt, naming gate, check, clippy, workspace-wide including OTLP builds).
  • cargo test -p storage-sqlite: 213 passed, including new coverage for admin tombstone via incremental projection, out-of-order delete-before-target delivery, full timeline rebuild preserving the moderated tombstone, and the reaction guard staying sender-equality.
  • cargo test -p marmot-app: 331 passed, including grant-helper cases (direct conversations never grant, whitespace-only names still count as unnamed, named two-member groups do grant, empty admin set grants nothing).

Closes #873


Open in Stage

Summary by CodeRabbit

  • New Features
    • Added persisted moderation authorization for delete events, so authorized admins can tombstone other members’ messages consistently across local projections and timeline rebuilds.
  • Bug Fixes
    • Prevented non-authorized deletes from being treated as moderation-granted, including for cross-sender delete cases.
    • Improved moderation-grant “freeze” and “refresh” behavior so grants stay stable when re-received but can be recomputed correctly.
  • Tests
    • Expanded coverage for admin authorization rules, direct-conversation exclusions, out-of-order handling, full rebuilds, and moderation-grant lifecycle.

@coderabbitai

coderabbitai Bot commented Jul 15, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 1d98fc8c-aaf2-4bab-8c1f-56d89fe07b4f

📥 Commits

Reviewing files that changed from the base of the PR and between b5761cb and f5757ff.

📒 Files selected for processing (14)
  • crates/marmot-app/src/client/mod.rs
  • crates/marmot-app/src/client/projection.rs
  • crates/marmot-app/src/client/sync.rs
  • crates/marmot-app/src/conversions.rs
  • crates/marmot-app/src/groups.rs
  • crates/marmot-app/src/lib.rs
  • crates/marmot-app/src/projection/tests.rs
  • crates/marmot-app/src/tests.rs
  • crates/storage-sqlite/src/account_projection/tests.rs
  • crates/storage-sqlite/src/chat_list/tests.rs
  • crates/storage-sqlite/src/migrations.rs
  • crates/storage-sqlite/src/migrations/0027_app_event_moderation_grant.rs
  • crates/storage-sqlite/src/timeline.rs
  • crates/storage-sqlite/src/timeline/tests.rs
🚧 Files skipped from review as they are similar to previous changes (13)
  • crates/marmot-app/src/projection/tests.rs
  • crates/storage-sqlite/src/migrations/0027_app_event_moderation_grant.rs
  • crates/marmot-app/src/conversions.rs
  • crates/marmot-app/src/client/sync.rs
  • crates/storage-sqlite/src/chat_list/tests.rs
  • crates/storage-sqlite/src/account_projection/tests.rs
  • crates/marmot-app/src/lib.rs
  • crates/marmot-app/src/groups.rs
  • crates/storage-sqlite/src/migrations.rs
  • crates/marmot-app/src/tests.rs
  • crates/storage-sqlite/src/timeline/tests.rs
  • crates/marmot-app/src/client/projection.rs
  • crates/storage-sqlite/src/timeline.rs

Walkthrough

This PR adds admin-moderated message deletion. A moderation_grant field is added to app-event projections and storage, computed for eligible group-admin deletes, persisted through SQLite, and honored during timeline projection while excluding unnamed direct conversations.

Changes

Admin moderation delete support

Layer / File(s) Summary
Moderation grant contract and storage schema
crates/marmot-app/src/lib.rs, crates/marmot-app/src/conversions.rs, crates/storage-sqlite/src/timeline.rs, crates/storage-sqlite/src/migrations*
Adds and migrates the moderation_grant field, maps it through conversions, and supports stored-event deserialization.
Group moderation grant policy
crates/marmot-app/src/groups.rs
Grants moderation to group admins except in unnamed direct conversations, with policy coverage.
Client projection and sync wiring
crates/marmot-app/src/client/*, crates/marmot-app/src/lib.rs
Computes and propagates moderation grants for local and synchronized deletes and adds refreshing account-event recording.
Timeline persistence and moderated tombstones
crates/storage-sqlite/src/timeline.rs
Persists frozen or refreshed grants, loads them through projection queries, and honors authorized cross-sender deletes.
Moderation tests and fixture updates
crates/marmot-app/src/*/tests.rs, crates/storage-sqlite/src/*/tests.rs
Covers moderated tombstones, ordering, rebuilds, grant replacement, and updated event fixtures.

Estimated code review effort: 4 (Complex) | ~45 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: enabling group-admin moderation deletes in timeline ingest.
Linked Issues check ✅ Passed The changes honor admin deletes at ingest, exclude direct two-member conversations, keep reaction delete sender checks, and preserve moderated tombstones.
Out of Scope Changes check ✅ Passed The added migration, persistence, helper logic, and tests all support the delete-moderation objective without obvious unrelated changes.
Docstring Coverage ✅ Passed Docstring coverage is 87.23% which is sufficient. The required threshold is 80.00%.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/admin-delete-timeline-ingest

Comment @coderabbitai help to get the list of available commands.

@stage-review

stage-review Bot commented Jul 15, 2026

Copy link
Copy Markdown

@jgmontoya

Copy link
Copy Markdown
Contributor

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jul 15, 2026

Copy link
Copy Markdown
✅ Action performed

Full review finished.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
crates/marmot-app/src/client/mod.rs (1)

1108-1190: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Stale pre-send moderation grant can be locked in

record_local_app_event_projection runs before sync_runtime_groups().await? and again after publish for the same event.id, but crates/storage-sqlite/src/timeline.rs keeps moderation_grant = app_events.moderation_grant on conflict. That means the first call can persist a stale grant and the later call cannot correct it if admin/member state changed in between.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/marmot-app/src/client/mod.rs` around lines 1108 - 1190, The pre-send
call to record_local_app_event_projection in
send_app_event_with_local_projection can persist a stale moderation grant before
sync_runtime_groups refreshes state. Ensure the post-publish projection can
replace the existing event’s moderation_grant with the current value, updating
the app_events conflict/upsert handling rather than preserving the original
grant unconditionally.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@crates/marmot-app/src/client/mod.rs`:
- Around line 1108-1190: The pre-send call to record_local_app_event_projection
in send_app_event_with_local_projection can persist a stale moderation grant
before sync_runtime_groups refreshes state. Ensure the post-publish projection
can replace the existing event’s moderation_grant with the current value,
updating the app_events conflict/upsert handling rather than preserving the
original grant unconditionally.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: cfcad042-7d9e-4716-8186-c332860162f3

📥 Commits

Reviewing files that changed from the base of the PR and between 363b1fe and f3ece0e.

📒 Files selected for processing (14)
  • crates/marmot-app/src/client/mod.rs
  • crates/marmot-app/src/client/projection.rs
  • crates/marmot-app/src/client/sync.rs
  • crates/marmot-app/src/conversions.rs
  • crates/marmot-app/src/groups.rs
  • crates/marmot-app/src/lib.rs
  • crates/marmot-app/src/projection/tests.rs
  • crates/marmot-app/src/tests.rs
  • crates/storage-sqlite/src/account_projection/tests.rs
  • crates/storage-sqlite/src/chat_list/tests.rs
  • crates/storage-sqlite/src/migrations.rs
  • crates/storage-sqlite/src/migrations/0027_app_event_moderation_grant.rs
  • crates/storage-sqlite/src/timeline.rs
  • crates/storage-sqlite/src/timeline/tests.rs

Comment thread crates/marmot-app/src/groups.rs
Comment thread crates/marmot-app/src/client/projection.rs
@mubarakcoded
mubarakcoded requested a review from jgmontoya July 16, 2026 14:52
jgmontoya
jgmontoya previously approved these changes Jul 16, 2026
@mubarakcoded
mubarakcoded merged commit 3071274 into master Jul 17, 2026
20 checks passed
@mubarakcoded
mubarakcoded deleted the fix/admin-delete-timeline-ingest branch July 17, 2026 07:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Timeline ingest ignores group-admin deletes of other members' messages (self-retraction only)

3 participants