purge: skip broadcasts of deleted authors, purge outbox log, guards, 410 inbox test

This commit is contained in:
2026-10-26 02:39:06 +01:00
parent 51a70b5260
commit 80dbdec107
9 changed files with 318 additions and 27 deletions

View File

@@ -34,9 +34,19 @@ pub const PURGED_USER_TABLES: &[(&str, &str)] = &[
/// the keypair is needed to sign queued federation deliveries.
pub const RETAINED_USER_TABLES: &[&str] = &["ap_local_actors"];
/// Tables with no user column whose rows hang off the user's content or actor
/// URL; the purge clears them by URL.
pub const URL_KEYED_USER_TABLES: &[&str] = &["ap_announces", "ap_followers", "ap_following"];
/// Columns that identify the user by actor or content URL rather than by id.
/// The purge matches rows on them; the guard test asserts each still exists so
/// a rename cannot silently stop the purge from clearing them.
pub const URL_KEYED_USER_COLUMNS: &[(&str, &str)] = &[
("ap_announces", "object_url"),
("ap_announces", "actor_url"),
("ap_followers", "remote_actor_url"),
("ap_following", "remote_actor_url"),
];
/// Columns holding serialized events and activities that carry the user's id
/// (the outbox log keeps finished rows, including authored activity JSON).
pub const PAYLOAD_KEYED_USER_COLUMNS: &[(&str, &str)] = &[("event_queue", "payload")];
pub struct SqliteAccountPurger {
pool: SqlitePool,
@@ -116,6 +126,22 @@ impl AccountPurger for SqliteAccountPurger {
.await
.map_err(map_sqlx_error)?;
// The outbox log: events and signed activities authored as this user.
// The `UserDeleted` row (which triggered this run) and the pending
// `Delete` deliveries must stay; both go with the tombstone.
sqlx::query(
"DELETE FROM event_queue
WHERE instr(payload, ?1) > 0
AND event_type != 'UserDeleted'
AND NOT (event_type = 'FederationDeliveryRequested'
AND status IN ('pending', 'processing')
AND instr(payload, '\"type\":\"Delete\"') > 0)",
)
.bind(&id)
.execute(&mut *tx)
.await
.map_err(map_sqlx_error)?;
// Other accounts' follows of this one.
for table in ["ap_followers", "ap_following"] {
sqlx::query(&format!("DELETE FROM {table} WHERE remote_actor_url = ?"))
@@ -158,6 +184,22 @@ impl AccountPurger for SqliteAccountPurger {
) -> Result<u64, DomainError> {
let cutoff = cutoff.format(TIMESTAMP_FORMAT).to_string();
let mut tx = self.pool.begin().await.map_err(map_sqlx_error)?;
let expiring: Vec<String> = sqlx::query_scalar(
"SELECT id FROM users WHERE purged_at IS NOT NULL AND purged_at < ?",
)
.bind(&cutoff)
.fetch_all(&mut *tx)
.await
.map_err(map_sqlx_error)?;
for id in &expiring {
// What the purge had to leave: the `UserDeleted` row and the
// `Delete` deliveries.
sqlx::query("DELETE FROM event_queue WHERE instr(payload, ?) > 0")
.bind(id)
.execute(&mut *tx)
.await
.map_err(map_sqlx_error)?;
}
for table in RETAINED_USER_TABLES {
sqlx::query(&format!(
"DELETE FROM {table} WHERE user_id IN (

View File

@@ -25,7 +25,8 @@ mod watchlist;
mod wrapup;
pub use account_purge::{
PURGED_USER_TABLES, RETAINED_USER_TABLES, SqliteAccountPurger, URL_KEYED_USER_TABLES,
PAYLOAD_KEYED_USER_COLUMNS, PURGED_USER_TABLES, RETAINED_USER_TABLES, SqliteAccountPurger,
URL_KEYED_USER_COLUMNS,
};
pub use ap_content::SqliteApContentQuery;
pub use diary::SqliteDiaryRepository;