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:
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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"]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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 _,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 _,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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(())
|
||||||
|
|||||||
@@ -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?;
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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!()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user