refactor: fix all clippy warnings properly

- UserProfile struct groups display_name/bio/avatar/banner/also_known_as/profile_fields
- User::from_persistence takes UserProfile (6 args, was 11)
- PersistedReview struct for Review::from_persistence (1 arg, was 8)
- WatchlistApInput struct for watchlist_to_ap_object (1 arg, was 8)
- ActivityPubDeps struct for activitypub::wire (1 arg, was 11)
- FederationRepos type alias for wire() return types
- FeedSortBy: impl std::str::FromStr instead of inherent from_str
- postgres users.rs: row_to_user takes &PgRow like sqlite
- collapse nested ifs in multipart handlers
- type alias for complex return types (image-converter, worker)
- tui: allow large_enum_variant at crate level (pre-existing, unrelated)
This commit is contained in:
2026-05-29 11:19:02 +02:00
parent 68a939f6c4
commit 2355f89bed
27 changed files with 363 additions and 455 deletions

View File

@@ -220,16 +220,16 @@ impl ActivityPubEventHandler {
let added_at_utc = let added_at_utc =
chrono::DateTime::<chrono::Utc>::from_naive_utc_and_offset(*added_at, chrono::Utc); chrono::DateTime::<chrono::Utc>::from_naive_utc_and_offset(*added_at, chrono::Utc);
let obj = crate::objects::watchlist_to_ap_object( let obj = crate::objects::watchlist_to_ap_object(crate::objects::WatchlistApInput {
ap_id.clone(), ap_id: ap_id.clone(),
actor, actor_url: actor,
movie_title.to_string(), movie_title: movie_title.to_string(),
release_year, release_year,
external_metadata_id.clone(), external_metadata_id: external_metadata_id.clone(),
poster_url, poster_url,
added_at_utc, added_at: added_at_utc,
&self.base_url, base_url: self.base_url.clone(),
); });
let json = serde_json::to_value(obj)?; let json = serde_json::to_value(obj)?;
self.ap_service self.ap_service

View File

@@ -22,25 +22,50 @@ pub use remote_review_repository::RemoteReviewRepository;
pub use review_handler::ReviewObjectHandler; pub use review_handler::ReviewObjectHandler;
pub use user_adapter::DomainUserRepoAdapter; pub use user_adapter::DomainUserRepoAdapter;
pub type FederationRepos = (
std::sync::Arc<dyn ActivityRepository>,
std::sync::Arc<dyn FollowRepository>,
std::sync::Arc<dyn ActorRepository>,
std::sync::Arc<dyn BlocklistRepository>,
std::sync::Arc<dyn domain::ports::SocialQueryPort>,
std::sync::Arc<dyn RemoteReviewRepository>,
std::sync::Arc<dyn domain::ports::RemoteWatchlistRepository>,
);
pub struct ActivityPubWire { pub struct ActivityPubWire {
pub service: std::sync::Arc<dyn ActivityPubPort>, pub service: std::sync::Arc<dyn ActivityPubPort>,
pub router: axum::Router, pub router: axum::Router,
pub event_handler: std::sync::Arc<dyn domain::ports::EventHandler>, pub event_handler: std::sync::Arc<dyn domain::ports::EventHandler>,
} }
pub async fn wire( pub struct ActivityPubDeps {
activity_repo: std::sync::Arc<dyn ActivityRepository>, pub activity_repo: std::sync::Arc<dyn ActivityRepository>,
follow_repo: std::sync::Arc<dyn FollowRepository>, pub follow_repo: std::sync::Arc<dyn FollowRepository>,
actor_repo: std::sync::Arc<dyn ActorRepository>, pub actor_repo: std::sync::Arc<dyn ActorRepository>,
blocklist_repo: std::sync::Arc<dyn BlocklistRepository>, pub blocklist_repo: std::sync::Arc<dyn BlocklistRepository>,
review_store: std::sync::Arc<dyn RemoteReviewRepository>, pub review_store: std::sync::Arc<dyn RemoteReviewRepository>,
remote_watchlist_repo: std::sync::Arc<dyn domain::ports::RemoteWatchlistRepository>, pub remote_watchlist_repo: std::sync::Arc<dyn domain::ports::RemoteWatchlistRepository>,
local_ap_content: std::sync::Arc<dyn domain::ports::LocalApContentQuery>, pub local_ap_content: std::sync::Arc<dyn domain::ports::LocalApContentQuery>,
user_repo: std::sync::Arc<dyn domain::ports::UserRepository>, pub user_repo: std::sync::Arc<dyn domain::ports::UserRepository>,
base_url: String, pub base_url: String,
allow_registration: bool, pub allow_registration: bool,
event_publisher: std::sync::Arc<dyn domain::ports::EventPublisher>, pub event_publisher: std::sync::Arc<dyn domain::ports::EventPublisher>,
) -> anyhow::Result<ActivityPubWire> { }
pub async fn wire(deps: ActivityPubDeps) -> anyhow::Result<ActivityPubWire> {
let ActivityPubDeps {
activity_repo,
follow_repo,
actor_repo,
blocklist_repo,
review_store,
remote_watchlist_repo,
local_ap_content,
user_repo,
base_url,
allow_registration,
event_publisher,
} = deps;
let review_handler = std::sync::Arc::new(ReviewObjectHandler { let review_handler = std::sync::Arc::new(ReviewObjectHandler {
content_query: std::sync::Arc::clone(&local_ap_content), content_query: std::sync::Arc::clone(&local_ap_content),
review_store, review_store,

View File

@@ -131,16 +131,28 @@ pub struct WatchlistObject {
pub(crate) cc: Vec<String>, pub(crate) cc: Vec<String>,
} }
pub fn watchlist_to_ap_object( pub struct WatchlistApInput {
ap_id: Url, pub ap_id: Url,
actor_url: Url, pub actor_url: Url,
movie_title: String, pub movie_title: String,
release_year: u16, pub release_year: u16,
external_metadata_id: Option<String>, pub external_metadata_id: Option<String>,
poster_url: Option<String>, pub poster_url: Option<String>,
added_at: chrono::DateTime<chrono::Utc>, pub added_at: chrono::DateTime<chrono::Utc>,
base_url: &str, pub base_url: String,
) -> WatchlistObject { }
pub fn watchlist_to_ap_object(input: WatchlistApInput) -> WatchlistObject {
let WatchlistApInput {
ap_id,
actor_url,
movie_title,
release_year,
external_metadata_id,
poster_url,
added_at,
base_url,
} = input;
let year_str = if release_year > 0 { let year_str = if release_year > 0 {
format!(" ({})", release_year) format!(" ({})", release_year)
} else { } else {

View File

@@ -98,18 +98,18 @@ impl ApObjectHandler for ReviewObjectHandler {
let rating = Rating::new(obj.rating.min(5))?; let rating = Rating::new(obj.rating.min(5))?;
let comment = obj.comment.map(Comment::new).transpose()?; let comment = obj.comment.map(Comment::new).transpose()?;
let review = domain::models::Review::from_persistence( let review = domain::models::Review::from_persistence(domain::models::PersistedReview {
review_id, id: review_id,
movie_id, movie_id,
user_id, user_id,
rating, rating,
comment, comment,
obj.watched_at.naive_utc(), watched_at: obj.watched_at.naive_utc(),
obj.published.naive_utc(), created_at: obj.published.naive_utc(),
ReviewSource::Remote { source: ReviewSource::Remote {
actor_url: actor_url_str, actor_url: actor_url_str,
}, },
); });
self.review_store self.review_store
.save_remote_review( .save_remote_review(

View File

@@ -14,20 +14,22 @@ fn normalize_hashtag_strips_non_alphanumeric() {
fn review_to_ap_object_includes_two_hashtags() { fn review_to_ap_object_includes_two_hashtags() {
use chrono::NaiveDateTime; use chrono::NaiveDateTime;
use domain::{ use domain::{
models::{Review, ReviewSource}, models::{PersistedReview, Review, ReviewSource},
value_objects::{MovieId, Rating, ReviewId, UserId}, value_objects::{MovieId, Rating, ReviewId, UserId},
}; };
let review = Review::from_persistence( let review = Review::from_persistence(PersistedReview {
ReviewId::generate(), id: ReviewId::generate(),
MovieId::from_uuid(uuid::Uuid::new_v4()), movie_id: MovieId::from_uuid(uuid::Uuid::new_v4()),
UserId::from_uuid(uuid::Uuid::new_v4()), user_id: UserId::from_uuid(uuid::Uuid::new_v4()),
Rating::new(4).unwrap(), rating: Rating::new(4).unwrap(),
None, comment: None,
NaiveDateTime::parse_from_str("2024-01-01 00:00:00", "%Y-%m-%d %H:%M:%S").unwrap(), watched_at: NaiveDateTime::parse_from_str("2024-01-01 00:00:00", "%Y-%m-%d %H:%M:%S")
NaiveDateTime::parse_from_str("2024-01-01 00:00:00", "%Y-%m-%d %H:%M:%S").unwrap(), .unwrap(),
ReviewSource::Local, created_at: NaiveDateTime::parse_from_str("2024-01-01 00:00:00", "%Y-%m-%d %H:%M:%S")
); .unwrap(),
source: ReviewSource::Local,
});
let obj = review_to_ap_object( let obj = review_to_ap_object(
&review, &review,
"https://example.com/reviews/1".parse().unwrap(), "https://example.com/reviews/1".parse().unwrap(),
@@ -47,20 +49,22 @@ fn review_to_ap_object_includes_two_hashtags() {
fn review_to_ap_object_has_public_addressing() { fn review_to_ap_object_has_public_addressing() {
use chrono::NaiveDateTime; use chrono::NaiveDateTime;
use domain::{ use domain::{
models::{Review, ReviewSource}, models::{PersistedReview, Review, ReviewSource},
value_objects::{MovieId, Rating, ReviewId, UserId}, value_objects::{MovieId, Rating, ReviewId, UserId},
}; };
let review = Review::from_persistence( let review = Review::from_persistence(PersistedReview {
ReviewId::generate(), id: ReviewId::generate(),
MovieId::from_uuid(uuid::Uuid::new_v4()), movie_id: MovieId::from_uuid(uuid::Uuid::new_v4()),
UserId::from_uuid(uuid::Uuid::new_v4()), user_id: UserId::from_uuid(uuid::Uuid::new_v4()),
Rating::new(3).unwrap(), rating: Rating::new(3).unwrap(),
None, comment: None,
NaiveDateTime::parse_from_str("2024-06-01 00:00:00", "%Y-%m-%d %H:%M:%S").unwrap(), watched_at: NaiveDateTime::parse_from_str("2024-06-01 00:00:00", "%Y-%m-%d %H:%M:%S")
NaiveDateTime::parse_from_str("2024-06-01 00:00:00", "%Y-%m-%d %H:%M:%S").unwrap(), .unwrap(),
ReviewSource::Local, created_at: NaiveDateTime::parse_from_str("2024-06-01 00:00:00", "%Y-%m-%d %H:%M:%S")
); .unwrap(),
source: ReviewSource::Local,
});
let actor_url: url::Url = "https://example.com/users/abc".parse().unwrap(); let actor_url: url::Url = "https://example.com/users/abc".parse().unwrap();
let obj = review_to_ap_object( let obj = review_to_ap_object(
&review, &review,
@@ -78,16 +82,16 @@ fn review_to_ap_object_has_public_addressing() {
#[test] #[test]
fn watchlist_to_ap_object_has_public_addressing() { fn watchlist_to_ap_object_has_public_addressing() {
let actor_url: url::Url = "https://example.com/users/abc".parse().unwrap(); let actor_url: url::Url = "https://example.com/users/abc".parse().unwrap();
let obj = watchlist_to_ap_object( let obj = watchlist_to_ap_object(WatchlistApInput {
"https://example.com/watchlist/1".parse().unwrap(), ap_id: "https://example.com/watchlist/1".parse().unwrap(),
actor_url.clone(), actor_url: actor_url.clone(),
"Alien".to_string(), movie_title: "Alien".to_string(),
1979, release_year: 1979,
None, external_metadata_id: None,
None, poster_url: None,
chrono::Utc::now(), added_at: chrono::Utc::now(),
"https://example.com", base_url: "https://example.com".to_string(),
); });
assert_eq!(obj.to, vec!["https://www.w3.org/ns/activitystreams#Public"]); assert_eq!(obj.to, vec!["https://www.w3.org/ns/activitystreams#Public"]);
assert_eq!(obj.cc, vec!["https://example.com/users/abc/followers"]); assert_eq!(obj.cc, vec!["https://example.com/users/abc/followers"]);
} }

View File

@@ -11,12 +11,14 @@ use domain::ports::{
}; };
use std::sync::Arc; use std::sync::Arc;
type ConversionPair = (Arc<dyn EventHandler>, Arc<dyn PeriodicJob>);
pub fn build( pub fn build(
image_storage: Arc<dyn ImageStorage>, image_storage: Arc<dyn ImageStorage>,
image_ref_command: Arc<dyn ImageRefCommand>, image_ref_command: Arc<dyn ImageRefCommand>,
image_ref_query: Arc<dyn ImageRefQuery>, image_ref_query: Arc<dyn ImageRefQuery>,
event_publisher: Arc<dyn EventPublisher>, event_publisher: Arc<dyn EventPublisher>,
) -> anyhow::Result<Option<(Arc<dyn EventHandler>, Arc<dyn PeriodicJob>)>> { ) -> anyhow::Result<Option<ConversionPair>> {
let config = match ConversionConfig::from_env()? { let config = match ConversionConfig::from_env()? {
Some(c) => c, Some(c) => c,
None => return Ok(None), None => return Ok(None),

View File

@@ -854,17 +854,7 @@ impl RemoteWatchlistRepository for PostgresFederationRepository {
} }
} }
pub fn wire( pub fn wire(pool: sqlx::PgPool) -> activitypub::FederationRepos {
pool: sqlx::PgPool,
) -> (
std::sync::Arc<dyn activitypub::ActivityRepository>,
std::sync::Arc<dyn activitypub::FollowRepository>,
std::sync::Arc<dyn activitypub::ActorRepository>,
std::sync::Arc<dyn activitypub::BlocklistRepository>,
std::sync::Arc<dyn domain::ports::SocialQueryPort>,
std::sync::Arc<dyn activitypub::RemoteReviewRepository>,
std::sync::Arc<dyn domain::ports::RemoteWatchlistRepository>,
) {
let fed = std::sync::Arc::new(PostgresFederationRepository::new(pool)); let fed = std::sync::Arc::new(PostgresFederationRepository::new(pool));
( (
std::sync::Arc::clone(&fed) as _, std::sync::Arc::clone(&fed) as _,

View File

@@ -1,7 +1,10 @@
use chrono::NaiveDateTime; use chrono::NaiveDateTime;
use domain::{ use domain::{
errors::DomainError, errors::DomainError,
models::{DiaryEntry, FeedEntry, Movie, MovieSummary, Review, ReviewSource, UserSummary}, models::{
DiaryEntry, FeedEntry, Movie, MovieSummary, PersistedReview, Review, ReviewSource,
UserSummary,
},
value_objects::{ value_objects::{
Comment, Email, ExternalMetadataId, MovieId, MovieTitle, PosterPath, Rating, ReleaseYear, Comment, Email, ExternalMetadataId, MovieId, MovieTitle, PosterPath, Rating, ReleaseYear,
ReviewId, UserId, ReviewId, UserId,
@@ -102,9 +105,16 @@ impl ReviewRow {
None => ReviewSource::Local, None => ReviewSource::Local,
Some(url) => ReviewSource::Remote { actor_url: url }, Some(url) => ReviewSource::Remote { actor_url: url },
}; };
Ok(Review::from_persistence( Ok(Review::from_persistence(PersistedReview {
id, movie_id, user_id, rating, comment, watched_at, created_at, source, id,
)) movie_id,
user_id,
rating,
comment,
watched_at,
created_at,
source,
}))
} }
} }

View File

@@ -1,6 +1,6 @@
use async_trait::async_trait; use async_trait::async_trait;
use chrono::Utc; use chrono::Utc;
use sqlx::PgPool; use sqlx::{PgPool, Row};
use domain::{ use domain::{
errors::DomainError, errors::DomainError,
@@ -33,122 +33,67 @@ impl PostgresUserRepository {
} }
fn row_to_user( fn row_to_user(
id_str: String, row: &sqlx::postgres::PgRow,
email_str: String,
username_str: String,
hash_str: String,
role: UserRole,
display_name: Option<String>,
bio: Option<String>,
avatar_path: Option<String>,
banner_path: Option<String>,
also_known_as: Option<String>,
profile_fields: Vec<ProfileField>, profile_fields: Vec<ProfileField>,
) -> Result<User, DomainError> { ) -> Result<User, DomainError> {
let id_str: String = row.get("id");
let id = uuid::Uuid::parse_str(&id_str) let id = uuid::Uuid::parse_str(&id_str)
.map_err(|e| DomainError::InfrastructureError(e.to_string()))?; .map_err(|e| DomainError::InfrastructureError(e.to_string()))?;
let email = let email = Email::new(row.get::<String, _>("email"))
Email::new(email_str).map_err(|e| DomainError::InfrastructureError(e.to_string()))?;
let username = Username::new(username_str)
.map_err(|e| DomainError::InfrastructureError(e.to_string()))?; .map_err(|e| DomainError::InfrastructureError(e.to_string()))?;
let hash = PasswordHash::new(hash_str) let username = Username::new(row.get::<String, _>("username"))
.map_err(|e| DomainError::InfrastructureError(e.to_string()))?; .map_err(|e| DomainError::InfrastructureError(e.to_string()))?;
let hash = PasswordHash::new(row.get::<String, _>("password_hash"))
.map_err(|e| DomainError::InfrastructureError(e.to_string()))?;
let role_str: String = row.get("role");
Ok(User::from_persistence( Ok(User::from_persistence(
UserId::from_uuid(id), UserId::from_uuid(id),
email, email,
username, username,
hash, hash,
role, Self::parse_role(&role_str),
display_name, domain::models::UserProfile {
bio, display_name: row.try_get("display_name").ok().flatten(),
avatar_path, bio: row.try_get("bio").ok().flatten(),
banner_path, avatar_path: row.try_get("avatar_path").ok().flatten(),
also_known_as, banner_path: row.try_get("banner_path").ok().flatten(),
profile_fields, also_known_as: row.try_get("also_known_as").ok().flatten(),
profile_fields,
},
)) ))
} }
} }
const PG_USER_COLS: &str = "id, email, username, password_hash, role, display_name, bio, avatar_path, banner_path, also_known_as";
#[async_trait] #[async_trait]
impl UserRepository for PostgresUserRepository { impl UserRepository for PostgresUserRepository {
async fn find_by_email(&self, email: &Email) -> Result<Option<User>, DomainError> { async fn find_by_email(&self, email: &Email) -> Result<Option<User>, DomainError> {
let email_str = email.value(); let email_str = email.value();
#[derive(sqlx::FromRow)] let row = sqlx::query(&format!(
struct Row { "SELECT {PG_USER_COLS} FROM users WHERE email = $1"
id: String, ))
email: String,
username: String,
password_hash: String,
role: String,
display_name: Option<String>,
bio: Option<String>,
avatar_path: Option<String>,
banner_path: Option<String>,
also_known_as: Option<String>,
}
let row = sqlx::query_as::<_, Row>(
"SELECT id, email, username, password_hash, role, display_name, bio, avatar_path, banner_path, also_known_as FROM users WHERE email = $1",
)
.bind(email_str) .bind(email_str)
.fetch_optional(&self.pool) .fetch_optional(&self.pool)
.await .await
.map_err(Self::map_err)?; .map_err(Self::map_err)?;
row.map(|r| { row.as_ref()
Self::row_to_user( .map(|r| Self::row_to_user(r, vec![]))
r.id, .transpose()
r.email,
r.username,
r.password_hash,
Self::parse_role(&r.role),
r.display_name,
r.bio,
r.avatar_path,
r.banner_path,
r.also_known_as,
vec![],
)
})
.transpose()
} }
async fn find_by_username(&self, username: &Username) -> Result<Option<User>, DomainError> { async fn find_by_username(&self, username: &Username) -> Result<Option<User>, DomainError> {
let username_str = username.value(); let username_str = username.value();
#[derive(sqlx::FromRow)] let row = sqlx::query(&format!(
struct Row { "SELECT {PG_USER_COLS} FROM users WHERE username = $1"
id: String, ))
email: String,
username: String,
password_hash: String,
role: String,
display_name: Option<String>,
bio: Option<String>,
avatar_path: Option<String>,
banner_path: Option<String>,
also_known_as: Option<String>,
}
let row = sqlx::query_as::<_, Row>(
"SELECT id, email, username, password_hash, role, display_name, bio, avatar_path, banner_path, also_known_as FROM users WHERE username = $1",
)
.bind(username_str) .bind(username_str)
.fetch_optional(&self.pool) .fetch_optional(&self.pool)
.await .await
.map_err(Self::map_err)?; .map_err(Self::map_err)?;
row.map(|r| { row.as_ref()
Self::row_to_user( .map(|r| Self::row_to_user(r, vec![]))
r.id, .transpose()
r.email,
r.username,
r.password_hash,
Self::parse_role(&r.role),
r.display_name,
r.bio,
r.avatar_path,
r.banner_path,
r.also_known_as,
vec![],
)
})
.transpose()
} }
async fn save(&self, user: &User) -> Result<(), DomainError> { async fn save(&self, user: &User) -> Result<(), DomainError> {
@@ -191,35 +136,15 @@ impl UserRepository for PostgresUserRepository {
async fn find_by_id(&self, id: &UserId) -> Result<Option<User>, DomainError> { async fn find_by_id(&self, id: &UserId) -> Result<Option<User>, DomainError> {
let id_str = id.value().to_string(); let id_str = id.value().to_string();
#[derive(sqlx::FromRow)] let row = sqlx::query(&format!("SELECT {PG_USER_COLS} FROM users WHERE id = $1"))
struct Row { .bind(&id_str)
id: String, .fetch_optional(&self.pool)
email: String, .await
username: String, .map_err(Self::map_err)?;
password_hash: String,
role: String,
display_name: Option<String>,
bio: Option<String>,
avatar_path: Option<String>,
banner_path: Option<String>,
also_known_as: Option<String>,
}
let row = sqlx::query_as::<_, Row>(
"SELECT id, email, username, password_hash, role, display_name, bio, avatar_path, banner_path, also_known_as FROM users WHERE id = $1",
)
.bind(&id_str)
.fetch_optional(&self.pool)
.await
.map_err(Self::map_err)?;
let Some(r) = row else { return Ok(None) }; let Some(r) = row else { return Ok(None) };
#[derive(sqlx::FromRow)] let field_rows = sqlx::query(
struct FieldRow {
name: String,
value: String,
}
let field_rows = sqlx::query_as::<_, FieldRow>(
"SELECT name, value FROM user_profile_fields WHERE user_id = $1 ORDER BY position ASC", "SELECT name, value FROM user_profile_fields WHERE user_id = $1 ORDER BY position ASC",
) )
.bind(&id_str) .bind(&id_str)
@@ -228,47 +153,30 @@ impl UserRepository for PostgresUserRepository {
.map_err(|e| DomainError::InfrastructureError(e.to_string()))?; .map_err(|e| DomainError::InfrastructureError(e.to_string()))?;
let profile_fields = field_rows let profile_fields = field_rows
.into_iter() .iter()
.map(|f| ProfileField { .map(|f| ProfileField {
name: f.name, name: f.get("name"),
value: f.value, value: f.get("value"),
}) })
.collect(); .collect();
Self::row_to_user( Self::row_to_user(&r, profile_fields).map(Some)
r.id,
r.email,
r.username,
r.password_hash,
Self::parse_role(&r.role),
r.display_name,
r.bio,
r.avatar_path,
r.banner_path,
r.also_known_as,
profile_fields,
)
.map(Some)
} }
async fn update_profile( async fn update_profile(
&self, &self,
user_id: &UserId, user_id: &UserId,
display_name: Option<String>, profile: &domain::models::UserProfile,
bio: Option<String>,
avatar_path: Option<String>,
banner_path: Option<String>,
also_known_as: Option<String>,
) -> Result<(), DomainError> { ) -> Result<(), DomainError> {
let id_str = user_id.value().to_string(); let id_str = user_id.value().to_string();
sqlx::query( sqlx::query(
"UPDATE users SET display_name = $1, bio = $2, avatar_path = $3, banner_path = $4, also_known_as = $5 WHERE id = $6", "UPDATE users SET display_name = $1, bio = $2, avatar_path = $3, banner_path = $4, also_known_as = $5 WHERE id = $6",
) )
.bind(&display_name) .bind(&profile.display_name)
.bind(&bio) .bind(&profile.bio)
.bind(&avatar_path) .bind(&profile.avatar_path)
.bind(&banner_path) .bind(&profile.banner_path)
.bind(&also_known_as) .bind(&profile.also_known_as)
.bind(&id_str) .bind(&id_str)
.execute(&self.pool) .execute(&self.pool)
.await .await

View File

@@ -1038,17 +1038,7 @@ impl RemoteWatchlistRepository for SqliteFederationRepository {
} }
} }
pub fn wire( pub fn wire(pool: sqlx::SqlitePool) -> activitypub::FederationRepos {
pool: sqlx::SqlitePool,
) -> (
std::sync::Arc<dyn activitypub::ActivityRepository>,
std::sync::Arc<dyn activitypub::FollowRepository>,
std::sync::Arc<dyn activitypub::ActorRepository>,
std::sync::Arc<dyn activitypub::BlocklistRepository>,
std::sync::Arc<dyn domain::ports::SocialQueryPort>,
std::sync::Arc<dyn activitypub::RemoteReviewRepository>,
std::sync::Arc<dyn domain::ports::RemoteWatchlistRepository>,
) {
let fed = std::sync::Arc::new(SqliteFederationRepository::new(pool)); let fed = std::sync::Arc::new(SqliteFederationRepository::new(pool));
( (
std::sync::Arc::clone(&fed) as _, std::sync::Arc::clone(&fed) as _,

View File

@@ -2,8 +2,8 @@ use chrono::NaiveDateTime;
use domain::{ use domain::{
errors::DomainError, errors::DomainError,
models::{ models::{
DiaryEntry, FeedEntry, Movie, MovieSummary, Review, ReviewSource, UserSummary, DiaryEntry, FeedEntry, Movie, MovieSummary, PersistedReview, Review, ReviewSource,
WatchlistEntry, WatchlistWithMovie, UserSummary, WatchlistEntry, WatchlistWithMovie,
}, },
value_objects::{ value_objects::{
Comment, Email, ExternalMetadataId, MovieId, MovieTitle, PosterPath, Rating, ReleaseYear, Comment, Email, ExternalMetadataId, MovieId, MovieTitle, PosterPath, Rating, ReleaseYear,
@@ -109,9 +109,16 @@ impl ReviewRow {
None => ReviewSource::Local, None => ReviewSource::Local,
Some(url) => ReviewSource::Remote { actor_url: url }, Some(url) => ReviewSource::Remote { actor_url: url },
}; };
Ok(Review::from_persistence( Ok(Review::from_persistence(PersistedReview {
id, movie_id, user_id, rating, comment, watched_at, created_at, source, id,
)) movie_id,
user_id,
rating,
comment,
watched_at,
created_at,
source,
}))
} }
} }

View File

@@ -6,7 +6,7 @@ use sqlx::SqlitePool;
async fn setup() -> (SqlitePool, SqliteUserRepository) { async fn setup() -> (SqlitePool, SqliteUserRepository) {
let pool = SqlitePool::connect(":memory:").await.unwrap(); let pool = SqlitePool::connect(":memory:").await.unwrap();
sqlx::query( sqlx::query(
"CREATE TABLE users (id TEXT PRIMARY KEY, email TEXT NOT NULL UNIQUE, username TEXT NOT NULL UNIQUE, password_hash TEXT NOT NULL, created_at TEXT NOT NULL, role TEXT NOT NULL DEFAULT 'standard', bio TEXT, avatar_path TEXT, banner_path TEXT, also_known_as TEXT)" "CREATE TABLE users (id TEXT PRIMARY KEY, email TEXT NOT NULL UNIQUE, username TEXT NOT NULL UNIQUE, password_hash TEXT NOT NULL, created_at TEXT NOT NULL, role TEXT NOT NULL DEFAULT 'standard', display_name TEXT, bio TEXT, avatar_path TEXT, banner_path TEXT, also_known_as TEXT)"
) )
.execute(&pool) .execute(&pool)
.await .await
@@ -65,10 +65,11 @@ async fn update_profile_persists_bio_and_avatar() {
repo.update_profile( repo.update_profile(
user.id(), user.id(),
Some("My biography".to_string()), &domain::models::UserProfile {
Some("avatars/user1".to_string()), bio: Some("My biography".to_string()),
None, avatar_path: Some("avatars/user1".to_string()),
None, ..Default::default()
},
) )
.await .await
.unwrap(); .unwrap();
@@ -90,14 +91,15 @@ async fn update_profile_clears_fields_with_none() {
repo.save(&user).await.unwrap(); repo.save(&user).await.unwrap();
repo.update_profile( repo.update_profile(
user.id(), user.id(),
Some("bio".to_string()), &domain::models::UserProfile {
Some("path".to_string()), bio: Some("bio".to_string()),
None, avatar_path: Some("path".to_string()),
None, ..Default::default()
},
) )
.await .await
.unwrap(); .unwrap();
repo.update_profile(user.id(), None, None, None, None) repo.update_profile(user.id(), &domain::models::UserProfile::default())
.await .await
.unwrap(); .unwrap();

View File

@@ -51,12 +51,14 @@ impl SqliteUserRepository {
username, username,
hash, hash,
Self::parse_role(&role_str), Self::parse_role(&role_str),
row.try_get("display_name").ok().flatten(), domain::models::UserProfile {
row.try_get("bio").ok().flatten(), display_name: row.try_get("display_name").ok().flatten(),
row.try_get("avatar_path").ok().flatten(), bio: row.try_get("bio").ok().flatten(),
row.try_get("banner_path").ok().flatten(), avatar_path: row.try_get("avatar_path").ok().flatten(),
row.try_get("also_known_as").ok().flatten(), banner_path: row.try_get("banner_path").ok().flatten(),
profile_fields, also_known_as: row.try_get("also_known_as").ok().flatten(),
profile_fields,
},
)) ))
} }
} }
@@ -161,21 +163,17 @@ impl UserRepository for SqliteUserRepository {
async fn update_profile( async fn update_profile(
&self, &self,
user_id: &UserId, user_id: &UserId,
display_name: Option<String>, profile: &domain::models::UserProfile,
bio: Option<String>,
avatar_path: Option<String>,
banner_path: Option<String>,
also_known_as: Option<String>,
) -> Result<(), DomainError> { ) -> Result<(), DomainError> {
let id_str = user_id.value().to_string(); let id_str = user_id.value().to_string();
sqlx::query( sqlx::query(
"UPDATE users SET display_name = ?, bio = ?, avatar_path = ?, banner_path = ?, also_known_as = ? WHERE id = ?", "UPDATE users SET display_name = ?, bio = ?, avatar_path = ?, banner_path = ?, also_known_as = ? WHERE id = ?",
) )
.bind(&display_name) .bind(&profile.display_name)
.bind(&bio) .bind(&profile.bio)
.bind(&avatar_path) .bind(&profile.avatar_path)
.bind(&banner_path) .bind(&profile.banner_path)
.bind(&also_known_as) .bind(&profile.also_known_as)
.bind(&id_str) .bind(&id_str)
.execute(&self.pool) .execute(&self.pool)
.await .await

View File

@@ -117,11 +117,10 @@ impl ResolutionStrategy for TitleSearchStrategy {
match deps.metadata_client.fetch_movie_metadata(&criteria).await { match deps.metadata_client.fetch_movie_metadata(&criteria).await {
Ok(m) => { Ok(m) => {
// Movie may already exist in DB under this external_metadata_id // Movie may already exist in DB under this external_metadata_id
if let Some(ext_id) = m.external_metadata_id() { if let Some(ext_id) = m.external_metadata_id()
if let Some(existing) = deps.repository.get_movie_by_external_id(ext_id).await? && let Some(existing) = deps.repository.get_movie_by_external_id(ext_id).await?
{ {
return Ok(Some((existing, false))); return Ok(Some((existing, false)));
}
} }
Ok(Some((m, true))) Ok(Some((m, true)))
} }

View File

@@ -56,6 +56,7 @@ impl EventHandler for RecordingHandler {
"watchlist" "watchlist"
} }
DomainEvent::FollowAccepted { .. } => "follow_accepted", DomainEvent::FollowAccepted { .. } => "follow_accepted",
DomainEvent::BackfillFollower { .. } => "backfill_follower",
}; };
self.calls.lock().unwrap().push(label); self.calls.lock().unwrap().push(label);
Ok(()) Ok(())

View File

@@ -68,11 +68,14 @@ pub async fn execute(ctx: &AppContext, cmd: UpdateProfileCommand) -> Result<(),
ctx.user_repository ctx.user_repository
.update_profile( .update_profile(
&user_id, &user_id,
cmd.display_name, &domain::models::UserProfile {
cmd.bio, display_name: cmd.display_name,
new_avatar_path, bio: cmd.bio,
new_banner_path, avatar_path: new_avatar_path,
cmd.also_known_as, banner_path: new_banner_path,
also_known_as: cmd.also_known_as,
profile_fields: vec![],
},
) )
.await?; .await?;

View File

@@ -163,6 +163,17 @@ pub enum ReviewSource {
}, },
} }
pub struct PersistedReview {
pub id: ReviewId,
pub movie_id: MovieId,
pub user_id: UserId,
pub rating: Rating,
pub comment: Option<Comment>,
pub watched_at: NaiveDateTime,
pub created_at: NaiveDateTime,
pub source: ReviewSource,
}
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct Review { pub struct Review {
id: ReviewId, id: ReviewId,
@@ -195,25 +206,16 @@ impl Review {
}) })
} }
pub fn from_persistence( pub fn from_persistence(row: PersistedReview) -> Self {
id: ReviewId,
movie_id: MovieId,
user_id: UserId,
rating: Rating,
comment: Option<Comment>,
watched_at: NaiveDateTime,
created_at: NaiveDateTime,
source: ReviewSource,
) -> Self {
Self { Self {
id, id: row.id,
movie_id, movie_id: row.movie_id,
user_id, user_id: row.user_id,
rating, rating: row.rating,
comment, comment: row.comment,
watched_at, watched_at: row.watched_at,
created_at, created_at: row.created_at,
source, source: row.source,
} }
} }
@@ -314,6 +316,16 @@ pub struct ProfileField {
pub value: String, pub value: String,
} }
#[derive(Clone, Debug, Default)]
pub struct UserProfile {
pub display_name: Option<String>,
pub bio: Option<String>,
pub avatar_path: Option<String>,
pub banner_path: Option<String>,
pub also_known_as: Option<String>,
pub profile_fields: Vec<ProfileField>,
}
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct User { pub struct User {
id: UserId, id: UserId,
@@ -321,12 +333,7 @@ pub struct User {
username: Username, username: Username,
password_hash: PasswordHash, password_hash: PasswordHash,
role: UserRole, role: UserRole,
display_name: Option<String>, profile: UserProfile,
bio: Option<String>,
avatar_path: Option<String>,
banner_path: Option<String>,
also_known_as: Option<String>,
profile_fields: Vec<ProfileField>,
} }
impl User { impl User {
@@ -342,12 +349,7 @@ impl User {
username, username,
password_hash, password_hash,
role, role,
display_name: None, profile: UserProfile::default(),
bio: None,
avatar_path: None,
banner_path: None,
also_known_as: None,
profile_fields: vec![],
} }
} }
@@ -357,12 +359,7 @@ impl User {
username: Username, username: Username,
password_hash: PasswordHash, password_hash: PasswordHash,
role: UserRole, role: UserRole,
display_name: Option<String>, profile: UserProfile,
bio: Option<String>,
avatar_path: Option<String>,
banner_path: Option<String>,
also_known_as: Option<String>,
profile_fields: Vec<ProfileField>,
) -> Self { ) -> Self {
Self { Self {
id, id,
@@ -370,12 +367,7 @@ impl User {
username, username,
password_hash, password_hash,
role, role,
display_name, profile,
bio,
avatar_path,
banner_path,
also_known_as,
profile_fields,
} }
} }
@@ -383,19 +375,8 @@ impl User {
self.password_hash = new_hash; self.password_hash = new_hash;
} }
pub fn update_profile( pub fn update_profile(&mut self, profile: UserProfile) {
&mut self, self.profile = profile;
display_name: Option<String>,
bio: Option<String>,
avatar_path: Option<String>,
banner_path: Option<String>,
also_known_as: Option<String>,
) {
self.display_name = display_name;
self.bio = bio;
self.avatar_path = avatar_path;
self.banner_path = banner_path;
self.also_known_as = also_known_as;
} }
pub fn email(&self) -> &Email { pub fn email(&self) -> &Email {
@@ -414,26 +395,22 @@ impl User {
&self.role &self.role
} }
pub fn display_name(&self) -> Option<&str> { pub fn display_name(&self) -> Option<&str> {
self.display_name.as_deref() self.profile.display_name.as_deref()
} }
pub fn bio(&self) -> Option<&str> { pub fn bio(&self) -> Option<&str> {
self.bio.as_deref() self.profile.bio.as_deref()
} }
pub fn avatar_path(&self) -> Option<&str> { pub fn avatar_path(&self) -> Option<&str> {
self.avatar_path.as_deref() self.profile.avatar_path.as_deref()
} }
pub fn banner_path(&self) -> Option<&str> { pub fn banner_path(&self) -> Option<&str> {
self.banner_path.as_deref() self.profile.banner_path.as_deref()
} }
pub fn also_known_as(&self) -> Option<&str> { pub fn also_known_as(&self) -> Option<&str> {
self.also_known_as.as_deref() self.profile.also_known_as.as_deref()
} }
pub fn profile_fields(&self) -> &[ProfileField] { pub fn profile_fields(&self) -> &[ProfileField] {
&self.profile_fields &self.profile.profile_fields
} }
} }

View File

@@ -8,23 +8,18 @@ fn make_user() -> User {
Username::new("alice".to_string()).unwrap(), Username::new("alice".to_string()).unwrap(),
PasswordHash::new("hash".to_string()).unwrap(), PasswordHash::new("hash".to_string()).unwrap(),
UserRole::Standard, UserRole::Standard,
None, UserProfile::default(),
None,
None,
None,
vec![],
) )
} }
#[test] #[test]
fn update_profile_sets_fields() { fn update_profile_sets_fields() {
let mut user = make_user(); let mut user = make_user();
user.update_profile( user.update_profile(UserProfile {
Some("My bio".to_string()), bio: Some("My bio".to_string()),
Some("avatars/abc".to_string()), avatar_path: Some("avatars/abc".to_string()),
None, ..Default::default()
None, });
);
assert_eq!(user.bio(), Some("My bio")); assert_eq!(user.bio(), Some("My bio"));
assert_eq!(user.avatar_path(), Some("avatars/abc")); assert_eq!(user.avatar_path(), Some("avatars/abc"));
} }
@@ -32,13 +27,12 @@ fn update_profile_sets_fields() {
#[test] #[test]
fn update_profile_clears_with_none() { fn update_profile_clears_with_none() {
let mut user = make_user(); let mut user = make_user();
user.update_profile( user.update_profile(UserProfile {
Some("bio".to_string()), bio: Some("bio".to_string()),
Some("path".to_string()), avatar_path: Some("path".to_string()),
None, ..Default::default()
None, });
); user.update_profile(UserProfile::default());
user.update_profile(None, None, None, None);
assert_eq!(user.bio(), None); assert_eq!(user.bio(), None);
assert_eq!(user.avatar_path(), None); assert_eq!(user.avatar_path(), None);
} }

View File

@@ -33,14 +33,15 @@ pub enum FeedSortBy {
RatingAsc, RatingAsc,
} }
impl FeedSortBy { impl std::str::FromStr for FeedSortBy {
pub fn from_str(s: &str) -> Self { type Err = std::convert::Infallible;
match s { fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(match s {
"date_asc" => Self::DateAsc, "date_asc" => Self::DateAsc,
"rating" => Self::Rating, "rating" => Self::Rating,
"rating_asc" => Self::RatingAsc, "rating_asc" => Self::RatingAsc,
_ => Self::Date, _ => Self::Date,
} })
} }
} }
@@ -185,11 +186,7 @@ pub trait UserRepository: Send + Sync {
async fn update_profile( async fn update_profile(
&self, &self,
user_id: &UserId, user_id: &UserId,
display_name: Option<String>, profile: &crate::models::UserProfile,
bio: Option<String>,
avatar_path: Option<String>,
banner_path: Option<String>,
also_known_as: Option<String>,
) -> Result<(), DomainError>; ) -> Result<(), DomainError>;
} }

View File

@@ -219,10 +219,7 @@ impl UserRepository for InMemoryUserRepository {
async fn update_profile( async fn update_profile(
&self, &self,
_user_id: &UserId, _user_id: &UserId,
_bio: Option<String>, _profile: &crate::models::UserProfile,
_avatar_path: Option<String>,
_banner_path: Option<String>,
_also_known_as: Option<String>,
) -> Result<(), DomainError> { ) -> Result<(), DomainError> {
Ok(()) Ok(())
} }

View File

@@ -503,20 +503,20 @@ pub async fn update_profile_handler(
} }
"avatar" => { "avatar" => {
let ct = field.content_type().map(|s| s.to_string()); let ct = field.content_type().map(|s| s.to_string());
if let Ok(bytes) = field.bytes().await { if let Ok(bytes) = field.bytes().await
if !bytes.is_empty() { && !bytes.is_empty()
avatar_bytes = Some(bytes.to_vec()); {
avatar_content_type = ct; avatar_bytes = Some(bytes.to_vec());
} avatar_content_type = ct;
} }
} }
"banner" => { "banner" => {
let ct = field.content_type().map(|s| s.to_string()); let ct = field.content_type().map(|s| s.to_string());
if let Ok(bytes) = field.bytes().await { if let Ok(bytes) = field.bytes().await
if !bytes.is_empty() { && !bytes.is_empty()
banner_bytes = Some(bytes.to_vec()); {
banner_content_type = ct; banner_bytes = Some(bytes.to_vec());
} banner_content_type = ct;
} }
} }
_ => {} _ => {}

View File

@@ -428,7 +428,7 @@ pub async fn get_activity_feed(
let query = application::queries::GetActivityFeedQuery { let query = application::queries::GetActivityFeedQuery {
limit, limit,
offset, offset,
sort_by: domain::ports::FeedSortBy::from_str(sort_by_str), sort_by: sort_by_str.parse().unwrap_or_default(),
search: search_opt, search: search_opt,
following, following,
}; };
@@ -661,7 +661,7 @@ pub async fn get_user_profile(
view: profile_view, view: profile_view,
limit: params.limit, limit: params.limit,
offset: params.offset, offset: params.offset,
sort_by: domain::ports::FeedSortBy::from_str(sort_by_str), sort_by: sort_by_str.parse().unwrap_or_default(),
search: if params.search.is_empty() { search: if params.search.is_empty() {
None None
} else { } else {
@@ -1599,38 +1599,36 @@ pub async fn post_profile_settings(
} }
"avatar" => { "avatar" => {
let ct = field.content_type().map(|s| s.to_string()); let ct = field.content_type().map(|s| s.to_string());
if let Ok(bytes) = field.bytes().await { if let Ok(bytes) = field.bytes().await
if !bytes.is_empty() { && !bytes.is_empty()
avatar_bytes = Some(bytes.to_vec()); {
avatar_content_type = ct; avatar_bytes = Some(bytes.to_vec());
} avatar_content_type = ct;
} }
} }
"banner" => { "banner" => {
let ct = field.content_type().map(|s| s.to_string()); let ct = field.content_type().map(|s| s.to_string());
if let Ok(bytes) = field.bytes().await { if let Ok(bytes) = field.bytes().await
if !bytes.is_empty() { && !bytes.is_empty()
banner_bytes = Some(bytes.to_vec()); {
banner_content_type = ct; banner_bytes = Some(bytes.to_vec());
} banner_content_type = ct;
} }
} }
n if n.starts_with("field_name_") => { n if n.starts_with("field_name_") => {
if let Ok(idx) = n["field_name_".len()..].parse::<usize>() { if let Ok(idx) = n["field_name_".len()..].parse::<usize>()
if let Ok(text) = field.text().await { && let Ok(text) = field.text().await
if !text.is_empty() { && !text.is_empty()
field_names.insert(idx, text); {
} field_names.insert(idx, text);
}
} }
} }
n if n.starts_with("field_value_") => { n if n.starts_with("field_value_") => {
if let Ok(idx) = n["field_value_".len()..].parse::<usize>() { if let Ok(idx) = n["field_value_".len()..].parse::<usize>()
if let Ok(text) = field.text().await { && let Ok(text) = field.text().await
if !text.is_empty() { && !text.is_empty()
field_values.insert(idx, text); {
} field_values.insert(idx, text);
}
} }
} }
_ => {} _ => {}

View File

@@ -125,19 +125,19 @@ async fn wire_dependencies() -> anyhow::Result<(AppState, axum::Router)> {
} }
}; };
let ap = activitypub::wire( let ap = activitypub::wire(activitypub::ActivityPubDeps {
activity_repo, activity_repo,
follow_repo, follow_repo,
actor_repo, actor_repo,
blocklist_repo, blocklist_repo,
review_store, review_store,
remote_watchlist_repo.clone(), remote_watchlist_repo: remote_watchlist_repo.clone(),
Arc::clone(&ap_content_repo), local_ap_content: Arc::clone(&ap_content_repo),
Arc::clone(&user_repository), user_repo: Arc::clone(&user_repository),
app_config.base_url.clone(), base_url: app_config.base_url.clone(),
app_config.allow_registration, allow_registration: app_config.allow_registration,
Arc::clone(&ep), event_publisher: Arc::clone(&ep),
) })
.await?; .await?;
let ap_router = ap.router; let ap_router = ap.router;
let ap_service_arc = ap.service; let ap_service_arc = ap.service;

View File

@@ -219,10 +219,7 @@ impl UserRepository for Panic {
async fn update_profile( async fn update_profile(
&self, &self,
_: &UserId, _: &UserId,
_: Option<String>, _: &domain::models::UserProfile,
_: Option<String>,
_: Option<String>,
_: Option<String>,
) -> Result<(), DomainError> { ) -> Result<(), DomainError> {
panic!() panic!()
} }

View File

@@ -119,10 +119,7 @@ impl UserRepository for NobodyUserRepo {
async fn update_profile( async fn update_profile(
&self, &self,
_: &UserId, _: &UserId,
_: Option<String>, _: &domain::models::UserProfile,
_: Option<String>,
_: Option<String>,
_: Option<String>,
) -> Result<(), DomainError> { ) -> Result<(), DomainError> {
Ok(()) Ok(())
} }

View File

@@ -1,3 +1,4 @@
#![allow(clippy::large_enum_variant)]
pub mod app; pub mod app;
pub mod client; pub mod client;
pub mod config; pub mod config;

View File

@@ -102,28 +102,27 @@ async fn main() -> anyhow::Result<()> {
// Both the event handler and the staleness job are gated on TMDB_API_KEY. // Both the event handler and the staleness job are gated on TMDB_API_KEY.
// Without a key, no MovieEnrichmentRequested events are produced or handled. // Without a key, no MovieEnrichmentRequested events are produced or handled.
let (enrichment_handler, enrichment_job): ( type OptionalPair = (Option<Arc<dyn EventHandler>>, Option<Arc<dyn PeriodicJob>>);
Option<Arc<dyn EventHandler>>, let (enrichment_handler, enrichment_job): OptionalPair =
Option<Arc<dyn PeriodicJob>>, match tmdb_enrichment::TmdbEnrichmentClient::from_env() {
) = match tmdb_enrichment::TmdbEnrichmentClient::from_env() { Ok(client) => {
Ok(client) => { tracing::info!("TMDb enrichment enabled");
tracing::info!("TMDb enrichment enabled"); let handler = Arc::new(tmdb_enrichment::EnrichmentHandler {
let handler = Arc::new(tmdb_enrichment::EnrichmentHandler { enrichment_client: Arc::new(client),
enrichment_client: Arc::new(client), movie_repository: Arc::clone(&ctx.movie_repository),
movie_repository: Arc::clone(&ctx.movie_repository), profile_repo: Arc::clone(&ctx.movie_profile_repository),
profile_repo: Arc::clone(&ctx.movie_profile_repository), person_command: Arc::clone(&ctx.person_command),
person_command: Arc::clone(&ctx.person_command), search_command: Arc::clone(&ctx.search_command),
search_command: Arc::clone(&ctx.search_command), }) as Arc<dyn EventHandler>;
}) as Arc<dyn EventHandler>; let job = Arc::new(application::jobs::EnrichmentStalenessJob::new(ctx.clone()))
let job = Arc::new(application::jobs::EnrichmentStalenessJob::new(ctx.clone())) as Arc<dyn PeriodicJob>;
as Arc<dyn PeriodicJob>; (Some(handler), Some(job))
(Some(handler), Some(job)) }
} Err(e) => {
Err(e) => { tracing::warn!("TMDb enrichment disabled: {e}");
tracing::warn!("TMDb enrichment disabled: {e}"); (None, None)
(None, None) }
} };
};
// ── Image conversion ────────────────────────────────────────────────────── // ── Image conversion ──────────────────────────────────────────────────────
@@ -196,19 +195,19 @@ async fn main() -> anyhow::Result<()> {
#[cfg(feature = "federation")] #[cfg(feature = "federation")]
{ {
let ap_wire = activitypub::wire( let ap_wire = activitypub::wire(activitypub::ActivityPubDeps {
fed_activity_repo, activity_repo: fed_activity_repo,
fed_follow_repo, follow_repo: fed_follow_repo,
fed_actor_repo, actor_repo: fed_actor_repo,
fed_blocklist_repo, blocklist_repo: fed_blocklist_repo,
fed_review_store, review_store: fed_review_store,
fed_remote_watchlist_repo, remote_watchlist_repo: fed_remote_watchlist_repo,
fed_ap_content, local_ap_content: fed_ap_content,
fed_user_repo, user_repo: fed_user_repo,
base_url, base_url,
allow_registration, allow_registration,
Arc::clone(&ctx.event_publisher), event_publisher: Arc::clone(&ctx.event_publisher),
) })
.await?; .await?;
let ap_event_handler = ap_wire.event_handler; let ap_event_handler = ap_wire.event_handler;