feat: v2 rewrite — hexagonal arch, ActivityPub federation, NATS, deployment-ready (#1)
Some checks failed
lint / lint (push) Has been cancelled
test / unit (push) Has been cancelled
test / integration (push) Has been cancelled

This commit was merged in pull request #1.
This commit is contained in:
2026-05-16 09:42:40 +00:00
parent 071809bc3f
commit 9aee4ceb6d
224 changed files with 35418 additions and 1469 deletions

View File

@@ -0,0 +1,150 @@
/// Test helpers for application-layer tests that need activitypub_base traits.
use activitypub_base::{ActivityPubRepository, ActorApUrls, OutboxEntry};
use async_trait::async_trait;
use domain::{
errors::DomainError,
models::user::User,
testing::TestStore,
value_objects::{Email, PasswordHash, ThoughtId, UserId, Username},
};
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
/// Extends `TestStore` with AP-specific lookup maps.
#[derive(Default, Clone)]
pub struct TestApRepo {
pub inner: TestStore,
/// UserId → ActorApUrls (for get_actor_ap_urls)
pub actor_ap_urls: Arc<Mutex<HashMap<UserId, ActorApUrls>>>,
}
impl TestApRepo {
pub fn new(inner: TestStore) -> Self {
Self {
inner,
actor_ap_urls: Default::default(),
}
}
}
#[async_trait]
impl ActivityPubRepository for TestApRepo {
async fn outbox_entries_for_actor(
&self,
_uid: &UserId,
) -> Result<Vec<OutboxEntry>, DomainError> {
Ok(vec![])
}
async fn outbox_page_for_actor(
&self,
_uid: &UserId,
_before: Option<chrono::DateTime<chrono::Utc>>,
_limit: usize,
) -> Result<Vec<OutboxEntry>, DomainError> {
Ok(vec![])
}
async fn find_remote_actor_id(
&self,
actor_ap_url: &str,
) -> Result<Option<UserId>, DomainError> {
Ok(self
.inner
.actor_ap_ids
.lock()
.unwrap()
.get(actor_ap_url)
.cloned())
}
async fn intern_remote_actor(&self, actor_ap_url: &str) -> Result<UserId, DomainError> {
if let Some(uid) = self.find_remote_actor_id(actor_ap_url).await? {
return Ok(uid);
}
let uid = UserId::new();
let handle = url::Url::parse(actor_ap_url)
.map(|u| u.path().trim_start_matches('/').replace('/', "_"))
.unwrap_or_else(|_| format!("remote_{}", &uid.to_string()[..8]));
let user = User {
id: uid.clone(),
username: Username::from_trusted(handle),
email: Email::from_trusted(format!("{}@remote", uid)),
password_hash: PasswordHash("".into()),
display_name: None,
bio: None,
avatar_url: None,
header_url: None,
custom_css: None,
local: false,
created_at: chrono::Utc::now(),
updated_at: chrono::Utc::now(),
};
self.inner.users.lock().unwrap().push(user);
self.inner
.actor_ap_ids
.lock()
.unwrap()
.insert(actor_ap_url.to_string(), uid.clone());
Ok(uid)
}
async fn update_remote_actor_display(
&self,
_user_id: &UserId,
_display_name: Option<&str>,
_avatar_url: Option<&str>,
) -> Result<(), DomainError> {
Ok(())
}
async fn accept_note(
&self,
_ap_id: &str,
_author_id: &UserId,
_content: &str,
_published: chrono::DateTime<chrono::Utc>,
_sensitive: bool,
_content_warning: Option<String>,
_visibility: &str,
_in_reply_to: Option<&str>,
) -> Result<ThoughtId, DomainError> {
Ok(ThoughtId::from_uuid(uuid::Uuid::new_v4()))
}
async fn apply_note_update(
&self,
_ap_id: &str,
_new_content: &str,
) -> Result<(), DomainError> {
Ok(())
}
async fn retract_note(&self, _ap_id: &str) -> Result<(), DomainError> {
Ok(())
}
async fn retract_actor_notes(&self, _actor_ap_url: &str) -> Result<(), DomainError> {
Ok(())
}
async fn count_local_notes(&self) -> Result<u64, DomainError> {
Ok(self
.inner
.thoughts
.lock()
.unwrap()
.iter()
.filter(|t| t.local)
.count() as u64)
}
async fn get_thought_ap_id(
&self,
thought_id: &ThoughtId,
) -> Result<Option<String>, DomainError> {
Ok(self
.inner
.thought_ap_ids
.lock()
.unwrap()
.get(thought_id)
.cloned())
}
async fn get_actor_ap_urls(
&self,
user_id: &UserId,
) -> Result<Option<ActorApUrls>, DomainError> {
Ok(self.actor_ap_urls.lock().unwrap().get(user_id).cloned())
}
}