tombstone gate all user paths; cleanup deletes late follower rows; spa self-delete to /; adr 0016 + purge comment
This commit is contained in:
@@ -27,10 +27,12 @@ pub async fn tombstone_gate(
|
||||
next.run(request).await
|
||||
}
|
||||
|
||||
/// The user id in `/users/{id}/outbox|featured|inbox`.
|
||||
/// The user id in any `/users/{id}/...` path (outbox, inbox, collections,
|
||||
/// content objects such as `/users/{id}/reviews/{rid}`). The bare actor path
|
||||
/// has its own tombstone response.
|
||||
pub fn gated_user(path: &str) -> Option<uuid::Uuid> {
|
||||
let rest = path.strip_prefix("/users/")?;
|
||||
let (id, endpoint) = rest.split_once('/')?;
|
||||
matches!(endpoint, "outbox" | "featured" | "inbox").then_some(())?;
|
||||
(!endpoint.is_empty()).then_some(())?;
|
||||
uuid::Uuid::parse_str(id).ok()
|
||||
}
|
||||
|
||||
@@ -46,14 +46,22 @@ async fn an_unknown_user_is_none() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn only_the_per_user_endpoints_are_gated() {
|
||||
fn every_per_user_path_is_gated() {
|
||||
use activitypub::tombstone_gate::gated_user;
|
||||
let id = uuid::Uuid::new_v4();
|
||||
for endpoint in ["outbox", "featured", "inbox"] {
|
||||
let rid = uuid::Uuid::new_v4();
|
||||
for endpoint in [
|
||||
"outbox",
|
||||
"featured",
|
||||
"inbox",
|
||||
"followers",
|
||||
"following",
|
||||
&format!("reviews/{rid}"),
|
||||
] {
|
||||
assert_eq!(gated_user(&format!("/users/{id}/{endpoint}")), Some(id));
|
||||
}
|
||||
assert_eq!(gated_user(&format!("/users/{id}")), None);
|
||||
assert_eq!(gated_user(&format!("/users/{id}/followers")), None);
|
||||
assert_eq!(gated_user(&format!("/users/{id}/")), None);
|
||||
assert_eq!(gated_user("/inbox"), None);
|
||||
assert_eq!(gated_user("/users/not-a-uuid/outbox"), None);
|
||||
}
|
||||
|
||||
@@ -223,6 +223,16 @@ impl AccountPurger for SqliteAccountPurger {
|
||||
.execute(&mut *tx)
|
||||
.await
|
||||
.map_err(map_sqlx_error)?;
|
||||
// A Follow arriving on the shared inbox after the purge can leave
|
||||
// a late row for the tombstone; delete it explicitly rather than
|
||||
// rely on the foreign-key cascade of the final `users` delete.
|
||||
for table in ["ap_followers", "ap_following"] {
|
||||
sqlx::query(&format!("DELETE FROM {table} WHERE local_user_id = ?"))
|
||||
.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 = ?"))
|
||||
.bind(id)
|
||||
|
||||
@@ -696,6 +696,14 @@ async fn tombstone_cleanup_removes_rows_older_than_retention_and_keeps_younger_o
|
||||
assert_eq!(count_for(&pool, "users", "id", &s.user).await, 1);
|
||||
assert_eq!(unfinished().await, before);
|
||||
|
||||
// A Follow that reached the shared inbox after the purge left a row.
|
||||
exec(
|
||||
&pool,
|
||||
"INSERT INTO ap_followers (local_user_id, remote_actor_url, status, created_at)
|
||||
VALUES (?, 'https://late.example/users/zed', 'pending', '2026-01-01T00:00:00Z')",
|
||||
&[&alice],
|
||||
)
|
||||
.await;
|
||||
// Once they finish, the cleanup takes the tombstone and the history.
|
||||
exec(&pool, "UPDATE event_queue SET status = 'done'", &[]).await;
|
||||
let removed = p
|
||||
@@ -712,6 +720,11 @@ async fn tombstone_cleanup_removes_rows_older_than_retention_and_keeps_younger_o
|
||||
left.iter().all(|p| !p.contains(&alice)),
|
||||
"no history of the user remains: {left:?}"
|
||||
);
|
||||
assert_eq!(
|
||||
count_for(&pool, "ap_followers", "local_user_id", &s.user).await,
|
||||
0,
|
||||
"late shared-inbox follower row"
|
||||
);
|
||||
assert_eq!(
|
||||
count_for(&pool, "ap_local_actors", "user_id", &s.user).await,
|
||||
0
|
||||
|
||||
@@ -20,6 +20,10 @@ use domain::{
|
||||
///
|
||||
/// Idempotent: every step is a no-op on a re-run, and the notification is
|
||||
/// skipped once the account is purged.
|
||||
///
|
||||
/// A purge that fails after the notification was queued is redelivered and
|
||||
/// queues the `Delete` again. That is harmless: a repeated `Delete` of an
|
||||
/// already deleted actor is idempotent on remote servers.
|
||||
pub struct AccountPurgeHandler {
|
||||
user: Arc<dyn UserRepository>,
|
||||
notifier: Arc<dyn FederationDeletionNotifier>,
|
||||
|
||||
Reference in New Issue
Block a user