feat: account purge handler, tombstone cleanup, AP tombstone signing + 410
This commit is contained in:
@@ -3,6 +3,7 @@ mod import_cleanup;
|
||||
mod movie_dedup;
|
||||
mod processed_events_cleanup;
|
||||
mod refresh_session_cleanup;
|
||||
mod tombstone_cleanup;
|
||||
mod watch_event_cleanup;
|
||||
mod wrapup;
|
||||
|
||||
@@ -11,5 +12,6 @@ pub use import_cleanup::ImportSessionCleanupJob;
|
||||
pub use movie_dedup::MovieDeduplicationJob;
|
||||
pub use processed_events_cleanup::{PROCESSED_EVENTS_RETENTION_DAYS, ProcessedEventsCleanupJob};
|
||||
pub use refresh_session_cleanup::RefreshSessionCleanupJob;
|
||||
pub use tombstone_cleanup::{TOMBSTONE_RETENTION_DAYS, TombstoneCleanupJob};
|
||||
pub use watch_event_cleanup::WatchEventCleanupJob;
|
||||
pub use wrapup::{WrapUpAutoGenerateJob, WrapUpCleanupJob};
|
||||
|
||||
40
crates/application/src/jobs/tombstone_cleanup.rs
Normal file
40
crates/application/src/jobs/tombstone_cleanup.rs
Normal file
@@ -0,0 +1,40 @@
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use domain::{
|
||||
errors::DomainError,
|
||||
ports::{AccountPurger, PeriodicJob},
|
||||
};
|
||||
|
||||
/// How long a purged account's stripped user row and signing keypair stay.
|
||||
/// Federation deliveries of the account's `Delete` are signed from them, and
|
||||
/// their retries are long over by then.
|
||||
pub const TOMBSTONE_RETENTION_DAYS: i64 = 7;
|
||||
|
||||
pub struct TombstoneCleanupJob {
|
||||
purger: Arc<dyn AccountPurger>,
|
||||
}
|
||||
|
||||
impl TombstoneCleanupJob {
|
||||
pub fn new(purger: Arc<dyn AccountPurger>) -> Self {
|
||||
Self { purger }
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl PeriodicJob for TombstoneCleanupJob {
|
||||
fn interval(&self) -> Duration {
|
||||
Duration::from_secs(24 * 3600)
|
||||
}
|
||||
|
||||
async fn run(&self) -> Result<(), DomainError> {
|
||||
let cutoff =
|
||||
chrono::Utc::now().naive_utc() - chrono::Duration::days(TOMBSTONE_RETENTION_DAYS);
|
||||
let n = self.purger.purge_expired_tombstones(cutoff).await?;
|
||||
if n > 0 {
|
||||
tracing::info!("tombstone cleanup: removed {n} purged accounts");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@ pub mod get_page_viewer;
|
||||
pub mod get_profile_settings;
|
||||
pub mod get_settings;
|
||||
pub mod get_users;
|
||||
pub mod purge_account;
|
||||
pub mod queries;
|
||||
pub mod request_account_deletion;
|
||||
pub mod resolve_username_to_id;
|
||||
|
||||
77
crates/application/src/users/purge_account.rs
Normal file
77
crates/application/src/users/purge_account.rs
Normal file
@@ -0,0 +1,77 @@
|
||||
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<dyn UserRepository>,
|
||||
notifier: Arc<dyn FederationDeletionNotifier>,
|
||||
purger: Arc<dyn AccountPurger>,
|
||||
object_storage: Arc<dyn ObjectStorage>,
|
||||
}
|
||||
|
||||
impl AccountPurgeHandler {
|
||||
pub fn new(
|
||||
user: Arc<dyn UserRepository>,
|
||||
notifier: Arc<dyn FederationDeletionNotifier>,
|
||||
purger: Arc<dyn AccountPurger>,
|
||||
object_storage: Arc<dyn ObjectStorage>,
|
||||
) -> 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(())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user