separation of activitypub

This commit is contained in:
2026-05-09 17:23:06 +02:00
parent 69f6587623
commit 8819266cf9
32 changed files with 2005 additions and 436 deletions

View File

@@ -243,35 +243,34 @@ mod tests {
sqlx::query("CREATE TABLE IF NOT EXISTS ap_following (local_user_id TEXT NOT NULL, remote_actor_url TEXT NOT NULL, PRIMARY KEY (local_user_id, remote_actor_url))")
.execute(&pool).await.unwrap();
let fed_repo = Arc::new(sqlite::SqliteFederationRepository::new(pool));
struct DummyUserRepo;
#[async_trait::async_trait] impl domain::ports::UserRepository for DummyUserRepo {
async fn find_by_email(&self, _: &domain::value_objects::Email) -> Result<Option<domain::models::User>, domain::errors::DomainError> { Ok(None) }
async fn save(&self, _: &domain::models::User) -> Result<(), domain::errors::DomainError> { Ok(()) }
async fn find_by_id(&self, _: &domain::value_objects::UserId) -> Result<Option<domain::models::User>, domain::errors::DomainError> { Ok(None) }
async fn find_by_username(&self, _: &domain::value_objects::Username) -> Result<Option<domain::models::User>, domain::errors::DomainError> { Ok(None) }
async fn list_with_stats(&self) -> Result<Vec<domain::models::UserSummary>, domain::errors::DomainError> { Ok(vec![]) }
struct DummyApUserRepo;
#[async_trait::async_trait]
impl activitypub::ApUserRepository for DummyApUserRepo {
async fn find_by_id(&self, _: uuid::Uuid) -> anyhow::Result<Option<activitypub::ApUser>> { Ok(None) }
async fn find_by_username(&self, _: &str) -> anyhow::Result<Option<activitypub::ApUser>> { Ok(None) }
}
struct DummyMovieRepo;
#[async_trait::async_trait] impl domain::ports::MovieRepository for DummyMovieRepo {
async fn get_movie_by_external_id(&self, _: &domain::value_objects::ExternalMetadataId) -> Result<Option<domain::models::Movie>, domain::errors::DomainError> { Ok(None) }
async fn get_movie_by_id(&self, _: &domain::value_objects::MovieId) -> Result<Option<domain::models::Movie>, domain::errors::DomainError> { Ok(None) }
async fn get_movies_by_title_and_year(&self, _: &domain::value_objects::MovieTitle, _: &domain::value_objects::ReleaseYear) -> Result<Vec<domain::models::Movie>, domain::errors::DomainError> { Ok(vec![]) }
async fn upsert_movie(&self, _: &domain::models::Movie) -> Result<(), domain::errors::DomainError> { Ok(()) }
async fn save_review(&self, _: &domain::models::Review) -> Result<domain::events::DomainEvent, domain::errors::DomainError> { panic!() }
async fn query_diary(&self, _: &domain::models::DiaryFilter) -> Result<domain::models::collections::Paginated<domain::models::DiaryEntry>, domain::errors::DomainError> { panic!() }
async fn get_review_history(&self, _: &domain::value_objects::MovieId) -> Result<domain::models::ReviewHistory, domain::errors::DomainError> { panic!() }
async fn get_review_by_id(&self, _: &domain::value_objects::ReviewId) -> Result<Option<domain::models::Review>, domain::errors::DomainError> { Ok(None) }
async fn delete_review(&self, _: &domain::value_objects::ReviewId) -> Result<(), domain::errors::DomainError> { Ok(()) }
async fn delete_movie(&self, _: &domain::value_objects::MovieId) -> Result<(), domain::errors::DomainError> { Ok(()) }
async fn query_activity_feed(&self, _: &domain::models::collections::PageParams) -> Result<domain::models::collections::Paginated<domain::models::FeedEntry>, domain::errors::DomainError> { panic!() }
async fn get_user_stats(&self, _: &domain::value_objects::UserId) -> Result<domain::models::UserStats, domain::errors::DomainError> { panic!() }
async fn get_user_history(&self, _: &domain::value_objects::UserId) -> Result<Vec<domain::models::DiaryEntry>, domain::errors::DomainError> { Ok(vec![]) }
async fn get_user_trends(&self, _: &domain::value_objects::UserId) -> Result<domain::models::UserTrends, domain::errors::DomainError> { panic!() }
struct DummyObjectHandler;
#[async_trait::async_trait]
impl activitypub::ApObjectHandler for DummyObjectHandler {
async fn get_local_objects_for_user(&self, _: uuid::Uuid) -> anyhow::Result<Vec<(url::Url, serde_json::Value)>> { Ok(vec![]) }
async fn on_create(&self, _: &url::Url, _: &url::Url, _: serde_json::Value) -> anyhow::Result<()> { Ok(()) }
async fn on_update(&self, _: &url::Url, _: &url::Url, _: serde_json::Value) -> anyhow::Result<()> { Ok(()) }
async fn on_delete(&self, _: &url::Url, _: &url::Url) -> anyhow::Result<()> { Ok(()) }
async fn on_actor_removed(&self, _: &url::Url) -> anyhow::Result<()> { Ok(()) }
}
Arc::new(
activitypub::ActivityPubService::new(fed_repo, Arc::new(DummyUserRepo), Arc::new(DummyMovieRepo), "http://localhost:3000".to_string(), true)
.await
.unwrap(),
activitypub::ActivityPubService::new(
fed_repo,
Arc::new(DummyApUserRepo),
Arc::new(DummyObjectHandler),
"http://localhost:3000".to_string(),
true,
)
.await
.unwrap(),
)
}

