refactor(ports): ActivityPubRepository takes &str instead of url::Url — infra type stays in adapter

This commit is contained in:
2026-05-15 14:06:33 +02:00
parent c76894e527
commit 3f6b91c943
5 changed files with 69 additions and 85 deletions

View File

@@ -401,14 +401,12 @@ pub trait ActivityPubRepository: Send + Sync {
// ── Remote actor resolution ──────────────────────────────────────
/// Find the local UserId for a remote actor by its AP URL.
async fn find_remote_actor_id(
&self,
actor_ap_url: &url::Url,
) -> Result<Option<UserId>, DomainError>;
async fn find_remote_actor_id(&self, actor_ap_url: &str)
-> Result<Option<UserId>, DomainError>;
/// Ensure a remote actor placeholder exists; create one if absent.
/// Idempotent — safe to call multiple times with the same URL.
async fn intern_remote_actor(&self, actor_ap_url: &url::Url) -> Result<UserId, DomainError>;
async fn intern_remote_actor(&self, actor_ap_url: &str) -> Result<UserId, DomainError>;
/// Update display_name and avatar_url for an already-interned remote actor.
async fn update_remote_actor_display(
@@ -424,29 +422,25 @@ pub trait ActivityPubRepository: Send + Sync {
#[allow(clippy::too_many_arguments)]
async fn accept_note(
&self,
ap_id: &url::Url,
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<&url::Url>,
in_reply_to: Option<&str>,
) -> Result<(), DomainError>;
/// Apply an Update to a previously accepted remote Note.
async fn apply_note_update(
&self,
ap_id: &url::Url,
new_content: &str,
) -> Result<(), DomainError>;
async fn apply_note_update(&self, ap_id: &str, new_content: &str) -> Result<(), DomainError>;
/// Remove a specific remote Note (Delete activity). Only touches
/// remotely-originated thoughts.
async fn retract_note(&self, ap_id: &url::Url) -> Result<(), DomainError>;
async fn retract_note(&self, ap_id: &str) -> Result<(), DomainError>;
/// Remove all Notes from a remote actor (actor-level Delete/Tombstone).
async fn retract_actor_notes(&self, actor_ap_url: &url::Url) -> Result<(), DomainError>;
async fn retract_actor_notes(&self, actor_ap_url: &str) -> Result<(), DomainError>;
// ── Node-level stats ─────────────────────────────────────────────

View File

@@ -21,7 +21,6 @@ use async_trait::async_trait;
use chrono::Utc;
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use url;
#[derive(Default, Clone)]
pub struct TestStore {
@@ -820,24 +819,18 @@ impl ActivityPubRepository for TestStore {
}
async fn find_remote_actor_id(
&self,
actor_ap_url: &url::Url,
actor_ap_url: &str,
) -> Result<Option<UserId>, DomainError> {
Ok(self
.actor_ap_ids
.lock()
.unwrap()
.get(actor_ap_url.as_str())
.cloned())
Ok(self.actor_ap_ids.lock().unwrap().get(actor_ap_url).cloned())
}
async fn intern_remote_actor(&self, actor_ap_url: &url::Url) -> Result<UserId, DomainError> {
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 = actor_ap_url
.path()
.trim_start_matches('/')
.replace('/', "_");
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 = crate::models::user::User {
id: uid.clone(),
username: Username::from_trusted(handle.clone()),
@@ -869,28 +862,24 @@ impl ActivityPubRepository for TestStore {
}
async fn accept_note(
&self,
_ap_id: &url::Url,
_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<&url::Url>,
_in_reply_to: Option<&str>,
) -> Result<(), DomainError> {
Ok(())
}
async fn apply_note_update(
&self,
_ap_id: &url::Url,
_new_content: &str,
) -> Result<(), DomainError> {
async fn apply_note_update(&self, _ap_id: &str, _new_content: &str) -> Result<(), DomainError> {
Ok(())
}
async fn retract_note(&self, _ap_id: &url::Url) -> Result<(), DomainError> {
async fn retract_note(&self, _ap_id: &str) -> Result<(), DomainError> {
Ok(())
}
async fn retract_actor_notes(&self, _actor_ap_url: &url::Url) -> Result<(), DomainError> {
async fn retract_actor_notes(&self, _actor_ap_url: &str) -> Result<(), DomainError> {
Ok(())
}
async fn count_local_notes(&self) -> Result<u64, DomainError> {
@@ -966,9 +955,9 @@ mod ap_repo_tests {
#[tokio::test]
async fn test_store_intern_creates_placeholder() {
let store = TestStore::default();
let url = url::Url::parse("https://example.com/users/alice").unwrap();
let id1 = store.intern_remote_actor(&url).await.unwrap();
let id2 = store.intern_remote_actor(&url).await.unwrap();
let url = "https://example.com/users/alice";
let id1 = store.intern_remote_actor(url).await.unwrap();
let id2 = store.intern_remote_actor(url).await.unwrap();
assert_eq!(id1, id2, "intern must be idempotent");
}
}