feat: v2 rewrite — hexagonal arch, ActivityPub federation, NATS, deployment-ready #1

Merged
GKaszewski merged 334 commits from v2 into master 2026-05-16 09:42:43 +00:00
6 changed files with 155 additions and 95 deletions
Showing only changes of commit 189901b778 - Show all commits

View File

@@ -1626,7 +1626,7 @@ impl domain::ports::FederationSchedulerPort for ActivityPubService {
}
#[async_trait::async_trait]
impl domain::ports::FederationActionPort for ActivityPubService {
impl domain::ports::FederationLookupPort for ActivityPubService {
async fn lookup_actor(
&self,
handle: &str,
@@ -1703,31 +1703,6 @@ impl domain::ports::FederationActionPort for ActivityPubService {
})
}
async fn follow_remote(
&self,
local_user_id: &domain::value_objects::UserId,
handle: &str,
) -> Result<(), domain::errors::DomainError> {
self.follow(local_user_id.as_uuid(), handle)
.await
.map_err(|e| domain::errors::DomainError::ExternalService(e.to_string()))
}
async fn unfollow_remote(
&self,
local_user_id: &domain::value_objects::UserId,
handle: &str,
) -> Result<(), domain::errors::DomainError> {
let data = self.federation_config.to_request_data();
let remote_actor: DbActor = Self::webfinger_https(handle, &data)
.await
.map_err(|e| domain::errors::DomainError::ExternalService(e.to_string()))?;
let actor_url = remote_actor.ap_id.to_string();
self.unfollow(local_user_id.as_uuid(), &actor_url)
.await
.map_err(|e| domain::errors::DomainError::ExternalService(e.to_string()))
}
async fn actor_json(
&self,
user_id: &domain::value_objects::UserId,
@@ -1832,7 +1807,10 @@ impl domain::ports::FederationActionPort for ActivityPubService {
serde_json::to_string(&obj)
.map_err(|e| domain::errors::DomainError::ExternalService(e.to_string()))
}
}
#[async_trait::async_trait]
impl domain::ports::FederationFetchPort for ActivityPubService {
async fn fetch_outbox_page(
&self,
outbox_url: &str,
@@ -2026,7 +2004,48 @@ impl domain::ports::FederationActionPort for ActivityPubService {
})
.collect()
}
}
#[async_trait::async_trait]
impl domain::ports::FederationFollowPort for ActivityPubService {
async fn follow_remote(
&self,
local_user_id: &domain::value_objects::UserId,
handle: &str,
) -> Result<(), domain::errors::DomainError> {
self.follow(local_user_id.as_uuid(), handle)
.await
.map_err(|e| domain::errors::DomainError::ExternalService(e.to_string()))
}
async fn unfollow_remote(
&self,
local_user_id: &domain::value_objects::UserId,
handle: &str,
) -> Result<(), domain::errors::DomainError> {
let data = self.federation_config.to_request_data();
let remote_actor: DbActor = Self::webfinger_https(handle, &data)
.await
.map_err(|e| domain::errors::DomainError::ExternalService(e.to_string()))?;
let actor_url = remote_actor.ap_id.to_string();
self.unfollow(local_user_id.as_uuid(), &actor_url)
.await
.map_err(|e| domain::errors::DomainError::ExternalService(e.to_string()))
}
async fn get_remote_following(
&self,
user_id: &domain::value_objects::UserId,
) -> Result<Vec<domain::models::remote_actor::RemoteActor>, domain::errors::DomainError> {
self.get_following(user_id.as_uuid())
.await
.map(|v| v.into_iter().map(Self::adapter_actor_to_domain).collect())
.map_err(|e| domain::errors::DomainError::ExternalService(e.to_string()))
}
}
#[async_trait::async_trait]
impl domain::ports::FederationFollowRequestPort for ActivityPubService {
async fn get_pending_followers(
&self,
user_id: &domain::value_objects::UserId,
@@ -2076,16 +2095,6 @@ impl domain::ports::FederationActionPort for ActivityPubService {
.await
.map_err(|e| domain::errors::DomainError::ExternalService(e.to_string()))
}
async fn get_remote_following(
&self,
user_id: &domain::value_objects::UserId,
) -> Result<Vec<domain::models::remote_actor::RemoteActor>, domain::errors::DomainError> {
self.get_following(user_id.as_uuid())
.await
.map(|v| v.into_iter().map(Self::adapter_actor_to_domain).collect())
.map_err(|e| domain::errors::DomainError::ExternalService(e.to_string()))
}
}
#[cfg(test)]

View File

@@ -1,10 +1,28 @@
fn _assert_impl_federation_action_port()
fn _assert_impl_federation_lookup_port()
where
crate::service::ActivityPubService: domain::ports::FederationActionPort,
crate::service::ActivityPubService: domain::ports::FederationLookupPort,
{
}
fn _assert_impl_federation_action_port_connections()
fn _assert_impl_federation_follow_port()
where
crate::service::ActivityPubService: domain::ports::FederationFollowPort,
{
}
fn _assert_impl_federation_follow_request_port()
where
crate::service::ActivityPubService: domain::ports::FederationFollowRequestPort,
{
}
fn _assert_impl_federation_fetch_port()
where
crate::service::ActivityPubService: domain::ports::FederationFetchPort,
{
}
fn _assert_impl_federation_action_port()
where
crate::service::ActivityPubService: domain::ports::FederationActionPort,
{

View File

@@ -6,8 +6,9 @@ use domain::{
remote_actor::RemoteActor,
},
ports::{
ActivityPubRepository, EventPublisher, FederationActionPort, FederationSchedulerPort,
FeedRepository, FollowRepository, RemoteActorConnectionRepository, UserReader,
ActivityPubRepository, EventPublisher, FederationActionPort, FederationFollowPort,
FederationFollowRequestPort, FederationSchedulerPort, FeedRepository, FollowRepository,
RemoteActorConnectionRepository, UserReader,
},
value_objects::UserId,
};
@@ -15,14 +16,14 @@ use domain::{
use super::social;
pub async fn list_pending_requests(
federation: &dyn FederationActionPort,
federation: &dyn FederationFollowRequestPort,
user_id: &UserId,
) -> Result<Vec<RemoteActor>, DomainError> {
federation.get_pending_followers(user_id).await
}
pub async fn accept_follow_request(
federation: &dyn FederationActionPort,
federation: &dyn FederationFollowRequestPort,
user_id: &UserId,
actor_url: &str,
) -> Result<(), DomainError> {
@@ -30,7 +31,7 @@ pub async fn accept_follow_request(
}
pub async fn reject_follow_request(
federation: &dyn FederationActionPort,
federation: &dyn FederationFollowRequestPort,
user_id: &UserId,
actor_url: &str,
) -> Result<(), DomainError> {
@@ -38,14 +39,14 @@ pub async fn reject_follow_request(
}
pub async fn list_remote_followers(
federation: &dyn FederationActionPort,
federation: &dyn FederationFollowRequestPort,
user_id: &UserId,
) -> Result<Vec<RemoteActor>, DomainError> {
federation.get_remote_followers(user_id).await
}
pub async fn remove_remote_follower(
federation: &dyn FederationActionPort,
federation: &dyn FederationFollowRequestPort,
user_id: &UserId,
actor_url: &str,
) -> Result<(), DomainError> {
@@ -53,7 +54,7 @@ pub async fn remove_remote_follower(
}
pub async fn list_remote_following(
federation: &dyn FederationActionPort,
federation: &dyn FederationFollowPort,
user_id: &UserId,
) -> Result<Vec<RemoteActor>, DomainError> {
federation.get_remote_following(user_id).await
@@ -62,7 +63,7 @@ pub async fn list_remote_following(
pub async fn remove_remote_following(
follows: &dyn FollowRepository,
users: &dyn UserReader,
federation: &dyn FederationActionPort,
federation: &dyn FederationFollowPort,
events: &dyn EventPublisher,
user_id: &UserId,
handle: &str,

View File

@@ -4,7 +4,7 @@ use domain::{
events::DomainEvent,
models::social::{Block, Boost, Follow, FollowState, Like},
ports::{
BlockRepository, BoostRepository, EventPublisher, FederationActionPort, FollowRepository,
BlockRepository, BoostRepository, EventPublisher, FederationFollowPort, FollowRepository,
LikeRepository, UserReader,
},
value_objects::{BoostId, LikeId, ThoughtId, UserId, Username},
@@ -93,7 +93,7 @@ pub async fn unboost_thought(
pub async fn follow_actor(
follows: &dyn FollowRepository,
users: &dyn UserReader,
federation: &dyn FederationActionPort,
federation: &dyn FederationFollowPort,
events: &dyn EventPublisher,
follower_id: &UserId,
username: &str,
@@ -140,7 +140,7 @@ pub async fn follow_user(
pub async fn unfollow_actor(
follows: &dyn FollowRepository,
users: &dyn UserReader,
federation: &dyn FederationActionPort,
federation: &dyn FederationFollowPort,
events: &dyn EventPublisher,
follower_id: &UserId,
username: &str,

View File

@@ -230,14 +230,35 @@ pub trait RemoteActorConnectionRepository: Send + Sync {
}
#[async_trait]
pub trait FederationActionPort: Send + Sync {
pub trait FederationLookupPort: Send + Sync {
async fn lookup_actor(&self, handle: &str) -> Result<RemoteActor, DomainError>;
async fn actor_json(&self, user_id: &UserId) -> Result<String, DomainError>;
async fn followers_collection_json(
&self,
user_id: &UserId,
page: Option<u32>,
) -> Result<String, DomainError>;
async fn following_collection_json(
&self,
user_id: &UserId,
page: Option<u32>,
) -> Result<String, DomainError>;
}
#[async_trait]
pub trait FederationFollowPort: Send + Sync {
async fn follow_remote(&self, local_user_id: &UserId, handle: &str) -> Result<(), DomainError>;
async fn unfollow_remote(
&self,
local_user_id: &UserId,
handle: &str,
) -> Result<(), DomainError>;
async fn get_remote_following(&self, user_id: &UserId)
-> Result<Vec<RemoteActor>, DomainError>;
}
#[async_trait]
pub trait FederationFollowRequestPort: Send + Sync {
async fn get_pending_followers(
&self,
user_id: &UserId,
@@ -259,36 +280,38 @@ pub trait FederationActionPort: Send + Sync {
user_id: &UserId,
actor_url: &str,
) -> Result<(), DomainError>;
async fn get_remote_following(&self, user_id: &UserId)
-> Result<Vec<RemoteActor>, DomainError>;
async fn actor_json(&self, user_id: &UserId) -> Result<String, DomainError>;
async fn followers_collection_json(
&self,
user_id: &UserId,
page: Option<u32>,
) -> Result<String, DomainError>;
async fn following_collection_json(
&self,
user_id: &UserId,
page: Option<u32>,
) -> Result<String, DomainError>;
}
#[async_trait]
pub trait FederationFetchPort: Send + Sync {
async fn fetch_outbox_page(
&self,
outbox_url: &str,
page: u32,
) -> Result<Vec<crate::models::remote_note::RemoteNote>, DomainError>;
async fn fetch_actor_urls_from_collection(
&self,
collection_url: &str,
) -> Result<Vec<String>, DomainError>;
async fn resolve_actor_profiles(
&self,
urls: Vec<String>,
) -> Vec<crate::models::actor_connection_summary::ActorConnectionSummary>;
}
pub trait FederationActionPort:
FederationLookupPort + FederationFollowPort + FederationFollowRequestPort + FederationFetchPort
{
}
impl<
T: FederationLookupPort
+ FederationFollowPort
+ FederationFollowRequestPort
+ FederationFetchPort,
> FederationActionPort for T
{
}
#[async_trait]
pub trait FeedRepository: Send + Sync {
async fn home_feed(

View File

@@ -555,11 +555,34 @@ impl RemoteActorRepository for TestStore {
}
#[async_trait]
impl FederationActionPort for TestStore {
impl FederationLookupPort for TestStore {
async fn lookup_actor(&self, _handle: &str) -> Result<RemoteActor, DomainError> {
Err(DomainError::NotFound)
}
async fn actor_json(&self, _user_id: &UserId) -> Result<String, DomainError> {
Err(DomainError::NotFound)
}
async fn followers_collection_json(
&self,
_user_id: &UserId,
_page: Option<u32>,
) -> Result<String, DomainError> {
Err(DomainError::NotFound)
}
async fn following_collection_json(
&self,
_user_id: &UserId,
_page: Option<u32>,
) -> Result<String, DomainError> {
Err(DomainError::NotFound)
}
}
#[async_trait]
impl FederationFollowPort for TestStore {
async fn follow_remote(
&self,
_local_user_id: &UserId,
@@ -576,6 +599,16 @@ impl FederationActionPort for TestStore {
Ok(())
}
async fn get_remote_following(
&self,
_user_id: &UserId,
) -> Result<Vec<RemoteActor>, DomainError> {
Ok(vec![])
}
}
#[async_trait]
impl FederationFollowRequestPort for TestStore {
async fn get_pending_followers(
&self,
_user_id: &UserId,
@@ -613,34 +646,10 @@ impl FederationActionPort for TestStore {
) -> Result<(), DomainError> {
Ok(())
}
}
async fn get_remote_following(
&self,
_user_id: &UserId,
) -> Result<Vec<RemoteActor>, DomainError> {
Ok(vec![])
}
async fn actor_json(&self, _user_id: &UserId) -> Result<String, DomainError> {
Err(DomainError::NotFound)
}
async fn followers_collection_json(
&self,
_user_id: &UserId,
_page: Option<u32>,
) -> Result<String, DomainError> {
Err(DomainError::NotFound)
}
async fn following_collection_json(
&self,
_user_id: &UserId,
_page: Option<u32>,
) -> Result<String, DomainError> {
Err(DomainError::NotFound)
}
#[async_trait]
impl FederationFetchPort for TestStore {
async fn fetch_outbox_page(
&self,
_outbox_url: &str,