feat: refactor user profile handling and integrate ApProfileField structure
This commit is contained in:
@@ -13,6 +13,7 @@ use url::Url;
|
|||||||
use crate::data::FederationData;
|
use crate::data::FederationData;
|
||||||
use crate::error::Error;
|
use crate::error::Error;
|
||||||
use crate::repository::RemoteActor;
|
use crate::repository::RemoteActor;
|
||||||
|
use crate::user::ApProfileField;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct DbActor {
|
pub struct DbActor {
|
||||||
@@ -31,7 +32,7 @@ pub struct DbActor {
|
|||||||
pub banner_url: Option<Url>,
|
pub banner_url: Option<Url>,
|
||||||
pub also_known_as: Option<String>,
|
pub also_known_as: Option<String>,
|
||||||
pub profile_url: Option<Url>,
|
pub profile_url: Option<Url>,
|
||||||
pub attachment: Vec<domain::models::ProfileField>,
|
pub attachment: Vec<ApProfileField>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||||
|
|||||||
@@ -24,4 +24,4 @@ pub use repository::{
|
|||||||
BlockedDomain, FederationRepository, Follower, FollowerStatus, FollowingStatus, RemoteActor,
|
BlockedDomain, FederationRepository, Follower, FollowerStatus, FollowingStatus, RemoteActor,
|
||||||
};
|
};
|
||||||
pub use service::ActivityPubService;
|
pub use service::ActivityPubService;
|
||||||
pub use user::{ApUser, ApUserRepository};
|
pub use user::{ApProfileField, ApUser, ApUserRepository};
|
||||||
|
|||||||
@@ -1,6 +1,12 @@
|
|||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct ApProfileField {
|
||||||
|
pub name: String,
|
||||||
|
pub value: String,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct ApUser {
|
pub struct ApUser {
|
||||||
pub id: uuid::Uuid,
|
pub id: uuid::Uuid,
|
||||||
@@ -10,7 +16,7 @@ pub struct ApUser {
|
|||||||
pub banner_url: Option<Url>,
|
pub banner_url: Option<Url>,
|
||||||
pub also_known_as: Option<String>,
|
pub also_known_as: Option<String>,
|
||||||
pub profile_url: Option<Url>,
|
pub profile_url: Option<Url>,
|
||||||
pub attachment: Vec<domain::models::ProfileField>,
|
pub attachment: Vec<ApProfileField>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
|
|||||||
@@ -31,7 +31,6 @@ pub async fn wire(
|
|||||||
review_store: std::sync::Arc<dyn RemoteReviewRepository>,
|
review_store: std::sync::Arc<dyn RemoteReviewRepository>,
|
||||||
remote_watchlist_repo: std::sync::Arc<dyn domain::ports::RemoteWatchlistRepository>,
|
remote_watchlist_repo: std::sync::Arc<dyn domain::ports::RemoteWatchlistRepository>,
|
||||||
user_repo: std::sync::Arc<dyn domain::ports::UserRepository>,
|
user_repo: std::sync::Arc<dyn domain::ports::UserRepository>,
|
||||||
profile_fields_repo: std::sync::Arc<dyn domain::ports::UserProfileFieldsRepository>,
|
|
||||||
movie_repo: std::sync::Arc<dyn domain::ports::MovieRepository>,
|
movie_repo: std::sync::Arc<dyn domain::ports::MovieRepository>,
|
||||||
review_repo: std::sync::Arc<dyn domain::ports::ReviewRepository>,
|
review_repo: std::sync::Arc<dyn domain::ports::ReviewRepository>,
|
||||||
diary_repo: std::sync::Arc<dyn domain::ports::DiaryRepository>,
|
diary_repo: std::sync::Arc<dyn domain::ports::DiaryRepository>,
|
||||||
@@ -67,7 +66,7 @@ pub async fn wire(
|
|||||||
let concrete = std::sync::Arc::new(
|
let concrete = std::sync::Arc::new(
|
||||||
ActivityPubService::new(
|
ActivityPubService::new(
|
||||||
federation_repo,
|
federation_repo,
|
||||||
std::sync::Arc::new(DomainUserRepoAdapter::new(user_repo, profile_fields_repo, base_url.clone())),
|
std::sync::Arc::new(DomainUserRepoAdapter::new(user_repo, base_url.clone())),
|
||||||
composite,
|
composite,
|
||||||
base_url.clone(),
|
base_url.clone(),
|
||||||
allow_registration,
|
allow_registration,
|
||||||
|
|||||||
@@ -1,30 +1,27 @@
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use activitypub_base::{ApUser, ApUserRepository};
|
use activitypub_base::{ApProfileField, ApUser, ApUserRepository};
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use domain::{
|
use domain::{
|
||||||
models::ProfileField,
|
ports::UserRepository,
|
||||||
ports::{UserProfileFieldsRepository, UserRepository},
|
|
||||||
value_objects::UserId,
|
value_objects::UserId,
|
||||||
};
|
};
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
pub struct DomainUserRepoAdapter {
|
pub struct DomainUserRepoAdapter {
|
||||||
pub repo: Arc<dyn UserRepository>,
|
pub repo: Arc<dyn UserRepository>,
|
||||||
pub fields_repo: Arc<dyn UserProfileFieldsRepository>,
|
|
||||||
pub base_url: String,
|
pub base_url: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DomainUserRepoAdapter {
|
impl DomainUserRepoAdapter {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
repo: Arc<dyn UserRepository>,
|
repo: Arc<dyn UserRepository>,
|
||||||
fields_repo: Arc<dyn UserProfileFieldsRepository>,
|
|
||||||
base_url: String,
|
base_url: String,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self { repo, fields_repo, base_url }
|
Self { repo, base_url }
|
||||||
}
|
}
|
||||||
|
|
||||||
fn build_user(&self, u: &domain::models::User, fields: Vec<ProfileField>) -> ApUser {
|
fn build_user(&self, u: &domain::models::User) -> ApUser {
|
||||||
let avatar_url = u.avatar_path().and_then(|p| {
|
let avatar_url = u.avatar_path().and_then(|p| {
|
||||||
Url::parse(&format!("{}/images/{}", self.base_url, p)).ok()
|
Url::parse(&format!("{}/images/{}", self.base_url, p)).ok()
|
||||||
});
|
});
|
||||||
@@ -40,7 +37,7 @@ impl DomainUserRepoAdapter {
|
|||||||
banner_url,
|
banner_url,
|
||||||
also_known_as: u.also_known_as().map(|s| s.to_string()),
|
also_known_as: u.also_known_as().map(|s| s.to_string()),
|
||||||
profile_url,
|
profile_url,
|
||||||
attachment: fields,
|
attachment: u.profile_fields().iter().map(|f| ApProfileField { name: f.name.clone(), value: f.value.clone() }).collect(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -53,8 +50,7 @@ impl ApUserRepository for DomainUserRepoAdapter {
|
|||||||
Some(u) => u,
|
Some(u) => u,
|
||||||
None => return Ok(None),
|
None => return Ok(None),
|
||||||
};
|
};
|
||||||
let fields = self.fields_repo.get_fields(&user_id).await.unwrap_or_default();
|
Ok(Some(self.build_user(&user)))
|
||||||
Ok(Some(self.build_user(&user, fields)))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn find_by_username(&self, username: &str) -> anyhow::Result<Option<ApUser>> {
|
async fn find_by_username(&self, username: &str) -> anyhow::Result<Option<ApUser>> {
|
||||||
@@ -64,8 +60,7 @@ impl ApUserRepository for DomainUserRepoAdapter {
|
|||||||
Some(u) => u,
|
Some(u) => u,
|
||||||
None => return Ok(None),
|
None => return Ok(None),
|
||||||
};
|
};
|
||||||
let fields = self.fields_repo.get_fields(user.id()).await.unwrap_or_default();
|
Ok(Some(self.build_user(&user)))
|
||||||
Ok(Some(self.build_user(&user, fields)))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn count_users(&self) -> anyhow::Result<usize> {
|
async fn count_users(&self) -> anyhow::Result<usize> {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ use sqlx::PgPool;
|
|||||||
|
|
||||||
use domain::{
|
use domain::{
|
||||||
errors::DomainError,
|
errors::DomainError,
|
||||||
models::{User, UserRole},
|
models::{ProfileField, User, UserRole},
|
||||||
ports::UserRepository,
|
ports::UserRepository,
|
||||||
value_objects::{Email, PasswordHash, UserId, Username},
|
value_objects::{Email, PasswordHash, UserId, Username},
|
||||||
};
|
};
|
||||||
@@ -42,6 +42,7 @@ impl PostgresUserRepository {
|
|||||||
avatar_path: Option<String>,
|
avatar_path: Option<String>,
|
||||||
banner_path: Option<String>,
|
banner_path: Option<String>,
|
||||||
also_known_as: Option<String>,
|
also_known_as: Option<String>,
|
||||||
|
profile_fields: Vec<ProfileField>,
|
||||||
) -> Result<User, DomainError> {
|
) -> Result<User, DomainError> {
|
||||||
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()))?;
|
||||||
@@ -61,6 +62,7 @@ impl PostgresUserRepository {
|
|||||||
avatar_path,
|
avatar_path,
|
||||||
banner_path,
|
banner_path,
|
||||||
also_known_as,
|
also_known_as,
|
||||||
|
profile_fields,
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -99,6 +101,7 @@ impl UserRepository for PostgresUserRepository {
|
|||||||
r.avatar_path,
|
r.avatar_path,
|
||||||
r.banner_path,
|
r.banner_path,
|
||||||
r.also_known_as,
|
r.also_known_as,
|
||||||
|
vec![],
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.transpose()
|
.transpose()
|
||||||
@@ -136,6 +139,7 @@ impl UserRepository for PostgresUserRepository {
|
|||||||
r.avatar_path,
|
r.avatar_path,
|
||||||
r.banner_path,
|
r.banner_path,
|
||||||
r.also_known_as,
|
r.also_known_as,
|
||||||
|
vec![],
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.transpose()
|
.transpose()
|
||||||
@@ -200,20 +204,33 @@ impl UserRepository for PostgresUserRepository {
|
|||||||
.fetch_optional(&self.pool)
|
.fetch_optional(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(Self::map_err)?;
|
.map_err(Self::map_err)?;
|
||||||
row.map(|r| {
|
|
||||||
Self::row_to_user(
|
let Some(r) = row else { return Ok(None) };
|
||||||
r.id,
|
|
||||||
r.email,
|
#[derive(sqlx::FromRow)]
|
||||||
r.username,
|
struct FieldRow { name: String, value: String }
|
||||||
r.password_hash,
|
let field_rows = sqlx::query_as::<_, FieldRow>(
|
||||||
Self::parse_role(&r.role),
|
"SELECT name, value FROM user_profile_fields WHERE user_id = $1 ORDER BY position ASC",
|
||||||
r.bio,
|
)
|
||||||
r.avatar_path,
|
.bind(&id_str)
|
||||||
r.banner_path,
|
.fetch_all(&self.pool)
|
||||||
r.also_known_as,
|
.await
|
||||||
)
|
.map_err(|e| DomainError::InfrastructureError(e.to_string()))?;
|
||||||
})
|
|
||||||
.transpose()
|
let profile_fields = field_rows.into_iter().map(|f| ProfileField { name: f.name, value: f.value }).collect();
|
||||||
|
|
||||||
|
Self::row_to_user(
|
||||||
|
r.id,
|
||||||
|
r.email,
|
||||||
|
r.username,
|
||||||
|
r.password_hash,
|
||||||
|
Self::parse_role(&r.role),
|
||||||
|
r.bio,
|
||||||
|
r.avatar_path,
|
||||||
|
r.banner_path,
|
||||||
|
r.also_known_as,
|
||||||
|
profile_fields,
|
||||||
|
).map(Some)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn update_profile(
|
async fn update_profile(
|
||||||
|
|||||||
@@ -11,6 +11,12 @@ async fn setup() -> (SqlitePool, SqliteUserRepository) {
|
|||||||
.execute(&pool)
|
.execute(&pool)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
sqlx::query(
|
||||||
|
"CREATE TABLE user_profile_fields (id TEXT PRIMARY KEY, user_id TEXT NOT NULL, name TEXT NOT NULL, value TEXT NOT NULL, position INTEGER NOT NULL DEFAULT 0)"
|
||||||
|
)
|
||||||
|
.execute(&pool)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
let repo = SqliteUserRepository::new(pool.clone());
|
let repo = SqliteUserRepository::new(pool.clone());
|
||||||
(pool, repo)
|
(pool, repo)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ use sqlx::SqlitePool;
|
|||||||
use super::models::UserSummaryRow;
|
use super::models::UserSummaryRow;
|
||||||
use domain::{
|
use domain::{
|
||||||
errors::DomainError,
|
errors::DomainError,
|
||||||
models::{User, UserRole},
|
models::{ProfileField, User, UserRole},
|
||||||
ports::UserRepository,
|
ports::UserRepository,
|
||||||
value_objects::{Email, PasswordHash, UserId, Username},
|
value_objects::{Email, PasswordHash, UserId, Username},
|
||||||
};
|
};
|
||||||
@@ -41,6 +41,7 @@ impl SqliteUserRepository {
|
|||||||
avatar_path: Option<String>,
|
avatar_path: Option<String>,
|
||||||
banner_path: Option<String>,
|
banner_path: Option<String>,
|
||||||
also_known_as: Option<String>,
|
also_known_as: Option<String>,
|
||||||
|
profile_fields: Vec<ProfileField>,
|
||||||
) -> Result<User, DomainError> {
|
) -> Result<User, DomainError> {
|
||||||
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()))?;
|
||||||
@@ -60,6 +61,7 @@ impl SqliteUserRepository {
|
|||||||
avatar_path,
|
avatar_path,
|
||||||
banner_path,
|
banner_path,
|
||||||
also_known_as,
|
also_known_as,
|
||||||
|
profile_fields,
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -87,6 +89,7 @@ impl UserRepository for SqliteUserRepository {
|
|||||||
r.avatar_path,
|
r.avatar_path,
|
||||||
r.banner_path,
|
r.banner_path,
|
||||||
r.also_known_as,
|
r.also_known_as,
|
||||||
|
vec![],
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.transpose()
|
.transpose()
|
||||||
@@ -113,6 +116,7 @@ impl UserRepository for SqliteUserRepository {
|
|||||||
r.avatar_path,
|
r.avatar_path,
|
||||||
r.banner_path,
|
r.banner_path,
|
||||||
r.also_known_as,
|
r.also_known_as,
|
||||||
|
vec![],
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.transpose()
|
.transpose()
|
||||||
@@ -163,20 +167,30 @@ impl UserRepository for SqliteUserRepository {
|
|||||||
.await
|
.await
|
||||||
.map_err(Self::map_err)?;
|
.map_err(Self::map_err)?;
|
||||||
|
|
||||||
row.map(|r| {
|
let Some(r) = row else { return Ok(None) };
|
||||||
Self::row_to_user(
|
|
||||||
r.id.unwrap_or_default(),
|
let field_rows = sqlx::query!(
|
||||||
r.email,
|
"SELECT name, value FROM user_profile_fields WHERE user_id = ? ORDER BY position ASC",
|
||||||
r.username,
|
id_str
|
||||||
r.password_hash,
|
)
|
||||||
Self::parse_role(&r.role),
|
.fetch_all(&self.pool)
|
||||||
r.bio,
|
.await
|
||||||
r.avatar_path,
|
.map_err(|e| DomainError::InfrastructureError(e.to_string()))?;
|
||||||
r.banner_path,
|
|
||||||
r.also_known_as,
|
let profile_fields = field_rows.into_iter().map(|f| ProfileField { name: f.name, value: f.value }).collect();
|
||||||
)
|
|
||||||
})
|
Self::row_to_user(
|
||||||
.transpose()
|
r.id.unwrap_or_default(),
|
||||||
|
r.email,
|
||||||
|
r.username,
|
||||||
|
r.password_hash,
|
||||||
|
Self::parse_role(&r.role),
|
||||||
|
r.bio,
|
||||||
|
r.avatar_path,
|
||||||
|
r.banner_path,
|
||||||
|
r.also_known_as,
|
||||||
|
profile_fields,
|
||||||
|
).map(Some)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn update_profile(
|
async fn update_profile(
|
||||||
|
|||||||
@@ -323,6 +323,7 @@ pub struct User {
|
|||||||
avatar_path: Option<String>,
|
avatar_path: Option<String>,
|
||||||
banner_path: Option<String>,
|
banner_path: Option<String>,
|
||||||
also_known_as: Option<String>,
|
also_known_as: Option<String>,
|
||||||
|
profile_fields: Vec<ProfileField>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl User {
|
impl User {
|
||||||
@@ -342,6 +343,7 @@ impl User {
|
|||||||
avatar_path: None,
|
avatar_path: None,
|
||||||
banner_path: None,
|
banner_path: None,
|
||||||
also_known_as: None,
|
also_known_as: None,
|
||||||
|
profile_fields: vec![],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -355,6 +357,7 @@ impl User {
|
|||||||
avatar_path: Option<String>,
|
avatar_path: Option<String>,
|
||||||
banner_path: Option<String>,
|
banner_path: Option<String>,
|
||||||
also_known_as: Option<String>,
|
also_known_as: Option<String>,
|
||||||
|
profile_fields: Vec<ProfileField>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
id,
|
id,
|
||||||
@@ -366,6 +369,7 @@ impl User {
|
|||||||
avatar_path,
|
avatar_path,
|
||||||
banner_path,
|
banner_path,
|
||||||
also_known_as,
|
also_known_as,
|
||||||
|
profile_fields,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -410,6 +414,10 @@ impl User {
|
|||||||
pub fn also_known_as(&self) -> Option<&str> {
|
pub fn also_known_as(&self) -> Option<&str> {
|
||||||
self.also_known_as.as_deref()
|
self.also_known_as.as_deref()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn profile_fields(&self) -> &[ProfileField] {
|
||||||
|
&self.profile_fields
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
|
|||||||
@@ -123,7 +123,6 @@ async fn wire_dependencies() -> anyhow::Result<(AppState, axum::Router)> {
|
|||||||
review_store,
|
review_store,
|
||||||
remote_watchlist_repo.clone(),
|
remote_watchlist_repo.clone(),
|
||||||
Arc::clone(&user_repository),
|
Arc::clone(&user_repository),
|
||||||
Arc::clone(&profile_fields_repo),
|
|
||||||
Arc::clone(&movie_repository),
|
Arc::clone(&movie_repository),
|
||||||
Arc::clone(&review_repository),
|
Arc::clone(&review_repository),
|
||||||
Arc::clone(&diary_repository),
|
Arc::clone(&diary_repository),
|
||||||
|
|||||||
@@ -42,12 +42,11 @@ async fn main() -> anyhow::Result<()> {
|
|||||||
|
|
||||||
// Clone refs federation handler needs before ctx consumes them.
|
// Clone refs federation handler needs before ctx consumes them.
|
||||||
#[cfg(feature = "federation")]
|
#[cfg(feature = "federation")]
|
||||||
let (fed_movie_repo, fed_review_repo, fed_diary_repo, fed_user_repo, fed_profile_fields_repo, base_url, allow_registration) = (
|
let (fed_movie_repo, fed_review_repo, fed_diary_repo, fed_user_repo, base_url, allow_registration) = (
|
||||||
Arc::clone(&repos.movie),
|
Arc::clone(&repos.movie),
|
||||||
Arc::clone(&repos.review),
|
Arc::clone(&repos.review),
|
||||||
Arc::clone(&repos.diary),
|
Arc::clone(&repos.diary),
|
||||||
Arc::clone(&repos.user),
|
Arc::clone(&repos.user),
|
||||||
Arc::clone(&repos.profile_fields),
|
|
||||||
app_config.base_url.clone(),
|
app_config.base_url.clone(),
|
||||||
app_config.allow_registration,
|
app_config.allow_registration,
|
||||||
);
|
);
|
||||||
@@ -175,7 +174,6 @@ async fn main() -> anyhow::Result<()> {
|
|||||||
fed_review_store,
|
fed_review_store,
|
||||||
fed_remote_watchlist_repo,
|
fed_remote_watchlist_repo,
|
||||||
fed_user_repo,
|
fed_user_repo,
|
||||||
fed_profile_fields_repo,
|
|
||||||
fed_movie_repo,
|
fed_movie_repo,
|
||||||
fed_review_repo,
|
fed_review_repo,
|
||||||
fed_diary_repo,
|
fed_diary_repo,
|
||||||
|
|||||||
Reference in New Issue
Block a user