feat: per-entity federation privacy toggles for reviews and watchlist

- add federate_reviews + federate_watchlist to UserSettings (default true)
- new UserFederationSettingsQuery port with FederationFlags struct
- remove get_user_federate_goals from LocalApContentQuery
- gate ReviewLogged, ReviewUpdated, WatchlistEntryAdded, on_poster_synced on flags
- goals gating migrated to UserFederationSettingsQuery
- ReviewDeleted and WatchlistEntryRemoved ungated (tombstones always fire)
- sqlite + postgres migrations and adapter impls
- settings API and SPA toggles
This commit is contained in:
2026-06-12 02:26:01 +02:00
parent 33aa5bdab3
commit ca7ca51949
25 changed files with 372 additions and 113 deletions

View File

@@ -4,7 +4,7 @@ use domain::ports::EventHandler;
use domain::{
errors::DomainError,
events::DomainEvent,
ports::LocalApContentQuery,
ports::{LocalApContentQuery, UserFederationSettingsQuery},
value_objects::{MovieId, ReviewId, UserId},
};
use std::sync::Arc;
@@ -17,6 +17,7 @@ use crate::urls::{actor_url, goal_url, review_url};
pub struct ActivityPubEventHandler {
ap_service: Arc<ActivityPubService>,
content_query: Arc<dyn LocalApContentQuery>,
federation_settings: Arc<dyn UserFederationSettingsQuery>,
base_url: String,
}
@@ -24,11 +25,13 @@ impl ActivityPubEventHandler {
pub fn new(
ap_service: Arc<ActivityPubService>,
content_query: Arc<dyn LocalApContentQuery>,
federation_settings: Arc<dyn UserFederationSettingsQuery>,
base_url: String,
) -> Self {
Self {
ap_service,
content_query,
federation_settings,
base_url,
}
}
@@ -131,6 +134,12 @@ impl EventHandler for ActivityPubEventHandler {
impl ActivityPubEventHandler {
async fn on_review_logged(&self, user_id: &UserId, review_id: &ReviewId) -> anyhow::Result<()> {
let flags = self.federation_settings.get_federation_flags(user_id).await
.unwrap_or(domain::ports::FederationFlags { goals: true, reviews: true, watchlist: true });
if !flags.reviews {
return Ok(());
}
let review = match self.content_query.get_review_by_id(review_id).await? {
Some(r) => r,
None => return Ok(()),
@@ -184,6 +193,12 @@ impl ActivityPubEventHandler {
user_id: &UserId,
review_id: &ReviewId,
) -> anyhow::Result<()> {
let flags = self.federation_settings.get_federation_flags(user_id).await
.unwrap_or(domain::ports::FederationFlags { goals: true, reviews: true, watchlist: true });
if !flags.reviews {
return Ok(());
}
let review = match self.content_query.get_review_by_id(review_id).await? {
Some(r) => r,
None => return Ok(()),
@@ -250,6 +265,12 @@ impl ActivityPubEventHandler {
external_metadata_id: &Option<String>,
added_at: &chrono::NaiveDateTime,
) -> anyhow::Result<()> {
let flags = self.federation_settings.get_federation_flags(user_id).await
.unwrap_or(domain::ports::FederationFlags { goals: true, reviews: true, watchlist: true });
if !flags.watchlist {
return Ok(());
}
use crate::urls::watchlist_entry_url;
let ap_id = watchlist_entry_url(&self.base_url, user_id.value(), movie_id.value());
let actor = actor_url(&self.base_url, user_id.value());
@@ -316,6 +337,13 @@ impl ActivityPubEventHandler {
for entry in entries {
let review = entry.review();
let user_id = review.user_id();
let flags = self.federation_settings.get_federation_flags(user_id).await
.unwrap_or(domain::ports::FederationFlags { goals: true, reviews: true, watchlist: true });
if !flags.reviews {
continue;
}
let ap_id = review_url(&self.base_url, review.id());
let actor = actor_url(&self.base_url, user_id.value());
@@ -343,12 +371,9 @@ impl ActivityPubEventHandler {
user_id: &UserId,
year: u16,
) -> anyhow::Result<()> {
if !self
.content_query
.get_user_federate_goals(user_id)
.await
.unwrap_or(false)
{
let flags = self.federation_settings.get_federation_flags(user_id).await
.unwrap_or(domain::ports::FederationFlags { goals: true, reviews: true, watchlist: true });
if !flags.goals {
return Ok(());
}
let Some((goal, current)) = self
@@ -384,12 +409,9 @@ impl ActivityPubEventHandler {
target_count: u32,
is_create: bool,
) -> anyhow::Result<()> {
if !self
.content_query
.get_user_federate_goals(user_id)
.await
.unwrap_or(false)
{
let flags = self.federation_settings.get_federation_flags(user_id).await
.unwrap_or(domain::ports::FederationFlags { goals: true, reviews: true, watchlist: true });
if !flags.goals {
return Ok(());
}
let current = self
@@ -418,12 +440,9 @@ impl ActivityPubEventHandler {
}
async fn on_goal_deleted(&self, user_id: &UserId, year: u16) -> anyhow::Result<()> {
if !self
.content_query
.get_user_federate_goals(user_id)
.await
.unwrap_or(false)
{
let flags = self.federation_settings.get_federation_flags(user_id).await
.unwrap_or(domain::ports::FederationFlags { goals: true, reviews: true, watchlist: true });
if !flags.goals {
return Ok(());
}
let ap_id = goal_url(&self.base_url, user_id.value(), year);