refactor(domain): remove ap_id/inbox_url from User and Thought; use ActivityPubRepository lookups
This commit is contained in:
@@ -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,7 +1428,8 @@ impl domain::ports::OutboundFederationPort for ActivityPubService {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let (ap_id, note) = thought_note_json(thought, &local_actor, &self.base_url)
|
||||
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 {
|
||||
@@ -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,7 +1491,8 @@ impl domain::ports::OutboundFederationPort for ActivityPubService {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let (_ap_id, note) = thought_note_json(thought, &local_actor, &self.base_url)
|
||||
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!(
|
||||
|
||||
@@ -29,8 +29,6 @@ struct FeedRow {
|
||||
t_user_id: uuid::Uuid,
|
||||
content: String,
|
||||
in_reply_to_id: Option<uuid::Uuid>,
|
||||
in_reply_to_url: Option<String>,
|
||||
t_ap_id: Option<String>,
|
||||
visibility: String,
|
||||
content_warning: Option<String>,
|
||||
sensitive: bool,
|
||||
@@ -47,8 +45,6 @@ struct FeedRow {
|
||||
header_url: Option<String>,
|
||||
custom_css: Option<String>,
|
||||
author_local: bool,
|
||||
u_ap_id: Option<String>,
|
||||
inbox_url: Option<String>,
|
||||
author_created_at: DateTime<Utc>,
|
||||
author_updated_at: DateTime<Utc>,
|
||||
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,
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -29,8 +29,6 @@ struct FeedRow {
|
||||
t_user_id: uuid::Uuid,
|
||||
content: String,
|
||||
in_reply_to_id: Option<uuid::Uuid>,
|
||||
in_reply_to_url: Option<String>,
|
||||
t_ap_id: Option<String>,
|
||||
visibility: String,
|
||||
content_warning: Option<String>,
|
||||
sensitive: bool,
|
||||
@@ -47,8 +45,6 @@ struct FeedRow {
|
||||
header_url: Option<String>,
|
||||
custom_css: Option<String>,
|
||||
author_local: bool,
|
||||
u_ap_id: Option<String>,
|
||||
inbox_url: Option<String>,
|
||||
author_created_at: DateTime<Utc>,
|
||||
author_updated_at: DateTime<Utc>,
|
||||
like_count: i64,
|
||||
@@ -83,7 +79,7 @@ fn feed_select(viewer: Option<uuid::Uuid>) -> 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<uuid::Uuid>) -> 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,
|
||||
};
|
||||
|
||||
@@ -28,8 +28,6 @@ pub(crate) struct ThoughtRow {
|
||||
pub user_id: uuid::Uuid,
|
||||
pub content: String,
|
||||
pub in_reply_to_id: Option<uuid::Uuid>,
|
||||
pub in_reply_to_url: Option<String>,
|
||||
pub ap_id: Option<String>,
|
||||
pub visibility: String,
|
||||
pub content_warning: Option<String>,
|
||||
pub sensitive: bool,
|
||||
@@ -45,8 +43,6 @@ impl From<ThoughtRow> 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<ThoughtRow> 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
|
||||
)
|
||||
|
||||
@@ -58,15 +58,13 @@ impl TopFriendRepository for PgTopFriendRepository {
|
||||
header_url: Option<String>,
|
||||
custom_css: Option<String>,
|
||||
local: bool,
|
||||
ap_id: Option<String>,
|
||||
inbox_url: Option<String>,
|
||||
created_at: chrono::DateTime<chrono::Utc>,
|
||||
updated_at: chrono::DateTime<chrono::Utc>,
|
||||
}
|
||||
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,
|
||||
};
|
||||
|
||||
@@ -30,8 +30,6 @@ pub struct UserRow {
|
||||
pub header_url: Option<String>,
|
||||
pub custom_css: Option<String>,
|
||||
pub local: bool,
|
||||
pub ap_id: Option<String>,
|
||||
pub inbox_url: Option<String>,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
}
|
||||
@@ -49,15 +47,15 @@ impl From<UserRow> 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)
|
||||
|
||||
@@ -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<String, DomainError> {
|
||||
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,
|
||||
}
|
||||
// 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
|
||||
}
|
||||
} 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();
|
||||
|
||||
|
||||
@@ -15,8 +15,6 @@ pub struct Thought {
|
||||
pub user_id: UserId,
|
||||
pub content: Content,
|
||||
pub in_reply_to_id: Option<ThoughtId>,
|
||||
pub in_reply_to_url: Option<String>,
|
||||
pub ap_id: Option<String>,
|
||||
pub visibility: Visibility,
|
||||
pub content_warning: Option<String>,
|
||||
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,
|
||||
|
||||
@@ -13,8 +13,6 @@ pub struct User {
|
||||
pub header_url: Option<String>,
|
||||
pub custom_css: Option<String>,
|
||||
pub local: bool,
|
||||
pub ap_id: Option<String>,
|
||||
pub inbox_url: Option<String>,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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<Mutex<Vec<TopFriend>>>,
|
||||
pub notifications: Arc<Mutex<Vec<Notification>>>,
|
||||
pub events: Arc<Mutex<Vec<DomainEvent>>>,
|
||||
/// AP URL → UserId for remote actors (used by find_remote_actor_id / intern_remote_actor)
|
||||
pub actor_ap_ids: Arc<Mutex<HashMap<String, UserId>>>,
|
||||
/// ThoughtId → AP object URL (used by get_thought_ap_id)
|
||||
pub thought_ap_ids: Arc<Mutex<HashMap<ThoughtId, String>>>,
|
||||
/// UserId → ActorApUrls (used by get_actor_ap_urls)
|
||||
pub actor_ap_urls: Arc<Mutex<HashMap<UserId, ActorApUrls>>>,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
@@ -802,14 +809,12 @@ impl ActivityPubRepository for TestStore {
|
||||
&self,
|
||||
actor_ap_url: &url::Url,
|
||||
) -> Result<Option<UserId>, 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<UserId, DomainError> {
|
||||
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<Option<String>, 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<Option<ActorApUrls>, DomainError> {
|
||||
Ok(None)
|
||||
Ok(self.actor_ap_urls.lock().unwrap().get(user_id).cloned())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user