View File

@@ -340,7 +340,7 @@ pub mod html {
let following_count = if is_own_profile {
if let Some(ref uid) = user_id {
state.ap_service.count_following(uid.clone()).await.unwrap_or(0)
state.ap_service.count_following(uid.value()).await.unwrap_or(0)
} else {
0
}
@@ -350,7 +350,7 @@ pub mod html {
let followers_count = if is_own_profile {
state.ap_service
.count_accepted_followers(domain::value_objects::UserId::from_uuid(profile_user_uuid))
.count_accepted_followers(profile_user_uuid)
.await
.unwrap_or(0)
} else {
@@ -359,7 +359,7 @@ pub mod html {
let pending_followers = if is_own_profile {
state.ap_service
.get_pending_followers(domain::value_objects::UserId::from_uuid(profile_user_uuid))
.get_pending_followers(profile_user_uuid)
.await
.unwrap_or_default()
.into_iter()
@@ -425,7 +425,7 @@ pub mod html {
if user_id.value() != profile_user_uuid {
return StatusCode::FORBIDDEN.into_response();
}
match state.ap_service.follow(user_id.clone(), &form.handle).await {
match state.ap_service.follow(user_id.value(), &form.handle).await {
Ok(()) => Redirect::to(&format!("/users/{}", profile_user_uuid)).into_response(),
Err(e) => {
tracing::error!("follow error: {:?}", e);
@@ -444,7 +444,7 @@ pub mod html {
if user_id.value() != profile_user_uuid {
return StatusCode::FORBIDDEN.into_response();
}
match state.ap_service.unfollow(user_id.clone(), &form.actor_url).await {
match state.ap_service.unfollow(user_id.value(), &form.actor_url).await {
Ok(()) => Redirect::to(&format!("/users/{}/following-list", profile_user_uuid)).into_response(),
Err(e) => {
let msg = encode_error(&e.to_string());
@@ -462,7 +462,7 @@ pub mod html {
if user_id.value() != profile_user_uuid {
return StatusCode::FORBIDDEN.into_response();
}
match state.ap_service.accept_follower(user_id, &form.actor_url).await {
match state.ap_service.accept_follower(user_id.value(), &form.actor_url).await {
Ok(_) => Redirect::to(&format!("/users/{}", profile_user_uuid)).into_response(),
Err(e) => {
let msg = encode_error(&e.to_string());
@@ -480,7 +480,7 @@ pub mod html {
if user_id.value() != profile_user_uuid {
return StatusCode::FORBIDDEN.into_response();
}
match state.ap_service.reject_follower(user_id, &form.actor_url).await {
match state.ap_service.reject_follower(user_id.value(), &form.actor_url).await {
Ok(_) => Redirect::to(&format!("/users/{}", profile_user_uuid)).into_response(),
Err(e) => {
let msg = encode_error(&e.to_string());
@@ -501,7 +501,7 @@ pub mod html {
let mut ctx = build_page_context(&state, Some(user_id.clone())).await;
ctx.page_title = "Following — Movies Diary".to_string();
ctx.canonical_url = format!("{}/users/{}/following-list", state.app_ctx.config.base_url, profile_user_uuid);
match state.ap_service.get_following(user_id).await {
match state.ap_service.get_following(user_id.value()).await {
Ok(following) => {
let actors = following.into_iter().map(|a| RemoteActorView {
handle: a.handle,
@@ -538,7 +538,7 @@ pub mod html {
let mut ctx = build_page_context(&state, Some(user_id.clone())).await;
ctx.page_title = "Followers — Movies Diary".to_string();
ctx.canonical_url = format!("{}/users/{}/followers-list", state.app_ctx.config.base_url, profile_user_uuid);
match state.ap_service.get_accepted_followers(user_id).await {
match state.ap_service.get_accepted_followers(user_id.value()).await {
Ok(followers) => {
let actors = followers.into_iter().map(|a| RemoteActorView {
handle: a.handle,
@@ -572,7 +572,7 @@ pub mod html {
if user_id.value() != profile_user_uuid {
return StatusCode::FORBIDDEN.into_response();
}
match state.ap_service.remove_follower(user_id, &form.actor_url).await {
match state.ap_service.remove_follower(user_id.value(), &form.actor_url).await {
Ok(_) => Redirect::to(&format!("/users/{}/followers-list", profile_user_uuid)).into_response(),
Err(e) => {
let msg = encode_error(&e.to_string());

View File

@@ -15,7 +15,7 @@ use auth::{AuthConfig, Argon2PasswordHasher, JwtAuthService};
use metadata::MetadataClientImpl;
use poster_fetcher::{PosterFetcherConfig, ReqwestPosterFetcher};
use poster_storage::{PosterStorageAdapter, StorageConfig};
use activitypub::ActivityPubService;
use activitypub::{ActivityPubEventHandler, ActivityPubService, DomainUserRepoAdapter, ReviewObjectHandler};
use sqlite::{SqliteFederationRepository, SqliteMovieRepository, SqliteUserRepository};
use rss::RssAdapter;
use template_askama::AskamaHtmlRenderer;
@@ -94,17 +94,27 @@ async fn wire_dependencies() -> anyhow::Result<AppState> {
// Federation
let federation_repo = Arc::new(SqliteFederationRepository::new(pool));
let user_repo_adapter = Arc::new(DomainUserRepoAdapter(Arc::clone(&user_repository)));
let review_handler = Arc::new(ReviewObjectHandler {
movie_repo: Arc::clone(&repository),
review_store: Arc::clone(&federation_repo) as Arc<dyn activitypub::RemoteReviewRepository>,
base_url: app_config.base_url.clone(),
});
let ap_service = Arc::new(
ActivityPubService::new(
federation_repo,
Arc::clone(&user_repository),
Arc::clone(&repository),
user_repo_adapter,
review_handler,
app_config.base_url.clone(),
cfg!(debug_assertions),
)
.await?,
);
let ap_event_handler = ap_service.event_handler();
let ap_event_handler = ActivityPubEventHandler::new(
Arc::clone(&ap_service),
Arc::clone(&repository),
app_config.base_url.clone(),
);
let poster_handler = PosterSyncHandler::new(handler_ctx, 3);
let (event_publisher, event_worker) = create_event_channel(