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:
@@ -4,7 +4,7 @@ use domain::ports::EventHandler;
|
|||||||
use domain::{
|
use domain::{
|
||||||
errors::DomainError,
|
errors::DomainError,
|
||||||
events::DomainEvent,
|
events::DomainEvent,
|
||||||
ports::LocalApContentQuery,
|
ports::{LocalApContentQuery, UserFederationSettingsQuery},
|
||||||
value_objects::{MovieId, ReviewId, UserId},
|
value_objects::{MovieId, ReviewId, UserId},
|
||||||
};
|
};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
@@ -17,6 +17,7 @@ use crate::urls::{actor_url, goal_url, review_url};
|
|||||||
pub struct ActivityPubEventHandler {
|
pub struct ActivityPubEventHandler {
|
||||||
ap_service: Arc<ActivityPubService>,
|
ap_service: Arc<ActivityPubService>,
|
||||||
content_query: Arc<dyn LocalApContentQuery>,
|
content_query: Arc<dyn LocalApContentQuery>,
|
||||||
|
federation_settings: Arc<dyn UserFederationSettingsQuery>,
|
||||||
base_url: String,
|
base_url: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -24,11 +25,13 @@ impl ActivityPubEventHandler {
|
|||||||
pub fn new(
|
pub fn new(
|
||||||
ap_service: Arc<ActivityPubService>,
|
ap_service: Arc<ActivityPubService>,
|
||||||
content_query: Arc<dyn LocalApContentQuery>,
|
content_query: Arc<dyn LocalApContentQuery>,
|
||||||
|
federation_settings: Arc<dyn UserFederationSettingsQuery>,
|
||||||
base_url: String,
|
base_url: String,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
ap_service,
|
ap_service,
|
||||||
content_query,
|
content_query,
|
||||||
|
federation_settings,
|
||||||
base_url,
|
base_url,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -131,6 +134,12 @@ impl EventHandler for ActivityPubEventHandler {
|
|||||||
|
|
||||||
impl ActivityPubEventHandler {
|
impl ActivityPubEventHandler {
|
||||||
async fn on_review_logged(&self, user_id: &UserId, review_id: &ReviewId) -> anyhow::Result<()> {
|
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? {
|
let review = match self.content_query.get_review_by_id(review_id).await? {
|
||||||
Some(r) => r,
|
Some(r) => r,
|
||||||
None => return Ok(()),
|
None => return Ok(()),
|
||||||
@@ -184,6 +193,12 @@ impl ActivityPubEventHandler {
|
|||||||
user_id: &UserId,
|
user_id: &UserId,
|
||||||
review_id: &ReviewId,
|
review_id: &ReviewId,
|
||||||
) -> anyhow::Result<()> {
|
) -> 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? {
|
let review = match self.content_query.get_review_by_id(review_id).await? {
|
||||||
Some(r) => r,
|
Some(r) => r,
|
||||||
None => return Ok(()),
|
None => return Ok(()),
|
||||||
@@ -250,6 +265,12 @@ impl ActivityPubEventHandler {
|
|||||||
external_metadata_id: &Option<String>,
|
external_metadata_id: &Option<String>,
|
||||||
added_at: &chrono::NaiveDateTime,
|
added_at: &chrono::NaiveDateTime,
|
||||||
) -> anyhow::Result<()> {
|
) -> 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;
|
use crate::urls::watchlist_entry_url;
|
||||||
let ap_id = watchlist_entry_url(&self.base_url, user_id.value(), movie_id.value());
|
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());
|
let actor = actor_url(&self.base_url, user_id.value());
|
||||||
@@ -316,6 +337,13 @@ impl ActivityPubEventHandler {
|
|||||||
for entry in entries {
|
for entry in entries {
|
||||||
let review = entry.review();
|
let review = entry.review();
|
||||||
let user_id = review.user_id();
|
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 ap_id = review_url(&self.base_url, review.id());
|
||||||
let actor = actor_url(&self.base_url, user_id.value());
|
let actor = actor_url(&self.base_url, user_id.value());
|
||||||
|
|
||||||
@@ -343,12 +371,9 @@ impl ActivityPubEventHandler {
|
|||||||
user_id: &UserId,
|
user_id: &UserId,
|
||||||
year: u16,
|
year: u16,
|
||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
if !self
|
let flags = self.federation_settings.get_federation_flags(user_id).await
|
||||||
.content_query
|
.unwrap_or(domain::ports::FederationFlags { goals: true, reviews: true, watchlist: true });
|
||||||
.get_user_federate_goals(user_id)
|
if !flags.goals {
|
||||||
.await
|
|
||||||
.unwrap_or(false)
|
|
||||||
{
|
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
let Some((goal, current)) = self
|
let Some((goal, current)) = self
|
||||||
@@ -384,12 +409,9 @@ impl ActivityPubEventHandler {
|
|||||||
target_count: u32,
|
target_count: u32,
|
||||||
is_create: bool,
|
is_create: bool,
|
||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
if !self
|
let flags = self.federation_settings.get_federation_flags(user_id).await
|
||||||
.content_query
|
.unwrap_or(domain::ports::FederationFlags { goals: true, reviews: true, watchlist: true });
|
||||||
.get_user_federate_goals(user_id)
|
if !flags.goals {
|
||||||
.await
|
|
||||||
.unwrap_or(false)
|
|
||||||
{
|
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
let current = self
|
let current = self
|
||||||
@@ -418,12 +440,9 @@ impl ActivityPubEventHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn on_goal_deleted(&self, user_id: &UserId, year: u16) -> anyhow::Result<()> {
|
async fn on_goal_deleted(&self, user_id: &UserId, year: u16) -> anyhow::Result<()> {
|
||||||
if !self
|
let flags = self.federation_settings.get_federation_flags(user_id).await
|
||||||
.content_query
|
.unwrap_or(domain::ports::FederationFlags { goals: true, reviews: true, watchlist: true });
|
||||||
.get_user_federate_goals(user_id)
|
if !flags.goals {
|
||||||
.await
|
|
||||||
.unwrap_or(false)
|
|
||||||
{
|
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
let ap_id = goal_url(&self.base_url, user_id.value(), year);
|
let ap_id = goal_url(&self.base_url, user_id.value(), year);
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ pub struct ActivityPubDeps {
|
|||||||
pub remote_goal_repo: std::sync::Arc<dyn domain::ports::RemoteGoalRepository>,
|
pub remote_goal_repo: std::sync::Arc<dyn domain::ports::RemoteGoalRepository>,
|
||||||
pub local_ap_content: std::sync::Arc<dyn domain::ports::LocalApContentQuery>,
|
pub local_ap_content: std::sync::Arc<dyn domain::ports::LocalApContentQuery>,
|
||||||
pub user_repo: std::sync::Arc<dyn domain::ports::UserRepository>,
|
pub user_repo: std::sync::Arc<dyn domain::ports::UserRepository>,
|
||||||
|
pub federation_settings: std::sync::Arc<dyn domain::ports::UserFederationSettingsQuery>,
|
||||||
pub base_url: String,
|
pub base_url: String,
|
||||||
pub allow_registration: bool,
|
pub allow_registration: bool,
|
||||||
pub event_publisher: std::sync::Arc<dyn domain::ports::EventPublisher>,
|
pub event_publisher: std::sync::Arc<dyn domain::ports::EventPublisher>,
|
||||||
@@ -68,6 +69,7 @@ pub async fn wire(deps: ActivityPubDeps) -> anyhow::Result<ActivityPubWire> {
|
|||||||
remote_goal_repo,
|
remote_goal_repo,
|
||||||
local_ap_content,
|
local_ap_content,
|
||||||
user_repo,
|
user_repo,
|
||||||
|
federation_settings,
|
||||||
base_url,
|
base_url,
|
||||||
allow_registration,
|
allow_registration,
|
||||||
event_publisher,
|
event_publisher,
|
||||||
@@ -129,6 +131,7 @@ pub async fn wire(deps: ActivityPubDeps) -> anyhow::Result<ActivityPubWire> {
|
|||||||
let event_handler = std::sync::Arc::new(ActivityPubEventHandler::new(
|
let event_handler = std::sync::Arc::new(ActivityPubEventHandler::new(
|
||||||
std::sync::Arc::clone(&concrete),
|
std::sync::Arc::clone(&concrete),
|
||||||
local_ap_content,
|
local_ap_content,
|
||||||
|
federation_settings,
|
||||||
base_url,
|
base_url,
|
||||||
)) as std::sync::Arc<dyn domain::ports::EventHandler>;
|
)) as std::sync::Arc<dyn domain::ports::EventHandler>;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE user_settings ADD COLUMN federate_reviews BOOLEAN NOT NULL DEFAULT TRUE;
|
||||||
|
ALTER TABLE user_settings ADD COLUMN federate_watchlist BOOLEAN NOT NULL DEFAULT TRUE;
|
||||||
@@ -229,23 +229,6 @@ impl LocalApContentQuery for PostgresApContentQuery {
|
|||||||
rows.into_iter().map(DiaryRow::into_domain).collect()
|
rows.into_iter().map(DiaryRow::into_domain).collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_user_federate_goals(&self, user_id: &UserId) -> Result<bool, DomainError> {
|
|
||||||
let uid = user_id.value().to_string();
|
|
||||||
let row = sqlx::query("SELECT federate_goals FROM user_settings WHERE user_id = $1")
|
|
||||||
.bind(&uid)
|
|
||||||
.fetch_optional(&self.pool)
|
|
||||||
.await
|
|
||||||
.map_err(Self::map_err)?;
|
|
||||||
|
|
||||||
match row {
|
|
||||||
Some(r) => {
|
|
||||||
let val: i64 = r.try_get("federate_goals").unwrap_or(0);
|
|
||||||
Ok(val != 0)
|
|
||||||
}
|
|
||||||
None => Ok(false),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn get_goal_with_progress(
|
async fn get_goal_with_progress(
|
||||||
&self,
|
&self,
|
||||||
user_id: &UserId,
|
user_id: &UserId,
|
||||||
|
|||||||
@@ -93,6 +93,7 @@ pub struct PostgresWireOutput {
|
|||||||
pub wrapup_stats: std::sync::Arc<dyn domain::ports::WrapUpStatsQuery>,
|
pub wrapup_stats: std::sync::Arc<dyn domain::ports::WrapUpStatsQuery>,
|
||||||
pub goal: std::sync::Arc<dyn domain::ports::GoalRepository>,
|
pub goal: std::sync::Arc<dyn domain::ports::GoalRepository>,
|
||||||
pub user_settings: std::sync::Arc<dyn domain::ports::UserSettingsRepository>,
|
pub user_settings: std::sync::Arc<dyn domain::ports::UserSettingsRepository>,
|
||||||
|
pub federation_settings: std::sync::Arc<dyn domain::ports::UserFederationSettingsQuery>,
|
||||||
pub remote_goal: std::sync::Arc<dyn domain::ports::RemoteGoalRepository>,
|
pub remote_goal: std::sync::Arc<dyn domain::ports::RemoteGoalRepository>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,6 +109,8 @@ pub async fn wire(database_url: &str) -> anyhow::Result<PostgresWireOutput> {
|
|||||||
.map_err(|e| anyhow::anyhow!("{e}"))
|
.map_err(|e| anyhow::anyhow!("{e}"))
|
||||||
.context("Database migration failed")?;
|
.context("Database migration failed")?;
|
||||||
|
|
||||||
|
let user_settings_repo = std::sync::Arc::new(user_settings::PostgresUserSettingsRepository::new(pool.clone()));
|
||||||
|
|
||||||
Ok(PostgresWireOutput {
|
Ok(PostgresWireOutput {
|
||||||
pool: pool.clone(),
|
pool: pool.clone(),
|
||||||
movie: std::sync::Arc::new(PostgresMovieRepository::new(pool.clone())) as _,
|
movie: std::sync::Arc::new(PostgresMovieRepository::new(pool.clone())) as _,
|
||||||
@@ -125,9 +128,8 @@ pub async fn wire(database_url: &str) -> anyhow::Result<PostgresWireOutput> {
|
|||||||
wrapup_repo: std::sync::Arc::new(PostgresWrapUpRepository::new(pool.clone())) as _,
|
wrapup_repo: std::sync::Arc::new(PostgresWrapUpRepository::new(pool.clone())) as _,
|
||||||
wrapup_stats: std::sync::Arc::new(PostgresWrapUpStatsQuery::new(pool.clone())) as _,
|
wrapup_stats: std::sync::Arc::new(PostgresWrapUpStatsQuery::new(pool.clone())) as _,
|
||||||
goal: std::sync::Arc::new(goals::PostgresGoalRepository::new(pool.clone())) as _,
|
goal: std::sync::Arc::new(goals::PostgresGoalRepository::new(pool.clone())) as _,
|
||||||
user_settings: std::sync::Arc::new(user_settings::PostgresUserSettingsRepository::new(
|
user_settings: std::sync::Arc::clone(&user_settings_repo) as _,
|
||||||
pool.clone(),
|
federation_settings: user_settings_repo as _,
|
||||||
)) as _,
|
|
||||||
remote_goal: std::sync::Arc::new(remote_goals::PostgresRemoteGoalRepository::new(pool))
|
remote_goal: std::sync::Arc::new(remote_goals::PostgresRemoteGoalRepository::new(pool))
|
||||||
as _,
|
as _,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use domain::{
|
use domain::{
|
||||||
errors::DomainError, models::UserSettings, ports::UserSettingsRepository, value_objects::UserId,
|
errors::DomainError,
|
||||||
|
models::UserSettings,
|
||||||
|
ports::{FederationFlags, UserFederationSettingsQuery, UserSettingsRepository},
|
||||||
|
value_objects::UserId,
|
||||||
};
|
};
|
||||||
use sqlx::{PgPool, Row};
|
use sqlx::{PgPool, Row};
|
||||||
|
|
||||||
@@ -23,20 +26,25 @@ impl PostgresUserSettingsRepository {
|
|||||||
impl UserSettingsRepository for PostgresUserSettingsRepository {
|
impl UserSettingsRepository for PostgresUserSettingsRepository {
|
||||||
async fn get(&self, user_id: &UserId) -> Result<UserSettings, DomainError> {
|
async fn get(&self, user_id: &UserId) -> Result<UserSettings, DomainError> {
|
||||||
let uid = user_id.value().to_string();
|
let uid = user_id.value().to_string();
|
||||||
|
let row = sqlx::query(
|
||||||
let row =
|
"SELECT federate_goals, federate_reviews, federate_watchlist \
|
||||||
sqlx::query("SELECT user_id, federate_goals FROM user_settings WHERE user_id = $1")
|
FROM user_settings WHERE user_id = $1",
|
||||||
.bind(&uid)
|
)
|
||||||
.fetch_optional(&self.pool)
|
.bind(&uid)
|
||||||
.await
|
.fetch_optional(&self.pool)
|
||||||
.map_err(Self::map_err)?;
|
.await
|
||||||
|
.map_err(Self::map_err)?;
|
||||||
|
|
||||||
match row {
|
match row {
|
||||||
Some(r) => {
|
Some(r) => {
|
||||||
let federate: i64 = r.try_get("federate_goals").unwrap_or(0);
|
let goals: bool = r.try_get("federate_goals").unwrap_or(true);
|
||||||
|
let reviews: bool = r.try_get("federate_reviews").unwrap_or(true);
|
||||||
|
let watchlist: bool = r.try_get("federate_watchlist").unwrap_or(true);
|
||||||
Ok(UserSettings::from_persistence(
|
Ok(UserSettings::from_persistence(
|
||||||
user_id.clone(),
|
user_id.clone(),
|
||||||
federate != 0,
|
goals,
|
||||||
|
reviews,
|
||||||
|
watchlist,
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
None => Ok(UserSettings::new(user_id.clone())),
|
None => Ok(UserSettings::new(user_id.clone())),
|
||||||
@@ -45,18 +53,48 @@ impl UserSettingsRepository for PostgresUserSettingsRepository {
|
|||||||
|
|
||||||
async fn save(&self, settings: &UserSettings) -> Result<(), DomainError> {
|
async fn save(&self, settings: &UserSettings) -> Result<(), DomainError> {
|
||||||
let uid = settings.user_id().value().to_string();
|
let uid = settings.user_id().value().to_string();
|
||||||
let federate = if settings.federate_goals() { 1i64 } else { 0 };
|
|
||||||
|
|
||||||
sqlx::query(
|
sqlx::query(
|
||||||
"INSERT INTO user_settings (user_id, federate_goals) VALUES ($1, $2) \
|
"INSERT INTO user_settings (user_id, federate_goals, federate_reviews, federate_watchlist) \
|
||||||
ON CONFLICT (user_id) DO UPDATE SET federate_goals = $2",
|
VALUES ($1, $2, $3, $4) \
|
||||||
|
ON CONFLICT (user_id) DO UPDATE \
|
||||||
|
SET federate_goals = $2, federate_reviews = $3, federate_watchlist = $4",
|
||||||
)
|
)
|
||||||
.bind(&uid)
|
.bind(&uid)
|
||||||
.bind(federate)
|
.bind(settings.federate_goals())
|
||||||
|
.bind(settings.federate_reviews())
|
||||||
|
.bind(settings.federate_watchlist())
|
||||||
.execute(&self.pool)
|
.execute(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(Self::map_err)?;
|
.map_err(Self::map_err)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl UserFederationSettingsQuery for PostgresUserSettingsRepository {
|
||||||
|
async fn get_federation_flags(&self, user_id: &UserId) -> Result<FederationFlags, DomainError> {
|
||||||
|
let uid = user_id.value().to_string();
|
||||||
|
let row = sqlx::query(
|
||||||
|
"SELECT federate_goals, federate_reviews, federate_watchlist \
|
||||||
|
FROM user_settings WHERE user_id = $1",
|
||||||
|
)
|
||||||
|
.bind(&uid)
|
||||||
|
.fetch_optional(&self.pool)
|
||||||
|
.await
|
||||||
|
.map_err(Self::map_err)?;
|
||||||
|
|
||||||
|
match row {
|
||||||
|
Some(r) => {
|
||||||
|
let goals: bool = r.try_get("federate_goals").unwrap_or(true);
|
||||||
|
let reviews: bool = r.try_get("federate_reviews").unwrap_or(true);
|
||||||
|
let watchlist: bool = r.try_get("federate_watchlist").unwrap_or(true);
|
||||||
|
Ok(FederationFlags { goals, reviews, watchlist })
|
||||||
|
}
|
||||||
|
None => Ok(FederationFlags {
|
||||||
|
goals: true,
|
||||||
|
reviews: true,
|
||||||
|
watchlist: true,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE user_settings ADD COLUMN federate_reviews BOOLEAN NOT NULL DEFAULT TRUE;
|
||||||
|
ALTER TABLE user_settings ADD COLUMN federate_watchlist BOOLEAN NOT NULL DEFAULT TRUE;
|
||||||
@@ -5,7 +5,7 @@ use domain::{
|
|||||||
ports::LocalApContentQuery,
|
ports::LocalApContentQuery,
|
||||||
value_objects::{MovieId, ReviewId, UserId},
|
value_objects::{MovieId, ReviewId, UserId},
|
||||||
};
|
};
|
||||||
use sqlx::{Row, SqlitePool};
|
use sqlx::SqlitePool;
|
||||||
|
|
||||||
use crate::models::{DiaryRow, MovieRow, ReviewRow, WatchlistRow};
|
use crate::models::{DiaryRow, MovieRow, ReviewRow, WatchlistRow};
|
||||||
|
|
||||||
@@ -169,23 +169,6 @@ impl LocalApContentQuery for SqliteApContentQuery {
|
|||||||
rows.into_iter().map(DiaryRow::into_domain).collect()
|
rows.into_iter().map(DiaryRow::into_domain).collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_user_federate_goals(&self, user_id: &UserId) -> Result<bool, DomainError> {
|
|
||||||
let uid = user_id.value().to_string();
|
|
||||||
let row = sqlx::query("SELECT federate_goals FROM user_settings WHERE user_id = ?")
|
|
||||||
.bind(&uid)
|
|
||||||
.fetch_optional(&self.pool)
|
|
||||||
.await
|
|
||||||
.map_err(Self::map_err)?;
|
|
||||||
|
|
||||||
match row {
|
|
||||||
Some(r) => {
|
|
||||||
let val: i64 = r.try_get("federate_goals").unwrap_or(0);
|
|
||||||
Ok(val != 0)
|
|
||||||
}
|
|
||||||
None => Ok(false),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn get_goal_with_progress(
|
async fn get_goal_with_progress(
|
||||||
&self,
|
&self,
|
||||||
user_id: &UserId,
|
user_id: &UserId,
|
||||||
|
|||||||
@@ -89,6 +89,7 @@ pub struct SqliteWireOutput {
|
|||||||
pub wrapup_stats: std::sync::Arc<dyn domain::ports::WrapUpStatsQuery>,
|
pub wrapup_stats: std::sync::Arc<dyn domain::ports::WrapUpStatsQuery>,
|
||||||
pub goal: std::sync::Arc<dyn domain::ports::GoalRepository>,
|
pub goal: std::sync::Arc<dyn domain::ports::GoalRepository>,
|
||||||
pub user_settings: std::sync::Arc<dyn domain::ports::UserSettingsRepository>,
|
pub user_settings: std::sync::Arc<dyn domain::ports::UserSettingsRepository>,
|
||||||
|
pub federation_settings: std::sync::Arc<dyn domain::ports::UserFederationSettingsQuery>,
|
||||||
pub remote_goal: std::sync::Arc<dyn domain::ports::RemoteGoalRepository>,
|
pub remote_goal: std::sync::Arc<dyn domain::ports::RemoteGoalRepository>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,6 +114,8 @@ pub async fn wire(database_url: &str) -> anyhow::Result<SqliteWireOutput> {
|
|||||||
.map_err(|e| anyhow::anyhow!("{e}"))
|
.map_err(|e| anyhow::anyhow!("{e}"))
|
||||||
.context("Database migration failed")?;
|
.context("Database migration failed")?;
|
||||||
|
|
||||||
|
let user_settings_repo = std::sync::Arc::new(user_settings::SqliteUserSettingsRepository::new(pool.clone()));
|
||||||
|
|
||||||
Ok(SqliteWireOutput {
|
Ok(SqliteWireOutput {
|
||||||
pool: pool.clone(),
|
pool: pool.clone(),
|
||||||
movie: std::sync::Arc::new(SqliteMovieRepository::new(pool.clone())) as _,
|
movie: std::sync::Arc::new(SqliteMovieRepository::new(pool.clone())) as _,
|
||||||
@@ -128,9 +131,8 @@ pub async fn wire(database_url: &str) -> anyhow::Result<SqliteWireOutput> {
|
|||||||
wrapup_repo: std::sync::Arc::new(SqliteWrapUpRepository::new(pool.clone())) as _,
|
wrapup_repo: std::sync::Arc::new(SqliteWrapUpRepository::new(pool.clone())) as _,
|
||||||
wrapup_stats: std::sync::Arc::new(SqliteWrapUpStatsQuery::new(pool.clone())) as _,
|
wrapup_stats: std::sync::Arc::new(SqliteWrapUpStatsQuery::new(pool.clone())) as _,
|
||||||
goal: std::sync::Arc::new(goals::SqliteGoalRepository::new(pool.clone())) as _,
|
goal: std::sync::Arc::new(goals::SqliteGoalRepository::new(pool.clone())) as _,
|
||||||
user_settings: std::sync::Arc::new(user_settings::SqliteUserSettingsRepository::new(
|
user_settings: std::sync::Arc::clone(&user_settings_repo) as _,
|
||||||
pool.clone(),
|
federation_settings: user_settings_repo as _,
|
||||||
)) as _,
|
|
||||||
remote_goal: std::sync::Arc::new(remote_goals::SqliteRemoteGoalRepository::new(pool)) as _,
|
remote_goal: std::sync::Arc::new(remote_goals::SqliteRemoteGoalRepository::new(pool)) as _,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use domain::{
|
use domain::{
|
||||||
errors::DomainError, models::UserSettings, ports::UserSettingsRepository, value_objects::UserId,
|
errors::DomainError,
|
||||||
|
models::UserSettings,
|
||||||
|
ports::{FederationFlags, UserFederationSettingsQuery, UserSettingsRepository},
|
||||||
|
value_objects::UserId,
|
||||||
};
|
};
|
||||||
use sqlx::{Row, SqlitePool};
|
use sqlx::{Row, SqlitePool};
|
||||||
|
|
||||||
@@ -23,20 +26,25 @@ impl SqliteUserSettingsRepository {
|
|||||||
impl UserSettingsRepository for SqliteUserSettingsRepository {
|
impl UserSettingsRepository for SqliteUserSettingsRepository {
|
||||||
async fn get(&self, user_id: &UserId) -> Result<UserSettings, DomainError> {
|
async fn get(&self, user_id: &UserId) -> Result<UserSettings, DomainError> {
|
||||||
let uid = user_id.value().to_string();
|
let uid = user_id.value().to_string();
|
||||||
|
let row = sqlx::query(
|
||||||
let row =
|
"SELECT federate_goals, federate_reviews, federate_watchlist \
|
||||||
sqlx::query("SELECT user_id, federate_goals FROM user_settings WHERE user_id = ?")
|
FROM user_settings WHERE user_id = ?",
|
||||||
.bind(&uid)
|
)
|
||||||
.fetch_optional(&self.pool)
|
.bind(&uid)
|
||||||
.await
|
.fetch_optional(&self.pool)
|
||||||
.map_err(Self::map_err)?;
|
.await
|
||||||
|
.map_err(Self::map_err)?;
|
||||||
|
|
||||||
match row {
|
match row {
|
||||||
Some(r) => {
|
Some(r) => {
|
||||||
let federate: i64 = r.try_get("federate_goals").unwrap_or(0);
|
let goals: i64 = r.try_get("federate_goals").unwrap_or(1);
|
||||||
|
let reviews: i64 = r.try_get("federate_reviews").unwrap_or(1);
|
||||||
|
let watchlist: i64 = r.try_get("federate_watchlist").unwrap_or(1);
|
||||||
Ok(UserSettings::from_persistence(
|
Ok(UserSettings::from_persistence(
|
||||||
user_id.clone(),
|
user_id.clone(),
|
||||||
federate != 0,
|
goals != 0,
|
||||||
|
reviews != 0,
|
||||||
|
watchlist != 0,
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
None => Ok(UserSettings::new(user_id.clone())),
|
None => Ok(UserSettings::new(user_id.clone())),
|
||||||
@@ -45,15 +53,51 @@ impl UserSettingsRepository for SqliteUserSettingsRepository {
|
|||||||
|
|
||||||
async fn save(&self, settings: &UserSettings) -> Result<(), DomainError> {
|
async fn save(&self, settings: &UserSettings) -> Result<(), DomainError> {
|
||||||
let uid = settings.user_id().value().to_string();
|
let uid = settings.user_id().value().to_string();
|
||||||
let federate = if settings.federate_goals() { 1i64 } else { 0 };
|
sqlx::query(
|
||||||
|
"INSERT OR REPLACE INTO user_settings \
|
||||||
sqlx::query("INSERT OR REPLACE INTO user_settings (user_id, federate_goals) VALUES (?, ?)")
|
(user_id, federate_goals, federate_reviews, federate_watchlist) \
|
||||||
.bind(&uid)
|
VALUES (?, ?, ?, ?)",
|
||||||
.bind(federate)
|
)
|
||||||
.execute(&self.pool)
|
.bind(&uid)
|
||||||
.await
|
.bind(if settings.federate_goals() { 1i64 } else { 0 })
|
||||||
.map_err(Self::map_err)?;
|
.bind(if settings.federate_reviews() { 1i64 } else { 0 })
|
||||||
|
.bind(if settings.federate_watchlist() { 1i64 } else { 0 })
|
||||||
|
.execute(&self.pool)
|
||||||
|
.await
|
||||||
|
.map_err(Self::map_err)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl UserFederationSettingsQuery for SqliteUserSettingsRepository {
|
||||||
|
async fn get_federation_flags(&self, user_id: &UserId) -> Result<FederationFlags, DomainError> {
|
||||||
|
let uid = user_id.value().to_string();
|
||||||
|
let row = sqlx::query(
|
||||||
|
"SELECT federate_goals, federate_reviews, federate_watchlist \
|
||||||
|
FROM user_settings WHERE user_id = ?",
|
||||||
|
)
|
||||||
|
.bind(&uid)
|
||||||
|
.fetch_optional(&self.pool)
|
||||||
|
.await
|
||||||
|
.map_err(Self::map_err)?;
|
||||||
|
|
||||||
|
match row {
|
||||||
|
Some(r) => {
|
||||||
|
let goals: i64 = r.try_get("federate_goals").unwrap_or(1);
|
||||||
|
let reviews: i64 = r.try_get("federate_reviews").unwrap_or(1);
|
||||||
|
let watchlist: i64 = r.try_get("federate_watchlist").unwrap_or(1);
|
||||||
|
Ok(FederationFlags {
|
||||||
|
goals: goals != 0,
|
||||||
|
reviews: reviews != 0,
|
||||||
|
watchlist: watchlist != 0,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
None => Ok(FederationFlags {
|
||||||
|
goals: true,
|
||||||
|
reviews: true,
|
||||||
|
watchlist: true,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -29,9 +29,13 @@ pub struct UpdateGoalRequest {
|
|||||||
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
|
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
|
||||||
pub struct UserSettingsDto {
|
pub struct UserSettingsDto {
|
||||||
pub federate_goals: bool,
|
pub federate_goals: bool,
|
||||||
|
pub federate_reviews: bool,
|
||||||
|
pub federate_watchlist: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize, utoipa::ToSchema)]
|
#[derive(Debug, Clone, Deserialize, utoipa::ToSchema)]
|
||||||
pub struct UpdateUserSettingsRequest {
|
pub struct UpdateUserSettingsRequest {
|
||||||
pub federate_goals: bool,
|
pub federate_goals: bool,
|
||||||
|
pub federate_reviews: bool,
|
||||||
|
pub federate_watchlist: bool,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,5 +11,7 @@ async fn returns_default_settings() {
|
|||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
assert!(!settings.federate_goals());
|
assert!(settings.federate_goals());
|
||||||
|
assert!(settings.federate_reviews());
|
||||||
|
assert!(settings.federate_watchlist());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,31 @@ async fn updates_federate_goals() {
|
|||||||
let settings_repo = InMemoryUserSettingsRepository::new();
|
let settings_repo = InMemoryUserSettingsRepository::new();
|
||||||
let b = TestContextBuilder::new().with_user_settings(Arc::clone(&settings_repo) as _);
|
let b = TestContextBuilder::new().with_user_settings(Arc::clone(&settings_repo) as _);
|
||||||
let user_settings = b.user_settings_repo.clone();
|
let user_settings = b.user_settings_repo.clone();
|
||||||
|
let uid = Uuid::nil();
|
||||||
|
|
||||||
|
crate::users::update_settings::execute(
|
||||||
|
user_settings.clone(),
|
||||||
|
UpdateUserSettingsCommand {
|
||||||
|
user_id: uid,
|
||||||
|
federate_goals: false,
|
||||||
|
federate_reviews: true,
|
||||||
|
federate_watchlist: true,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let settings = get_settings::execute(user_settings, uid).await.unwrap();
|
||||||
|
assert!(!settings.federate_goals());
|
||||||
|
assert!(settings.federate_reviews());
|
||||||
|
assert!(settings.federate_watchlist());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn updates_federate_reviews() {
|
||||||
|
let settings_repo = InMemoryUserSettingsRepository::new();
|
||||||
|
let b = TestContextBuilder::new().with_user_settings(Arc::clone(&settings_repo) as _);
|
||||||
|
let user_settings = b.user_settings_repo.clone();
|
||||||
let uid = Uuid::nil();
|
let uid = Uuid::nil();
|
||||||
|
|
||||||
crate::users::update_settings::execute(
|
crate::users::update_settings::execute(
|
||||||
@@ -21,6 +45,8 @@ async fn updates_federate_goals() {
|
|||||||
UpdateUserSettingsCommand {
|
UpdateUserSettingsCommand {
|
||||||
user_id: uid,
|
user_id: uid,
|
||||||
federate_goals: true,
|
federate_goals: true,
|
||||||
|
federate_reviews: false,
|
||||||
|
federate_watchlist: true,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
@@ -28,4 +54,31 @@ async fn updates_federate_goals() {
|
|||||||
|
|
||||||
let settings = get_settings::execute(user_settings, uid).await.unwrap();
|
let settings = get_settings::execute(user_settings, uid).await.unwrap();
|
||||||
assert!(settings.federate_goals());
|
assert!(settings.federate_goals());
|
||||||
|
assert!(!settings.federate_reviews());
|
||||||
|
assert!(settings.federate_watchlist());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn updates_federate_watchlist() {
|
||||||
|
let settings_repo = InMemoryUserSettingsRepository::new();
|
||||||
|
let b = TestContextBuilder::new().with_user_settings(Arc::clone(&settings_repo) as _);
|
||||||
|
let user_settings = b.user_settings_repo.clone();
|
||||||
|
let uid = Uuid::nil();
|
||||||
|
|
||||||
|
crate::users::update_settings::execute(
|
||||||
|
user_settings.clone(),
|
||||||
|
UpdateUserSettingsCommand {
|
||||||
|
user_id: uid,
|
||||||
|
federate_goals: true,
|
||||||
|
federate_reviews: true,
|
||||||
|
federate_watchlist: false,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let settings = get_settings::execute(user_settings, uid).await.unwrap();
|
||||||
|
assert!(settings.federate_goals());
|
||||||
|
assert!(settings.federate_reviews());
|
||||||
|
assert!(!settings.federate_watchlist());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ use domain::{errors::DomainError, ports::UserSettingsRepository, value_objects::
|
|||||||
pub struct UpdateUserSettingsCommand {
|
pub struct UpdateUserSettingsCommand {
|
||||||
pub user_id: uuid::Uuid,
|
pub user_id: uuid::Uuid,
|
||||||
pub federate_goals: bool,
|
pub federate_goals: bool,
|
||||||
|
pub federate_reviews: bool,
|
||||||
|
pub federate_watchlist: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn execute(
|
pub async fn execute(
|
||||||
@@ -14,6 +16,8 @@ pub async fn execute(
|
|||||||
let uid = UserId::from_uuid(cmd.user_id);
|
let uid = UserId::from_uuid(cmd.user_id);
|
||||||
let mut settings = user_settings.get(&uid).await?;
|
let mut settings = user_settings.get(&uid).await?;
|
||||||
settings.set_federate_goals(cmd.federate_goals);
|
settings.set_federate_goals(cmd.federate_goals);
|
||||||
|
settings.set_federate_reviews(cmd.federate_reviews);
|
||||||
|
settings.set_federate_watchlist(cmd.federate_watchlist);
|
||||||
user_settings.save(&settings).await
|
user_settings.save(&settings).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,27 +4,34 @@ use crate::value_objects::UserId;
|
|||||||
pub struct UserSettings {
|
pub struct UserSettings {
|
||||||
user_id: UserId,
|
user_id: UserId,
|
||||||
federate_goals: bool,
|
federate_goals: bool,
|
||||||
|
federate_reviews: bool,
|
||||||
|
federate_watchlist: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl UserSettings {
|
impl UserSettings {
|
||||||
pub fn new(user_id: UserId) -> Self {
|
pub fn new(user_id: UserId) -> Self {
|
||||||
Self {
|
Self {
|
||||||
user_id,
|
user_id,
|
||||||
federate_goals: false,
|
federate_goals: true,
|
||||||
|
federate_reviews: true,
|
||||||
|
federate_watchlist: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn from_persistence(user_id: UserId, federate_goals: bool) -> Self {
|
pub fn from_persistence(
|
||||||
|
user_id: UserId,
|
||||||
|
federate_goals: bool,
|
||||||
|
federate_reviews: bool,
|
||||||
|
federate_watchlist: bool,
|
||||||
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
user_id,
|
user_id,
|
||||||
federate_goals,
|
federate_goals,
|
||||||
|
federate_reviews,
|
||||||
|
federate_watchlist,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_federate_goals(&mut self, value: bool) {
|
|
||||||
self.federate_goals = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn user_id(&self) -> &UserId {
|
pub fn user_id(&self) -> &UserId {
|
||||||
&self.user_id
|
&self.user_id
|
||||||
}
|
}
|
||||||
@@ -32,4 +39,24 @@ impl UserSettings {
|
|||||||
pub fn federate_goals(&self) -> bool {
|
pub fn federate_goals(&self) -> bool {
|
||||||
self.federate_goals
|
self.federate_goals
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn set_federate_goals(&mut self, value: bool) {
|
||||||
|
self.federate_goals = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn federate_reviews(&self) -> bool {
|
||||||
|
self.federate_reviews
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_federate_reviews(&mut self, value: bool) {
|
||||||
|
self.federate_reviews = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn federate_watchlist(&self) -> bool {
|
||||||
|
self.federate_watchlist
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_federate_watchlist(&mut self, value: bool) {
|
||||||
|
self.federate_watchlist = value;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -456,6 +456,17 @@ pub trait UserSettingsRepository: Send + Sync {
|
|||||||
async fn save(&self, settings: &UserSettings) -> Result<(), DomainError>;
|
async fn save(&self, settings: &UserSettings) -> Result<(), DomainError>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct FederationFlags {
|
||||||
|
pub goals: bool,
|
||||||
|
pub reviews: bool,
|
||||||
|
pub watchlist: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
pub trait UserFederationSettingsQuery: Send + Sync {
|
||||||
|
async fn get_federation_flags(&self, user_id: &UserId) -> Result<FederationFlags, DomainError>;
|
||||||
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
pub trait RemoteGoalRepository: Send + Sync {
|
pub trait RemoteGoalRepository: Send + Sync {
|
||||||
async fn save(&self, entry: RemoteGoalEntry) -> Result<(), DomainError>;
|
async fn save(&self, entry: RemoteGoalEntry) -> Result<(), DomainError>;
|
||||||
@@ -502,8 +513,6 @@ pub trait LocalApContentQuery: Send + Sync {
|
|||||||
limit: usize,
|
limit: usize,
|
||||||
) -> Result<Vec<DiaryEntry>, DomainError>;
|
) -> Result<Vec<DiaryEntry>, DomainError>;
|
||||||
|
|
||||||
async fn get_user_federate_goals(&self, user_id: &UserId) -> Result<bool, DomainError>;
|
|
||||||
|
|
||||||
async fn get_goal_with_progress(
|
async fn get_goal_with_progress(
|
||||||
&self,
|
&self,
|
||||||
user_id: &UserId,
|
user_id: &UserId,
|
||||||
|
|||||||
@@ -18,9 +18,10 @@ use crate::{
|
|||||||
collections::{PageParams, Paginated},
|
collections::{PageParams, Paginated},
|
||||||
},
|
},
|
||||||
ports::{
|
ports::{
|
||||||
GoalRepository, ImportProfileRepository, ImportSessionRepository, MovieProfileRepository,
|
FederationFlags, GoalRepository, ImportProfileRepository, ImportSessionRepository,
|
||||||
MovieRepository, RefreshSessionRepository, ReviewRepository, UserProfileFieldsRepository,
|
MovieProfileRepository, MovieRepository, RefreshSessionRepository, ReviewRepository,
|
||||||
UserRepository, UserSettingsRepository, WatchEventRepository, WatchlistRepository,
|
UserFederationSettingsQuery, UserProfileFieldsRepository, UserRepository,
|
||||||
|
UserSettingsRepository, WatchEventRepository, WatchlistRepository,
|
||||||
WebhookTokenRepository,
|
WebhookTokenRepository,
|
||||||
},
|
},
|
||||||
value_objects::{
|
value_objects::{
|
||||||
@@ -441,6 +442,22 @@ impl UserSettingsRepository for InMemoryUserSettingsRepository {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl UserFederationSettingsQuery for InMemoryUserSettingsRepository {
|
||||||
|
async fn get_federation_flags(&self, user_id: &UserId) -> Result<FederationFlags, DomainError> {
|
||||||
|
let store = self.store.lock().unwrap();
|
||||||
|
let settings = store
|
||||||
|
.get(&user_id.value())
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or_else(|| UserSettings::new(user_id.clone()));
|
||||||
|
Ok(FederationFlags {
|
||||||
|
goals: settings.federate_goals(),
|
||||||
|
reviews: settings.federate_reviews(),
|
||||||
|
watchlist: settings.federate_watchlist(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ── InMemoryWebhookTokenRepository ──────────────────────────────────────────
|
// ── InMemoryWebhookTokenRepository ──────────────────────────────────────────
|
||||||
|
|
||||||
pub struct InMemoryWebhookTokenRepository {
|
pub struct InMemoryWebhookTokenRepository {
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ pub struct DatabaseOutput {
|
|||||||
pub wrapup_repo: Arc<dyn domain::ports::WrapUpRepository>,
|
pub wrapup_repo: Arc<dyn domain::ports::WrapUpRepository>,
|
||||||
pub goal: Arc<dyn domain::ports::GoalRepository>,
|
pub goal: Arc<dyn domain::ports::GoalRepository>,
|
||||||
pub user_settings: Arc<dyn domain::ports::UserSettingsRepository>,
|
pub user_settings: Arc<dyn domain::ports::UserSettingsRepository>,
|
||||||
|
pub federation_settings: std::sync::Arc<dyn domain::ports::UserFederationSettingsQuery>,
|
||||||
pub remote_goal: Arc<dyn domain::ports::RemoteGoalRepository>,
|
pub remote_goal: Arc<dyn domain::ports::RemoteGoalRepository>,
|
||||||
pub refresh_session: Arc<dyn RefreshSessionRepository>,
|
pub refresh_session: Arc<dyn RefreshSessionRepository>,
|
||||||
pub db_pool: DbPool,
|
pub db_pool: DbPool,
|
||||||
@@ -78,6 +79,7 @@ pub async fn build_database_adapters(backend: &str, url: &str) -> anyhow::Result
|
|||||||
wrapup_repo: w.wrapup_repo,
|
wrapup_repo: w.wrapup_repo,
|
||||||
goal: w.goal,
|
goal: w.goal,
|
||||||
user_settings: w.user_settings,
|
user_settings: w.user_settings,
|
||||||
|
federation_settings: w.federation_settings,
|
||||||
remote_goal: w.remote_goal,
|
remote_goal: w.remote_goal,
|
||||||
refresh_session: Arc::new(postgres::PostgresRefreshSessionAdapter::new(
|
refresh_session: Arc::new(postgres::PostgresRefreshSessionAdapter::new(
|
||||||
w.pool.clone(),
|
w.pool.clone(),
|
||||||
@@ -119,6 +121,7 @@ pub async fn build_database_adapters(backend: &str, url: &str) -> anyhow::Result
|
|||||||
wrapup_repo: w.wrapup_repo,
|
wrapup_repo: w.wrapup_repo,
|
||||||
goal: w.goal,
|
goal: w.goal,
|
||||||
user_settings: w.user_settings,
|
user_settings: w.user_settings,
|
||||||
|
federation_settings: w.federation_settings,
|
||||||
remote_goal: w.remote_goal,
|
remote_goal: w.remote_goal,
|
||||||
refresh_session: Arc::new(sqlite::SqliteRefreshSessionAdapter::new(w.pool.clone()))
|
refresh_session: Arc::new(sqlite::SqliteRefreshSessionAdapter::new(w.pool.clone()))
|
||||||
as _,
|
as _,
|
||||||
|
|||||||
@@ -176,6 +176,8 @@ pub async fn get_settings(
|
|||||||
.await?;
|
.await?;
|
||||||
Ok(Json(UserSettingsDto {
|
Ok(Json(UserSettingsDto {
|
||||||
federate_goals: settings.federate_goals(),
|
federate_goals: settings.federate_goals(),
|
||||||
|
federate_reviews: settings.federate_reviews(),
|
||||||
|
federate_watchlist: settings.federate_watchlist(),
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -198,6 +200,8 @@ pub async fn update_settings(
|
|||||||
application::users::update_settings::UpdateUserSettingsCommand {
|
application::users::update_settings::UpdateUserSettingsCommand {
|
||||||
user_id: user.0.value(),
|
user_id: user.0.value(),
|
||||||
federate_goals: req.federate_goals,
|
federate_goals: req.federate_goals,
|
||||||
|
federate_reviews: req.federate_reviews,
|
||||||
|
federate_watchlist: req.federate_watchlist,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|||||||
@@ -119,6 +119,7 @@ async fn wire_dependencies() -> anyhow::Result<(AppState, axum::Router)> {
|
|||||||
remote_goal_repo: Arc::clone(&db.remote_goal),
|
remote_goal_repo: Arc::clone(&db.remote_goal),
|
||||||
local_ap_content: Arc::clone(&ap_content_repo),
|
local_ap_content: Arc::clone(&ap_content_repo),
|
||||||
user_repo: Arc::clone(&db.user),
|
user_repo: Arc::clone(&db.user),
|
||||||
|
federation_settings: std::sync::Arc::clone(&db.federation_settings),
|
||||||
base_url: app_config.base_url.clone(),
|
base_url: app_config.base_url.clone(),
|
||||||
allow_registration: app_config.allow_registration,
|
allow_registration: app_config.allow_registration,
|
||||||
event_publisher: Arc::clone(&ep),
|
event_publisher: Arc::clone(&ep),
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ use anyhow::Context;
|
|||||||
use domain::ports::{
|
use domain::ports::{
|
||||||
ImageRefCommand, ImageRefQuery, ImportSessionRepository, LocalApContentQuery,
|
ImageRefCommand, ImageRefQuery, ImportSessionRepository, LocalApContentQuery,
|
||||||
MovieProfileRepository, MovieRepository, PersonCommand, PersonQuery, SearchCommand,
|
MovieProfileRepository, MovieRepository, PersonCommand, PersonQuery, SearchCommand,
|
||||||
UserRepository, WatchEventRepository,
|
UserFederationSettingsQuery, UserRepository, WatchEventRepository,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub enum DbPool {
|
pub enum DbPool {
|
||||||
@@ -30,6 +30,7 @@ pub struct WorkerDbOutput {
|
|||||||
pub wrapup_repo: Arc<dyn domain::ports::WrapUpRepository>,
|
pub wrapup_repo: Arc<dyn domain::ports::WrapUpRepository>,
|
||||||
pub remote_goal: Arc<dyn domain::ports::RemoteGoalRepository>,
|
pub remote_goal: Arc<dyn domain::ports::RemoteGoalRepository>,
|
||||||
pub refresh_session: Arc<dyn domain::ports::RefreshSessionRepository>,
|
pub refresh_session: Arc<dyn domain::ports::RefreshSessionRepository>,
|
||||||
|
pub federation_settings: Arc<dyn domain::ports::UserFederationSettingsQuery>,
|
||||||
pub db_pool: DbPool,
|
pub db_pool: DbPool,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,6 +65,7 @@ pub async fn connect(database_url: &str, backend: &str) -> anyhow::Result<Worker
|
|||||||
refresh_session: Arc::new(postgres::PostgresRefreshSessionAdapter::new(
|
refresh_session: Arc::new(postgres::PostgresRefreshSessionAdapter::new(
|
||||||
w.pool.clone(),
|
w.pool.clone(),
|
||||||
)) as _,
|
)) as _,
|
||||||
|
federation_settings: w.federation_settings,
|
||||||
db_pool: DbPool::Postgres(w.pool),
|
db_pool: DbPool::Postgres(w.pool),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -95,6 +97,7 @@ pub async fn connect(database_url: &str, backend: &str) -> anyhow::Result<Worker
|
|||||||
remote_goal: w.remote_goal,
|
remote_goal: w.remote_goal,
|
||||||
refresh_session: Arc::new(sqlite::SqliteRefreshSessionAdapter::new(w.pool.clone()))
|
refresh_session: Arc::new(sqlite::SqliteRefreshSessionAdapter::new(w.pool.clone()))
|
||||||
as _,
|
as _,
|
||||||
|
federation_settings: w.federation_settings,
|
||||||
db_pool: DbPool::Sqlite(w.pool),
|
db_pool: DbPool::Sqlite(w.pool),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -241,6 +241,7 @@ async fn main() -> anyhow::Result<()> {
|
|||||||
base_url,
|
base_url,
|
||||||
allow_registration,
|
allow_registration,
|
||||||
event_publisher: Arc::clone(&event_publisher),
|
event_publisher: Arc::clone(&event_publisher),
|
||||||
|
federation_settings: std::sync::Arc::clone(&db.federation_settings),
|
||||||
})
|
})
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
|||||||
@@ -18,11 +18,15 @@ export type UpdateGoalRequest = {
|
|||||||
|
|
||||||
export const userSettingsDtoSchema = z.object({
|
export const userSettingsDtoSchema = z.object({
|
||||||
federate_goals: z.boolean(),
|
federate_goals: z.boolean(),
|
||||||
|
federate_reviews: z.boolean(),
|
||||||
|
federate_watchlist: z.boolean(),
|
||||||
})
|
})
|
||||||
export type UserSettingsDto = z.infer<typeof userSettingsDtoSchema>
|
export type UserSettingsDto = z.infer<typeof userSettingsDtoSchema>
|
||||||
|
|
||||||
export type UpdateUserSettingsRequest = {
|
export type UpdateUserSettingsRequest = {
|
||||||
federate_goals: boolean
|
federate_goals: boolean
|
||||||
|
federate_reviews: boolean
|
||||||
|
federate_watchlist: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getGoals() {
|
export function getGoals() {
|
||||||
|
|||||||
@@ -178,6 +178,10 @@
|
|||||||
"privacy": "Privacy",
|
"privacy": "Privacy",
|
||||||
"federateGoals": "Share goals on Fediverse",
|
"federateGoals": "Share goals on Fediverse",
|
||||||
"federateGoalsDesc": "Broadcast goal progress to followers",
|
"federateGoalsDesc": "Broadcast goal progress to followers",
|
||||||
|
"federateReviews": "Share reviews on Fediverse",
|
||||||
|
"federateReviewsDesc": "Broadcast diary entries to followers",
|
||||||
|
"federateWatchlist": "Share watchlist on Fediverse",
|
||||||
|
"federateWatchlistDesc": "Broadcast watchlist additions to followers",
|
||||||
"export": "Export",
|
"export": "Export",
|
||||||
"exportDesc": "Download your diary",
|
"exportDesc": "Download your diary",
|
||||||
"exportCsv": "CSV",
|
"exportCsv": "CSV",
|
||||||
|
|||||||
@@ -3,9 +3,11 @@ import { useTranslation } from "react-i18next"
|
|||||||
import { useMutation } from "@tanstack/react-query"
|
import { useMutation } from "@tanstack/react-query"
|
||||||
import {
|
import {
|
||||||
ArrowLeft,
|
ArrowLeft,
|
||||||
|
BookOpen,
|
||||||
ChevronRight,
|
ChevronRight,
|
||||||
Download,
|
Download,
|
||||||
Key,
|
Key,
|
||||||
|
List,
|
||||||
LogOut,
|
LogOut,
|
||||||
RefreshCw,
|
RefreshCw,
|
||||||
ShieldBan,
|
ShieldBan,
|
||||||
@@ -19,6 +21,7 @@ import { Switch } from "@/components/ui/switch"
|
|||||||
import { useAuth, useIsAdmin } from "@/components/auth-provider"
|
import { useAuth, useIsAdmin } from "@/components/auth-provider"
|
||||||
import { reindexSearch } from "@/lib/api/users"
|
import { reindexSearch } from "@/lib/api/users"
|
||||||
import { useSettings, useUpdateSettings } from "@/hooks/use-goals"
|
import { useSettings, useUpdateSettings } from "@/hooks/use-goals"
|
||||||
|
import { UpdateUserSettingsRequest } from "@/lib/api/goals"
|
||||||
import { useDocumentTitle } from "@/hooks/use-document-title"
|
import { useDocumentTitle } from "@/hooks/use-document-title"
|
||||||
|
|
||||||
export const Route = createFileRoute("/_app/settings/")({
|
export const Route = createFileRoute("/_app/settings/")({
|
||||||
@@ -128,6 +131,18 @@ function PrivacySection() {
|
|||||||
const { data: settings } = useSettings()
|
const { data: settings } = useSettings()
|
||||||
const updateMutation = useUpdateSettings()
|
const updateMutation = useUpdateSettings()
|
||||||
|
|
||||||
|
const disabled = updateMutation.isPending
|
||||||
|
|
||||||
|
const toggle = (patch: Partial<UpdateUserSettingsRequest>) => {
|
||||||
|
if (!settings) return
|
||||||
|
updateMutation.mutate({
|
||||||
|
federate_goals: settings.federate_goals,
|
||||||
|
federate_reviews: settings.federate_reviews,
|
||||||
|
federate_watchlist: settings.federate_watchlist,
|
||||||
|
...patch,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<p className="mb-1.5 px-1 text-xs font-medium text-muted-foreground">
|
<p className="mb-1.5 px-1 text-xs font-medium text-muted-foreground">
|
||||||
@@ -145,11 +160,41 @@ function PrivacySection() {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Switch
|
<Switch
|
||||||
checked={settings?.federate_goals ?? false}
|
checked={settings?.federate_goals ?? true}
|
||||||
onCheckedChange={(checked) =>
|
onCheckedChange={(checked) => toggle({ federate_goals: checked })}
|
||||||
updateMutation.mutate({ federate_goals: checked })
|
disabled={disabled}
|
||||||
}
|
/>
|
||||||
disabled={updateMutation.isPending}
|
</div>
|
||||||
|
<div className="flex items-center gap-3 p-3">
|
||||||
|
<span className="text-muted-foreground">
|
||||||
|
<BookOpen className="size-4" />
|
||||||
|
</span>
|
||||||
|
<div className="flex-1">
|
||||||
|
<p className="text-sm font-medium">{t("settings.federateReviews")}</p>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
{t("settings.federateReviewsDesc")}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Switch
|
||||||
|
checked={settings?.federate_reviews ?? true}
|
||||||
|
onCheckedChange={(checked) => toggle({ federate_reviews: checked })}
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-3 p-3">
|
||||||
|
<span className="text-muted-foreground">
|
||||||
|
<List className="size-4" />
|
||||||
|
</span>
|
||||||
|
<div className="flex-1">
|
||||||
|
<p className="text-sm font-medium">{t("settings.federateWatchlist")}</p>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
{t("settings.federateWatchlistDesc")}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Switch
|
||||||
|
checked={settings?.federate_watchlist ?? true}
|
||||||
|
onCheckedChange={(checked) => toggle({ federate_watchlist: checked })}
|
||||||
|
disabled={disabled}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user