From 7cf9223684707be48ca6b59ebfe8f19ab9af9818 Mon Sep 17 00:00:00 2001 From: uOJackDu Date: Thu, 26 Feb 2026 19:10:05 -0500 Subject: [PATCH 01/19] add md5 hashing for generate_announce_activity_id --- Cargo.lock | 7 +++++++ crates/apub/activities/Cargo.toml | 1 + crates/apub/activities/src/community/announce.rs | 12 ++++++++---- crates/apub/activities/src/community/report.rs | 2 +- .../activities/src/community/resolve_report.rs | 2 +- crates/apub/activities/src/lib.rs | 14 ++++++++++++-- .../apub/apub/src/collections/community_outbox.rs | 6 +++++- 7 files changed, 35 insertions(+), 9 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e1334d52c8..576cd2098e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3525,6 +3525,7 @@ dependencies = [ "lemmy_db_views_site", "lemmy_diesel_utils", "lemmy_utils 1.0.0-test-arm-qemu.0", + "md5", "serde", "serde_json", "serde_with", @@ -4638,6 +4639,12 @@ dependencies = [ "digest", ] +[[package]] +name = "md5" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae960838283323069879657ca3de837e9f7bbb4c7bf6ea7f1b290d5e9476d2e0" + [[package]] name = "mdurl" version = "0.3.1" diff --git a/crates/apub/activities/Cargo.toml b/crates/apub/activities/Cargo.toml index b43b0e038a..6cefb887a2 100644 --- a/crates/apub/activities/Cargo.toml +++ b/crates/apub/activities/Cargo.toml @@ -50,6 +50,7 @@ serde_with.workspace = true enum_delegate = "0.2.0" either = { workspace = true } lemmy_diesel_utils = { workspace = true } +md5 = "0.8.0" [dev-dependencies] diff --git a/crates/apub/activities/src/community/announce.rs b/crates/apub/activities/src/community/announce.rs index f5b89d3cbe..fd18425dce 100644 --- a/crates/apub/activities/src/community/announce.rs +++ b/crates/apub/activities/src/community/announce.rs @@ -83,14 +83,18 @@ impl AnnounceActivity { object: RawAnnouncableActivities, community: &ApubCommunity, context: &Data, + object_id: Option<&Url>, ) -> LemmyResult { let inner_kind = object .other .get("type") .and_then(serde_json::Value::as_str) .unwrap_or("other"); - let id = - generate_announce_activity_id(inner_kind, &context.settings().get_protocol_and_hostname())?; + let id = generate_announce_activity_id( + inner_kind, + &context.settings().get_protocol_and_hostname(), + object_id, + )?; Ok(AnnounceActivity { actor: community.id().clone().into(), to: generate_to(community)?, @@ -111,7 +115,7 @@ impl AnnounceActivity { community: &ApubCommunity, context: &Data, ) -> LemmyResult<()> { - let announce = AnnounceActivity::new(object.clone(), community, context)?; + let announce = AnnounceActivity::new(object.clone(), community, context, None)?; let inboxes = ActivitySendTargets::to_local_community_followers(community.id); send_lemmy_activity(context, announce, community, inboxes.clone(), false).await?; @@ -129,7 +133,7 @@ impl AnnounceActivity { .ok_or(UntranslatedError::Unreachable)? .clone(), }; - let announce_compat = AnnounceActivity::new(announcable_page, community, context)?; + let announce_compat = AnnounceActivity::new(announcable_page, community, context, None)?; send_lemmy_activity(context, announce_compat, community, inboxes, false).await?; } Ok(()) diff --git a/crates/apub/activities/src/community/report.rs b/crates/apub/activities/src/community/report.rs index b6592561bb..db23c824a2 100644 --- a/crates/apub/activities/src/community/report.rs +++ b/crates/apub/activities/src/community/report.rs @@ -172,7 +172,7 @@ impl Activity for Report { // forward to remote mods let object_id = self.object.object_id(context).await?; let announce = AnnouncableActivities::Report(self); - let announce = AnnounceActivity::new(announce.try_into()?, community, context)?; + let announce = AnnounceActivity::new(announce.try_into()?, community, context, None)?; let inboxes = report_inboxes(object_id, &receiver, &actor, context).await?; send_lemmy_activity(context, announce, community, inboxes.clone(), false).await?; } diff --git a/crates/apub/activities/src/community/resolve_report.rs b/crates/apub/activities/src/community/resolve_report.rs index 93e5f5cfec..6ea0d4f66b 100644 --- a/crates/apub/activities/src/community/resolve_report.rs +++ b/crates/apub/activities/src/community/resolve_report.rs @@ -106,7 +106,7 @@ impl Activity for ResolveReport { // forward to remote mods let object_id = self.object.object.object_id(context).await?; let announce = AnnouncableActivities::ResolveReport(self); - let announce = AnnounceActivity::new(announce.try_into()?, community, context)?; + let announce = AnnounceActivity::new(announce.try_into()?, community, context, None)?; let inboxes = report_inboxes(object_id, &receiver, &reporter, context).await?; send_lemmy_activity(context, announce, community, inboxes.clone(), false).await?; } diff --git a/crates/apub/activities/src/lib.rs b/crates/apub/activities/src/lib.rs index 4a1c199dde..948383418c 100644 --- a/crates/apub/activities/src/lib.rs +++ b/crates/apub/activities/src/lib.rs @@ -99,13 +99,23 @@ where fn generate_announce_activity_id( inner_kind: &str, protocol_and_hostname: &str, + object_id: Option<&Url>, ) -> Result { + let inner = inner_kind.to_lowercase(); + + let uuid_str = if let Some(o) = object_id { + let input = format!("announce:{}:{}", inner, o.as_str()); // add "announce:" in front to avoid collision with generate_activity_id + Uuid::from_bytes(md5::compute(input.as_bytes()).0).to_string() + } else { + Uuid::new_v4().to_string() + }; + let id = format!( "{}/activities/{}/{}/{}", protocol_and_hostname, AnnounceType::Announce.to_string().to_lowercase(), - inner_kind.to_lowercase(), - Uuid::new_v4() + inner, + uuid_str ); Url::parse(&id) } diff --git a/crates/apub/apub/src/collections/community_outbox.rs b/crates/apub/apub/src/collections/community_outbox.rs index 296eb64896..83cb76862f 100644 --- a/crates/apub/apub/src/collections/community_outbox.rs +++ b/crates/apub/apub/src/collections/community_outbox.rs @@ -54,6 +54,8 @@ impl Collection for ApubCommunityOutbox { let mut ordered_items = vec![]; for post_view in post_views { + let post_ap_id = post_view.post.ap_id.clone(); + // ignore errors, in particular if post creator was deleted if let Ok(create) = CreateOrUpdatePage::new( post_view.post.into(), @@ -65,7 +67,9 @@ impl Collection for ApubCommunityOutbox { .await { let announcable = AnnouncableActivities::CreateOrUpdatePost(create); - if let Ok(announce) = AnnounceActivity::new(announcable.try_into()?, owner, data) { + if let Ok(announce) = + AnnounceActivity::new(announcable.try_into()?, owner, data, Some(&post_ap_id)) + { ordered_items.push(announce); } } From 36f48feab5a2bef876c6f509d53c3290170e2191 Mon Sep 17 00:00:00 2001 From: uOJackDu Date: Thu, 26 Feb 2026 20:25:04 -0500 Subject: [PATCH 02/19] modify parameter order --- crates/apub/activities/src/community/announce.rs | 6 +++--- crates/apub/activities/src/community/report.rs | 2 +- crates/apub/activities/src/community/resolve_report.rs | 2 +- crates/apub/apub/src/collections/community_outbox.rs | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/crates/apub/activities/src/community/announce.rs b/crates/apub/activities/src/community/announce.rs index fd18425dce..254ee98008 100644 --- a/crates/apub/activities/src/community/announce.rs +++ b/crates/apub/activities/src/community/announce.rs @@ -82,8 +82,8 @@ impl AnnounceActivity { pub fn new( object: RawAnnouncableActivities, community: &ApubCommunity, - context: &Data, object_id: Option<&Url>, + context: &Data, ) -> LemmyResult { let inner_kind = object .other @@ -115,7 +115,7 @@ impl AnnounceActivity { community: &ApubCommunity, context: &Data, ) -> LemmyResult<()> { - let announce = AnnounceActivity::new(object.clone(), community, context, None)?; + let announce = AnnounceActivity::new(object.clone(), community, None, context)?; let inboxes = ActivitySendTargets::to_local_community_followers(community.id); send_lemmy_activity(context, announce, community, inboxes.clone(), false).await?; @@ -133,7 +133,7 @@ impl AnnounceActivity { .ok_or(UntranslatedError::Unreachable)? .clone(), }; - let announce_compat = AnnounceActivity::new(announcable_page, community, context, None)?; + let announce_compat = AnnounceActivity::new(announcable_page, community, None, context)?; send_lemmy_activity(context, announce_compat, community, inboxes, false).await?; } Ok(()) diff --git a/crates/apub/activities/src/community/report.rs b/crates/apub/activities/src/community/report.rs index db23c824a2..bb2a93a615 100644 --- a/crates/apub/activities/src/community/report.rs +++ b/crates/apub/activities/src/community/report.rs @@ -172,7 +172,7 @@ impl Activity for Report { // forward to remote mods let object_id = self.object.object_id(context).await?; let announce = AnnouncableActivities::Report(self); - let announce = AnnounceActivity::new(announce.try_into()?, community, context, None)?; + let announce = AnnounceActivity::new(announce.try_into()?, community, None, context)?; let inboxes = report_inboxes(object_id, &receiver, &actor, context).await?; send_lemmy_activity(context, announce, community, inboxes.clone(), false).await?; } diff --git a/crates/apub/activities/src/community/resolve_report.rs b/crates/apub/activities/src/community/resolve_report.rs index 6ea0d4f66b..746f559fd5 100644 --- a/crates/apub/activities/src/community/resolve_report.rs +++ b/crates/apub/activities/src/community/resolve_report.rs @@ -106,7 +106,7 @@ impl Activity for ResolveReport { // forward to remote mods let object_id = self.object.object.object_id(context).await?; let announce = AnnouncableActivities::ResolveReport(self); - let announce = AnnounceActivity::new(announce.try_into()?, community, context, None)?; + let announce = AnnounceActivity::new(announce.try_into()?, community, None, context)?; let inboxes = report_inboxes(object_id, &receiver, &reporter, context).await?; send_lemmy_activity(context, announce, community, inboxes.clone(), false).await?; } diff --git a/crates/apub/apub/src/collections/community_outbox.rs b/crates/apub/apub/src/collections/community_outbox.rs index 83cb76862f..b8e0392647 100644 --- a/crates/apub/apub/src/collections/community_outbox.rs +++ b/crates/apub/apub/src/collections/community_outbox.rs @@ -68,7 +68,7 @@ impl Collection for ApubCommunityOutbox { { let announcable = AnnouncableActivities::CreateOrUpdatePost(create); if let Ok(announce) = - AnnounceActivity::new(announcable.try_into()?, owner, data, Some(&post_ap_id)) + AnnounceActivity::new(announcable.try_into()?, owner, Some(&post_ap_id), data) { ordered_items.push(announce); } From b4bab81e89662c64add3ab7ca5c31485cc6263d8 Mon Sep 17 00:00:00 2001 From: uOJackDu Date: Thu, 26 Feb 2026 20:44:00 -0500 Subject: [PATCH 03/19] add md5 hashing to generate_activity_id --- .../apub/activities/src/block/block_user.rs | 2 +- .../activities/src/block/undo_block_user.rs | 2 +- .../apub/activities/src/community/announce.rs | 2 +- .../src/community/collection_add.rs | 4 +-- .../src/community/collection_remove.rs | 4 +-- crates/apub/activities/src/community/lock.rs | 4 +-- .../apub/activities/src/community/report.rs | 2 +- .../src/community/resolve_report.rs | 2 +- .../apub/activities/src/community/update.rs | 4 +-- .../src/create_or_update/comment.rs | 2 +- .../activities/src/create_or_update/post.rs | 5 ++-- .../src/create_or_update/private_message.rs | 2 +- crates/apub/activities/src/deletion/delete.rs | 2 +- .../activities/src/deletion/undo_delete.rs | 2 +- .../apub/activities/src/following/accept.rs | 2 +- .../apub/activities/src/following/follow.rs | 2 +- crates/apub/activities/src/following/mod.rs | 2 +- .../apub/activities/src/following/reject.rs | 2 +- .../activities/src/following/undo_follow.rs | 2 +- crates/apub/activities/src/lib.rs | 30 ++++++++++++------- .../apub/activities/src/voting/undo_vote.rs | 2 +- crates/apub/activities/src/voting/vote.rs | 2 +- .../apub/src/collections/community_outbox.rs | 1 + 23 files changed, 48 insertions(+), 36 deletions(-) diff --git a/crates/apub/activities/src/block/block_user.rs b/crates/apub/activities/src/block/block_user.rs index 3a13801152..feef86bbe3 100644 --- a/crates/apub/activities/src/block/block_user.rs +++ b/crates/apub/activities/src/block/block_user.rs @@ -56,7 +56,7 @@ impl BlockUser { kind: BlockType::Block, remove_data, summary: Some(reason), - id: generate_activity_id(BlockType::Block, context)?, + id: generate_activity_id(BlockType::Block, None, context)?, end_time: expires, audience: target.as_ref().right().map(|c| c.ap_id.clone().into()), }) diff --git a/crates/apub/activities/src/block/undo_block_user.rs b/crates/apub/activities/src/block/undo_block_user.rs index 4c7b319073..f87c68f207 100644 --- a/crates/apub/activities/src/block/undo_block_user.rs +++ b/crates/apub/activities/src/block/undo_block_user.rs @@ -47,7 +47,7 @@ impl UndoBlockUser { let block = BlockUser::new(target, user, mod_, None, reason, None, context).await?; let to = to(target)?; - let id = generate_activity_id(UndoType::Undo, context)?; + let id = generate_activity_id(UndoType::Undo, None, context)?; let undo = UndoBlockUser { actor: mod_.id().clone().into(), to, diff --git a/crates/apub/activities/src/community/announce.rs b/crates/apub/activities/src/community/announce.rs index 254ee98008..132d53afe8 100644 --- a/crates/apub/activities/src/community/announce.rs +++ b/crates/apub/activities/src/community/announce.rs @@ -126,7 +126,7 @@ impl AnnounceActivity { // Hack: need to convert Page into a format which can be sent as activity, which requires // adding actor field. let announcable_page = RawAnnouncableActivities { - id: generate_activity_id(AnnounceType::Announce, context)?, + id: generate_activity_id(AnnounceType::Announce, None, context)?, actor: c.actor.clone().into_inner(), other: serde_json::to_value(c.object)? .as_object() diff --git a/crates/apub/activities/src/community/collection_add.rs b/crates/apub/activities/src/community/collection_add.rs index 6c2a45f723..314d942e83 100644 --- a/crates/apub/activities/src/community/collection_add.rs +++ b/crates/apub/activities/src/community/collection_add.rs @@ -46,7 +46,7 @@ impl CollectionAdd { actor: &ApubPerson, context: &Data, ) -> LemmyResult<()> { - let id = generate_activity_id(AddType::Add, context)?; + let id = generate_activity_id(AddType::Add, None, context)?; let add = CollectionAdd { actor: actor.id().clone().into(), to: generate_to(community)?, @@ -69,7 +69,7 @@ impl CollectionAdd { actor: &ApubPerson, context: &Data, ) -> LemmyResult<()> { - let id = generate_activity_id(AddType::Add, context)?; + let id = generate_activity_id(AddType::Add, None, context)?; let add = CollectionAdd { actor: actor.id().clone().into(), to: generate_to(community)?, diff --git a/crates/apub/activities/src/community/collection_remove.rs b/crates/apub/activities/src/community/collection_remove.rs index fb0336ea77..2fca9e3dfc 100644 --- a/crates/apub/activities/src/community/collection_remove.rs +++ b/crates/apub/activities/src/community/collection_remove.rs @@ -43,7 +43,7 @@ impl CollectionRemove { actor: &ApubPerson, context: &Data, ) -> LemmyResult<()> { - let id = generate_activity_id(RemoveType::Remove, context)?; + let id = generate_activity_id(RemoveType::Remove, None, context)?; let remove = CollectionRemove { actor: actor.id().clone().into(), to: generate_to(community)?, @@ -66,7 +66,7 @@ impl CollectionRemove { actor: &ApubPerson, context: &Data, ) -> LemmyResult<()> { - let id = generate_activity_id(RemoveType::Remove, context)?; + let id = generate_activity_id(RemoveType::Remove, None, context)?; let remove = CollectionRemove { actor: actor.id().clone().into(), to: generate_to(community)?, diff --git a/crates/apub/activities/src/community/lock.rs b/crates/apub/activities/src/community/lock.rs index 3876c43fee..8f9c3d0adf 100644 --- a/crates/apub/activities/src/community/lock.rs +++ b/crates/apub/activities/src/community/lock.rs @@ -155,7 +155,7 @@ pub(crate) async fn send_lock( context: Data, ) -> LemmyResult<()> { let community: ApubCommunity = post_or_comment_community(&object, &context).await?.into(); - let id = generate_activity_id(LockType::Lock, &context)?; + let id = generate_activity_id(LockType::Lock, None, &context)?; let community_id = community.ap_id.inner().clone(); let ap_id = match object { PostOrComment::Left(p) => p.ap_id.clone(), @@ -175,7 +175,7 @@ pub(crate) async fn send_lock( let activity = if locked { AnnouncableActivities::Lock(lock) } else { - let id = generate_activity_id(UndoType::Undo, &context)?; + let id = generate_activity_id(UndoType::Undo, None, &context)?; let undo = UndoLockPageOrNote { actor: lock.actor.clone(), to: generate_to(&community)?, diff --git a/crates/apub/activities/src/community/report.rs b/crates/apub/activities/src/community/report.rs index bb2a93a615..9280ef5680 100644 --- a/crates/apub/activities/src/community/report.rs +++ b/crates/apub/activities/src/community/report.rs @@ -57,7 +57,7 @@ impl Report { context: &Data, ) -> LemmyResult { let kind = FlagType::Flag; - let id = generate_activity_id(kind.clone(), context)?; + let id = generate_activity_id(kind.clone(), None, context)?; Ok(Report { actor: actor.id().clone().into(), to: [receiver.id().clone().into()], diff --git a/crates/apub/activities/src/community/resolve_report.rs b/crates/apub/activities/src/community/resolve_report.rs index 746f559fd5..e04e827da1 100644 --- a/crates/apub/activities/src/community/resolve_report.rs +++ b/crates/apub/activities/src/community/resolve_report.rs @@ -47,7 +47,7 @@ impl ResolveReport { context: Data, ) -> LemmyResult<()> { let kind = ResolveType::Resolve; - let id = generate_activity_id(kind.clone(), &context)?; + let id = generate_activity_id(kind.clone(), None, &context)?; let object = Report::new(&object_id, report_creator, receiver, None, &context)?; let resolve = ResolveReport { actor: actor.id().clone().into(), diff --git a/crates/apub/activities/src/community/update.rs b/crates/apub/activities/src/community/update.rs index 0bc170b59d..3b7a66363f 100644 --- a/crates/apub/activities/src/community/update.rs +++ b/crates/apub/activities/src/community/update.rs @@ -36,7 +36,7 @@ pub(crate) async fn send_update_community( ) -> LemmyResult<()> { let community: ApubCommunity = community.into(); let actor: ApubPerson = actor.into(); - let id = generate_activity_id(UpdateType::Update, &context)?; + let id = generate_activity_id(UpdateType::Update, None, &context)?; let update = Update { actor: actor.id().clone().into(), to: generate_to(&community)?, @@ -66,7 +66,7 @@ pub(crate) async fn send_update_multi_community( ) -> LemmyResult<()> { let multi: ApubMultiCommunity = multi.into(); let actor: ApubPerson = actor.into(); - let id = generate_activity_id(UpdateType::Update, &context)?; + let id = generate_activity_id(UpdateType::Update, None, &context)?; let update = Update { actor: actor.id().clone().into(), to: vec![multi.ap_id.clone().into(), public()], diff --git a/crates/apub/activities/src/create_or_update/comment.rs b/crates/apub/activities/src/create_or_update/comment.rs index f307100ee1..7054e0b37e 100644 --- a/crates/apub/activities/src/create_or_update/comment.rs +++ b/crates/apub/activities/src/create_or_update/comment.rs @@ -55,7 +55,7 @@ impl CreateOrUpdateNote { .await? .into(); - let id = generate_activity_id(kind.clone(), &context)?; + let id = generate_activity_id(kind.clone(), None, &context)?; let note = ApubComment(comment).into_json(&context).await?; let create_or_update = CreateOrUpdateNote { diff --git a/crates/apub/activities/src/create_or_update/post.rs b/crates/apub/activities/src/create_or_update/post.rs index ed1789577f..f34383f2a3 100644 --- a/crates/apub/activities/src/create_or_update/post.rs +++ b/crates/apub/activities/src/create_or_update/post.rs @@ -44,9 +44,10 @@ impl CreateOrUpdatePage { actor: &ApubPerson, community: &ApubCommunity, kind: CreateOrUpdateType, + object_id: Option<&Url>, context: &Data, ) -> LemmyResult { - let id = generate_activity_id(kind.clone(), context)?; + let id = generate_activity_id(kind.clone(), object_id, context)?; Ok(CreateOrUpdatePage { actor: actor.id().clone().into(), to: generate_to(community)?, @@ -71,7 +72,7 @@ impl CreateOrUpdatePage { .into(); let create_or_update = - CreateOrUpdatePage::new(post.into(), &person, &community, kind, &context).await?; + CreateOrUpdatePage::new(post.into(), &person, &community, kind, None, &context).await?; let inboxes = tagged_user_inboxes(&create_or_update.object.tag, &context).await?; let activity = AnnouncableActivities::CreateOrUpdatePost(create_or_update); send_activity_in_community(activity, &person, &community, inboxes, false, &context).await?; diff --git a/crates/apub/activities/src/create_or_update/private_message.rs b/crates/apub/activities/src/create_or_update/private_message.rs index 2b1e78c0e6..facc2a8446 100644 --- a/crates/apub/activities/src/create_or_update/private_message.rs +++ b/crates/apub/activities/src/create_or_update/private_message.rs @@ -24,7 +24,7 @@ pub(crate) async fn send_create_or_update_pm( let actor: ApubPerson = pm_view.creator.into(); let recipient: ApubPerson = pm_view.recipient.into(); - let id = generate_activity_id(kind.clone(), &context)?; + let id = generate_activity_id(kind.clone(), None, &context)?; let create_or_update = CreateOrUpdatePrivateMessage { id: id.clone(), actor: actor.id().clone().into(), diff --git a/crates/apub/activities/src/deletion/delete.rs b/crates/apub/activities/src/deletion/delete.rs index d530f2c50e..97d4e644d7 100644 --- a/crates/apub/activities/src/deletion/delete.rs +++ b/crates/apub/activities/src/deletion/delete.rs @@ -82,7 +82,7 @@ impl Delete { with_replies: Option, context: &Data, ) -> LemmyResult { - let id = generate_activity_id(DeleteType::Delete, context)?; + let id = generate_activity_id(DeleteType::Delete, None, context)?; let cc: Option = community.map(|c| c.ap_id.clone().into()); Ok(Delete { actor: actor.ap_id.clone().into(), diff --git a/crates/apub/activities/src/deletion/undo_delete.rs b/crates/apub/activities/src/deletion/undo_delete.rs index 0c9dea0fe6..8f0a88d918 100644 --- a/crates/apub/activities/src/deletion/undo_delete.rs +++ b/crates/apub/activities/src/deletion/undo_delete.rs @@ -72,7 +72,7 @@ impl UndoDelete { context, )?; - let id = generate_activity_id(UndoType::Undo, context)?; + let id = generate_activity_id(UndoType::Undo, None, context)?; let cc: Option = community.map(|c| c.ap_id.clone().into()); Ok(UndoDelete { actor: actor.ap_id.clone().into(), diff --git a/crates/apub/activities/src/following/accept.rs b/crates/apub/activities/src/following/accept.rs index bd8708b236..ceca5a5c56 100644 --- a/crates/apub/activities/src/following/accept.rs +++ b/crates/apub/activities/src/following/accept.rs @@ -27,7 +27,7 @@ impl AcceptFollow { to: Some([person.id().clone().into()]), object: follow, kind: AcceptType::Accept, - id: generate_activity_id(AcceptType::Accept, context)?, + id: generate_activity_id(AcceptType::Accept, None, context)?, }; let inbox = ActivitySendTargets::to_inbox(person.shared_inbox_or_inbox()); send_lemmy_activity(context, accept, &target, inbox, true).await diff --git a/crates/apub/activities/src/following/follow.rs b/crates/apub/activities/src/following/follow.rs index 486a8a01ff..3a71d1503f 100644 --- a/crates/apub/activities/src/following/follow.rs +++ b/crates/apub/activities/src/following/follow.rs @@ -40,7 +40,7 @@ impl Follow { object: target.id().clone().into(), to: Some([target.id().clone().into()]), kind: FollowType::Follow, - id: generate_activity_id(FollowType::Follow, context)?, + id: generate_activity_id(FollowType::Follow, None, context)?, }) } diff --git a/crates/apub/activities/src/following/mod.rs b/crates/apub/activities/src/following/mod.rs index c5a7d715c7..ea56e85348 100644 --- a/crates/apub/activities/src/following/mod.rs +++ b/crates/apub/activities/src/following/mod.rs @@ -51,7 +51,7 @@ pub async fn send_accept_or_reject_follow( to: Some([community.ap_id.clone().into()]), object: community.ap_id.into(), kind: FollowType::Follow, - id: generate_activity_id(FollowType::Follow, context)?, + id: generate_activity_id(FollowType::Follow, None, context)?, }; if accepted { AcceptFollow::send(follow, context).await diff --git a/crates/apub/activities/src/following/reject.rs b/crates/apub/activities/src/following/reject.rs index 84189ce981..318adbbfaf 100644 --- a/crates/apub/activities/src/following/reject.rs +++ b/crates/apub/activities/src/following/reject.rs @@ -27,7 +27,7 @@ impl RejectFollow { to: Some([person.id().clone().into()]), object: follow, kind: RejectType::Reject, - id: generate_activity_id(RejectType::Reject, context)?, + id: generate_activity_id(RejectType::Reject, None, context)?, }; let inbox = ActivitySendTargets::to_inbox(person.shared_inbox_or_inbox()); send_activity_from_user_or_community_or_multi(context, reject, user_or_community, inbox).await diff --git a/crates/apub/activities/src/following/undo_follow.rs b/crates/apub/activities/src/following/undo_follow.rs index 3a776c27d2..062edfb140 100644 --- a/crates/apub/activities/src/following/undo_follow.rs +++ b/crates/apub/activities/src/following/undo_follow.rs @@ -39,7 +39,7 @@ impl UndoFollow { to: Some([target.id().clone().into()]), object, kind: UndoType::Undo, - id: generate_activity_id(UndoType::Undo, context)?, + id: generate_activity_id(UndoType::Undo, None, context)?, }; let inbox = ActivitySendTargets::to_inbox(target.shared_inbox_or_inbox()); send_lemmy_activity(context, undo, actor, inbox, true).await diff --git a/crates/apub/activities/src/lib.rs b/crates/apub/activities/src/lib.rs index 948383418c..f4e641e9fd 100644 --- a/crates/apub/activities/src/lib.rs +++ b/crates/apub/activities/src/lib.rs @@ -82,16 +82,25 @@ pub(crate) fn check_community_deleted_or_removed(community: &Community) -> Lemmy /// Generate a unique ID for an activity, in the format: /// `http(s)://example.com/receive/create/202daf0a-1489-45df-8d2e-c8a3173fed36` -fn generate_activity_id(kind: T, context: &LemmyContext) -> Result +fn generate_activity_id( + kind: T, + object_id: Option<&Url>, + context: &LemmyContext, +) -> Result where T: ToString, { - let id = format!( - "{}/activities/{}/{}", - &context.settings().get_protocol_and_hostname(), - kind.to_string().to_lowercase(), - Uuid::new_v4() - ); + let hostname = context.settings().get_protocol_and_hostname(); + let kind_str = kind.to_string().to_lowercase(); + + let uuid_str = if let Some(o) = object_id { + let input = format!("{}:{}", kind_str, o.as_str()); + Uuid::from_bytes(md5::compute(input.as_bytes()).0).to_string() + } else { + Uuid::new_v4().to_string() + }; + + let id = format!("{}/activities/{}/{}", hostname, kind_str, uuid_str); Url::parse(&id) } @@ -101,10 +110,11 @@ fn generate_announce_activity_id( protocol_and_hostname: &str, object_id: Option<&Url>, ) -> Result { - let inner = inner_kind.to_lowercase(); + let inner_kind_str = inner_kind.to_lowercase(); let uuid_str = if let Some(o) = object_id { - let input = format!("announce:{}:{}", inner, o.as_str()); // add "announce:" in front to avoid collision with generate_activity_id + // add "announce:" in front to avoid collision with generate_activity_id + let input = format!("announce:{}:{}", inner_kind_str, o.as_str()); Uuid::from_bytes(md5::compute(input.as_bytes()).0).to_string() } else { Uuid::new_v4().to_string() @@ -114,7 +124,7 @@ fn generate_announce_activity_id( "{}/activities/{}/{}/{}", protocol_and_hostname, AnnounceType::Announce.to_string().to_lowercase(), - inner, + inner_kind_str, uuid_str ); Url::parse(&id) diff --git a/crates/apub/activities/src/voting/undo_vote.rs b/crates/apub/activities/src/voting/undo_vote.rs index 1887ad73d9..26c555d802 100644 --- a/crates/apub/activities/src/voting/undo_vote.rs +++ b/crates/apub/activities/src/voting/undo_vote.rs @@ -29,7 +29,7 @@ impl UndoVote { actor: actor.id().clone().into(), object: vote, kind: UndoType::Undo, - id: generate_activity_id(UndoType::Undo, context)?, + id: generate_activity_id(UndoType::Undo, None, context)?, audience: Some(community.ap_id.clone().into()), }) } diff --git a/crates/apub/activities/src/voting/vote.rs b/crates/apub/activities/src/voting/vote.rs index 20287551b3..68a1df934c 100644 --- a/crates/apub/activities/src/voting/vote.rs +++ b/crates/apub/activities/src/voting/vote.rs @@ -31,7 +31,7 @@ impl Vote { actor: actor.id().clone().into(), object: object_id, kind: kind.clone(), - id: generate_activity_id(kind, context)?, + id: generate_activity_id(kind, None, context)?, audience: Some(community.ap_id.clone().into()), }) } diff --git a/crates/apub/apub/src/collections/community_outbox.rs b/crates/apub/apub/src/collections/community_outbox.rs index b8e0392647..8aecb5876c 100644 --- a/crates/apub/apub/src/collections/community_outbox.rs +++ b/crates/apub/apub/src/collections/community_outbox.rs @@ -62,6 +62,7 @@ impl Collection for ApubCommunityOutbox { &post_view.creator.into(), owner, CreateOrUpdateType::Create, + Some(&post_ap_id), data, ) .await From a9f41955c51b5ab232fc93f46c7d1650deae4f9d Mon Sep 17 00:00:00 2001 From: uOJackDu Date: Fri, 27 Feb 2026 02:23:04 -0500 Subject: [PATCH 04/19] modify object_id type --- crates/apub/activities/src/community/announce.rs | 2 +- crates/apub/activities/src/create_or_update/post.rs | 2 +- crates/apub/activities/src/lib.rs | 8 ++++---- crates/apub/apub/src/collections/community_outbox.rs | 11 +++++++---- 4 files changed, 13 insertions(+), 10 deletions(-) diff --git a/crates/apub/activities/src/community/announce.rs b/crates/apub/activities/src/community/announce.rs index 132d53afe8..4dd48ff999 100644 --- a/crates/apub/activities/src/community/announce.rs +++ b/crates/apub/activities/src/community/announce.rs @@ -82,7 +82,7 @@ impl AnnounceActivity { pub fn new( object: RawAnnouncableActivities, community: &ApubCommunity, - object_id: Option<&Url>, + object_id: Option<&str>, context: &Data, ) -> LemmyResult { let inner_kind = object diff --git a/crates/apub/activities/src/create_or_update/post.rs b/crates/apub/activities/src/create_or_update/post.rs index f34383f2a3..559b13c22a 100644 --- a/crates/apub/activities/src/create_or_update/post.rs +++ b/crates/apub/activities/src/create_or_update/post.rs @@ -44,7 +44,7 @@ impl CreateOrUpdatePage { actor: &ApubPerson, community: &ApubCommunity, kind: CreateOrUpdateType, - object_id: Option<&Url>, + object_id: Option<&str>, context: &Data, ) -> LemmyResult { let id = generate_activity_id(kind.clone(), object_id, context)?; diff --git a/crates/apub/activities/src/lib.rs b/crates/apub/activities/src/lib.rs index f4e641e9fd..42089ee773 100644 --- a/crates/apub/activities/src/lib.rs +++ b/crates/apub/activities/src/lib.rs @@ -84,7 +84,7 @@ pub(crate) fn check_community_deleted_or_removed(community: &Community) -> Lemmy /// `http(s)://example.com/receive/create/202daf0a-1489-45df-8d2e-c8a3173fed36` fn generate_activity_id( kind: T, - object_id: Option<&Url>, + object_id: Option<&str>, context: &LemmyContext, ) -> Result where @@ -94,7 +94,7 @@ where let kind_str = kind.to_string().to_lowercase(); let uuid_str = if let Some(o) = object_id { - let input = format!("{}:{}", kind_str, o.as_str()); + let input = format!("{}:{}", kind_str, o); Uuid::from_bytes(md5::compute(input.as_bytes()).0).to_string() } else { Uuid::new_v4().to_string() @@ -108,13 +108,13 @@ where fn generate_announce_activity_id( inner_kind: &str, protocol_and_hostname: &str, - object_id: Option<&Url>, + object_id: Option<&str>, ) -> Result { let inner_kind_str = inner_kind.to_lowercase(); let uuid_str = if let Some(o) = object_id { // add "announce:" in front to avoid collision with generate_activity_id - let input = format!("announce:{}:{}", inner_kind_str, o.as_str()); + let input = format!("announce:{}:{}", inner_kind_str, o); Uuid::from_bytes(md5::compute(input.as_bytes()).0).to_string() } else { Uuid::new_v4().to_string() diff --git a/crates/apub/apub/src/collections/community_outbox.rs b/crates/apub/apub/src/collections/community_outbox.rs index 8aecb5876c..bf1b6a3b42 100644 --- a/crates/apub/apub/src/collections/community_outbox.rs +++ b/crates/apub/apub/src/collections/community_outbox.rs @@ -62,15 +62,18 @@ impl Collection for ApubCommunityOutbox { &post_view.creator.into(), owner, CreateOrUpdateType::Create, - Some(&post_ap_id), + Some(post_ap_id.as_str()), data, ) .await { let announcable = AnnouncableActivities::CreateOrUpdatePost(create); - if let Ok(announce) = - AnnounceActivity::new(announcable.try_into()?, owner, Some(&post_ap_id), data) - { + if let Ok(announce) = AnnounceActivity::new( + announcable.try_into()?, + owner, + Some(post_ap_id.as_str()), + data, + ) { ordered_items.push(announce); } } From 91f043f299a2c2125c850adb14841fa9d5c652a2 Mon Sep 17 00:00:00 2001 From: uOJackDu Date: Fri, 27 Feb 2026 02:40:32 -0500 Subject: [PATCH 05/19] remove as_bytes calls on input string when hashing --- crates/apub/activities/src/lib.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/apub/activities/src/lib.rs b/crates/apub/activities/src/lib.rs index 42089ee773..2252a7ed47 100644 --- a/crates/apub/activities/src/lib.rs +++ b/crates/apub/activities/src/lib.rs @@ -95,7 +95,7 @@ where let uuid_str = if let Some(o) = object_id { let input = format!("{}:{}", kind_str, o); - Uuid::from_bytes(md5::compute(input.as_bytes()).0).to_string() + Uuid::from_bytes(md5::compute(input).0).to_string() } else { Uuid::new_v4().to_string() }; @@ -115,7 +115,7 @@ fn generate_announce_activity_id( let uuid_str = if let Some(o) = object_id { // add "announce:" in front to avoid collision with generate_activity_id let input = format!("announce:{}:{}", inner_kind_str, o); - Uuid::from_bytes(md5::compute(input.as_bytes()).0).to_string() + Uuid::from_bytes(md5::compute(input).0).to_string() } else { Uuid::new_v4().to_string() }; From b6a25e9fe802313f2ec870cf29b4da7c336eab48 Mon Sep 17 00:00:00 2001 From: uOJackDu Date: Tue, 7 Apr 2026 14:49:38 -0400 Subject: [PATCH 06/19] modify object_id type from str to url --- crates/apub/activities/src/community/announce.rs | 2 +- crates/apub/activities/src/create_or_update/post.rs | 2 +- crates/apub/activities/src/lib.rs | 8 ++++---- .../apub/apub/src/collections/community_outbox.rs | 13 +++++-------- 4 files changed, 11 insertions(+), 14 deletions(-) diff --git a/crates/apub/activities/src/community/announce.rs b/crates/apub/activities/src/community/announce.rs index 4dd48ff999..132d53afe8 100644 --- a/crates/apub/activities/src/community/announce.rs +++ b/crates/apub/activities/src/community/announce.rs @@ -82,7 +82,7 @@ impl AnnounceActivity { pub fn new( object: RawAnnouncableActivities, community: &ApubCommunity, - object_id: Option<&str>, + object_id: Option<&Url>, context: &Data, ) -> LemmyResult { let inner_kind = object diff --git a/crates/apub/activities/src/create_or_update/post.rs b/crates/apub/activities/src/create_or_update/post.rs index 559b13c22a..f34383f2a3 100644 --- a/crates/apub/activities/src/create_or_update/post.rs +++ b/crates/apub/activities/src/create_or_update/post.rs @@ -44,7 +44,7 @@ impl CreateOrUpdatePage { actor: &ApubPerson, community: &ApubCommunity, kind: CreateOrUpdateType, - object_id: Option<&str>, + object_id: Option<&Url>, context: &Data, ) -> LemmyResult { let id = generate_activity_id(kind.clone(), object_id, context)?; diff --git a/crates/apub/activities/src/lib.rs b/crates/apub/activities/src/lib.rs index 2252a7ed47..1dc71246da 100644 --- a/crates/apub/activities/src/lib.rs +++ b/crates/apub/activities/src/lib.rs @@ -84,7 +84,7 @@ pub(crate) fn check_community_deleted_or_removed(community: &Community) -> Lemmy /// `http(s)://example.com/receive/create/202daf0a-1489-45df-8d2e-c8a3173fed36` fn generate_activity_id( kind: T, - object_id: Option<&str>, + object_id: Option<&Url>, context: &LemmyContext, ) -> Result where @@ -94,7 +94,7 @@ where let kind_str = kind.to_string().to_lowercase(); let uuid_str = if let Some(o) = object_id { - let input = format!("{}:{}", kind_str, o); + let input = format!("{}:{}", kind_str, o.as_str()); Uuid::from_bytes(md5::compute(input).0).to_string() } else { Uuid::new_v4().to_string() @@ -108,13 +108,13 @@ where fn generate_announce_activity_id( inner_kind: &str, protocol_and_hostname: &str, - object_id: Option<&str>, + object_id: Option<&Url>, ) -> Result { let inner_kind_str = inner_kind.to_lowercase(); let uuid_str = if let Some(o) = object_id { // add "announce:" in front to avoid collision with generate_activity_id - let input = format!("announce:{}:{}", inner_kind_str, o); + let input = format!("announce:{}:{}", inner_kind_str, o.as_str()); Uuid::from_bytes(md5::compute(input).0).to_string() } else { Uuid::new_v4().to_string() diff --git a/crates/apub/apub/src/collections/community_outbox.rs b/crates/apub/apub/src/collections/community_outbox.rs index bf1b6a3b42..1570f8a331 100644 --- a/crates/apub/apub/src/collections/community_outbox.rs +++ b/crates/apub/apub/src/collections/community_outbox.rs @@ -54,7 +54,7 @@ impl Collection for ApubCommunityOutbox { let mut ordered_items = vec![]; for post_view in post_views { - let post_ap_id = post_view.post.ap_id.clone(); + let post_ap_id = Some(&*post_view.post.ap_id.0.clone()); // ignore errors, in particular if post creator was deleted if let Ok(create) = CreateOrUpdatePage::new( @@ -62,18 +62,15 @@ impl Collection for ApubCommunityOutbox { &post_view.creator.into(), owner, CreateOrUpdateType::Create, - Some(post_ap_id.as_str()), + post_ap_id, data, ) .await { let announcable = AnnouncableActivities::CreateOrUpdatePost(create); - if let Ok(announce) = AnnounceActivity::new( - announcable.try_into()?, - owner, - Some(post_ap_id.as_str()), - data, - ) { + if let Ok(announce) = + AnnounceActivity::new(announcable.try_into()?, owner, post_ap_id, data) + { ordered_items.push(announce); } } From 0a619defdd29049f39f19654e7c643d895a8ae01 Mon Sep 17 00:00:00 2001 From: uOJackDu Date: Tue, 7 Apr 2026 16:50:00 -0400 Subject: [PATCH 07/19] ensure unique activity ids for posts for Create and Update --- .../apub/activities/src/create_or_update/post.rs | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/crates/apub/activities/src/create_or_update/post.rs b/crates/apub/activities/src/create_or_update/post.rs index f34383f2a3..56aa5b7bde 100644 --- a/crates/apub/activities/src/create_or_update/post.rs +++ b/crates/apub/activities/src/create_or_update/post.rs @@ -71,8 +71,22 @@ impl CreateOrUpdatePage { .await? .into(); + // get object_id for activity id generation + let post_ap_id = (*post.ap_id.0).clone(); + let object_id = match kind { + // for Create, use the post's ap id + CreateOrUpdateType::Create => Some(&post_ap_id), + // for Update, use a timestamp to ensure each Update activity is unique + CreateOrUpdateType::Update => { + let timestamp = post.updated_at.unwrap_or(post.published_at); // use the latest timestamp + let mut seed_url = post_ap_id; + seed_url.set_fragment(Some(×tamp.to_rfc3339())); + Some(&seed_url.clone()) + } + }; + let create_or_update = - CreateOrUpdatePage::new(post.into(), &person, &community, kind, None, &context).await?; + CreateOrUpdatePage::new(post.into(), &person, &community, kind, object_id, &context).await?; let inboxes = tagged_user_inboxes(&create_or_update.object.tag, &context).await?; let activity = AnnouncableActivities::CreateOrUpdatePost(create_or_update); send_activity_in_community(activity, &person, &community, inboxes, false, &context).await?; From 99d078240e827c44fe92a86cfb2194d7e95c8ed9 Mon Sep 17 00:00:00 2001 From: uOJackDu Date: Wed, 8 Apr 2026 00:20:42 -0400 Subject: [PATCH 08/19] switch to sha2 for hashing --- Cargo.lock | 8 +------ crates/apub/activities/Cargo.toml | 2 +- crates/apub/activities/src/lib.rs | 40 +++++++++++++++++++++---------- crates/utils/src/error.rs | 1 + 4 files changed, 30 insertions(+), 21 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 576cd2098e..1c056d8089 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3525,10 +3525,10 @@ dependencies = [ "lemmy_db_views_site", "lemmy_diesel_utils", "lemmy_utils 1.0.0-test-arm-qemu.0", - "md5", "serde", "serde_json", "serde_with", + "sha2", "strum 0.28.0", "tracing", "url", @@ -4639,12 +4639,6 @@ dependencies = [ "digest", ] -[[package]] -name = "md5" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae960838283323069879657ca3de837e9f7bbb4c7bf6ea7f1b290d5e9476d2e0" - [[package]] name = "mdurl" version = "0.3.1" diff --git a/crates/apub/activities/Cargo.toml b/crates/apub/activities/Cargo.toml index 6cefb887a2..b19055d68b 100644 --- a/crates/apub/activities/Cargo.toml +++ b/crates/apub/activities/Cargo.toml @@ -50,7 +50,7 @@ serde_with.workspace = true enum_delegate = "0.2.0" either = { workspace = true } lemmy_diesel_utils = { workspace = true } -md5 = "0.8.0" +sha2 = "0.10.9" [dev-dependencies] diff --git a/crates/apub/activities/src/lib.rs b/crates/apub/activities/src/lib.rs index 1dc71246da..52fd405bfe 100644 --- a/crates/apub/activities/src/lib.rs +++ b/crates/apub/activities/src/lib.rs @@ -46,8 +46,9 @@ use lemmy_db_views_site::SiteView; use lemmy_diesel_utils::traits::Crud; use lemmy_utils::error::{LemmyError, LemmyResult, UntranslatedError}; use serde::Serialize; +use sha2::{Digest, Sha256}; use tracing::info; -use url::{ParseError, Url}; +use url::Url; use uuid::Uuid; pub mod activity_lists; @@ -86,22 +87,22 @@ fn generate_activity_id( kind: T, object_id: Option<&Url>, context: &LemmyContext, -) -> Result +) -> LemmyResult where T: ToString, { let hostname = context.settings().get_protocol_and_hostname(); let kind_str = kind.to_string().to_lowercase(); - let uuid_str = if let Some(o) = object_id { + let uuid = if let Some(o) = object_id { let input = format!("{}:{}", kind_str, o.as_str()); - Uuid::from_bytes(md5::compute(input).0).to_string() + generate_hash(&input)? } else { - Uuid::new_v4().to_string() + Uuid::new_v4() }; - let id = format!("{}/activities/{}/{}", hostname, kind_str, uuid_str); - Url::parse(&id) + let id = format!("{}/activities/{}/{}", hostname, kind_str, uuid); + Url::parse(&id).map_err(|e| LemmyError::from(anyhow::anyhow!(e))) } /// like generate_activity_id but also add the inner kind for easier debugging @@ -109,15 +110,15 @@ fn generate_announce_activity_id( inner_kind: &str, protocol_and_hostname: &str, object_id: Option<&Url>, -) -> Result { +) -> LemmyResult { let inner_kind_str = inner_kind.to_lowercase(); - let uuid_str = if let Some(o) = object_id { + let uuid = if let Some(o) = object_id { // add "announce:" in front to avoid collision with generate_activity_id let input = format!("announce:{}:{}", inner_kind_str, o.as_str()); - Uuid::from_bytes(md5::compute(input).0).to_string() + generate_hash(&input)? } else { - Uuid::new_v4().to_string() + Uuid::new_v4() }; let id = format!( @@ -125,9 +126,22 @@ fn generate_announce_activity_id( protocol_and_hostname, AnnounceType::Announce.to_string().to_lowercase(), inner_kind_str, - uuid_str + uuid ); - Url::parse(&id) + Url::parse(&id).map_err(|e| LemmyError::from(anyhow::anyhow!(e))) +} + +/// generate a hash from input string, returning first 16 bytes as UUID +fn generate_hash(input: &str) -> LemmyResult { + let mut hasher = Sha256::new(); + hasher.update(input); + let digest = hasher.finalize(); // 32 bytes + Ok(Uuid::from_bytes( + digest + .get(..16) + .ok_or(UntranslatedError::CouldntGenerateHash)? + .try_into()?, + )) } async fn send_lemmy_activity( diff --git a/crates/utils/src/error.rs b/crates/utils/src/error.rs index 273d4b788c..b2fd19898f 100644 --- a/crates/utils/src/error.rs +++ b/crates/utils/src/error.rs @@ -158,6 +158,7 @@ pub enum UntranslatedError { /// A remote community sent an activity to us, but actually no local user follows the community /// so the activity was rejected. CommunityHasNoFollowers(String), + CouldntGenerateHash, } cfg_if! { From 5ae79aa7f8c93f621695f080e56277117f48290a Mon Sep 17 00:00:00 2001 From: uOJackDu Date: Wed, 8 Apr 2026 00:32:25 -0400 Subject: [PATCH 09/19] change hashing input format to adhere to the issue --- crates/apub/activities/src/lib.rs | 2 +- crates/apub/apub/src/collections/community_outbox.rs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/apub/activities/src/lib.rs b/crates/apub/activities/src/lib.rs index 52fd405bfe..92f93d7e51 100644 --- a/crates/apub/activities/src/lib.rs +++ b/crates/apub/activities/src/lib.rs @@ -115,7 +115,7 @@ fn generate_announce_activity_id( let uuid = if let Some(o) = object_id { // add "announce:" in front to avoid collision with generate_activity_id - let input = format!("announce:{}:{}", inner_kind_str, o.as_str()); + let input = format!("announce:{}", o.as_str()); generate_hash(&input)? } else { Uuid::new_v4() diff --git a/crates/apub/apub/src/collections/community_outbox.rs b/crates/apub/apub/src/collections/community_outbox.rs index 1570f8a331..77f5703dc6 100644 --- a/crates/apub/apub/src/collections/community_outbox.rs +++ b/crates/apub/apub/src/collections/community_outbox.rs @@ -67,9 +67,9 @@ impl Collection for ApubCommunityOutbox { ) .await { - let announcable = AnnouncableActivities::CreateOrUpdatePost(create); + let announcable = AnnouncableActivities::CreateOrUpdatePost(create.clone()); if let Ok(announce) = - AnnounceActivity::new(announcable.try_into()?, owner, post_ap_id, data) + AnnounceActivity::new(announcable.try_into()?, owner, Some(create.id()), data) { ordered_items.push(announce); } From ef854efce630877e515e38c6470548f614144e6d Mon Sep 17 00:00:00 2001 From: uOJackDu Date: Wed, 8 Apr 2026 00:39:46 -0400 Subject: [PATCH 10/19] add comment --- crates/apub/activities/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/apub/activities/src/lib.rs b/crates/apub/activities/src/lib.rs index 92f93d7e51..2fd1baba8f 100644 --- a/crates/apub/activities/src/lib.rs +++ b/crates/apub/activities/src/lib.rs @@ -138,7 +138,7 @@ fn generate_hash(input: &str) -> LemmyResult { let digest = hasher.finalize(); // 32 bytes Ok(Uuid::from_bytes( digest - .get(..16) + .get(..16) // should not fail .ok_or(UntranslatedError::CouldntGenerateHash)? .try_into()?, )) From 73db5844d07bc82cdc4277474332c3a39fd30ad0 Mon Sep 17 00:00:00 2001 From: uOJackDu Date: Wed, 8 Apr 2026 00:47:27 -0400 Subject: [PATCH 11/19] edit comments --- crates/apub/activities/src/lib.rs | 1 - crates/apub/apub/src/collections/community_outbox.rs | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/apub/activities/src/lib.rs b/crates/apub/activities/src/lib.rs index 2fd1baba8f..99bbc4cde5 100644 --- a/crates/apub/activities/src/lib.rs +++ b/crates/apub/activities/src/lib.rs @@ -114,7 +114,6 @@ fn generate_announce_activity_id( let inner_kind_str = inner_kind.to_lowercase(); let uuid = if let Some(o) = object_id { - // add "announce:" in front to avoid collision with generate_activity_id let input = format!("announce:{}", o.as_str()); generate_hash(&input)? } else { diff --git a/crates/apub/apub/src/collections/community_outbox.rs b/crates/apub/apub/src/collections/community_outbox.rs index 77f5703dc6..023b078b09 100644 --- a/crates/apub/apub/src/collections/community_outbox.rs +++ b/crates/apub/apub/src/collections/community_outbox.rs @@ -68,6 +68,7 @@ impl Collection for ApubCommunityOutbox { .await { let announcable = AnnouncableActivities::CreateOrUpdatePost(create.clone()); + // create id is like https://lemmy.example.com/activities/create/{UUID} if let Ok(announce) = AnnounceActivity::new(announcable.try_into()?, owner, Some(create.id()), data) { From 5a727200cfe2b330fc39a3ba62c79b887b218dc8 Mon Sep 17 00:00:00 2001 From: uOJackDu Date: Fri, 10 Apr 2026 02:13:01 -0400 Subject: [PATCH 12/19] ensure unique activity ids for comments for Create and Update --- .../activities/src/create_or_update/comment.rs | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/crates/apub/activities/src/create_or_update/comment.rs b/crates/apub/activities/src/create_or_update/comment.rs index 7054e0b37e..a5f11f99e5 100644 --- a/crates/apub/activities/src/create_or_update/comment.rs +++ b/crates/apub/activities/src/create_or_update/comment.rs @@ -55,7 +55,21 @@ impl CreateOrUpdateNote { .await? .into(); - let id = generate_activity_id(kind.clone(), None, &context)?; + // get object_id for activity id generation + let ap_id = (*comment.ap_id.0).clone(); + let object_id = match kind { + // for Create, use the comment's ap id + CreateOrUpdateType::Create => Some(&ap_id), + // for Update, use a timestamp to ensure each Update activity is unique + CreateOrUpdateType::Update => { + let timestamp = comment.updated_at.unwrap_or(comment.published_at); // use the latest timestamp + let mut seed_url = ap_id; + seed_url.set_fragment(Some(×tamp.to_rfc3339())); + Some(&seed_url.clone()) + } + }; + + let id = generate_activity_id(kind.clone(), object_id, &context)?; let note = ApubComment(comment).into_json(&context).await?; let create_or_update = CreateOrUpdateNote { From de6b70ce405c35ad2e772a055ccda3854c301470 Mon Sep 17 00:00:00 2001 From: uOJackDu Date: Fri, 10 Apr 2026 02:13:57 -0400 Subject: [PATCH 13/19] modify naming --- crates/apub/activities/src/create_or_update/post.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/apub/activities/src/create_or_update/post.rs b/crates/apub/activities/src/create_or_update/post.rs index 56aa5b7bde..1902d6970d 100644 --- a/crates/apub/activities/src/create_or_update/post.rs +++ b/crates/apub/activities/src/create_or_update/post.rs @@ -72,14 +72,14 @@ impl CreateOrUpdatePage { .into(); // get object_id for activity id generation - let post_ap_id = (*post.ap_id.0).clone(); + let ap_id = (*post.ap_id.0).clone(); let object_id = match kind { // for Create, use the post's ap id - CreateOrUpdateType::Create => Some(&post_ap_id), + CreateOrUpdateType::Create => Some(&ap_id), // for Update, use a timestamp to ensure each Update activity is unique CreateOrUpdateType::Update => { let timestamp = post.updated_at.unwrap_or(post.published_at); // use the latest timestamp - let mut seed_url = post_ap_id; + let mut seed_url = ap_id; seed_url.set_fragment(Some(×tamp.to_rfc3339())); Some(&seed_url.clone()) } From b7ac661089eb511ee66d6cc9fff4b05be657d008 Mon Sep 17 00:00:00 2001 From: uOJackDu Date: Fri, 10 Apr 2026 03:08:14 -0400 Subject: [PATCH 14/19] add convenient function --- crates/apub/activities/src/block/block_user.rs | 4 ++-- crates/apub/activities/src/block/undo_block_user.rs | 4 ++-- crates/apub/activities/src/community/announce.rs | 4 ++-- .../apub/activities/src/community/collection_add.rs | 6 +++--- .../activities/src/community/collection_remove.rs | 6 +++--- crates/apub/activities/src/community/lock.rs | 6 +++--- crates/apub/activities/src/community/report.rs | 4 ++-- .../apub/activities/src/community/resolve_report.rs | 4 ++-- crates/apub/activities/src/community/update.rs | 6 +++--- .../src/create_or_update/private_message.rs | 4 ++-- crates/apub/activities/src/deletion/delete.rs | 4 ++-- crates/apub/activities/src/deletion/undo_delete.rs | 4 ++-- crates/apub/activities/src/following/accept.rs | 4 ++-- crates/apub/activities/src/following/follow.rs | 4 ++-- crates/apub/activities/src/following/mod.rs | 4 ++-- crates/apub/activities/src/following/reject.rs | 4 ++-- crates/apub/activities/src/following/undo_follow.rs | 4 ++-- crates/apub/activities/src/lib.rs | 12 +++++++++--- crates/apub/activities/src/voting/undo_vote.rs | 4 ++-- crates/apub/activities/src/voting/vote.rs | 4 ++-- 20 files changed, 51 insertions(+), 45 deletions(-) diff --git a/crates/apub/activities/src/block/block_user.rs b/crates/apub/activities/src/block/block_user.rs index feef86bbe3..9c3fa9bddd 100644 --- a/crates/apub/activities/src/block/block_user.rs +++ b/crates/apub/activities/src/block/block_user.rs @@ -5,7 +5,7 @@ use crate::{ block::{SiteOrCommunity, generate_cc}, check_community_deleted_or_removed, community::send_activity_in_community, - generate_activity_id, + generate_activity_id_with_object_id, protocol::block::block_user::BlockUser, send_lemmy_activity, }; @@ -56,7 +56,7 @@ impl BlockUser { kind: BlockType::Block, remove_data, summary: Some(reason), - id: generate_activity_id(BlockType::Block, None, context)?, + id: generate_activity_id_with_object_id(BlockType::Block, context)?, end_time: expires, audience: target.as_ref().right().map(|c| c.ap_id.clone().into()), }) diff --git a/crates/apub/activities/src/block/undo_block_user.rs b/crates/apub/activities/src/block/undo_block_user.rs index f87c68f207..8060e75b04 100644 --- a/crates/apub/activities/src/block/undo_block_user.rs +++ b/crates/apub/activities/src/block/undo_block_user.rs @@ -4,7 +4,7 @@ use crate::{ activity_lists::AnnouncableActivities, block::{SiteOrCommunity, generate_cc}, community::send_activity_in_community, - generate_activity_id, + generate_activity_id_with_object_id, protocol::block::{block_user::BlockUser, undo_block_user::UndoBlockUser}, send_lemmy_activity, }; @@ -47,7 +47,7 @@ impl UndoBlockUser { let block = BlockUser::new(target, user, mod_, None, reason, None, context).await?; let to = to(target)?; - let id = generate_activity_id(UndoType::Undo, None, context)?; + let id = generate_activity_id_with_object_id(UndoType::Undo, context)?; let undo = UndoBlockUser { actor: mod_.id().clone().into(), to, diff --git a/crates/apub/activities/src/community/announce.rs b/crates/apub/activities/src/community/announce.rs index 132d53afe8..ac13cae7f6 100644 --- a/crates/apub/activities/src/community/announce.rs +++ b/crates/apub/activities/src/community/announce.rs @@ -1,6 +1,6 @@ use crate::{ activity_lists::AnnouncableActivities, - generate_activity_id, + generate_activity_id_with_object_id, generate_announce_activity_id, protocol::{ IdOrNestedObject, @@ -126,7 +126,7 @@ impl AnnounceActivity { // Hack: need to convert Page into a format which can be sent as activity, which requires // adding actor field. let announcable_page = RawAnnouncableActivities { - id: generate_activity_id(AnnounceType::Announce, None, context)?, + id: generate_activity_id_with_object_id(AnnounceType::Announce, context)?, actor: c.actor.clone().into_inner(), other: serde_json::to_value(c.object)? .as_object() diff --git a/crates/apub/activities/src/community/collection_add.rs b/crates/apub/activities/src/community/collection_add.rs index 314d942e83..454e73050f 100644 --- a/crates/apub/activities/src/community/collection_add.rs +++ b/crates/apub/activities/src/community/collection_add.rs @@ -2,7 +2,7 @@ use crate::{ activity_lists::AnnouncableActivities, check_community_deleted_or_removed, community::send_activity_in_community, - generate_activity_id, + generate_activity_id_with_object_id, protocol::community::{collection_add::CollectionAdd, collection_remove::CollectionRemove}, }; use activitypub_federation::{ @@ -46,7 +46,7 @@ impl CollectionAdd { actor: &ApubPerson, context: &Data, ) -> LemmyResult<()> { - let id = generate_activity_id(AddType::Add, None, context)?; + let id = generate_activity_id_with_object_id(AddType::Add, context)?; let add = CollectionAdd { actor: actor.id().clone().into(), to: generate_to(community)?, @@ -69,7 +69,7 @@ impl CollectionAdd { actor: &ApubPerson, context: &Data, ) -> LemmyResult<()> { - let id = generate_activity_id(AddType::Add, None, context)?; + let id = generate_activity_id_with_object_id(AddType::Add, context)?; let add = CollectionAdd { actor: actor.id().clone().into(), to: generate_to(community)?, diff --git a/crates/apub/activities/src/community/collection_remove.rs b/crates/apub/activities/src/community/collection_remove.rs index 2fca9e3dfc..eeb53ff829 100644 --- a/crates/apub/activities/src/community/collection_remove.rs +++ b/crates/apub/activities/src/community/collection_remove.rs @@ -2,7 +2,7 @@ use crate::{ activity_lists::AnnouncableActivities, check_community_deleted_or_removed, community::send_activity_in_community, - generate_activity_id, + generate_activity_id_with_object_id, protocol::community::collection_remove::CollectionRemove, }; use activitypub_federation::{ @@ -43,7 +43,7 @@ impl CollectionRemove { actor: &ApubPerson, context: &Data, ) -> LemmyResult<()> { - let id = generate_activity_id(RemoveType::Remove, None, context)?; + let id = generate_activity_id_with_object_id(RemoveType::Remove, context)?; let remove = CollectionRemove { actor: actor.id().clone().into(), to: generate_to(community)?, @@ -66,7 +66,7 @@ impl CollectionRemove { actor: &ApubPerson, context: &Data, ) -> LemmyResult<()> { - let id = generate_activity_id(RemoveType::Remove, None, context)?; + let id = generate_activity_id_with_object_id(RemoveType::Remove, context)?; let remove = CollectionRemove { actor: actor.id().clone().into(), to: generate_to(community)?, diff --git a/crates/apub/activities/src/community/lock.rs b/crates/apub/activities/src/community/lock.rs index 8f9c3d0adf..64cfeb638a 100644 --- a/crates/apub/activities/src/community/lock.rs +++ b/crates/apub/activities/src/community/lock.rs @@ -3,7 +3,7 @@ use crate::{ activity_lists::AnnouncableActivities, check_community_deleted_or_removed, community::send_activity_in_community, - generate_activity_id, + generate_activity_id_with_object_id, post_or_comment_community, protocol::community::lock::{LockPageOrNote, LockType, UndoLockPageOrNote}, }; @@ -155,7 +155,7 @@ pub(crate) async fn send_lock( context: Data, ) -> LemmyResult<()> { let community: ApubCommunity = post_or_comment_community(&object, &context).await?.into(); - let id = generate_activity_id(LockType::Lock, None, &context)?; + let id = generate_activity_id_with_object_id(LockType::Lock, &context)?; let community_id = community.ap_id.inner().clone(); let ap_id = match object { PostOrComment::Left(p) => p.ap_id.clone(), @@ -175,7 +175,7 @@ pub(crate) async fn send_lock( let activity = if locked { AnnouncableActivities::Lock(lock) } else { - let id = generate_activity_id(UndoType::Undo, None, &context)?; + let id = generate_activity_id_with_object_id(UndoType::Undo, &context)?; let undo = UndoLockPageOrNote { actor: lock.actor.clone(), to: generate_to(&community)?, diff --git a/crates/apub/activities/src/community/report.rs b/crates/apub/activities/src/community/report.rs index 9280ef5680..d8e9ffa0d9 100644 --- a/crates/apub/activities/src/community/report.rs +++ b/crates/apub/activities/src/community/report.rs @@ -2,7 +2,7 @@ use super::{local_community, report_inboxes}; use crate::{ activity_lists::AnnouncableActivities, check_community_deleted_or_removed, - generate_activity_id, + generate_activity_id_with_object_id, protocol::community::{ announce::AnnounceActivity, report::{Report, ReportObject}, @@ -57,7 +57,7 @@ impl Report { context: &Data, ) -> LemmyResult { let kind = FlagType::Flag; - let id = generate_activity_id(kind.clone(), None, context)?; + let id = generate_activity_id_with_object_id(kind.clone(), context)?; Ok(Report { actor: actor.id().clone().into(), to: [receiver.id().clone().into()], diff --git a/crates/apub/activities/src/community/resolve_report.rs b/crates/apub/activities/src/community/resolve_report.rs index e04e827da1..953c4fdbfb 100644 --- a/crates/apub/activities/src/community/resolve_report.rs +++ b/crates/apub/activities/src/community/resolve_report.rs @@ -1,7 +1,7 @@ use super::{local_community, report_inboxes, verify_mod_or_admin_action}; use crate::{ activity_lists::AnnouncableActivities, - generate_activity_id, + generate_activity_id_with_object_id, protocol::community::{ announce::AnnounceActivity, report::Report, @@ -47,7 +47,7 @@ impl ResolveReport { context: Data, ) -> LemmyResult<()> { let kind = ResolveType::Resolve; - let id = generate_activity_id(kind.clone(), None, &context)?; + let id = generate_activity_id_with_object_id(kind.clone(), &context)?; let object = Report::new(&object_id, report_creator, receiver, None, &context)?; let resolve = ResolveReport { actor: actor.id().clone().into(), diff --git a/crates/apub/activities/src/community/update.rs b/crates/apub/activities/src/community/update.rs index 3b7a66363f..d40e8c222e 100644 --- a/crates/apub/activities/src/community/update.rs +++ b/crates/apub/activities/src/community/update.rs @@ -1,7 +1,7 @@ use crate::{ check_community_deleted_or_removed, community::{AnnouncableActivities, send_activity_in_community}, - generate_activity_id, + generate_activity_id_with_object_id, protocol::community::update::Update, send_lemmy_activity, }; @@ -36,7 +36,7 @@ pub(crate) async fn send_update_community( ) -> LemmyResult<()> { let community: ApubCommunity = community.into(); let actor: ApubPerson = actor.into(); - let id = generate_activity_id(UpdateType::Update, None, &context)?; + let id = generate_activity_id_with_object_id(UpdateType::Update, &context)?; let update = Update { actor: actor.id().clone().into(), to: generate_to(&community)?, @@ -66,7 +66,7 @@ pub(crate) async fn send_update_multi_community( ) -> LemmyResult<()> { let multi: ApubMultiCommunity = multi.into(); let actor: ApubPerson = actor.into(); - let id = generate_activity_id(UpdateType::Update, None, &context)?; + let id = generate_activity_id_with_object_id(UpdateType::Update, &context)?; let update = Update { actor: actor.id().clone().into(), to: vec![multi.ap_id.clone().into(), public()], diff --git a/crates/apub/activities/src/create_or_update/private_message.rs b/crates/apub/activities/src/create_or_update/private_message.rs index facc2a8446..84e5937e89 100644 --- a/crates/apub/activities/src/create_or_update/private_message.rs +++ b/crates/apub/activities/src/create_or_update/private_message.rs @@ -1,5 +1,5 @@ use crate::{ - generate_activity_id, + generate_activity_id_with_object_id, protocol::{CreateOrUpdateType, create_or_update::private_message::CreateOrUpdatePrivateMessage}, send_lemmy_activity, verify_person, @@ -24,7 +24,7 @@ pub(crate) async fn send_create_or_update_pm( let actor: ApubPerson = pm_view.creator.into(); let recipient: ApubPerson = pm_view.recipient.into(); - let id = generate_activity_id(kind.clone(), None, &context)?; + let id = generate_activity_id_with_object_id(kind.clone(), &context)?; let create_or_update = CreateOrUpdatePrivateMessage { id: id.clone(), actor: actor.id().clone().into(), diff --git a/crates/apub/activities/src/deletion/delete.rs b/crates/apub/activities/src/deletion/delete.rs index 97d4e644d7..29b8e3d818 100644 --- a/crates/apub/activities/src/deletion/delete.rs +++ b/crates/apub/activities/src/deletion/delete.rs @@ -1,7 +1,7 @@ use crate::{ MOD_ACTION_DEFAULT_REASON, deletion::{DeletableObjects, receive_delete_action, verify_delete_activity}, - generate_activity_id, + generate_activity_id_with_object_id, protocol::{IdOrNestedObject, deletion::delete::Delete}, }; use activitypub_federation::{config::Data, kinds::activity::DeleteType, traits::Activity}; @@ -82,7 +82,7 @@ impl Delete { with_replies: Option, context: &Data, ) -> LemmyResult { - let id = generate_activity_id(DeleteType::Delete, None, context)?; + let id = generate_activity_id_with_object_id(DeleteType::Delete, context)?; let cc: Option = community.map(|c| c.ap_id.clone().into()); Ok(Delete { actor: actor.ap_id.clone().into(), diff --git a/crates/apub/activities/src/deletion/undo_delete.rs b/crates/apub/activities/src/deletion/undo_delete.rs index 8f0a88d918..decbde1e8e 100644 --- a/crates/apub/activities/src/deletion/undo_delete.rs +++ b/crates/apub/activities/src/deletion/undo_delete.rs @@ -1,6 +1,6 @@ use crate::{ deletion::{DeletableObjects, receive_delete_action, verify_delete_activity}, - generate_activity_id, + generate_activity_id_with_object_id, protocol::deletion::{delete::Delete, undo_delete::UndoDelete}, }; use activitypub_federation::{config::Data, kinds::activity::UndoType, traits::Activity}; @@ -72,7 +72,7 @@ impl UndoDelete { context, )?; - let id = generate_activity_id(UndoType::Undo, None, context)?; + let id = generate_activity_id_with_object_id(UndoType::Undo, context)?; let cc: Option = community.map(|c| c.ap_id.clone().into()); Ok(UndoDelete { actor: actor.ap_id.clone().into(), diff --git a/crates/apub/activities/src/following/accept.rs b/crates/apub/activities/src/following/accept.rs index ceca5a5c56..c90f016c72 100644 --- a/crates/apub/activities/src/following/accept.rs +++ b/crates/apub/activities/src/following/accept.rs @@ -1,6 +1,6 @@ use crate::{ check_community_deleted_or_removed, - generate_activity_id, + generate_activity_id_with_object_id, protocol::following::{accept::AcceptFollow, follow::Follow}, send_lemmy_activity, }; @@ -27,7 +27,7 @@ impl AcceptFollow { to: Some([person.id().clone().into()]), object: follow, kind: AcceptType::Accept, - id: generate_activity_id(AcceptType::Accept, None, context)?, + id: generate_activity_id_with_object_id(AcceptType::Accept, context)?, }; let inbox = ActivitySendTargets::to_inbox(person.shared_inbox_or_inbox()); send_lemmy_activity(context, accept, &target, inbox, true).await diff --git a/crates/apub/activities/src/following/follow.rs b/crates/apub/activities/src/following/follow.rs index 3a71d1503f..5a9bc1b992 100644 --- a/crates/apub/activities/src/following/follow.rs +++ b/crates/apub/activities/src/following/follow.rs @@ -1,6 +1,6 @@ use crate::{ check_community_deleted_or_removed, - generate_activity_id, + generate_activity_id_with_object_id, protocol::following::{accept::AcceptFollow, follow::Follow}, send_lemmy_activity, }; @@ -40,7 +40,7 @@ impl Follow { object: target.id().clone().into(), to: Some([target.id().clone().into()]), kind: FollowType::Follow, - id: generate_activity_id(FollowType::Follow, None, context)?, + id: generate_activity_id_with_object_id(FollowType::Follow, context)?, }) } diff --git a/crates/apub/activities/src/following/mod.rs b/crates/apub/activities/src/following/mod.rs index ea56e85348..851dded37b 100644 --- a/crates/apub/activities/src/following/mod.rs +++ b/crates/apub/activities/src/following/mod.rs @@ -1,4 +1,4 @@ -use super::{generate_activity_id, send_lemmy_activity}; +use super::{generate_activity_id_with_object_id, send_lemmy_activity}; use crate::protocol::following::{ accept::AcceptFollow, follow::Follow, @@ -51,7 +51,7 @@ pub async fn send_accept_or_reject_follow( to: Some([community.ap_id.clone().into()]), object: community.ap_id.into(), kind: FollowType::Follow, - id: generate_activity_id(FollowType::Follow, None, context)?, + id: generate_activity_id_with_object_id(FollowType::Follow, context)?, }; if accepted { AcceptFollow::send(follow, context).await diff --git a/crates/apub/activities/src/following/reject.rs b/crates/apub/activities/src/following/reject.rs index 318adbbfaf..5d379893be 100644 --- a/crates/apub/activities/src/following/reject.rs +++ b/crates/apub/activities/src/following/reject.rs @@ -1,7 +1,7 @@ use super::send_activity_from_user_or_community_or_multi; use crate::{ check_community_deleted_or_removed, - generate_activity_id, + generate_activity_id_with_object_id, protocol::following::{follow::Follow, reject::RejectFollow}, }; use activitypub_federation::{ @@ -27,7 +27,7 @@ impl RejectFollow { to: Some([person.id().clone().into()]), object: follow, kind: RejectType::Reject, - id: generate_activity_id(RejectType::Reject, None, context)?, + id: generate_activity_id_with_object_id(RejectType::Reject, context)?, }; let inbox = ActivitySendTargets::to_inbox(person.shared_inbox_or_inbox()); send_activity_from_user_or_community_or_multi(context, reject, user_or_community, inbox).await diff --git a/crates/apub/activities/src/following/undo_follow.rs b/crates/apub/activities/src/following/undo_follow.rs index 062edfb140..c84931f393 100644 --- a/crates/apub/activities/src/following/undo_follow.rs +++ b/crates/apub/activities/src/following/undo_follow.rs @@ -1,6 +1,6 @@ use crate::{ check_community_deleted_or_removed, - generate_activity_id, + generate_activity_id_with_object_id, protocol::following::{follow::Follow, undo_follow::UndoFollow}, send_lemmy_activity, }; @@ -39,7 +39,7 @@ impl UndoFollow { to: Some([target.id().clone().into()]), object, kind: UndoType::Undo, - id: generate_activity_id(UndoType::Undo, None, context)?, + id: generate_activity_id_with_object_id(UndoType::Undo, context)?, }; let inbox = ActivitySendTargets::to_inbox(target.shared_inbox_or_inbox()); send_lemmy_activity(context, undo, actor, inbox, true).await diff --git a/crates/apub/activities/src/lib.rs b/crates/apub/activities/src/lib.rs index 99bbc4cde5..c8d9d3d882 100644 --- a/crates/apub/activities/src/lib.rs +++ b/crates/apub/activities/src/lib.rs @@ -81,6 +81,14 @@ pub(crate) fn check_community_deleted_or_removed(community: &Community) -> Lemmy } } +/// convenient function for generate_activity_id +fn generate_activity_id_with_object_id(kind: T, context: &LemmyContext) -> LemmyResult +where + T: ToString, +{ + generate_activity_id::(kind, None, context) +} + /// Generate a unique ID for an activity, in the format: /// `http(s)://example.com/receive/create/202daf0a-1489-45df-8d2e-c8a3173fed36` fn generate_activity_id( @@ -111,8 +119,6 @@ fn generate_announce_activity_id( protocol_and_hostname: &str, object_id: Option<&Url>, ) -> LemmyResult { - let inner_kind_str = inner_kind.to_lowercase(); - let uuid = if let Some(o) = object_id { let input = format!("announce:{}", o.as_str()); generate_hash(&input)? @@ -124,7 +130,7 @@ fn generate_announce_activity_id( "{}/activities/{}/{}/{}", protocol_and_hostname, AnnounceType::Announce.to_string().to_lowercase(), - inner_kind_str, + inner_kind.to_lowercase(), uuid ); Url::parse(&id).map_err(|e| LemmyError::from(anyhow::anyhow!(e))) diff --git a/crates/apub/activities/src/voting/undo_vote.rs b/crates/apub/activities/src/voting/undo_vote.rs index 26c555d802..9c0dd31e82 100644 --- a/crates/apub/activities/src/voting/undo_vote.rs +++ b/crates/apub/activities/src/voting/undo_vote.rs @@ -1,6 +1,6 @@ use crate::{ check_community_deleted_or_removed, - generate_activity_id, + generate_activity_id_with_object_id, protocol::voting::{undo_vote::UndoVote, vote::Vote}, voting::{undo_vote_comment, undo_vote_post}, }; @@ -29,7 +29,7 @@ impl UndoVote { actor: actor.id().clone().into(), object: vote, kind: UndoType::Undo, - id: generate_activity_id(UndoType::Undo, None, context)?, + id: generate_activity_id_with_object_id(UndoType::Undo, context)?, audience: Some(community.ap_id.clone().into()), }) } diff --git a/crates/apub/activities/src/voting/vote.rs b/crates/apub/activities/src/voting/vote.rs index 68a1df934c..337aceb471 100644 --- a/crates/apub/activities/src/voting/vote.rs +++ b/crates/apub/activities/src/voting/vote.rs @@ -1,6 +1,6 @@ use crate::{ check_community_deleted_or_removed, - generate_activity_id, + generate_activity_id_with_object_id, protocol::voting::vote::{Vote, VoteType}, voting::{undo_vote_comment, undo_vote_post, vote_comment, vote_post}, }; @@ -31,7 +31,7 @@ impl Vote { actor: actor.id().clone().into(), object: object_id, kind: kind.clone(), - id: generate_activity_id(kind, None, context)?, + id: generate_activity_id_with_object_id(kind, context)?, audience: Some(community.ap_id.clone().into()), }) } From f1b062ffa4dbd9a6813a8b0742f48cb926369904 Mon Sep 17 00:00:00 2001 From: uOJackDu Date: Fri, 17 Apr 2026 20:34:27 -0400 Subject: [PATCH 15/19] modify hashing to use digest to string directly --- crates/apub/activities/src/.DS_Store | Bin 0 -> 6148 bytes crates/apub/activities/src/lib.rs | 41 ++++++++++++--------------- crates/utils/src/error.rs | 1 - 3 files changed, 18 insertions(+), 24 deletions(-) create mode 100644 crates/apub/activities/src/.DS_Store diff --git a/crates/apub/activities/src/.DS_Store b/crates/apub/activities/src/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..1e2fa48773fd44b46fad07951ab68870b2cdf820 GIT binary patch literal 6148 zcmeHK%}T>S5T3176N-?7qQ?cV1^XkScnMWsz=$4HYGQ&0W45%ZJ(NPO`a-^m&*RMQ zRxDM#h+vt4nQwP?W|#Rk>}CN#bcS&gzykmWm9XNX`9f%%bWSSPLn!nd1tib~1%0$E z|A7J8I}dJg#}WwP^ZpH?2eI)*qd_mqRD`jwQ7=meMWgX1DofSnl~u=aYR+|VuSP); z7Q;a&>|avvOsOoIw!`Q=O-5aB{Yd3SnC3}eC!}$LAy*e^9;;DD4fA-Qa|1KrxUSpv zHpke+BD@H5o1W^oEH*!Z!`r=Xu_ zD!;?n={(bXMrME+UZ1b<{XWuom5>D6 z^p+r$7JZAQK^#F5CKb`73j4$mCLR6K#`zXYgC-q>UK!`HD+~KV5qfp>OC1ivH^`P5 zU4whu^*16*7u9c`as3a6u8vIDXhQ5k1 fmagJ0R4wS2)IjttmIl#-!ao9<1~$yVpEB?c4;@l> literal 0 HcmV?d00001 diff --git a/crates/apub/activities/src/lib.rs b/crates/apub/activities/src/lib.rs index c8d9d3d882..3ed43678de 100644 --- a/crates/apub/activities/src/lib.rs +++ b/crates/apub/activities/src/lib.rs @@ -102,15 +102,19 @@ where let hostname = context.settings().get_protocol_and_hostname(); let kind_str = kind.to_string().to_lowercase(); - let uuid = if let Some(o) = object_id { + let uuid_str = if let Some(o) = object_id { let input = format!("{}:{}", kind_str, o.as_str()); - generate_hash(&input)? + // hash + let mut hasher = Sha256::new(); + hasher.update(input); + let digest = hasher.finalize(); // 32 bytes + format!("{:x}", digest) } else { - Uuid::new_v4() + Uuid::new_v4().to_string() }; - let id = format!("{}/activities/{}/{}", hostname, kind_str, uuid); - Url::parse(&id).map_err(|e| LemmyError::from(anyhow::anyhow!(e))) + let id = format!("{}/activities/{}/{}", hostname, kind_str, uuid_str); + Ok(Url::parse(&id)?) } /// like generate_activity_id but also add the inner kind for easier debugging @@ -119,11 +123,15 @@ fn generate_announce_activity_id( protocol_and_hostname: &str, object_id: Option<&Url>, ) -> LemmyResult { - let uuid = if let Some(o) = object_id { + let uuid_str = if let Some(o) = object_id { let input = format!("announce:{}", o.as_str()); - generate_hash(&input)? + // hash + let mut hasher = Sha256::new(); + hasher.update(input); + let digest = hasher.finalize(); + format!("{:x}", digest) } else { - Uuid::new_v4() + Uuid::new_v4().to_string() }; let id = format!( @@ -131,22 +139,9 @@ fn generate_announce_activity_id( protocol_and_hostname, AnnounceType::Announce.to_string().to_lowercase(), inner_kind.to_lowercase(), - uuid + uuid_str ); - Url::parse(&id).map_err(|e| LemmyError::from(anyhow::anyhow!(e))) -} - -/// generate a hash from input string, returning first 16 bytes as UUID -fn generate_hash(input: &str) -> LemmyResult { - let mut hasher = Sha256::new(); - hasher.update(input); - let digest = hasher.finalize(); // 32 bytes - Ok(Uuid::from_bytes( - digest - .get(..16) // should not fail - .ok_or(UntranslatedError::CouldntGenerateHash)? - .try_into()?, - )) + Ok(Url::parse(&id)?) } async fn send_lemmy_activity( diff --git a/crates/utils/src/error.rs b/crates/utils/src/error.rs index b2fd19898f..273d4b788c 100644 --- a/crates/utils/src/error.rs +++ b/crates/utils/src/error.rs @@ -158,7 +158,6 @@ pub enum UntranslatedError { /// A remote community sent an activity to us, but actually no local user follows the community /// so the activity was rejected. CommunityHasNoFollowers(String), - CouldntGenerateHash, } cfg_if! { From 3195399363c924bc849a4c0631101af752ebf9e7 Mon Sep 17 00:00:00 2001 From: uOJackDu Date: Sat, 18 Apr 2026 01:04:48 -0400 Subject: [PATCH 16/19] use timestamps for create activities --- crates/apub/activities/src/create_or_update/post.rs | 12 ++++++++---- crates/apub/apub/src/collections/community_outbox.rs | 3 --- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/crates/apub/activities/src/create_or_update/post.rs b/crates/apub/activities/src/create_or_update/post.rs index 1902d6970d..82be49761e 100644 --- a/crates/apub/activities/src/create_or_update/post.rs +++ b/crates/apub/activities/src/create_or_update/post.rs @@ -44,10 +44,14 @@ impl CreateOrUpdatePage { actor: &ApubPerson, community: &ApubCommunity, kind: CreateOrUpdateType, - object_id: Option<&Url>, context: &Data, ) -> LemmyResult { - let id = generate_activity_id(kind.clone(), object_id, context)?; + // get object_id + let timestamp = post.updated_at.unwrap_or(post.published_at); // use the latest timestamp + let mut object_id = (*post.ap_id.0).clone(); + object_id.set_fragment(Some(×tamp.to_rfc3339())); + + let id = generate_activity_id(kind.clone(), Some(&object_id), context)?; Ok(CreateOrUpdatePage { actor: actor.id().clone().into(), to: generate_to(community)?, @@ -73,7 +77,7 @@ impl CreateOrUpdatePage { // get object_id for activity id generation let ap_id = (*post.ap_id.0).clone(); - let object_id = match kind { + let _object_id = match kind { // for Create, use the post's ap id CreateOrUpdateType::Create => Some(&ap_id), // for Update, use a timestamp to ensure each Update activity is unique @@ -86,7 +90,7 @@ impl CreateOrUpdatePage { }; let create_or_update = - CreateOrUpdatePage::new(post.into(), &person, &community, kind, object_id, &context).await?; + CreateOrUpdatePage::new(post.into(), &person, &community, kind, &context).await?; let inboxes = tagged_user_inboxes(&create_or_update.object.tag, &context).await?; let activity = AnnouncableActivities::CreateOrUpdatePost(create_or_update); send_activity_in_community(activity, &person, &community, inboxes, false, &context).await?; diff --git a/crates/apub/apub/src/collections/community_outbox.rs b/crates/apub/apub/src/collections/community_outbox.rs index 023b078b09..750126b1bc 100644 --- a/crates/apub/apub/src/collections/community_outbox.rs +++ b/crates/apub/apub/src/collections/community_outbox.rs @@ -54,15 +54,12 @@ impl Collection for ApubCommunityOutbox { let mut ordered_items = vec![]; for post_view in post_views { - let post_ap_id = Some(&*post_view.post.ap_id.0.clone()); - // ignore errors, in particular if post creator was deleted if let Ok(create) = CreateOrUpdatePage::new( post_view.post.into(), &post_view.creator.into(), owner, CreateOrUpdateType::Create, - post_ap_id, data, ) .await From 6039b711fc15e8e274a6b70cd602f923226b74b0 Mon Sep 17 00:00:00 2001 From: uOJackDu Date: Tue, 28 Apr 2026 20:52:21 -0400 Subject: [PATCH 17/19] add hashing to create or update private message --- .../activities/src/create_or_update/private_message.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/crates/apub/activities/src/create_or_update/private_message.rs b/crates/apub/activities/src/create_or_update/private_message.rs index 84e5937e89..b9acf34e21 100644 --- a/crates/apub/activities/src/create_or_update/private_message.rs +++ b/crates/apub/activities/src/create_or_update/private_message.rs @@ -1,5 +1,5 @@ use crate::{ - generate_activity_id_with_object_id, + generate_activity_id, protocol::{CreateOrUpdateType, create_or_update::private_message::CreateOrUpdatePrivateMessage}, send_lemmy_activity, verify_person, @@ -24,7 +24,13 @@ pub(crate) async fn send_create_or_update_pm( let actor: ApubPerson = pm_view.creator.into(); let recipient: ApubPerson = pm_view.recipient.into(); - let id = generate_activity_id_with_object_id(kind.clone(), &context)?; + // get object_id + let pm = &pm_view.private_message; + let timestamp = pm.updated_at.unwrap_or(pm.published_at); + let mut object_id = (*pm.ap_id.0).clone(); + object_id.set_fragment(Some(×tamp.to_rfc3339())); + + let id = generate_activity_id(kind.clone(), Some(&object_id), &context)?; let create_or_update = CreateOrUpdatePrivateMessage { id: id.clone(), actor: actor.id().clone().into(), From 10bdad24039c63f8245f8aa5d64e96cd4d13823c Mon Sep 17 00:00:00 2001 From: uOJackDu Date: Tue, 28 Apr 2026 21:20:08 -0400 Subject: [PATCH 18/19] modify comment hashing to using timestamp --- .../src/create_or_update/comment.rs | 19 +++++-------------- 1 file changed, 5 insertions(+), 14 deletions(-) diff --git a/crates/apub/activities/src/create_or_update/comment.rs b/crates/apub/activities/src/create_or_update/comment.rs index a5f11f99e5..83518e9da6 100644 --- a/crates/apub/activities/src/create_or_update/comment.rs +++ b/crates/apub/activities/src/create_or_update/comment.rs @@ -55,21 +55,12 @@ impl CreateOrUpdateNote { .await? .into(); - // get object_id for activity id generation - let ap_id = (*comment.ap_id.0).clone(); - let object_id = match kind { - // for Create, use the comment's ap id - CreateOrUpdateType::Create => Some(&ap_id), - // for Update, use a timestamp to ensure each Update activity is unique - CreateOrUpdateType::Update => { - let timestamp = comment.updated_at.unwrap_or(comment.published_at); // use the latest timestamp - let mut seed_url = ap_id; - seed_url.set_fragment(Some(×tamp.to_rfc3339())); - Some(&seed_url.clone()) - } - }; + // get object_id + let timestamp = comment.updated_at.unwrap_or(comment.published_at); + let mut object_id = (*comment.ap_id.0).clone(); + object_id.set_fragment(Some(×tamp.to_rfc3339())); - let id = generate_activity_id(kind.clone(), object_id, &context)?; + let id = generate_activity_id(kind.clone(), Some(&object_id), &context)?; let note = ApubComment(comment).into_json(&context).await?; let create_or_update = CreateOrUpdateNote { From 2de5147196caddcba79ea878227849f48557f183 Mon Sep 17 00:00:00 2001 From: uOJackDu Date: Wed, 6 May 2026 15:46:56 -0400 Subject: [PATCH 19/19] move timstamp logic to impl --- .../src/create_or_update/comment.rs | 4 +--- .../activities/src/create_or_update/post.rs | 19 +------------------ .../src/create_or_update/private_message.rs | 4 +--- crates/db_schema/src/impls/comment.rs | 8 ++++++++ crates/db_schema/src/impls/post.rs | 8 ++++++++ crates/db_schema/src/impls/private_message.rs | 8 ++++++++ 6 files changed, 27 insertions(+), 24 deletions(-) diff --git a/crates/apub/activities/src/create_or_update/comment.rs b/crates/apub/activities/src/create_or_update/comment.rs index 83518e9da6..3a131fe5ed 100644 --- a/crates/apub/activities/src/create_or_update/comment.rs +++ b/crates/apub/activities/src/create_or_update/comment.rs @@ -56,9 +56,7 @@ impl CreateOrUpdateNote { .into(); // get object_id - let timestamp = comment.updated_at.unwrap_or(comment.published_at); - let mut object_id = (*comment.ap_id.0).clone(); - object_id.set_fragment(Some(×tamp.to_rfc3339())); + let object_id = comment.activity_object_id(); let id = generate_activity_id(kind.clone(), Some(&object_id), &context)?; let note = ApubComment(comment).into_json(&context).await?; diff --git a/crates/apub/activities/src/create_or_update/post.rs b/crates/apub/activities/src/create_or_update/post.rs index ca162ae600..c270592bc2 100644 --- a/crates/apub/activities/src/create_or_update/post.rs +++ b/crates/apub/activities/src/create_or_update/post.rs @@ -47,10 +47,7 @@ impl CreateOrUpdatePage { context: &Data, ) -> LemmyResult { // get object_id - let timestamp = post.updated_at.unwrap_or(post.published_at); // use the latest timestamp - let mut object_id = (*post.ap_id.0).clone(); - object_id.set_fragment(Some(×tamp.to_rfc3339())); - + let object_id = post.activity_object_id(); let id = generate_activity_id(kind.clone(), Some(&object_id), context)?; Ok(CreateOrUpdatePage { actor: actor.id().clone().into(), @@ -75,20 +72,6 @@ impl CreateOrUpdatePage { .await? .into(); - // get object_id for activity id generation - let ap_id = (*post.ap_id.0).clone(); - let _object_id = match kind { - // for Create, use the post's ap id - CreateOrUpdateType::Create => Some(&ap_id), - // for Update, use a timestamp to ensure each Update activity is unique - CreateOrUpdateType::Update => { - let timestamp = post.updated_at.unwrap_or(post.published_at); // use the latest timestamp - let mut seed_url = ap_id; - seed_url.set_fragment(Some(×tamp.to_rfc3339())); - Some(&seed_url.clone()) - } - }; - let create_or_update = CreateOrUpdatePage::new(post.into(), &person, &community, kind, &context).await?; let inboxes = tagged_user_inboxes(&create_or_update.object.tag, &context).await?; diff --git a/crates/apub/activities/src/create_or_update/private_message.rs b/crates/apub/activities/src/create_or_update/private_message.rs index b9acf34e21..1a2adb0a9c 100644 --- a/crates/apub/activities/src/create_or_update/private_message.rs +++ b/crates/apub/activities/src/create_or_update/private_message.rs @@ -26,9 +26,7 @@ pub(crate) async fn send_create_or_update_pm( // get object_id let pm = &pm_view.private_message; - let timestamp = pm.updated_at.unwrap_or(pm.published_at); - let mut object_id = (*pm.ap_id.0).clone(); - object_id.set_fragment(Some(×tamp.to_rfc3339())); + let object_id = pm.activity_object_id(); let id = generate_activity_id(kind.clone(), Some(&object_id), &context)?; let create_or_update = CreateOrUpdatePrivateMessage { diff --git a/crates/db_schema/src/impls/comment.rs b/crates/db_schema/src/impls/comment.rs index 034427e847..2faad1a493 100644 --- a/crates/db_schema/src/impls/comment.rs +++ b/crates/db_schema/src/impls/comment.rs @@ -320,6 +320,14 @@ impl Comment { .await .with_lemmy_type(LemmyErrorType::NotFound) } + + /// Return the comment's ap_id with a timestamp fragment appended. + pub fn activity_object_id(&self) -> Url { + let timestamp = self.updated_at.unwrap_or(self.published_at); + let mut object_id = (*self.ap_id.0).clone(); + object_id.set_fragment(Some(×tamp.to_rfc3339())); + object_id + } } impl Crud for Comment { diff --git a/crates/db_schema/src/impls/post.rs b/crates/db_schema/src/impls/post.rs index 1dc4742c95..e980c9f015 100644 --- a/crates/db_schema/src/impls/post.rs +++ b/crates/db_schema/src/impls/post.rs @@ -343,6 +343,14 @@ impl Post { } Ok(()) } + + /// Return the post's ap_id with a timestamp fragment appended. + pub fn activity_object_id(&self) -> Url { + let timestamp = self.updated_at.unwrap_or(self.published_at); + let mut object_id = (*self.ap_id.0).clone(); + object_id.set_fragment(Some(×tamp.to_rfc3339())); + object_id + } } impl Likeable for PostActions { diff --git a/crates/db_schema/src/impls/private_message.rs b/crates/db_schema/src/impls/private_message.rs index c1c81fe9e4..394ed1d653 100644 --- a/crates/db_schema/src/impls/private_message.rs +++ b/crates/db_schema/src/impls/private_message.rs @@ -109,6 +109,14 @@ impl PrivateMessage { self.deleted_by_recipient = false; } } + + /// Return the private message's ap_id with a timestamp fragment appended. + pub fn activity_object_id(&self) -> Url { + let timestamp = self.updated_at.unwrap_or(self.published_at); + let mut object_id = (*self.ap_id.0).clone(); + object_id.set_fragment(Some(×tamp.to_rfc3339())); + object_id + } } #[cfg(test)]