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

@@ -6,7 +6,7 @@ use domain::{
events::DomainEvent,
ports::{
GoalQuery, LocalApContentQuery, MovieQuery, ReviewRepository, StatsRepository,
UserFederationSettingsQuery,
UserFederationSettingsQuery, UserRepository,
},
value_objects::{InstanceIdentity, MovieId, ReviewId, UserId},
};
@@ -25,6 +25,7 @@ pub struct ActivityPubEventHandler {
goal_repo: Arc<dyn GoalQuery>,
stats_repo: Arc<dyn StatsRepository>,
federation_settings: Arc<dyn UserFederationSettingsQuery>,
user_repo: Arc<dyn UserRepository>,
instance: InstanceIdentity,
}
@@ -38,6 +39,7 @@ impl ActivityPubEventHandler {
goal_repo: Arc<dyn GoalQuery>,
stats_repo: Arc<dyn StatsRepository>,
federation_settings: Arc<dyn UserFederationSettingsQuery>,
user_repo: Arc<dyn UserRepository>,
instance: InstanceIdentity,
) -> Self {
Self {
@@ -48,6 +50,7 @@ impl ActivityPubEventHandler {
goal_repo,
stats_repo,
federation_settings,
user_repo,
instance,
}
}
@@ -60,6 +63,15 @@ impl EventHandler for ActivityPubEventHandler {
}
async fn handle(&self, event: &DomainEvent) -> Result<(), DomainError> {
// A deleted account must not speak again: its queued events would
// otherwise sign and send from the tombstone after the Delete. Only
// the Delete itself (sent by the deletion notifier) goes out.
if let Some(author) = self.outbound_author(event)
&& self.is_tombstoned(&author).await
{
tracing::debug!(user = %author.value(), "skipping federation event of a deleted account");
return Ok(());
}
match event {
DomainEvent::ReviewLogged {
review_id, user_id, ..
@@ -161,6 +173,38 @@ impl EventHandler for ActivityPubEventHandler {
}
impl ActivityPubEventHandler {
/// The local account an outbound activity would be sent as. A queued
/// delivery counts unless it is the account's `Delete`.
fn outbound_author(&self, event: &DomainEvent) -> Option<UserId> {
match event {
DomainEvent::ReviewLogged { user_id, .. }
| DomainEvent::ReviewUpdated { user_id, .. }
| DomainEvent::ReviewDeleted { user_id, .. }
| DomainEvent::UserUpdated { user_id }
| DomainEvent::WatchlistEntryAdded { user_id, .. }
| DomainEvent::WatchlistEntryRemoved { user_id, .. }
| DomainEvent::GoalCreated { user_id, .. }
| DomainEvent::GoalUpdated { user_id, .. }
| DomainEvent::GoalDeleted { user_id, .. }
| DomainEvent::UserAccountMoved { user_id, .. } => Some(user_id.clone()),
DomainEvent::FederationDeliveryRequested {
activity_json,
signing_actor_id,
..
} if activity_json.get("type").and_then(|t| t.as_str()) != Some("Delete") => {
Some(UserId::from_uuid(*signing_actor_id))
}
_ => None,
}
}
async fn is_tombstoned(&self, user: &UserId) -> bool {
matches!(
self.user_repo.find_tombstoned_by_id(user).await,
Ok(Some(_))
)
}
async fn on_review_logged(&self, user_id: &UserId, review_id: &ReviewId) -> anyhow::Result<()> {
let flags = self
.federation_settings

View File

@@ -179,6 +179,7 @@ pub async fn wire(deps: ActivityPubDeps) -> anyhow::Result<ActivityPubWire> {
goal_repo,
stats_repo,
federation_settings,
std::sync::Arc::clone(&user_repo),
instance,
)) as std::sync::Arc<dyn domain::ports::EventHandler>;