diff --git a/crates/adapters/activitypub-base/src/service.rs b/crates/adapters/activitypub-base/src/service.rs index 52ec70a..8d07d3e 100644 --- a/crates/adapters/activitypub-base/src/service.rs +++ b/crates/adapters/activitypub-base/src/service.rs @@ -74,6 +74,7 @@ fn thought_note_json( thought: &domain::models::thought::Thought, local_actor: &crate::actors::DbActor, base_url: &str, + in_reply_to_url: Option<&str>, ) -> anyhow::Result<(url::Url, serde_json::Value)> { let ap_id = url::Url::parse(&format!("{}/thoughts/{}", base_url, thought.id))?; @@ -107,7 +108,7 @@ fn thought_note_json( if let Some(ref cw) = thought.content_warning { note["summary"] = serde_json::json!(cw); } - if let Some(ref reply_url) = thought.in_reply_to_url { + if let Some(reply_url) = in_reply_to_url { note["inReplyTo"] = serde_json::json!(reply_url); } if let Some(updated_at) = thought.updated_at { @@ -1415,6 +1416,7 @@ impl domain::ports::OutboundFederationPort for ActivityPubService { author_user_id: &domain::value_objects::UserId, thought: &domain::models::thought::Thought, _author_username: &str, + in_reply_to_url: Option<&str>, ) -> Result<(), domain::errors::DomainError> { let user_uuid = author_user_id.as_uuid(); let data = self.federation_config.to_request_data(); @@ -1426,8 +1428,9 @@ impl domain::ports::OutboundFederationPort for ActivityPubService { return Ok(()); }; - let (ap_id, note) = thought_note_json(thought, &local_actor, &self.base_url) - .map_err(|e| domain::errors::DomainError::Internal(e.to_string()))?; + let (ap_id, note) = + thought_note_json(thought, &local_actor, &self.base_url, in_reply_to_url) + .map_err(|e| domain::errors::DomainError::Internal(e.to_string()))?; let create = crate::activities::CreateActivity { id: ap_id, @@ -1476,6 +1479,7 @@ impl domain::ports::OutboundFederationPort for ActivityPubService { author_user_id: &domain::value_objects::UserId, thought: &domain::models::thought::Thought, _author_username: &str, + in_reply_to_url: Option<&str>, ) -> Result<(), domain::errors::DomainError> { let user_uuid = author_user_id.as_uuid(); let data = self.federation_config.to_request_data(); @@ -1487,8 +1491,9 @@ impl domain::ports::OutboundFederationPort for ActivityPubService { return Ok(()); }; - let (_ap_id, note) = thought_note_json(thought, &local_actor, &self.base_url) - .map_err(|e| domain::errors::DomainError::Internal(e.to_string()))?; + let (_ap_id, note) = + thought_note_json(thought, &local_actor, &self.base_url, in_reply_to_url) + .map_err(|e| domain::errors::DomainError::Internal(e.to_string()))?; let update_id = url::Url::parse(&format!( "{}/activities/update/{}", diff --git a/crates/adapters/postgres-search/src/lib.rs b/crates/adapters/postgres-search/src/lib.rs index dd67998..0faec17 100644 --- a/crates/adapters/postgres-search/src/lib.rs +++ b/crates/adapters/postgres-search/src/lib.rs @@ -29,8 +29,6 @@ struct FeedRow { t_user_id: uuid::Uuid, content: String, in_reply_to_id: Option, - in_reply_to_url: Option, - t_ap_id: Option, visibility: String, content_warning: Option, sensitive: bool, @@ -47,8 +45,6 @@ struct FeedRow { header_url: Option, custom_css: Option, author_local: bool, - u_ap_id: Option, - inbox_url: Option, author_created_at: DateTime, author_updated_at: DateTime, like_count: i64, @@ -59,12 +55,12 @@ struct FeedRow { const FEED_SELECT: &str = " SELECT t.id AS thought_id, t.user_id AS t_user_id, t.content, - t.in_reply_to_id, t.in_reply_to_url, t.ap_id AS t_ap_id, + t.in_reply_to_id, t.visibility, t.content_warning, t.sensitive, t.local AS t_local, t.created_at AS thought_created_at, t.updated_at, u.id AS author_id, u.username, u.email, u.password_hash, u.display_name, u.bio, u.avatar_url, u.header_url, u.custom_css, - u.local AS author_local, u.ap_id AS u_ap_id, u.inbox_url, + u.local AS author_local, u.created_at AS author_created_at, u.updated_at AS author_updated_at, (SELECT COUNT(*) FROM likes l WHERE l.thought_id=t.id) AS like_count, (SELECT COUNT(*) FROM boosts b WHERE b.thought_id=t.id) AS boost_count, @@ -77,8 +73,6 @@ fn row_to_entry(r: FeedRow) -> FeedEntry { user_id: UserId::from_uuid(r.t_user_id), content: Content::new_remote(r.content), in_reply_to_id: r.in_reply_to_id.map(ThoughtId::from_uuid), - in_reply_to_url: r.in_reply_to_url, - ap_id: r.t_ap_id, visibility: Visibility::from_db_str(&r.visibility), content_warning: r.content_warning, sensitive: r.sensitive, @@ -97,8 +91,6 @@ fn row_to_entry(r: FeedRow) -> FeedEntry { header_url: r.header_url, custom_css: r.custom_css, local: r.author_local, - ap_id: r.u_ap_id, - inbox_url: r.inbox_url, created_at: r.author_created_at, updated_at: r.author_updated_at, }; diff --git a/crates/adapters/postgres/src/activitypub.rs b/crates/adapters/postgres/src/activitypub.rs index b51a184..2276ee4 100644 --- a/crates/adapters/postgres/src/activitypub.rs +++ b/crates/adapters/postgres/src/activitypub.rs @@ -60,8 +60,6 @@ impl ActivityPubRepository for PgActivityPubRepository { user_id: UserId::from_uuid(r.user_id), content: Content::new_remote(r.content), in_reply_to_id: r.in_reply_to_id.map(ThoughtId::from_uuid), - in_reply_to_url: None, - ap_id: None, visibility: Visibility::Public, content_warning: r.content_warning, sensitive: r.sensitive, @@ -127,8 +125,6 @@ impl ActivityPubRepository for PgActivityPubRepository { user_id: UserId::from_uuid(r.user_id), content: Content::new_remote(r.content), in_reply_to_id: r.in_reply_to_id.map(ThoughtId::from_uuid), - in_reply_to_url: None, - ap_id: None, visibility: Visibility::Public, content_warning: r.content_warning, sensitive: r.sensitive, diff --git a/crates/adapters/postgres/src/feed.rs b/crates/adapters/postgres/src/feed.rs index 14c0f0d..9519194 100644 --- a/crates/adapters/postgres/src/feed.rs +++ b/crates/adapters/postgres/src/feed.rs @@ -29,8 +29,6 @@ struct FeedRow { t_user_id: uuid::Uuid, content: String, in_reply_to_id: Option, - in_reply_to_url: Option, - t_ap_id: Option, visibility: String, content_warning: Option, sensitive: bool, @@ -47,8 +45,6 @@ struct FeedRow { header_url: Option, custom_css: Option, author_local: bool, - u_ap_id: Option, - inbox_url: Option, author_created_at: DateTime, author_updated_at: DateTime, like_count: i64, @@ -83,7 +79,7 @@ fn feed_select(viewer: Option) -> String { " SELECT t.id AS thought_id, t.user_id AS t_user_id, t.content, - t.in_reply_to_id, t.in_reply_to_url, t.ap_id AS t_ap_id, + t.in_reply_to_id, t.visibility, t.content_warning, t.sensitive, t.local AS t_local, t.created_at AS thought_created_at, t.updated_at, u.id AS author_id, @@ -98,7 +94,7 @@ fn feed_select(viewer: Option) -> String { u.bio, COALESCE(ra.avatar_url, u.avatar_url) AS avatar_url, u.header_url, u.custom_css, - u.local AS author_local, u.ap_id AS u_ap_id, u.inbox_url, + u.local AS author_local, u.created_at AS author_created_at, u.updated_at AS author_updated_at, (SELECT COUNT(*) FROM likes l WHERE l.thought_id=t.id) AS like_count, (SELECT COUNT(*) FROM boosts b WHERE b.thought_id=t.id) AS boost_count, @@ -116,8 +112,6 @@ fn row_to_entry(r: FeedRow) -> FeedEntry { user_id: UserId::from_uuid(r.t_user_id), content: Content::new_remote(r.content), in_reply_to_id: r.in_reply_to_id.map(ThoughtId::from_uuid), - in_reply_to_url: r.in_reply_to_url, - ap_id: r.t_ap_id, visibility: Visibility::from_db_str(&r.visibility), content_warning: r.content_warning, sensitive: r.sensitive, @@ -136,8 +130,6 @@ fn row_to_entry(r: FeedRow) -> FeedEntry { header_url: r.header_url, custom_css: r.custom_css, local: r.author_local, - ap_id: r.u_ap_id, - inbox_url: r.inbox_url, created_at: r.author_created_at, updated_at: r.author_updated_at, }; diff --git a/crates/adapters/postgres/src/thought.rs b/crates/adapters/postgres/src/thought.rs index ed193d8..be78c7b 100644 --- a/crates/adapters/postgres/src/thought.rs +++ b/crates/adapters/postgres/src/thought.rs @@ -28,8 +28,6 @@ pub(crate) struct ThoughtRow { pub user_id: uuid::Uuid, pub content: String, pub in_reply_to_id: Option, - pub in_reply_to_url: Option, - pub ap_id: Option, pub visibility: String, pub content_warning: Option, pub sensitive: bool, @@ -45,8 +43,6 @@ impl From for Thought { user_id: UserId::from_uuid(r.user_id), content: Content::new_remote(r.content), in_reply_to_id: r.in_reply_to_id.map(ThoughtId::from_uuid), - in_reply_to_url: r.in_reply_to_url, - ap_id: r.ap_id, visibility: Visibility::from_db_str(&r.visibility), content_warning: r.content_warning, sensitive: r.sensitive, @@ -58,22 +54,20 @@ impl From for Thought { } const THOUGHT_SELECT: &str = - "SELECT id,user_id,content,in_reply_to_id,in_reply_to_url,ap_id,visibility,content_warning,sensitive,local,created_at,updated_at FROM thoughts"; + "SELECT id,user_id,content,in_reply_to_id,visibility,content_warning,sensitive,local,created_at,updated_at FROM thoughts"; #[async_trait] impl ThoughtRepository for PgThoughtRepository { async fn save(&self, t: &Thought) -> Result<(), DomainError> { sqlx::query( - "INSERT INTO thoughts(id,user_id,content,in_reply_to_id,in_reply_to_url,ap_id,visibility,content_warning,sensitive,local,created_at) - VALUES($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11) + "INSERT INTO thoughts(id,user_id,content,in_reply_to_id,visibility,content_warning,sensitive,local,created_at) + VALUES($1,$2,$3,$4,$5,$6,$7,$8,$9) ON CONFLICT(id) DO UPDATE SET content=EXCLUDED.content,updated_at=NOW()" ) .bind(t.id.as_uuid()) .bind(t.user_id.as_uuid()) .bind(t.content.as_str()) .bind(t.in_reply_to_id.as_ref().map(|x| x.as_uuid())) - .bind(&t.in_reply_to_url) - .bind(&t.ap_id) .bind(t.visibility.as_str()) .bind(&t.content_warning) .bind(t.sensitive) @@ -121,11 +115,11 @@ impl ThoughtRepository for PgThoughtRepository { // Recursive CTE: fetches the root thought and all nested replies at any depth. sqlx::query_as::<_, ThoughtRow>( "WITH RECURSIVE thread AS ( - SELECT id,user_id,content,in_reply_to_id,in_reply_to_url,ap_id, + SELECT id,user_id,content,in_reply_to_id, visibility,content_warning,sensitive,local,created_at,updated_at FROM thoughts WHERE id = $1 UNION ALL - SELECT t.id,t.user_id,t.content,t.in_reply_to_id,t.in_reply_to_url,t.ap_id, + SELECT t.id,t.user_id,t.content,t.in_reply_to_id, t.visibility,t.content_warning,t.sensitive,t.local,t.created_at,t.updated_at FROM thoughts t JOIN thread ON t.in_reply_to_id = thread.id ) diff --git a/crates/adapters/postgres/src/top_friend.rs b/crates/adapters/postgres/src/top_friend.rs index 00c1356..a580c9e 100644 --- a/crates/adapters/postgres/src/top_friend.rs +++ b/crates/adapters/postgres/src/top_friend.rs @@ -58,15 +58,13 @@ impl TopFriendRepository for PgTopFriendRepository { header_url: Option, custom_css: Option, local: bool, - ap_id: Option, - inbox_url: Option, created_at: chrono::DateTime, updated_at: chrono::DateTime, } let rows = sqlx::query_as::<_, Row>( "SELECT tf.user_id AS tf_user_id, tf.friend_id, tf.position, u.id, u.username, u.email, u.password_hash, u.display_name, u.bio, - u.avatar_url, u.header_url, u.custom_css, u.local, u.ap_id, u.inbox_url, + u.avatar_url, u.header_url, u.custom_css, u.local, u.created_at, u.updated_at FROM top_friends tf JOIN users u ON u.id=tf.friend_id WHERE tf.user_id=$1 ORDER BY tf.position", @@ -96,8 +94,6 @@ impl TopFriendRepository for PgTopFriendRepository { header_url: r.header_url, custom_css: r.custom_css, local: r.local, - ap_id: r.ap_id, - inbox_url: r.inbox_url, created_at: r.created_at, updated_at: r.updated_at, }; diff --git a/crates/adapters/postgres/src/user.rs b/crates/adapters/postgres/src/user.rs index 85b1ed2..3d5bf93 100644 --- a/crates/adapters/postgres/src/user.rs +++ b/crates/adapters/postgres/src/user.rs @@ -30,8 +30,6 @@ pub struct UserRow { pub header_url: Option, pub custom_css: Option, pub local: bool, - pub ap_id: Option, - pub inbox_url: Option, pub created_at: DateTime, pub updated_at: DateTime, } @@ -49,15 +47,15 @@ impl From for User { header_url: r.header_url, custom_css: r.custom_css, local: r.local, - ap_id: r.ap_id, - inbox_url: r.inbox_url, created_at: r.created_at, updated_at: r.updated_at, } } } -pub const USER_SELECT: &str = "SELECT id,username,email,password_hash,display_name,bio,avatar_url,header_url,custom_css,local,ap_id,inbox_url,created_at,updated_at FROM users"; +pub const USER_SELECT: &str = + "SELECT id,username,email,password_hash,display_name,bio,avatar_url,header_url,\ + custom_css,local,created_at,updated_at FROM users"; #[async_trait] impl UserRepository for PgUserRepository { @@ -90,14 +88,14 @@ impl UserRepository for PgUserRepository { async fn save(&self, user: &User) -> Result<(), DomainError> { sqlx::query( - "INSERT INTO users (id,username,email,password_hash,display_name,bio,avatar_url,header_url,custom_css,local,ap_id,inbox_url,created_at,updated_at) - VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14) + "INSERT INTO users (id,username,email,password_hash,display_name,bio,avatar_url,header_url,custom_css,local,created_at,updated_at) + VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12) ON CONFLICT(id) DO UPDATE SET username=EXCLUDED.username, email=EXCLUDED.email, password_hash=EXCLUDED.password_hash, display_name=EXCLUDED.display_name, bio=EXCLUDED.bio, avatar_url=EXCLUDED.avatar_url, header_url=EXCLUDED.header_url, custom_css=EXCLUDED.custom_css, - local=EXCLUDED.local, ap_id=EXCLUDED.ap_id, inbox_url=EXCLUDED.inbox_url, + local=EXCLUDED.local, updated_at=NOW()" ) .bind(user.id.as_uuid()) @@ -110,8 +108,6 @@ impl UserRepository for PgUserRepository { .bind(&user.header_url) .bind(&user.custom_css) .bind(user.local) - .bind(&user.ap_id) - .bind(&user.inbox_url) .bind(user.created_at) .bind(user.updated_at) .execute(&self.pool) diff --git a/crates/application/src/services/federation_event.rs b/crates/application/src/services/federation_event.rs index 5fa432f..2b7d347 100644 --- a/crates/application/src/services/federation_event.rs +++ b/crates/application/src/services/federation_event.rs @@ -1,7 +1,7 @@ use domain::{ errors::DomainError, events::DomainEvent, - models::thought::{Thought, Visibility}, + models::thought::Visibility, ports::{ActivityPubRepository, OutboundFederationPort, ThoughtRepository, UserRepository}, value_objects::ThoughtId, }; @@ -18,11 +18,11 @@ pub struct FederationEventService { } impl FederationEventService { - fn object_ap_id(&self, thought: &Thought, thought_id: &ThoughtId) -> String { - thought - .ap_id - .clone() - .unwrap_or_else(|| format!("{}/thoughts/{}", self.base_url, thought_id)) + async fn object_ap_id(&self, thought_id: &ThoughtId) -> Result { + if let Some(ap_id) = self.ap_repo.get_thought_ap_id(thought_id).await? { + return Ok(ap_id); + } + Ok(format!("{}/thoughts/{}", self.base_url, thought_id)) } pub async fn process(&self, event: &DomainEvent) -> Result<(), DomainError> { @@ -48,31 +48,24 @@ impl FederationEventService { Some(u) => u, None => return Ok(()), }; - // For replies to remote posts: in_reply_to_url is None but in_reply_to_id - // points to the locally-stored remote thought. Resolve its ap_id so the - // outbound Note includes inReplyTo and Mastodon threads it correctly. - let thought = if thought.in_reply_to_url.is_none() { - if let Some(ref reply_id) = thought.in_reply_to_id { - match self.thoughts.find_by_id(reply_id).await? { - Some(parent) => { - let parent_ap_url = parent.ap_id.unwrap_or_else(|| { - format!("{}/thoughts/{}", self.base_url, reply_id) - }); - domain::models::thought::Thought { - in_reply_to_url: Some(parent_ap_url), - ..thought - } - } - None => thought, - } - } else { - thought - } + // Resolve in_reply_to_url for the parent thought via AP repo. + let in_reply_to_url = if let Some(ref reply_id) = thought.in_reply_to_id { + let ap_id = self + .ap_repo + .get_thought_ap_id(reply_id) + .await? + .unwrap_or_else(|| format!("{}/thoughts/{}", self.base_url, reply_id)); + Some(ap_id) } else { - thought + None }; self.ap - .broadcast_create(user_id, &thought, user.username.as_str()) + .broadcast_create( + user_id, + &thought, + user.username.as_str(), + in_reply_to_url.as_deref(), + ) .await } @@ -106,8 +99,21 @@ impl FederationEventService { Some(u) => u, None => return Ok(()), }; + let in_reply_to_url = if let Some(ref reply_id) = thought.in_reply_to_id { + self.ap_repo + .get_thought_ap_id(reply_id) + .await? + .or_else(|| Some(format!("{}/thoughts/{}", self.base_url, reply_id))) + } else { + None + }; self.ap - .broadcast_update(user_id, &thought, user.username.as_str()) + .broadcast_update( + user_id, + &thought, + user.username.as_str(), + in_reply_to_url.as_deref(), + ) .await } @@ -122,11 +128,10 @@ impl FederationEventService { _ => return Ok(()), }; let _ = booster; - let thought = match self.thoughts.find_by_id(thought_id).await? { - Some(t) => t, - None => return Ok(()), - }; - let object_ap_id = self.object_ap_id(&thought, thought_id); + if self.thoughts.find_by_id(thought_id).await?.is_none() { + return Ok(()); + } + let object_ap_id = self.object_ap_id(thought_id).await?; self.ap.broadcast_announce(user_id, &object_ap_id).await } @@ -134,11 +139,10 @@ impl FederationEventService { user_id, thought_id, } => { - let thought = match self.thoughts.find_by_id(thought_id).await? { - Some(t) => t, - None => return Ok(()), - }; - let object_ap_id = self.object_ap_id(&thought, thought_id); + if self.thoughts.find_by_id(thought_id).await?.is_none() { + return Ok(()); + } + let object_ap_id = self.object_ap_id(thought_id).await?; self.ap .broadcast_undo_announce(user_id, &object_ap_id) .await @@ -262,17 +266,19 @@ impl FederationEventService { }; let _ = liker; let thought = match self.thoughts.find_by_id(thought_id).await? { - Some(t) if t.ap_id.is_some() => t, + Some(t) => t, _ => return Ok(()), }; - let author = match self.users.find_by_id(&thought.user_id).await? { - Some(u) if u.inbox_url.is_some() => u, - _ => return Ok(()), + let thought_ap_id = match self.ap_repo.get_thought_ap_id(thought_id).await? { + Some(id) => id, + None => return Ok(()), // local thought — no federation needed + }; + let actor_urls = match self.ap_repo.get_actor_ap_urls(&thought.user_id).await? { + Some(u) => u, + None => return Ok(()), }; - let object_ap_id = thought.ap_id.unwrap(); - let inbox_url = author.inbox_url.unwrap(); self.ap - .broadcast_like(user_id, &object_ap_id, &inbox_url) + .broadcast_like(user_id, &thought_ap_id, &actor_urls.inbox_url) .await } @@ -286,17 +292,19 @@ impl FederationEventService { }; let _ = liker; let thought = match self.thoughts.find_by_id(thought_id).await? { - Some(t) if t.ap_id.is_some() => t, + Some(t) => t, _ => return Ok(()), }; - let author = match self.users.find_by_id(&thought.user_id).await? { - Some(u) if u.inbox_url.is_some() => u, - _ => return Ok(()), + let thought_ap_id = match self.ap_repo.get_thought_ap_id(thought_id).await? { + Some(id) => id, + None => return Ok(()), + }; + let actor_urls = match self.ap_repo.get_actor_ap_urls(&thought.user_id).await? { + Some(u) => u, + None => return Ok(()), }; - let object_ap_id = thought.ap_id.unwrap(); - let inbox_url = author.inbox_url.unwrap(); self.ap - .broadcast_undo_like(user_id, &object_ap_id, &inbox_url) + .broadcast_undo_like(user_id, &thought_ap_id, &actor_urls.inbox_url) .await } @@ -345,6 +353,7 @@ mod tests { _: &UserId, thought: &Thought, _: &str, + _in_reply_to_url: Option<&str>, ) -> Result<(), DomainError> { self.created.lock().unwrap().push(thought.id.clone()); Ok(()) @@ -358,6 +367,7 @@ mod tests { _: &UserId, thought: &Thought, _: &str, + _in_reply_to_url: Option<&str>, ) -> Result<(), DomainError> { self.updated.lock().unwrap().push(thought.id.clone()); Ok(()) @@ -460,10 +470,9 @@ mod tests { async fn remote_thought_created_does_not_broadcast() { let store = TestStore::default(); let alice = alice(); - // Remote thought: local = false, ap_id = Some(...) + // Remote thought: local = false let mut thought = local_thought(alice.id.clone()); thought.local = false; - thought.ap_id = Some("https://remote.example/notes/1".into()); store.users.lock().unwrap().push(alice.clone()); store.thoughts.lock().unwrap().push(thought.clone()); @@ -553,7 +562,10 @@ mod tests { let alice = alice(); let mut thought = local_thought(alice.id.clone()); thought.local = false; - thought.ap_id = Some("https://mastodon.social/users/bob/statuses/123".into()); + store.thought_ap_ids.lock().unwrap().insert( + thought.id.clone(), + "https://mastodon.social/users/bob/statuses/123".into(), + ); store.users.lock().unwrap().push(alice.clone()); store.thoughts.lock().unwrap().push(thought.clone()); @@ -702,7 +714,10 @@ mod tests { let alice = alice(); let mut thought = local_thought(alice.id.clone()); thought.local = false; - thought.ap_id = Some("https://mastodon.social/users/bob/statuses/456".into()); + store.thought_ap_ids.lock().unwrap().insert( + thought.id.clone(), + "https://mastodon.social/users/bob/statuses/456".into(), + ); store.thoughts.lock().unwrap().push(thought.clone()); let spy = Arc::new(SpyPort::default()); @@ -797,10 +812,19 @@ mod tests { PasswordHash("h".into()), ); author.local = false; - author.inbox_url = Some("https://mastodon.social/users/author/inbox".into()); + store.actor_ap_urls.lock().unwrap().insert( + author.id.clone(), + domain::ports::ActorApUrls { + ap_id: "https://mastodon.social/users/author".into(), + inbox_url: "https://mastodon.social/users/author/inbox".into(), + }, + ); - let mut thought = local_thought(author.id.clone()); - thought.ap_id = Some("https://mastodon.social/posts/123".into()); + let thought = local_thought(author.id.clone()); + store.thought_ap_ids.lock().unwrap().insert( + thought.id.clone(), + "https://mastodon.social/posts/123".into(), + ); let liker = alice(); diff --git a/crates/domain/src/models/thought.rs b/crates/domain/src/models/thought.rs index d9980f7..2e99773 100644 --- a/crates/domain/src/models/thought.rs +++ b/crates/domain/src/models/thought.rs @@ -15,8 +15,6 @@ pub struct Thought { pub user_id: UserId, pub content: Content, pub in_reply_to_id: Option, - pub in_reply_to_url: Option, - pub ap_id: Option, pub visibility: Visibility, pub content_warning: Option, pub sensitive: bool, @@ -60,8 +58,6 @@ impl Thought { user_id, content, in_reply_to_id, - in_reply_to_url: None, - ap_id: None, visibility, content_warning, sensitive, diff --git a/crates/domain/src/models/user.rs b/crates/domain/src/models/user.rs index f5577c2..4e61f3c 100644 --- a/crates/domain/src/models/user.rs +++ b/crates/domain/src/models/user.rs @@ -13,8 +13,6 @@ pub struct User { pub header_url: Option, pub custom_css: Option, pub local: bool, - pub ap_id: Option, - pub inbox_url: Option, pub created_at: DateTime, pub updated_at: DateTime, } @@ -38,8 +36,6 @@ impl User { header_url: None, custom_css: None, local: true, - ap_id: None, - inbox_url: None, created_at: now, updated_at: now, } diff --git a/crates/domain/src/ports.rs b/crates/domain/src/ports.rs index 93147b7..a3a4e95 100644 --- a/crates/domain/src/ports.rs +++ b/crates/domain/src/ports.rs @@ -442,6 +442,7 @@ pub trait OutboundFederationPort: Send + Sync { author_user_id: &UserId, thought: &Thought, author_username: &str, + in_reply_to_url: Option<&str>, ) -> Result<(), DomainError>; /// Fan out a Delete tombstone for a now-deleted local Note. @@ -459,6 +460,7 @@ pub trait OutboundFederationPort: Send + Sync { author_user_id: &UserId, thought: &Thought, author_username: &str, + in_reply_to_url: Option<&str>, ) -> Result<(), DomainError>; /// Fan out an Announce(object_ap_id) for a boost. diff --git a/crates/domain/src/testing.rs b/crates/domain/src/testing.rs index 8fbce63..2c2bfb2 100644 --- a/crates/domain/src/testing.rs +++ b/crates/domain/src/testing.rs @@ -19,6 +19,7 @@ use crate::{ }; use async_trait::async_trait; use chrono::Utc; +use std::collections::HashMap; use std::sync::{Arc, Mutex}; use url; @@ -35,6 +36,12 @@ pub struct TestStore { pub top_friends: Arc>>, pub notifications: Arc>>, pub events: Arc>>, + /// AP URL → UserId for remote actors (used by find_remote_actor_id / intern_remote_actor) + pub actor_ap_ids: Arc>>, + /// ThoughtId → AP object URL (used by get_thought_ap_id) + pub thought_ap_ids: Arc>>, + /// UserId → ActorApUrls (used by get_actor_ap_urls) + pub actor_ap_urls: Arc>>, } #[async_trait] @@ -802,14 +809,12 @@ impl ActivityPubRepository for TestStore { &self, actor_ap_url: &url::Url, ) -> Result, DomainError> { - let url = actor_ap_url.to_string(); Ok(self - .users + .actor_ap_ids .lock() .unwrap() - .iter() - .find(|u| u.ap_id.as_deref() == Some(&url)) - .map(|u| u.id.clone())) + .get(actor_ap_url.as_str()) + .cloned()) } async fn intern_remote_actor(&self, actor_ap_url: &url::Url) -> Result { if let Some(uid) = self.find_remote_actor_id(actor_ap_url).await? { @@ -831,12 +836,14 @@ impl ActivityPubRepository for TestStore { header_url: None, custom_css: None, local: false, - ap_id: Some(actor_ap_url.to_string()), - inbox_url: None, created_at: chrono::Utc::now(), updated_at: chrono::Utc::now(), }; self.users.lock().unwrap().push(user); + self.actor_ap_ids + .lock() + .unwrap() + .insert(actor_ap_url.to_string(), uid.clone()); Ok(uid) } async fn update_remote_actor_display( @@ -884,15 +891,15 @@ impl ActivityPubRepository for TestStore { } async fn get_thought_ap_id( &self, - _thought_id: &ThoughtId, + thought_id: &ThoughtId, ) -> Result, DomainError> { - Ok(None) + Ok(self.thought_ap_ids.lock().unwrap().get(thought_id).cloned()) } async fn get_actor_ap_urls( &self, - _user_id: &UserId, + user_id: &UserId, ) -> Result, DomainError> { - Ok(None) + Ok(self.actor_ap_urls.lock().unwrap().get(user_id).cloned()) } } diff --git a/crates/presentation/src/handlers/feed.rs b/crates/presentation/src/handlers/feed.rs index db6907c..b9f279c 100644 --- a/crates/presentation/src/handlers/feed.rs +++ b/crates/presentation/src/handlers/feed.rs @@ -26,7 +26,7 @@ pub fn to_thought_response(e: &domain::models::feed::FeedEntry) -> ThoughtRespon content: e.thought.content.as_str().to_string(), author: to_user_response(&e.author), in_reply_to_id: e.thought.in_reply_to_id.as_ref().map(|id| id.as_uuid()), - in_reply_to_url: e.thought.in_reply_to_url.clone(), + in_reply_to_url: None, visibility: e.thought.visibility.as_str().to_string(), content_warning: e.thought.content_warning.clone(), sensitive: e.thought.sensitive,