use std::sync::Arc; use async_trait::async_trait; use domain::{ errors::DomainError, events::DomainEvent, ports::{ AccountPurger, EventHandler, FederationDeletionNotifier, ObjectStorage, UserRepository, }, }; /// Purges a deleted account once its `UserDeleted` event arrives. /// /// Order matters. Federation is told first (deliveries are only enqueued, and /// signed later from the tombstone the purge leaves behind). A notifier /// failure never blocks the purge: unreachable servers must not keep a /// person's data. Then the database rows go in one transaction, and the /// image objects after the commit. Image references are dropped only once the /// objects are gone, so a crash in between is recovered by the redelivery. /// /// Idempotent: every step is a no-op on a re-run, and the notification is /// skipped once the account is purged. pub struct AccountPurgeHandler { user: Arc, notifier: Arc, purger: Arc, object_storage: Arc, } impl AccountPurgeHandler { pub fn new( user: Arc, notifier: Arc, purger: Arc, object_storage: Arc, ) -> Self { Self { user, notifier, purger, object_storage, } } } #[async_trait] impl EventHandler for AccountPurgeHandler { fn name(&self) -> &'static str { "account_purge" } async fn handle(&self, event: &DomainEvent) -> Result<(), DomainError> { let DomainEvent::UserDeleted { user_id } = event else { return Ok(()); }; // Gone already (removed by the tombstone cleanup): nothing left to do. if self.user.find_tombstoned_by_id(user_id).await?.is_none() { return Ok(()); } if !self.purger.is_purged(user_id).await? && let Err(e) = self.notifier.notify_deleted(user_id).await { tracing::warn!(user = %user_id.value(), "federation delete notice failed, purging anyway: {e}"); } let keys = self.purger.owned_image_keys(user_id).await?; self.purger.purge(user_id).await?; for key in &keys { self.object_storage.delete(key).await?; } if !keys.is_empty() { self.purger.forget_images(user_id).await?; } Ok(()) } }