feat: account purge handler, tombstone cleanup, AP tombstone signing + 410

This commit is contained in:
2026-10-26 02:30:09 +01:00
parent 9a03de8956
commit 51a70b5260
32 changed files with 1708 additions and 27 deletions

View File

@@ -0,0 +1,44 @@
use async_trait::async_trait;
use crate::{errors::DomainError, value_objects::UserId};
/// Tells the fediverse that a local account is gone (an ActivityPub `Delete`
/// to followers). Federation-off builds wire a no-op.
///
/// Delivery is asynchronous: the implementation may only enqueue deliveries.
/// Those are signed later with the actor's keypair and the tombstoned user row,
/// which is why the purge keeps both until the tombstone cleanup.
#[async_trait]
pub trait FederationDeletionNotifier: Send + Sync {
async fn notify_deleted(&self, user: &UserId) -> Result<(), DomainError>;
}
/// Removes everything a deleted account owns, in one transaction, and strips
/// the user row down to a data-free tombstone.
///
/// What stays: the username (retired for good), role, `deleted_at`, the
/// `purged_at` stamp and the signing keypair, so queued federation deliveries
/// can still be signed. `purge_expired_tombstones` removes those later.
#[async_trait]
pub trait AccountPurger: Send + Sync {
/// Whether the account's data has already been purged.
async fn is_purged(&self, user: &UserId) -> Result<bool, DomainError>;
/// Object-storage keys the account owns (avatar, banner). Still readable
/// after `purge`, until `forget_images`.
async fn owned_image_keys(&self, user: &UserId) -> Result<Vec<String>, DomainError>;
/// One transaction; every delete is "if present", so a re-run is a no-op.
/// Frees the email. Does nothing for an account that is gone or purged.
async fn purge(&self, user: &UserId) -> Result<(), DomainError>;
/// Drops the image references once their objects are deleted.
async fn forget_images(&self, user: &UserId) -> Result<(), DomainError>;
/// Deletes tombstones (user row and keypair) purged before `cutoff`.
/// Returns how many were removed.
async fn purge_expired_tombstones(
&self,
cutoff: chrono::NaiveDateTime,
) -> Result<u64, DomainError>;
}

View File

@@ -8,6 +8,11 @@ use crate::{
#[async_trait]
pub trait ApDocumentPort: Send + Sync {
/// Whether the actor belonged to a deleted account: its documents are
/// answered `410 Gone` instead of being served.
async fn actor_is_gone(&self, _user_id: Uuid) -> Result<bool, DomainError> {
Ok(false)
}
async fn actor_json(&self, user_id: &str) -> Result<String, DomainError>;
async fn followers_collection_json(
&self,

View File

@@ -1,3 +1,4 @@
pub mod account_purge;
pub mod auth;
pub mod diary;
pub mod events;
@@ -21,6 +22,7 @@ pub mod social;
pub mod watchlist;
pub mod wrapup;
pub use account_purge::*;
pub use auth::*;
pub use diary::*;
pub use events::*;

View File

@@ -34,6 +34,18 @@ impl super::RemoteWatchlistRepository for NoopRemoteWatchlistRepository {
}
}
// ── NoopFederationDeletionNotifier ───────────────────────────────────────────
/// Federation compiled out: nobody remote to tell.
pub struct NoopFederationDeletionNotifier;
#[async_trait]
impl super::FederationDeletionNotifier for NoopFederationDeletionNotifier {
async fn notify_deleted(&self, _: &UserId) -> Result<(), DomainError> {
Ok(())
}
}
// ── NoopSocialCommand ────────────────────────────────────────────────────────
pub struct NoopSocialCommand;