structural refactor and codebase improvements
This commit is contained in:
@@ -8,7 +8,7 @@ use domain::{
|
||||
GoalQuery, LocalApContentQuery, MovieQuery, ReviewRepository, StatsRepository,
|
||||
UserFederationSettingsQuery,
|
||||
},
|
||||
value_objects::{MovieId, ReviewId, UserId},
|
||||
value_objects::{InstanceIdentity, MovieId, ReviewId, UserId},
|
||||
};
|
||||
use std::sync::Arc;
|
||||
|
||||
@@ -25,7 +25,7 @@ pub struct ActivityPubEventHandler {
|
||||
goal_repo: Arc<dyn GoalQuery>,
|
||||
stats_repo: Arc<dyn StatsRepository>,
|
||||
federation_settings: Arc<dyn UserFederationSettingsQuery>,
|
||||
base_url: String,
|
||||
instance: InstanceIdentity,
|
||||
}
|
||||
|
||||
impl ActivityPubEventHandler {
|
||||
@@ -38,7 +38,7 @@ impl ActivityPubEventHandler {
|
||||
goal_repo: Arc<dyn GoalQuery>,
|
||||
stats_repo: Arc<dyn StatsRepository>,
|
||||
federation_settings: Arc<dyn UserFederationSettingsQuery>,
|
||||
base_url: String,
|
||||
instance: InstanceIdentity,
|
||||
) -> Self {
|
||||
Self {
|
||||
ap_service,
|
||||
@@ -48,7 +48,7 @@ impl ActivityPubEventHandler {
|
||||
goal_repo,
|
||||
stats_repo,
|
||||
federation_settings,
|
||||
base_url,
|
||||
instance,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -140,7 +140,7 @@ impl EventHandler for ActivityPubEventHandler {
|
||||
.await
|
||||
.map_err(|e| DomainError::InfrastructureError(e.to_string())),
|
||||
DomainEvent::UserDeleted { user_id } => {
|
||||
let ap_id = actor_url(&self.base_url, user_id.value());
|
||||
let ap_id = actor_url(&self.instance, user_id.value());
|
||||
self.ap_service
|
||||
.broadcast_delete_to_followers(user_id.value(), ap_id)
|
||||
.await
|
||||
@@ -179,8 +179,8 @@ impl ActivityPubEventHandler {
|
||||
None => return Ok(()),
|
||||
};
|
||||
|
||||
let ap_id = review_url(&self.base_url, review_id);
|
||||
let actor = actor_url(&self.base_url, user_id.value());
|
||||
let ap_id = review_url(&self.instance, review_id);
|
||||
let actor = actor_url(&self.instance, user_id.value());
|
||||
|
||||
let movie = self
|
||||
.movie_repo
|
||||
@@ -210,8 +210,8 @@ impl ActivityPubEventHandler {
|
||||
poster_url: movie
|
||||
.as_ref()
|
||||
.and_then(|m| m.poster_path())
|
||||
.map(|p| format!("{}/images/{}", self.base_url, p.value())),
|
||||
base_url: self.base_url.clone(),
|
||||
.map(|p| self.instance.image_url_for(p.value())),
|
||||
base_url: self.instance.base_url().to_string(),
|
||||
},
|
||||
);
|
||||
let json = serde_json::to_value(obj)?;
|
||||
@@ -245,8 +245,8 @@ impl ActivityPubEventHandler {
|
||||
None => return Ok(()),
|
||||
};
|
||||
|
||||
let ap_id = review_url(&self.base_url, review_id);
|
||||
let actor = actor_url(&self.base_url, user_id.value());
|
||||
let ap_id = review_url(&self.instance, review_id);
|
||||
let actor = actor_url(&self.instance, user_id.value());
|
||||
|
||||
let movie = self
|
||||
.movie_repo
|
||||
@@ -276,8 +276,8 @@ impl ActivityPubEventHandler {
|
||||
poster_url: movie
|
||||
.as_ref()
|
||||
.and_then(|m| m.poster_path())
|
||||
.map(|p| format!("{}/images/{}", self.base_url, p.value())),
|
||||
base_url: self.base_url.clone(),
|
||||
.map(|p| self.instance.image_url_for(p.value())),
|
||||
base_url: self.instance.base_url().to_string(),
|
||||
},
|
||||
);
|
||||
let json = serde_json::to_value(obj)?;
|
||||
@@ -294,7 +294,7 @@ impl ActivityPubEventHandler {
|
||||
user_id: &UserId,
|
||||
review_id: &ReviewId,
|
||||
) -> anyhow::Result<()> {
|
||||
let ap_id = review_url(&self.base_url, review_id);
|
||||
let ap_id = review_url(&self.instance, review_id);
|
||||
self.ap_service
|
||||
.broadcast_delete_to_followers(user_id.value(), ap_id)
|
||||
.await?;
|
||||
@@ -320,8 +320,8 @@ impl ActivityPubEventHandler {
|
||||
}
|
||||
|
||||
use crate::urls::watchlist_entry_url;
|
||||
let ap_id = watchlist_entry_url(&self.base_url, user_id.value(), movie_id.value());
|
||||
let actor = actor_url(&self.base_url, user_id.value());
|
||||
let ap_id = watchlist_entry_url(&self.instance, user_id.value(), movie_id.value());
|
||||
let actor = actor_url(&self.instance, user_id.value());
|
||||
|
||||
let poster_url = self
|
||||
.movie_repo
|
||||
@@ -331,7 +331,7 @@ impl ActivityPubEventHandler {
|
||||
.flatten()
|
||||
.and_then(|m| {
|
||||
m.poster_path()
|
||||
.map(|p| format!("{}/images/{}", self.base_url, p.value()))
|
||||
.map(|p| self.instance.image_url_for(p.value()))
|
||||
});
|
||||
|
||||
let added_at_utc =
|
||||
@@ -344,7 +344,7 @@ impl ActivityPubEventHandler {
|
||||
external_metadata_id: external_metadata_id.clone(),
|
||||
poster_url,
|
||||
added_at: added_at_utc,
|
||||
base_url: self.base_url.clone(),
|
||||
base_url: self.instance.base_url().to_string(),
|
||||
});
|
||||
let json = serde_json::to_value(obj)?;
|
||||
|
||||
@@ -360,7 +360,7 @@ impl ActivityPubEventHandler {
|
||||
movie_id: &domain::value_objects::MovieId,
|
||||
) -> anyhow::Result<()> {
|
||||
use crate::urls::watchlist_entry_url;
|
||||
let ap_id = watchlist_entry_url(&self.base_url, user_id.value(), movie_id.value());
|
||||
let ap_id = watchlist_entry_url(&self.instance, user_id.value(), movie_id.value());
|
||||
self.ap_service
|
||||
.broadcast_delete_to_followers(user_id.value(), ap_id)
|
||||
.await?;
|
||||
@@ -383,7 +383,7 @@ impl ActivityPubEventHandler {
|
||||
.map(|id| id.value().to_string());
|
||||
let poster_url = movie
|
||||
.poster_path()
|
||||
.map(|p| format!("{}/images/{}", self.base_url, p.value()));
|
||||
.map(|p| self.instance.image_url_for(p.value()));
|
||||
|
||||
for entry in entries {
|
||||
let review = entry.review();
|
||||
@@ -398,8 +398,8 @@ impl ActivityPubEventHandler {
|
||||
continue;
|
||||
}
|
||||
|
||||
let ap_id = review_url(&self.base_url, review.id());
|
||||
let actor = actor_url(&self.base_url, user_id.value());
|
||||
let ap_id = review_url(&self.instance, review.id());
|
||||
let actor = actor_url(&self.instance, user_id.value());
|
||||
|
||||
let obj = review_to_ap_object(
|
||||
review,
|
||||
@@ -410,7 +410,7 @@ impl ActivityPubEventHandler {
|
||||
release_year: movie.release_year().value(),
|
||||
external_metadata_id: external_metadata_id.clone(),
|
||||
poster_url: poster_url.clone(),
|
||||
base_url: self.base_url.clone(),
|
||||
base_url: self.instance.base_url().to_string(),
|
||||
},
|
||||
);
|
||||
let json = serde_json::to_value(obj)?;
|
||||
@@ -450,15 +450,15 @@ impl ActivityPubEventHandler {
|
||||
.count_reviews_in_year(user_id, year)
|
||||
.await
|
||||
.unwrap_or(0);
|
||||
let ap_id = goal_url(&self.base_url, user_id.value(), year);
|
||||
let actor = actor_url(&self.base_url, user_id.value());
|
||||
let ap_id = goal_url(&self.instance, user_id.value(), year);
|
||||
let actor = actor_url(&self.instance, user_id.value());
|
||||
let obj = goal_to_ap_object(
|
||||
ap_id,
|
||||
actor,
|
||||
year,
|
||||
goal.target_count(),
|
||||
current,
|
||||
&self.base_url,
|
||||
self.instance.base_url(),
|
||||
);
|
||||
let json = serde_json::to_value(obj)?;
|
||||
self.ap_service
|
||||
@@ -488,9 +488,16 @@ impl ActivityPubEventHandler {
|
||||
.await
|
||||
.unwrap_or(0);
|
||||
|
||||
let ap_id = goal_url(&self.base_url, user_id.value(), year);
|
||||
let actor = actor_url(&self.base_url, user_id.value());
|
||||
let obj = goal_to_ap_object(ap_id, actor, year, target_count, current, &self.base_url);
|
||||
let ap_id = goal_url(&self.instance, user_id.value(), year);
|
||||
let actor = actor_url(&self.instance, user_id.value());
|
||||
let obj = goal_to_ap_object(
|
||||
ap_id,
|
||||
actor,
|
||||
year,
|
||||
target_count,
|
||||
current,
|
||||
self.instance.base_url(),
|
||||
);
|
||||
let json = serde_json::to_value(obj)?;
|
||||
if is_create {
|
||||
self.ap_service
|
||||
@@ -513,7 +520,7 @@ impl ActivityPubEventHandler {
|
||||
if !flags.goals {
|
||||
return Ok(());
|
||||
}
|
||||
let ap_id = goal_url(&self.base_url, user_id.value(), year);
|
||||
let ap_id = goal_url(&self.instance, user_id.value(), year);
|
||||
self.ap_service
|
||||
.broadcast_delete_to_followers(user_id.value(), ap_id)
|
||||
.await?;
|
||||
|
||||
137
crates/adapters/activitypub/src/federation_ports.rs
Normal file
137
crates/adapters/activitypub/src/federation_ports.rs
Normal file
@@ -0,0 +1,137 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use domain::{
|
||||
errors::DomainError,
|
||||
models::{BlockedDomainInfo, FollowedActorInfo},
|
||||
ports::{ApBackfillPort, ApDocumentPort, InstanceBlocklistPort},
|
||||
};
|
||||
use k_ap::ActivityPubService;
|
||||
use uuid::Uuid;
|
||||
|
||||
/// Adapts the federation library's service to the three domain-owned ports.
|
||||
///
|
||||
/// A wrapper rather than a bare `impl ... for ActivityPubService` because the
|
||||
/// orphan rule forbids implementing a foreign trait for a foreign type, and
|
||||
/// from this crate both `ApDocumentPort` (owned by `domain`) and
|
||||
/// `ActivityPubService` (owned by the external `k-ap` crate) are foreign.
|
||||
///
|
||||
/// One type carrying all three impls mirrors `CompositeSocialAdapter`, which
|
||||
/// serves `SocialCommand`, `FollowGraphQuery`, and `BlockQuery` the same way.
|
||||
pub struct ApServiceAdapter {
|
||||
service: Arc<ActivityPubService>,
|
||||
}
|
||||
|
||||
impl ApServiceAdapter {
|
||||
pub fn new(service: Arc<ActivityPubService>) -> Self {
|
||||
Self { service }
|
||||
}
|
||||
}
|
||||
|
||||
/// Single conversion point from the federation library's `anyhow` errors to
|
||||
/// `DomainError`. The log line and the resulting error string reproduce what
|
||||
/// `presentation::handlers::social::ap_to_domain` produced before the port
|
||||
/// inversion moved the boundary here.
|
||||
fn ap_err(e: anyhow::Error) -> DomainError {
|
||||
tracing::error!("ActivityPub error: {:?}", e);
|
||||
DomainError::InfrastructureError(e.to_string())
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl ApDocumentPort for ApServiceAdapter {
|
||||
async fn actor_json(&self, user_id: &str) -> Result<String, DomainError> {
|
||||
self.service.actor_json(user_id).await.map_err(ap_err)
|
||||
}
|
||||
async fn followers_collection_json(
|
||||
&self,
|
||||
user_id: Uuid,
|
||||
page: Option<u32>,
|
||||
) -> Result<String, DomainError> {
|
||||
self.service
|
||||
.followers_collection_json(user_id, page)
|
||||
.await
|
||||
.map_err(ap_err)
|
||||
}
|
||||
async fn following_collection_json(
|
||||
&self,
|
||||
user_id: Uuid,
|
||||
page: Option<u32>,
|
||||
) -> Result<String, DomainError> {
|
||||
self.service
|
||||
.following_collection_json(user_id, page)
|
||||
.await
|
||||
.map_err(ap_err)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl InstanceBlocklistPort for ApServiceAdapter {
|
||||
async fn get_blocked_domains(&self) -> Result<Vec<BlockedDomainInfo>, DomainError> {
|
||||
let domains = self.service.get_blocked_domains().await.map_err(ap_err)?;
|
||||
Ok(domains
|
||||
.into_iter()
|
||||
.map(|d| BlockedDomainInfo {
|
||||
domain: d.domain,
|
||||
reason: d.reason,
|
||||
blocked_at: d.blocked_at,
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
async fn add_blocked_domain(
|
||||
&self,
|
||||
domain: &str,
|
||||
reason: Option<&str>,
|
||||
) -> Result<(), DomainError> {
|
||||
self.service
|
||||
.add_blocked_domain(domain, reason)
|
||||
.await
|
||||
.map_err(ap_err)
|
||||
}
|
||||
async fn remove_blocked_domain(&self, domain: &str) -> Result<(), DomainError> {
|
||||
self.service
|
||||
.remove_blocked_domain(domain)
|
||||
.await
|
||||
.map_err(ap_err)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl ApBackfillPort for ApServiceAdapter {
|
||||
async fn get_following(
|
||||
&self,
|
||||
local_user_id: Uuid,
|
||||
) -> Result<Vec<FollowedActorInfo>, DomainError> {
|
||||
let actors = self
|
||||
.service
|
||||
.get_following(local_user_id)
|
||||
.await
|
||||
.map_err(ap_err)?;
|
||||
Ok(actors
|
||||
.into_iter()
|
||||
.map(|a| FollowedActorInfo {
|
||||
url: a.url,
|
||||
outbox_url: a.outbox_url,
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
async fn import_remote_outbox(
|
||||
&self,
|
||||
outbox_url: &str,
|
||||
actor_url: &str,
|
||||
) -> Result<(), DomainError> {
|
||||
self.service
|
||||
.import_remote_outbox(outbox_url, actor_url)
|
||||
.await
|
||||
.map_err(ap_err)
|
||||
}
|
||||
async fn run_backfill_for_follower(
|
||||
&self,
|
||||
owner_user_id: Uuid,
|
||||
follower_inbox_url: String,
|
||||
) -> Result<(), DomainError> {
|
||||
self.service
|
||||
.run_backfill_for_follower(owner_user_id, follower_inbox_url)
|
||||
.await
|
||||
.map_err(ap_err)
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@ use chrono::DateTime;
|
||||
use domain::{
|
||||
models::RemoteGoalEntry,
|
||||
ports::{GoalQuery, RemoteGoalRepository},
|
||||
value_objects::UserId,
|
||||
value_objects::{InstanceIdentity, UserId},
|
||||
};
|
||||
use k_ap::{AS_PUBLIC, ApContentReader, ApObjectHandler, LocalObject};
|
||||
use url::Url;
|
||||
@@ -16,7 +16,7 @@ use crate::urls::{actor_url, goal_url};
|
||||
pub struct GoalObjectHandler {
|
||||
pub remote_goal_repo: Arc<dyn RemoteGoalRepository>,
|
||||
pub goal_repo: Arc<dyn GoalQuery>,
|
||||
pub base_url: String,
|
||||
pub instance: InstanceIdentity,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
@@ -34,11 +34,11 @@ impl ApContentReader for GoalObjectHandler {
|
||||
.await
|
||||
.map_err(|e| anyhow::anyhow!(e.to_string()))?;
|
||||
|
||||
let actor = actor_url(&self.base_url, user_id);
|
||||
let actor = actor_url(&self.instance, user_id);
|
||||
let follower_cc = format!("{}/followers", actor);
|
||||
let mut results = Vec::new();
|
||||
for goal in goals {
|
||||
let ap_id = goal_url(&self.base_url, user_id, goal.year());
|
||||
let ap_id = goal_url(&self.instance, user_id, goal.year());
|
||||
let published = DateTime::from_naive_utc_and_offset(*goal.created_at(), chrono::Utc);
|
||||
let obj = goal_to_ap_object(
|
||||
ap_id.clone(),
|
||||
@@ -46,7 +46,7 @@ impl ApContentReader for GoalObjectHandler {
|
||||
goal.year(),
|
||||
goal.target_count(),
|
||||
0,
|
||||
&self.base_url,
|
||||
self.instance.base_url(),
|
||||
);
|
||||
results.push(LocalObject {
|
||||
ap_id,
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
pub mod composite_handler;
|
||||
pub mod event_handler;
|
||||
pub mod federation_event_bridge;
|
||||
pub mod federation_ports;
|
||||
pub mod goal_handler;
|
||||
pub mod objects;
|
||||
pub mod port;
|
||||
pub mod remote_review_repository;
|
||||
pub mod review_handler;
|
||||
pub mod social_adapter;
|
||||
@@ -22,7 +22,7 @@ pub use k_ap::{
|
||||
};
|
||||
|
||||
pub use event_handler::ActivityPubEventHandler;
|
||||
pub use port::{ActivityPubPort, NoopActivityPubService};
|
||||
pub use federation_ports::ApServiceAdapter;
|
||||
pub use remote_review_repository::{RemoteReviewRepository, RemoteReviewUpdate};
|
||||
pub use review_handler::ReviewObjectHandler;
|
||||
pub use social_adapter::CompositeSocialAdapter;
|
||||
@@ -41,7 +41,16 @@ pub struct FederationRepos {
|
||||
}
|
||||
|
||||
pub struct ActivityPubWire {
|
||||
pub service: std::sync::Arc<dyn ActivityPubPort>,
|
||||
/// AP document serving. Prefer this over `service` from outside this crate.
|
||||
pub document: std::sync::Arc<dyn domain::ports::ApDocumentPort>,
|
||||
/// Instance domain blocklist. Prefer this over `service` from outside this crate.
|
||||
pub blocklist: std::sync::Arc<dyn domain::ports::InstanceBlocklistPort>,
|
||||
/// Post-follow content backfill. Prefer this over `service` from outside this crate.
|
||||
pub backfill: std::sync::Arc<dyn domain::ports::ApBackfillPort>,
|
||||
/// The concrete service, consumed by `crates/server` to construct
|
||||
/// `CompositeSocialAdapter`. Everything else should use
|
||||
/// `document`/`blocklist`/`backfill`.
|
||||
pub service: std::sync::Arc<ActivityPubService>,
|
||||
pub router: axum::Router,
|
||||
pub event_handler: std::sync::Arc<dyn domain::ports::EventHandler>,
|
||||
}
|
||||
@@ -64,7 +73,7 @@ pub struct ActivityPubDeps {
|
||||
pub federation_settings: std::sync::Arc<dyn domain::ports::UserFederationSettingsQuery>,
|
||||
pub follow_command: std::sync::Arc<dyn domain::ports::FollowCommand>,
|
||||
pub follow_query: std::sync::Arc<dyn domain::ports::FollowQuery>,
|
||||
pub base_url: String,
|
||||
pub instance: domain::value_objects::InstanceIdentity,
|
||||
pub allow_registration: bool,
|
||||
pub event_publisher: std::sync::Arc<dyn domain::ports::EventPublisher>,
|
||||
}
|
||||
@@ -88,7 +97,7 @@ pub async fn wire(deps: ActivityPubDeps) -> anyhow::Result<ActivityPubWire> {
|
||||
federation_settings,
|
||||
follow_command: _,
|
||||
follow_query: _,
|
||||
base_url,
|
||||
instance,
|
||||
allow_registration,
|
||||
event_publisher,
|
||||
} = deps;
|
||||
@@ -98,17 +107,17 @@ pub async fn wire(deps: ActivityPubDeps) -> anyhow::Result<ActivityPubWire> {
|
||||
diary_repo,
|
||||
review_store,
|
||||
event_publisher: std::sync::Arc::clone(&event_publisher),
|
||||
base_url: base_url.clone(),
|
||||
instance: instance.clone(),
|
||||
});
|
||||
let watchlist_handler = std::sync::Arc::new(watchlist_handler::WatchlistObjectHandler {
|
||||
remote_watchlist_repo,
|
||||
content_query: std::sync::Arc::clone(&local_ap_content),
|
||||
base_url: base_url.clone(),
|
||||
instance: instance.clone(),
|
||||
});
|
||||
let goal_handler = std::sync::Arc::new(goal_handler::GoalObjectHandler {
|
||||
remote_goal_repo,
|
||||
goal_repo: std::sync::Arc::clone(&goal_repo),
|
||||
base_url: base_url.clone(),
|
||||
instance: instance.clone(),
|
||||
});
|
||||
let composite = std::sync::Arc::new(composite_handler::CompositeObjectHandler {
|
||||
review: review_handler,
|
||||
@@ -132,14 +141,14 @@ pub async fn wire(deps: ActivityPubDeps) -> anyhow::Result<ActivityPubWire> {
|
||||
);
|
||||
|
||||
let concrete = std::sync::Arc::new(
|
||||
ActivityPubService::builder(base_url.clone())
|
||||
ActivityPubService::builder(instance.base_url().to_string())
|
||||
.activity_repo(activity_repo)
|
||||
.follow_repo(follow_repo)
|
||||
.actor_repo(actor_repo)
|
||||
.blocklist_repo(blocklist_repo)
|
||||
.user_repo(std::sync::Arc::new(DomainUserRepoAdapter::new(
|
||||
user_repo,
|
||||
base_url.clone(),
|
||||
instance.clone(),
|
||||
)))
|
||||
.signed_fetch_actor_id(INSTANCE_ACTOR_ID)
|
||||
.content_reader(composite.clone() as std::sync::Arc<dyn ApContentReader>)
|
||||
@@ -165,11 +174,20 @@ pub async fn wire(deps: ActivityPubDeps) -> anyhow::Result<ActivityPubWire> {
|
||||
goal_repo,
|
||||
stats_repo,
|
||||
federation_settings,
|
||||
base_url,
|
||||
instance,
|
||||
)) as std::sync::Arc<dyn domain::ports::EventHandler>;
|
||||
|
||||
let ports = std::sync::Arc::new(federation_ports::ApServiceAdapter::new(
|
||||
std::sync::Arc::clone(&concrete),
|
||||
));
|
||||
|
||||
Ok(ActivityPubWire {
|
||||
service: concrete as std::sync::Arc<dyn ActivityPubPort>,
|
||||
document: std::sync::Arc::clone(&ports)
|
||||
as std::sync::Arc<dyn domain::ports::ApDocumentPort>,
|
||||
blocklist: std::sync::Arc::clone(&ports)
|
||||
as std::sync::Arc<dyn domain::ports::InstanceBlocklistPort>,
|
||||
backfill: ports as std::sync::Arc<dyn domain::ports::ApBackfillPort>,
|
||||
service: concrete,
|
||||
router,
|
||||
event_handler,
|
||||
})
|
||||
|
||||
@@ -1,178 +0,0 @@
|
||||
use async_trait::async_trait;
|
||||
use uuid::Uuid;
|
||||
|
||||
use k_ap::{ActivityPubService, BlockedDomain, RemoteActor};
|
||||
|
||||
#[async_trait]
|
||||
pub trait ActivityPubPort: Send + Sync {
|
||||
async fn actor_json(&self, user_id: &str) -> anyhow::Result<String>;
|
||||
async fn follow(&self, local_user_id: Uuid, handle: &str) -> anyhow::Result<()>;
|
||||
async fn unfollow(&self, local_user_id: Uuid, actor_url: &str) -> anyhow::Result<()>;
|
||||
async fn accept_follower(
|
||||
&self,
|
||||
local_user_id: Uuid,
|
||||
remote_actor_url: &str,
|
||||
) -> anyhow::Result<()>;
|
||||
async fn reject_follower(
|
||||
&self,
|
||||
local_user_id: Uuid,
|
||||
remote_actor_url: &str,
|
||||
) -> anyhow::Result<()>;
|
||||
async fn get_following(&self, local_user_id: Uuid) -> anyhow::Result<Vec<RemoteActor>>;
|
||||
async fn remove_follower(&self, local_user_id: Uuid, actor_url: &str) -> anyhow::Result<()>;
|
||||
async fn block_actor(&self, local_user_id: Uuid, actor_url: &str) -> anyhow::Result<()>;
|
||||
async fn unblock_actor(&self, local_user_id: Uuid, actor_url: &str) -> anyhow::Result<()>;
|
||||
async fn get_blocked_actors(&self, local_user_id: Uuid) -> anyhow::Result<Vec<RemoteActor>>;
|
||||
async fn add_blocked_domain(&self, domain: &str, reason: Option<&str>) -> anyhow::Result<()>;
|
||||
async fn remove_blocked_domain(&self, domain: &str) -> anyhow::Result<()>;
|
||||
async fn get_blocked_domains(&self) -> anyhow::Result<Vec<BlockedDomain>>;
|
||||
async fn import_remote_outbox(&self, outbox_url: &str, actor_url: &str) -> anyhow::Result<()>;
|
||||
async fn followers_collection_json(
|
||||
&self,
|
||||
user_id: Uuid,
|
||||
page: Option<u32>,
|
||||
) -> anyhow::Result<String>;
|
||||
async fn following_collection_json(
|
||||
&self,
|
||||
user_id: Uuid,
|
||||
page: Option<u32>,
|
||||
) -> anyhow::Result<String>;
|
||||
async fn run_backfill_for_follower(
|
||||
&self,
|
||||
owner_user_id: Uuid,
|
||||
follower_inbox_url: String,
|
||||
) -> anyhow::Result<()>;
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl ActivityPubPort for ActivityPubService {
|
||||
async fn actor_json(&self, user_id: &str) -> anyhow::Result<String> {
|
||||
self.actor_json(user_id).await
|
||||
}
|
||||
async fn follow(&self, local_user_id: Uuid, handle: &str) -> anyhow::Result<()> {
|
||||
self.follow(local_user_id, handle).await
|
||||
}
|
||||
async fn unfollow(&self, local_user_id: Uuid, actor_url: &str) -> anyhow::Result<()> {
|
||||
self.unfollow(local_user_id, actor_url).await
|
||||
}
|
||||
async fn accept_follower(
|
||||
&self,
|
||||
local_user_id: Uuid,
|
||||
remote_actor_url: &str,
|
||||
) -> anyhow::Result<()> {
|
||||
self.accept_follower(local_user_id, remote_actor_url).await
|
||||
}
|
||||
async fn reject_follower(
|
||||
&self,
|
||||
local_user_id: Uuid,
|
||||
remote_actor_url: &str,
|
||||
) -> anyhow::Result<()> {
|
||||
self.reject_follower(local_user_id, remote_actor_url).await
|
||||
}
|
||||
async fn get_following(&self, local_user_id: Uuid) -> anyhow::Result<Vec<RemoteActor>> {
|
||||
self.get_following(local_user_id).await
|
||||
}
|
||||
async fn remove_follower(&self, local_user_id: Uuid, actor_url: &str) -> anyhow::Result<()> {
|
||||
self.remove_follower(local_user_id, actor_url).await
|
||||
}
|
||||
async fn block_actor(&self, local_user_id: Uuid, actor_url: &str) -> anyhow::Result<()> {
|
||||
self.block_actor(local_user_id, actor_url).await
|
||||
}
|
||||
async fn unblock_actor(&self, local_user_id: Uuid, actor_url: &str) -> anyhow::Result<()> {
|
||||
self.unblock_actor(local_user_id, actor_url).await
|
||||
}
|
||||
async fn get_blocked_actors(&self, local_user_id: Uuid) -> anyhow::Result<Vec<RemoteActor>> {
|
||||
self.get_blocked_actors(local_user_id).await
|
||||
}
|
||||
async fn add_blocked_domain(&self, domain: &str, reason: Option<&str>) -> anyhow::Result<()> {
|
||||
self.add_blocked_domain(domain, reason).await
|
||||
}
|
||||
async fn remove_blocked_domain(&self, domain: &str) -> anyhow::Result<()> {
|
||||
self.remove_blocked_domain(domain).await
|
||||
}
|
||||
async fn get_blocked_domains(&self) -> anyhow::Result<Vec<BlockedDomain>> {
|
||||
self.get_blocked_domains().await
|
||||
}
|
||||
async fn import_remote_outbox(&self, outbox_url: &str, actor_url: &str) -> anyhow::Result<()> {
|
||||
self.import_remote_outbox(outbox_url, actor_url).await
|
||||
}
|
||||
async fn followers_collection_json(
|
||||
&self,
|
||||
user_id: Uuid,
|
||||
page: Option<u32>,
|
||||
) -> anyhow::Result<String> {
|
||||
self.followers_collection_json(user_id, page).await
|
||||
}
|
||||
async fn following_collection_json(
|
||||
&self,
|
||||
user_id: Uuid,
|
||||
page: Option<u32>,
|
||||
) -> anyhow::Result<String> {
|
||||
self.following_collection_json(user_id, page).await
|
||||
}
|
||||
async fn run_backfill_for_follower(
|
||||
&self,
|
||||
owner_user_id: Uuid,
|
||||
follower_inbox_url: String,
|
||||
) -> anyhow::Result<()> {
|
||||
self.run_backfill_for_follower(owner_user_id, follower_inbox_url)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
pub struct NoopActivityPubService;
|
||||
|
||||
#[async_trait]
|
||||
impl ActivityPubPort for NoopActivityPubService {
|
||||
async fn actor_json(&self, _: &str) -> anyhow::Result<String> {
|
||||
Ok(String::new())
|
||||
}
|
||||
async fn follow(&self, _: Uuid, _: &str) -> anyhow::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
async fn unfollow(&self, _: Uuid, _: &str) -> anyhow::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
async fn accept_follower(&self, _: Uuid, _: &str) -> anyhow::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
async fn reject_follower(&self, _: Uuid, _: &str) -> anyhow::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
async fn get_following(&self, _: Uuid) -> anyhow::Result<Vec<RemoteActor>> {
|
||||
Ok(vec![])
|
||||
}
|
||||
async fn remove_follower(&self, _: Uuid, _: &str) -> anyhow::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
async fn block_actor(&self, _: Uuid, _: &str) -> anyhow::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
async fn unblock_actor(&self, _: Uuid, _: &str) -> anyhow::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
async fn get_blocked_actors(&self, _: Uuid) -> anyhow::Result<Vec<RemoteActor>> {
|
||||
Ok(vec![])
|
||||
}
|
||||
async fn add_blocked_domain(&self, _: &str, _: Option<&str>) -> anyhow::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
async fn remove_blocked_domain(&self, _: &str) -> anyhow::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
async fn get_blocked_domains(&self) -> anyhow::Result<Vec<BlockedDomain>> {
|
||||
Ok(vec![])
|
||||
}
|
||||
async fn import_remote_outbox(&self, _: &str, _: &str) -> anyhow::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
async fn followers_collection_json(&self, _: Uuid, _: Option<u32>) -> anyhow::Result<String> {
|
||||
Ok(String::new())
|
||||
}
|
||||
async fn following_collection_json(&self, _: Uuid, _: Option<u32>) -> anyhow::Result<String> {
|
||||
Ok(String::new())
|
||||
}
|
||||
async fn run_backfill_for_follower(&self, _: Uuid, _: String) -> anyhow::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,9 @@ use domain::{
|
||||
events::DomainEvent,
|
||||
models::ReviewSource,
|
||||
ports::{DiaryQuery, EventPublisher, LocalApContentQuery, MovieQuery},
|
||||
value_objects::{Comment, ExternalMetadataId, MovieId, Rating, ReviewId, UserId},
|
||||
value_objects::{
|
||||
Comment, ExternalMetadataId, InstanceIdentity, MovieId, Rating, ReviewId, UserId,
|
||||
},
|
||||
};
|
||||
use k_ap::{AS_PUBLIC, ApContentReader, ApObjectHandler, LocalObject};
|
||||
use url::Url;
|
||||
@@ -20,7 +22,7 @@ pub struct ReviewObjectHandler {
|
||||
pub diary_repo: Arc<dyn DiaryQuery>,
|
||||
pub review_store: Arc<dyn RemoteReviewRepository>,
|
||||
pub event_publisher: Arc<dyn EventPublisher>,
|
||||
pub base_url: String,
|
||||
pub instance: InstanceIdentity,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
@@ -39,17 +41,17 @@ impl ApContentReader for ReviewObjectHandler {
|
||||
.await
|
||||
.map_err(|e| anyhow::anyhow!(e.to_string()))?;
|
||||
|
||||
let actor = actor_url(&self.base_url, user_id);
|
||||
let actor = actor_url(&self.instance, user_id);
|
||||
let mut results = Vec::new();
|
||||
for entry in entries {
|
||||
let review = entry.review();
|
||||
let published =
|
||||
chrono::DateTime::from_naive_utc_and_offset(*review.watched_at(), chrono::Utc);
|
||||
let movie = entry.movie();
|
||||
let ap_id = review_url(&self.base_url, review.id());
|
||||
let ap_id = review_url(&self.instance, review.id());
|
||||
let poster_url = movie
|
||||
.poster_path()
|
||||
.map(|p| format!("{}/images/{}", self.base_url, p.value()));
|
||||
.map(|p| self.instance.image_url_for(p.value()));
|
||||
|
||||
let obj = review_to_ap_object(
|
||||
review,
|
||||
@@ -62,7 +64,7 @@ impl ApContentReader for ReviewObjectHandler {
|
||||
.external_metadata_id()
|
||||
.map(|id| id.value().to_string()),
|
||||
poster_url,
|
||||
base_url: self.base_url.clone(),
|
||||
base_url: self.instance.base_url().to_string(),
|
||||
},
|
||||
);
|
||||
let follower_cc = format!("{}/followers", actor);
|
||||
|
||||
@@ -3,73 +3,33 @@ use std::sync::Arc;
|
||||
use async_trait::async_trait;
|
||||
use domain::{
|
||||
errors::DomainError,
|
||||
ports::{FollowCommand, FollowQuery, SocialCommand, SocialQuery, UserRepository},
|
||||
value_objects::{FollowStatus, FollowTarget, SocialActor, SocialIdentity, UserId, Username},
|
||||
ports::{BlockQuery, FollowGraphQuery, LocalSocial, SocialCommand, UserRepository},
|
||||
value_objects::{
|
||||
FollowRelation, FollowTarget, InstanceIdentity, SocialActor, SocialIdentity, UserId,
|
||||
},
|
||||
};
|
||||
|
||||
use super::ActivityPubPort;
|
||||
use k_ap::ActivityPubService;
|
||||
|
||||
pub struct CompositeSocialAdapter {
|
||||
ap_service: Arc<dyn ActivityPubPort>,
|
||||
local: Arc<dyn LocalSocial>,
|
||||
ap_service: Arc<ActivityPubService>,
|
||||
user_repo: Arc<dyn UserRepository>,
|
||||
follow_command: Arc<dyn FollowCommand>,
|
||||
follow_query: Arc<dyn FollowQuery>,
|
||||
base_url: String,
|
||||
instance: InstanceIdentity,
|
||||
}
|
||||
|
||||
impl CompositeSocialAdapter {
|
||||
pub fn new(
|
||||
ap_service: Arc<dyn ActivityPubPort>,
|
||||
local: Arc<dyn LocalSocial>,
|
||||
ap_service: Arc<ActivityPubService>,
|
||||
user_repo: Arc<dyn UserRepository>,
|
||||
follow_command: Arc<dyn FollowCommand>,
|
||||
follow_query: Arc<dyn FollowQuery>,
|
||||
base_url: String,
|
||||
instance: InstanceIdentity,
|
||||
) -> Self {
|
||||
Self {
|
||||
local,
|
||||
ap_service,
|
||||
user_repo,
|
||||
follow_command,
|
||||
follow_query,
|
||||
base_url,
|
||||
}
|
||||
}
|
||||
|
||||
fn local_actor_url(&self, user_id: &UserId) -> String {
|
||||
format!("{}/users/{}", self.base_url, user_id.value())
|
||||
}
|
||||
|
||||
fn actor_url_from_identity(&self, identity: &SocialIdentity) -> String {
|
||||
match identity {
|
||||
SocialIdentity::Local(uid) => self.local_actor_url(uid),
|
||||
SocialIdentity::Remote { actor_url } => actor_url.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
async fn resolve_target_identity(
|
||||
&self,
|
||||
target: &FollowTarget,
|
||||
) -> Result<SocialIdentity, DomainError> {
|
||||
match target {
|
||||
FollowTarget::Identity(id) => Ok(id.clone()),
|
||||
FollowTarget::Handle(handle) => {
|
||||
let host = handle.rsplit_once('@').map(|(_, h)| h).unwrap_or("");
|
||||
let local_host = SocialIdentity::host_from_base_url(&self.base_url);
|
||||
if host == local_host {
|
||||
let username_str = handle
|
||||
.trim_start_matches('@')
|
||||
.split('@')
|
||||
.next()
|
||||
.unwrap_or("");
|
||||
if let Ok(username) = Username::new(username_str.to_string())
|
||||
&& let Some(user) = self.user_repo.find_by_username(&username).await?
|
||||
{
|
||||
return Ok(SocialIdentity::Local(user.id().clone()));
|
||||
}
|
||||
}
|
||||
Ok(SocialIdentity::Remote {
|
||||
actor_url: handle.clone(),
|
||||
})
|
||||
}
|
||||
instance,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -81,23 +41,10 @@ fn ap_err(e: anyhow::Error) -> DomainError {
|
||||
#[async_trait]
|
||||
impl SocialCommand for CompositeSocialAdapter {
|
||||
async fn follow(&self, follower: &UserId, target: &FollowTarget) -> Result<(), DomainError> {
|
||||
let identity = self.resolve_target_identity(target).await?;
|
||||
let identity = self.local.resolve_target(target).await?;
|
||||
|
||||
if let SocialIdentity::Local(ref target_id) = identity {
|
||||
if follower == target_id {
|
||||
return Err(DomainError::ValidationError(
|
||||
"Cannot follow yourself".into(),
|
||||
));
|
||||
}
|
||||
let follower_url = self.local_actor_url(follower);
|
||||
let target_url = self.local_actor_url(target_id);
|
||||
self.follow_command
|
||||
.add_follower(target_id.value(), &follower_url, FollowStatus::Pending)
|
||||
.await?;
|
||||
self.follow_command
|
||||
.add_follow(follower.value(), &target_url, FollowStatus::Pending)
|
||||
.await?;
|
||||
return Ok(());
|
||||
if let SocialIdentity::Local(_) = identity {
|
||||
return self.local.follow_resolved(follower, &identity).await;
|
||||
}
|
||||
|
||||
let handle = match target {
|
||||
@@ -109,7 +56,7 @@ impl SocialCommand for CompositeSocialAdapter {
|
||||
.find_by_id(uid)
|
||||
.await?
|
||||
.ok_or_else(|| DomainError::NotFound("User not found".into()))?;
|
||||
SocialIdentity::format_local_handle(user.username().value(), &self.base_url)
|
||||
self.instance.handle_for(user.username().value())
|
||||
}
|
||||
SocialIdentity::Remote { actor_url } => actor_url.clone(),
|
||||
},
|
||||
@@ -125,21 +72,11 @@ impl SocialCommand for CompositeSocialAdapter {
|
||||
follower: &UserId,
|
||||
target: &SocialIdentity,
|
||||
) -> Result<(), DomainError> {
|
||||
let actor_url = self.actor_url_from_identity(target);
|
||||
match target {
|
||||
SocialIdentity::Local(target_id) => {
|
||||
let follower_url = self.local_actor_url(follower);
|
||||
self.follow_command
|
||||
.remove_follow(follower.value(), &actor_url)
|
||||
.await?;
|
||||
self.follow_command
|
||||
.remove_follower_record(target_id.value(), &follower_url)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
SocialIdentity::Local(_) => self.local.unfollow(follower, target).await,
|
||||
SocialIdentity::Remote { .. } => self
|
||||
.ap_service
|
||||
.unfollow(follower.value(), &actor_url)
|
||||
.unfollow(follower.value(), &self.instance.actor_url_of(target))
|
||||
.await
|
||||
.map_err(ap_err),
|
||||
}
|
||||
@@ -150,21 +87,11 @@ impl SocialCommand for CompositeSocialAdapter {
|
||||
owner: &UserId,
|
||||
requester: &SocialIdentity,
|
||||
) -> Result<(), DomainError> {
|
||||
let actor_url = self.actor_url_from_identity(requester);
|
||||
match requester {
|
||||
SocialIdentity::Local(requester_id) => {
|
||||
let owner_url = self.local_actor_url(owner);
|
||||
self.follow_command
|
||||
.update_follower_status(owner.value(), &actor_url, FollowStatus::Accepted)
|
||||
.await?;
|
||||
self.follow_command
|
||||
.update_follow_status(requester_id.value(), &owner_url, FollowStatus::Accepted)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
SocialIdentity::Local(_) => self.local.accept_follow(owner, requester).await,
|
||||
SocialIdentity::Remote { .. } => self
|
||||
.ap_service
|
||||
.accept_follower(owner.value(), &actor_url)
|
||||
.accept_follower(owner.value(), &self.instance.actor_url_of(requester))
|
||||
.await
|
||||
.map_err(ap_err),
|
||||
}
|
||||
@@ -175,21 +102,11 @@ impl SocialCommand for CompositeSocialAdapter {
|
||||
owner: &UserId,
|
||||
requester: &SocialIdentity,
|
||||
) -> Result<(), DomainError> {
|
||||
let actor_url = self.actor_url_from_identity(requester);
|
||||
match requester {
|
||||
SocialIdentity::Local(requester_id) => {
|
||||
let owner_url = self.local_actor_url(owner);
|
||||
self.follow_command
|
||||
.update_follower_status(owner.value(), &actor_url, FollowStatus::Rejected)
|
||||
.await?;
|
||||
self.follow_command
|
||||
.remove_follow(requester_id.value(), &owner_url)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
SocialIdentity::Local(_) => self.local.reject_follow(owner, requester).await,
|
||||
SocialIdentity::Remote { .. } => self
|
||||
.ap_service
|
||||
.reject_follower(owner.value(), &actor_url)
|
||||
.reject_follower(owner.value(), &self.instance.actor_url_of(requester))
|
||||
.await
|
||||
.map_err(ap_err),
|
||||
}
|
||||
@@ -200,28 +117,18 @@ impl SocialCommand for CompositeSocialAdapter {
|
||||
owner: &UserId,
|
||||
follower: &SocialIdentity,
|
||||
) -> Result<(), DomainError> {
|
||||
let actor_url = self.actor_url_from_identity(follower);
|
||||
match follower {
|
||||
SocialIdentity::Local(follower_id) => {
|
||||
let owner_url = self.local_actor_url(owner);
|
||||
self.follow_command
|
||||
.remove_follower_record(owner.value(), &actor_url)
|
||||
.await?;
|
||||
self.follow_command
|
||||
.remove_follow(follower_id.value(), &owner_url)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
SocialIdentity::Local(_) => self.local.remove_follower(owner, follower).await,
|
||||
SocialIdentity::Remote { .. } => self
|
||||
.ap_service
|
||||
.remove_follower(owner.value(), &actor_url)
|
||||
.remove_follower(owner.value(), &self.instance.actor_url_of(follower))
|
||||
.await
|
||||
.map_err(ap_err),
|
||||
}
|
||||
}
|
||||
|
||||
async fn block(&self, blocker: &UserId, target: &SocialIdentity) -> Result<(), DomainError> {
|
||||
let actor_url = self.actor_url_from_identity(target);
|
||||
let actor_url = self.instance.actor_url_of(target);
|
||||
self.ap_service
|
||||
.block_actor(blocker.value(), &actor_url)
|
||||
.await
|
||||
@@ -229,7 +136,7 @@ impl SocialCommand for CompositeSocialAdapter {
|
||||
}
|
||||
|
||||
async fn unblock(&self, blocker: &UserId, target: &SocialIdentity) -> Result<(), DomainError> {
|
||||
let actor_url = self.actor_url_from_identity(target);
|
||||
let actor_url = self.instance.actor_url_of(target);
|
||||
self.ap_service
|
||||
.unblock_actor(blocker.value(), &actor_url)
|
||||
.await
|
||||
@@ -238,33 +145,46 @@ impl SocialCommand for CompositeSocialAdapter {
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl SocialQuery for CompositeSocialAdapter {
|
||||
impl FollowGraphQuery for CompositeSocialAdapter {
|
||||
async fn get_following(&self, user: &UserId) -> Result<Vec<SocialActor>, DomainError> {
|
||||
self.follow_query
|
||||
.get_following(user.value(), &self.base_url)
|
||||
.await
|
||||
self.local.get_following(user).await
|
||||
}
|
||||
|
||||
async fn get_followers(&self, user: &UserId) -> Result<Vec<SocialActor>, DomainError> {
|
||||
self.follow_query
|
||||
.get_followers(user.value(), &self.base_url)
|
||||
.await
|
||||
self.local.get_followers(user).await
|
||||
}
|
||||
|
||||
async fn get_pending_followers(&self, user: &UserId) -> Result<Vec<SocialActor>, DomainError> {
|
||||
self.follow_query
|
||||
.get_pending_followers(user.value(), &self.base_url)
|
||||
.await
|
||||
self.local.get_pending_followers(user).await
|
||||
}
|
||||
|
||||
async fn get_pending_following(&self, user: &UserId) -> Result<Vec<SocialActor>, DomainError> {
|
||||
self.local.get_pending_following(user).await
|
||||
}
|
||||
|
||||
async fn count_following(&self, user: &UserId) -> Result<usize, DomainError> {
|
||||
self.follow_query.count_following(user.value()).await
|
||||
self.local.count_following(user).await
|
||||
}
|
||||
|
||||
async fn count_followers(&self, user: &UserId) -> Result<usize, DomainError> {
|
||||
self.follow_query.count_followers(user.value()).await
|
||||
self.local.count_followers(user).await
|
||||
}
|
||||
|
||||
async fn count_pending_followers(&self, user: &UserId) -> Result<usize, DomainError> {
|
||||
self.local.count_pending_followers(user).await
|
||||
}
|
||||
|
||||
async fn get_relation(
|
||||
&self,
|
||||
viewer: &UserId,
|
||||
target: &SocialIdentity,
|
||||
) -> Result<FollowRelation, DomainError> {
|
||||
self.local.get_relation(viewer, target).await
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl BlockQuery for CompositeSocialAdapter {
|
||||
async fn get_blocked(&self, user: &UserId) -> Result<Vec<SocialActor>, DomainError> {
|
||||
let actors = self
|
||||
.ap_service
|
||||
@@ -274,7 +194,7 @@ impl SocialQuery for CompositeSocialAdapter {
|
||||
Ok(actors
|
||||
.into_iter()
|
||||
.map(|a| {
|
||||
let identity = SocialIdentity::from_actor_url(&a.url, &self.base_url);
|
||||
let identity = self.instance.identify(&a.url);
|
||||
SocialActor {
|
||||
identity,
|
||||
handle: a.handle,
|
||||
@@ -284,15 +204,4 @@ impl SocialQuery for CompositeSocialAdapter {
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
async fn is_following(
|
||||
&self,
|
||||
follower: &UserId,
|
||||
target: &SocialIdentity,
|
||||
) -> Result<bool, DomainError> {
|
||||
let actor_url = self.actor_url_from_identity(target);
|
||||
self.follow_query
|
||||
.is_following(follower.value(), &actor_url)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,28 +1,43 @@
|
||||
use domain::value_objects::ReviewId;
|
||||
use domain::value_objects::{InstanceIdentity, ReviewId, UserId};
|
||||
use url::Url;
|
||||
|
||||
/// Builds the canonical actor URL: `{base_url}/users/{user_id}`
|
||||
pub fn actor_url(base_url: &str, user_id: uuid::Uuid) -> Url {
|
||||
Url::parse(&format!("{}/users/{}", base_url, user_id))
|
||||
pub fn actor_url(instance: &InstanceIdentity, user_id: uuid::Uuid) -> Url {
|
||||
Url::parse(&instance.actor_url_for(&UserId::from_uuid(user_id)))
|
||||
.expect("base_url is always a valid URL prefix")
|
||||
}
|
||||
|
||||
/// Builds the canonical review URL: `{base_url}/reviews/{review_id}`
|
||||
pub fn review_url(base_url: &str, review_id: &ReviewId) -> Url {
|
||||
Url::parse(&format!("{}/reviews/{}", base_url, review_id.value()))
|
||||
.expect("base_url is always a valid URL prefix")
|
||||
}
|
||||
|
||||
pub fn goal_url(base_url: &str, user_id: uuid::Uuid, year: u16) -> Url {
|
||||
Url::parse(&format!("{}/users/{}/goals/{}", base_url, user_id, year))
|
||||
.expect("base_url is always a valid URL prefix")
|
||||
}
|
||||
|
||||
/// Builds the canonical watchlist entry URL: `{base_url}/users/{user_id}/watchlist/{movie_id}`
|
||||
pub fn watchlist_entry_url(base_url: &str, user_id: uuid::Uuid, movie_id: uuid::Uuid) -> Url {
|
||||
pub fn review_url(instance: &InstanceIdentity, review_id: &ReviewId) -> Url {
|
||||
Url::parse(&format!(
|
||||
"{}/users/{}/watchlist/{}",
|
||||
base_url, user_id, movie_id
|
||||
"{}/reviews/{}",
|
||||
instance.base_url(),
|
||||
review_id.value()
|
||||
))
|
||||
.expect("base_url is always a valid URL prefix")
|
||||
}
|
||||
|
||||
pub fn goal_url(instance: &InstanceIdentity, user_id: uuid::Uuid, year: u16) -> Url {
|
||||
Url::parse(&format!(
|
||||
"{}/users/{}/goals/{}",
|
||||
instance.base_url(),
|
||||
user_id,
|
||||
year
|
||||
))
|
||||
.expect("base_url is always a valid URL prefix")
|
||||
}
|
||||
|
||||
/// Builds the canonical watchlist entry URL: `{base_url}/users/{user_id}/watchlist/{movie_id}`
|
||||
pub fn watchlist_entry_url(
|
||||
instance: &InstanceIdentity,
|
||||
user_id: uuid::Uuid,
|
||||
movie_id: uuid::Uuid,
|
||||
) -> Url {
|
||||
Url::parse(&format!(
|
||||
"{}/users/{}/watchlist/{}",
|
||||
instance.base_url(),
|
||||
user_id,
|
||||
movie_id
|
||||
))
|
||||
.expect("base_url is always a valid URL prefix")
|
||||
}
|
||||
|
||||
@@ -1,28 +1,36 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use domain::{ports::UserRepository, value_objects::UserId};
|
||||
use domain::{
|
||||
ports::UserRepository,
|
||||
value_objects::{InstanceIdentity, UserId},
|
||||
};
|
||||
use k_ap::{ApProfileField, ApUser, ApUserRepository};
|
||||
use url::Url;
|
||||
|
||||
pub struct DomainUserRepoAdapter {
|
||||
pub repo: Arc<dyn UserRepository>,
|
||||
pub base_url: String,
|
||||
pub instance: InstanceIdentity,
|
||||
}
|
||||
|
||||
impl DomainUserRepoAdapter {
|
||||
pub fn new(repo: Arc<dyn UserRepository>, base_url: String) -> Self {
|
||||
Self { repo, base_url }
|
||||
pub fn new(repo: Arc<dyn UserRepository>, instance: InstanceIdentity) -> Self {
|
||||
Self { repo, instance }
|
||||
}
|
||||
|
||||
fn build_user(&self, u: &domain::models::User) -> ApUser {
|
||||
let avatar_url = u
|
||||
.avatar_path()
|
||||
.and_then(|p| Url::parse(&format!("{}/images/{}", self.base_url, p)).ok());
|
||||
.and_then(|p| Url::parse(&self.instance.image_url_for(p)).ok());
|
||||
let banner_url = u
|
||||
.banner_path()
|
||||
.and_then(|p| Url::parse(&format!("{}/images/{}", self.base_url, p)).ok());
|
||||
let profile_url = Url::parse(&format!("{}/u/{}", self.base_url, u.username().value())).ok();
|
||||
.and_then(|p| Url::parse(&self.instance.image_url_for(p)).ok());
|
||||
let profile_url = Url::parse(&format!(
|
||||
"{}/u/{}",
|
||||
self.instance.base_url(),
|
||||
u.username().value()
|
||||
))
|
||||
.ok();
|
||||
ApUser {
|
||||
id: u.id().value(),
|
||||
username: u.username().value().to_string(),
|
||||
@@ -46,12 +54,8 @@ impl DomainUserRepoAdapter {
|
||||
manually_approves_followers: true,
|
||||
discoverable: true,
|
||||
actor_type: Default::default(),
|
||||
featured_url: Url::parse(&format!(
|
||||
"{}/users/{}/featured",
|
||||
self.base_url,
|
||||
u.id().value()
|
||||
))
|
||||
.ok(),
|
||||
featured_url: Url::parse(&format!("{}/featured", self.instance.actor_url_for(u.id())))
|
||||
.ok(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ use chrono::DateTime;
|
||||
use domain::{
|
||||
models::{RemoteWatchlistEntry, WatchlistWithMovie},
|
||||
ports::{LocalApContentQuery, RemoteWatchlistRepository},
|
||||
value_objects::UserId,
|
||||
value_objects::{InstanceIdentity, UserId},
|
||||
};
|
||||
use k_ap::{AS_PUBLIC, ApContentReader, ApObjectHandler, LocalObject};
|
||||
use url::Url;
|
||||
@@ -16,7 +16,7 @@ use crate::urls::{actor_url, watchlist_entry_url};
|
||||
pub struct WatchlistObjectHandler {
|
||||
pub remote_watchlist_repo: Arc<dyn RemoteWatchlistRepository>,
|
||||
pub content_query: Arc<dyn LocalApContentQuery>,
|
||||
pub base_url: String,
|
||||
pub instance: InstanceIdentity,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
@@ -34,15 +34,15 @@ impl ApContentReader for WatchlistObjectHandler {
|
||||
.await
|
||||
.map_err(|e| anyhow::anyhow!(e.to_string()))?;
|
||||
|
||||
let actor = actor_url(&self.base_url, user_id);
|
||||
let actor = actor_url(&self.instance, user_id);
|
||||
let follower_cc = format!("{}/followers", actor);
|
||||
let mut results = Vec::new();
|
||||
for WatchlistWithMovie { entry, movie } in entries {
|
||||
let ap_id = watchlist_entry_url(&self.base_url, user_id, entry.movie_id.value());
|
||||
let ap_id = watchlist_entry_url(&self.instance, user_id, entry.movie_id.value());
|
||||
let published = DateTime::from_naive_utc_and_offset(entry.added_at, chrono::Utc);
|
||||
let poster_url = movie
|
||||
.poster_path()
|
||||
.map(|p| format!("{}/images/{}", self.base_url, p.value()));
|
||||
.map(|p| self.instance.image_url_for(p.value()));
|
||||
let obj = watchlist_to_ap_object(WatchlistApInput {
|
||||
ap_id: ap_id.clone(),
|
||||
actor_url: actor.clone(),
|
||||
@@ -53,7 +53,7 @@ impl ApContentReader for WatchlistObjectHandler {
|
||||
.map(|id| id.value().to_string()),
|
||||
poster_url,
|
||||
added_at: published,
|
||||
base_url: self.base_url.clone(),
|
||||
base_url: self.instance.base_url().to_string(),
|
||||
});
|
||||
results.push(LocalObject {
|
||||
ap_id,
|
||||
|
||||
Reference in New Issue
Block a user