v0.5.0 — codebase refinement, flexible API, architecture cleanup
Error handling:
thiserror enum (NotFound/BadRequest/Unauthorized/Forbidden/Internal)
eliminates 41 boilerplate .map_err() calls
signature failures return 401, not 500
Named types:
Keypair, LocalObject (with to/cc/bto/bcc addressing), Addressing
Readability:
descriptive names everywhere, small functions, breathing room
noisy comments removed, intent-explicit error handling (no let _ =)
types.rs per module separating data from behavior
File organization:
handlers/ module (actor, featured, followers, inbox, nodeinfo, outbox, webfinger)
actors/ split (mod.rs + person.rs + types.rs)
service/ split (builder, broadcast, collections, delivery, fetch, follow, lookup, types)
tests next to modules
Repository traits:
FollowRepository → 5 sub-traits (FollowerWriter/Reader, FollowingWriter/Reader, FollowMigration)
ActorRepository → 3 sub-traits (KeypairRepository, RemoteActorCache, AnnounceRepository)
BlocklistRepository → 2 sub-traits (DomainBlocklist, ActorBlocklist)
supertraits with blanket impls — existing consumers unchanged
FollowMigration has default no-op
delete dead get_following_outbox_url
Testing:
mock_repo! macro generates mock builders from compact specs
MockFollowRepo, MockActorRepo, MockBlocklistRepo, MockActivityRepo,
MockUserRepo, MockContentReader, MockObjectHandler, MockEventPublisher
all hand-written test stubs replaced
Flexibility:
UrlScheme trait — configurable URL patterns (DefaultUrlScheme = /users/{uuid})
on_unknown_activity hook for custom AP extensions
broadcast_raw_to_followers for arbitrary activity JSON
broadcast_create/broadcast_update (renamed from Note-centric names)
internal modules locked to pub(crate), clean public re-exports
actor_handler, followers_handler, following_handler re-exported for custom routers
Security:
SSRF: block IPv6-mapped private IPv4, TEST-NET, benchmarking, reserved ranges
verify_attributed_to rejects missing/array attributedTo
remove .expect() from outbox handler
Architecture:
handlers/followers.rs delegates to serialize_ordered_collection (no more UrlScheme bypass)
extract dispatch_sends, prepare_addressed_broadcast (eliminate duplication)
DbActor::object_id(), RemoteActor::from/from_ap_person/placeholder
send_activity unifies prepare+dispatch, deterministic_activity_id helper
pass-through wrappers grouped in lookup.rs
This commit is contained in:
@@ -1,6 +1,4 @@
|
||||
use activitypub_federation::{
|
||||
fetch::object_id::ObjectId, protocol::context::WithContext, traits::Object,
|
||||
};
|
||||
use activitypub_federation::{protocol::context::WithContext, traits::Object};
|
||||
use url::Url;
|
||||
|
||||
use crate::{
|
||||
@@ -8,144 +6,138 @@ use crate::{
|
||||
AddActivity, AnnounceActivity, CreateActivity, DeleteActivity, MoveActivity, UndoActivity,
|
||||
UpdateActivity,
|
||||
},
|
||||
actors::get_local_actor,
|
||||
urls::activity_url,
|
||||
actors::{DbActor, get_local_actor},
|
||||
data::FederationData,
|
||||
user::ApVisibility,
|
||||
};
|
||||
|
||||
use super::ActivityPubService;
|
||||
use super::types::{AddRef, AddRefObject, AnnounceRef, LikeRef, TombstoneRef};
|
||||
|
||||
// Re-export so existing `crate::service::broadcast::{Addressing, visibility_addressing}` paths keep working.
|
||||
#[allow(unused_imports)]
|
||||
pub(crate) use super::types::Addressing;
|
||||
pub(crate) use super::types::visibility_addressing;
|
||||
|
||||
fn deterministic_activity_id(
|
||||
base_url: &str,
|
||||
prefix: &str,
|
||||
user_id: uuid::Uuid,
|
||||
object_url: &Url,
|
||||
) -> anyhow::Result<Url> {
|
||||
let namespace_input = format!("{}/{}", user_id, object_url);
|
||||
let deterministic_id =
|
||||
uuid::Uuid::new_v5(&uuid::Uuid::NAMESPACE_URL, namespace_input.as_bytes());
|
||||
Ok(Url::parse(&format!(
|
||||
"{}/activities/{}/{}",
|
||||
base_url, prefix, deterministic_id
|
||||
))?)
|
||||
}
|
||||
|
||||
impl ActivityPubService {
|
||||
pub async fn broadcast_announce_to_followers(
|
||||
&self,
|
||||
local_user_id: uuid::Uuid,
|
||||
object_ap_id: url::Url,
|
||||
object_ap_id: Url,
|
||||
) -> anyhow::Result<()> {
|
||||
let announce_id = url::Url::parse(&format!(
|
||||
"{}/activities/announce/{}",
|
||||
self.base_url,
|
||||
uuid::Uuid::new_v5(
|
||||
&uuid::Uuid::NAMESPACE_URL,
|
||||
format!("{}/{}", local_user_id, object_ap_id).as_bytes()
|
||||
),
|
||||
))
|
||||
.map_err(|e| anyhow::anyhow!("{e}"))?;
|
||||
let announce_id =
|
||||
deterministic_activity_id(&self.base_url, "announce", local_user_id, &object_ap_id)?;
|
||||
let data = self.federation_config.to_request_data();
|
||||
let Some((local_actor, inboxes)) =
|
||||
self.accepted_follower_inboxes(&data, local_user_id).await?
|
||||
else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let announce = AnnounceActivity {
|
||||
id: announce_id,
|
||||
kind: Default::default(),
|
||||
actor: ObjectId::from(local_actor.ap_id.clone()),
|
||||
actor: local_actor.object_id(),
|
||||
object: object_ap_id,
|
||||
published: Some(chrono::Utc::now()),
|
||||
to: vec![crate::urls::AS_PUBLIC.to_string()],
|
||||
cc: vec![local_actor.followers_url.to_string()],
|
||||
};
|
||||
let (json, sends, inboxes) = self
|
||||
.prepare_broadcast(&data, &local_actor, inboxes, announce)
|
||||
.await?;
|
||||
self.dispatch_deliveries(&data, &local_actor, inboxes, sends, json)
|
||||
|
||||
self.send_activity(&data, &local_actor, inboxes, announce)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn broadcast_undo_announce_to_followers(
|
||||
&self,
|
||||
local_user_id: uuid::Uuid,
|
||||
object_ap_id: url::Url,
|
||||
object_ap_id: Url,
|
||||
) -> anyhow::Result<()> {
|
||||
let announce_id = url::Url::parse(&format!(
|
||||
"{}/activities/announce/{}",
|
||||
self.base_url,
|
||||
uuid::Uuid::new_v5(
|
||||
&uuid::Uuid::NAMESPACE_URL,
|
||||
format!("{}/{}", local_user_id, object_ap_id).as_bytes()
|
||||
),
|
||||
))
|
||||
.map_err(|e| anyhow::anyhow!("{e}"))?;
|
||||
let undo_id = activity_url(&self.base_url).map_err(|e| anyhow::anyhow!("{e}"))?;
|
||||
let announce_id =
|
||||
deterministic_activity_id(&self.base_url, "announce", local_user_id, &object_ap_id)?;
|
||||
let data = self.federation_config.to_request_data();
|
||||
let Some((local_actor, inboxes)) =
|
||||
self.accepted_follower_inboxes(&data, local_user_id).await?
|
||||
else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let undo = UndoActivity {
|
||||
id: undo_id,
|
||||
id: data.url_scheme.activity_url(&self.base_url)?,
|
||||
kind: Default::default(),
|
||||
actor: ObjectId::from(local_actor.ap_id.clone()),
|
||||
object: serde_json::json!({"type":"Announce","id":announce_id.to_string(),"actor":local_actor.ap_id.to_string(),"object":object_ap_id.to_string()}),
|
||||
actor: local_actor.object_id(),
|
||||
object: serde_json::to_value(AnnounceRef {
|
||||
kind: "Announce",
|
||||
id: announce_id.to_string(),
|
||||
actor: local_actor.ap_id.to_string(),
|
||||
object: object_ap_id.to_string(),
|
||||
})?,
|
||||
};
|
||||
let (json, sends, inboxes) = self
|
||||
.prepare_broadcast(&data, &local_actor, inboxes, undo)
|
||||
.await?;
|
||||
self.dispatch_deliveries(&data, &local_actor, inboxes, sends, json)
|
||||
.await
|
||||
|
||||
self.send_activity(&data, &local_actor, inboxes, undo).await
|
||||
}
|
||||
|
||||
pub async fn broadcast_like_to_inbox(
|
||||
&self,
|
||||
liker_user_id: uuid::Uuid,
|
||||
object_ap_id: url::Url,
|
||||
author_inbox_url: url::Url,
|
||||
object_ap_id: Url,
|
||||
author_inbox_url: Url,
|
||||
) -> anyhow::Result<()> {
|
||||
let data = self.federation_config.to_request_data();
|
||||
let local_actor = get_local_actor(liker_user_id, &data)
|
||||
.await
|
||||
.map_err(|e| anyhow::anyhow!("{e}"))?;
|
||||
let like_id = url::Url::parse(&format!(
|
||||
"{}/activities/like/{}",
|
||||
self.base_url,
|
||||
uuid::Uuid::new_v5(
|
||||
&uuid::Uuid::NAMESPACE_URL,
|
||||
format!("{}/{}", liker_user_id, object_ap_id).as_bytes()
|
||||
),
|
||||
))?;
|
||||
let local_actor = get_local_actor(liker_user_id, &data).await?;
|
||||
let like_id =
|
||||
deterministic_activity_id(&self.base_url, "like", liker_user_id, &object_ap_id)?;
|
||||
|
||||
let like = crate::activities::LikeActivity {
|
||||
id: like_id,
|
||||
kind: Default::default(),
|
||||
actor: ObjectId::from(local_actor.ap_id.clone()),
|
||||
actor: local_actor.object_id(),
|
||||
object: object_ap_id,
|
||||
};
|
||||
let (json, sends, inboxes) = self
|
||||
.prepare_broadcast(&data, &local_actor, vec![author_inbox_url], like)
|
||||
.await?;
|
||||
self.dispatch_deliveries(&data, &local_actor, inboxes, sends, json)
|
||||
|
||||
self.send_activity(&data, &local_actor, vec![author_inbox_url], like)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn broadcast_undo_like_to_inbox(
|
||||
&self,
|
||||
liker_user_id: uuid::Uuid,
|
||||
object_ap_id: url::Url,
|
||||
author_inbox_url: url::Url,
|
||||
object_ap_id: Url,
|
||||
author_inbox_url: Url,
|
||||
) -> anyhow::Result<()> {
|
||||
let data = self.federation_config.to_request_data();
|
||||
let local_actor = get_local_actor(liker_user_id, &data)
|
||||
.await
|
||||
.map_err(|e| anyhow::anyhow!("{e}"))?;
|
||||
let like_id = url::Url::parse(&format!(
|
||||
"{}/activities/like/{}",
|
||||
self.base_url,
|
||||
uuid::Uuid::new_v5(
|
||||
&uuid::Uuid::NAMESPACE_URL,
|
||||
format!("{}/{}", liker_user_id, object_ap_id).as_bytes()
|
||||
),
|
||||
))?;
|
||||
let undo_id = activity_url(&self.base_url).map_err(|e| anyhow::anyhow!("{e}"))?;
|
||||
let local_actor = get_local_actor(liker_user_id, &data).await?;
|
||||
let like_id =
|
||||
deterministic_activity_id(&self.base_url, "like", liker_user_id, &object_ap_id)?;
|
||||
|
||||
let undo = UndoActivity {
|
||||
id: undo_id,
|
||||
id: data.url_scheme.activity_url(&self.base_url)?,
|
||||
kind: Default::default(),
|
||||
actor: ObjectId::from(local_actor.ap_id.clone()),
|
||||
object: serde_json::json!({"type":"Like","id":like_id.to_string(),"actor":local_actor.ap_id.to_string(),"object":object_ap_id.to_string()}),
|
||||
actor: local_actor.object_id(),
|
||||
object: serde_json::to_value(LikeRef {
|
||||
kind: "Like",
|
||||
id: like_id.to_string(),
|
||||
actor: local_actor.ap_id.to_string(),
|
||||
object: object_ap_id.to_string(),
|
||||
})?,
|
||||
};
|
||||
let (json, sends, inboxes) = self
|
||||
.prepare_broadcast(&data, &local_actor, vec![author_inbox_url], undo)
|
||||
.await?;
|
||||
self.dispatch_deliveries(&data, &local_actor, inboxes, sends, json)
|
||||
|
||||
self.send_activity(&data, &local_actor, vec![author_inbox_url], undo)
|
||||
.await
|
||||
}
|
||||
|
||||
@@ -160,18 +152,20 @@ impl ActivityPubService {
|
||||
else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let delete = DeleteActivity {
|
||||
id: activity_url(&self.base_url).map_err(|e| anyhow::anyhow!("{e}"))?,
|
||||
id: data.url_scheme.activity_url(&self.base_url)?,
|
||||
kind: Default::default(),
|
||||
actor: ObjectId::from(local_actor.ap_id.clone()),
|
||||
object: serde_json::json!({"type": "Tombstone", "id": ap_id.to_string()}),
|
||||
actor: local_actor.object_id(),
|
||||
object: serde_json::to_value(TombstoneRef {
|
||||
kind: "Tombstone",
|
||||
id: ap_id.to_string(),
|
||||
})?,
|
||||
to: vec![crate::urls::AS_PUBLIC.to_string()],
|
||||
cc: vec![local_actor.followers_url.to_string()],
|
||||
};
|
||||
let (json, sends, inboxes) = self
|
||||
.prepare_broadcast(&data, &local_actor, inboxes, delete)
|
||||
.await?;
|
||||
self.dispatch_deliveries(&data, &local_actor, inboxes, sends, json)
|
||||
|
||||
self.send_activity(&data, &local_actor, inboxes, delete)
|
||||
.await
|
||||
}
|
||||
|
||||
@@ -187,25 +181,23 @@ impl ActivityPubService {
|
||||
else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let add = AddActivity {
|
||||
id: ap_id,
|
||||
kind: Default::default(),
|
||||
actor: ObjectId::from(local_actor.ap_id.clone()),
|
||||
actor: local_actor.object_id(),
|
||||
object,
|
||||
to: vec![crate::urls::AS_PUBLIC.to_string()],
|
||||
cc: vec![local_actor.followers_url.to_string()],
|
||||
};
|
||||
let (json, sends, inboxes) = self
|
||||
.prepare_broadcast(&data, &local_actor, inboxes, add)
|
||||
.await?;
|
||||
self.dispatch_deliveries(&data, &local_actor, inboxes, sends, json)
|
||||
.await
|
||||
|
||||
self.send_activity(&data, &local_actor, inboxes, add).await
|
||||
}
|
||||
|
||||
pub async fn broadcast_undo_add_to_followers(
|
||||
&self,
|
||||
local_user_id: uuid::Uuid,
|
||||
watchlist_entry_ap_id: Url,
|
||||
object_ap_id: Url,
|
||||
) -> anyhow::Result<()> {
|
||||
let data = self.federation_config.to_request_data();
|
||||
let Some((local_actor, inboxes)) =
|
||||
@@ -213,153 +205,136 @@ impl ActivityPubService {
|
||||
else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let undo = UndoActivity {
|
||||
id: activity_url(&self.base_url).map_err(|e| anyhow::anyhow!("{e}"))?,
|
||||
id: data.url_scheme.activity_url(&self.base_url)?,
|
||||
kind: Default::default(),
|
||||
actor: ObjectId::from(local_actor.ap_id.clone()),
|
||||
object: serde_json::json!({"type":"Add","id":watchlist_entry_ap_id.as_str(),"object":{"id":watchlist_entry_ap_id.as_str()}}),
|
||||
actor: local_actor.object_id(),
|
||||
object: serde_json::to_value(AddRef {
|
||||
kind: "Add",
|
||||
id: object_ap_id.to_string(),
|
||||
object: AddRefObject {
|
||||
id: object_ap_id.to_string(),
|
||||
},
|
||||
})?,
|
||||
};
|
||||
let (json, sends, inboxes) = self
|
||||
.prepare_broadcast(&data, &local_actor, inboxes, undo)
|
||||
.await?;
|
||||
self.dispatch_deliveries(&data, &local_actor, inboxes, sends, json)
|
||||
.await
|
||||
|
||||
self.send_activity(&data, &local_actor, inboxes, undo).await
|
||||
}
|
||||
|
||||
/// Fan out a Create(Note) activity to accepted followers and any explicitly
|
||||
/// Resolve the local actor, gather follower + mentioned inboxes, and compute
|
||||
/// `to`/`cc` addressing. Returns `None` when visibility is `Private` or there
|
||||
/// are no inboxes to deliver to.
|
||||
async fn prepare_addressed_broadcast(
|
||||
&self,
|
||||
local_user_id: uuid::Uuid,
|
||||
visibility: ApVisibility,
|
||||
mentioned_inboxes: Vec<Url>,
|
||||
) -> anyhow::Result<
|
||||
Option<(
|
||||
activitypub_federation::config::Data<FederationData>,
|
||||
DbActor,
|
||||
Vec<Url>,
|
||||
Addressing,
|
||||
)>,
|
||||
> {
|
||||
if visibility == ApVisibility::Private {
|
||||
return Ok(None);
|
||||
}
|
||||
let data = self.federation_config.to_request_data();
|
||||
let local_actor = get_local_actor(local_user_id, &data).await?;
|
||||
let follower_inboxes = data
|
||||
.follow_repo
|
||||
.get_accepted_follower_inboxes(local_user_id)
|
||||
.await?;
|
||||
let inboxes = merge_inboxes(follower_inboxes, mentioned_inboxes);
|
||||
if inboxes.is_empty() {
|
||||
return Ok(None);
|
||||
}
|
||||
let addressing = visibility_addressing(visibility, &local_actor.followers_url);
|
||||
Ok(Some((data, local_actor, inboxes, addressing)))
|
||||
}
|
||||
|
||||
/// Fan out a Create activity to accepted followers and any explicitly
|
||||
/// mentioned actors.
|
||||
///
|
||||
/// `visibility` controls `to`/`cc` addressing and whether the note is public:
|
||||
/// `visibility` controls `to`/`cc` addressing:
|
||||
/// - `Public` / `FollowersOnly`: delivered to followers + `mentioned_inboxes`
|
||||
/// - `Private`: returns immediately — no delivery to anyone
|
||||
///
|
||||
/// `mentioned_inboxes` should contain the inbox URLs of remote actors
|
||||
/// explicitly tagged in the note who are not already followers. Resolve them
|
||||
/// explicitly tagged in the object who are not already followers. Resolve them
|
||||
/// via [`ActivityPubService::lookup_actor_by_handle`] before calling. Pass an
|
||||
/// empty `Vec` if there are no external mentions.
|
||||
pub async fn broadcast_create_note(
|
||||
pub async fn broadcast_create(
|
||||
&self,
|
||||
local_user_id: uuid::Uuid,
|
||||
note: serde_json::Value,
|
||||
object: serde_json::Value,
|
||||
visibility: ApVisibility,
|
||||
mentioned_inboxes: Vec<Url>,
|
||||
) -> anyhow::Result<()> {
|
||||
if visibility == ApVisibility::Private {
|
||||
let Some((data, local_actor, inboxes, addressing)) = self
|
||||
.prepare_addressed_broadcast(local_user_id, visibility, mentioned_inboxes)
|
||||
.await?
|
||||
else {
|
||||
return Ok(());
|
||||
}
|
||||
let data = self.federation_config.to_request_data();
|
||||
let local_actor = crate::actors::get_local_actor(local_user_id, &data)
|
||||
.await
|
||||
.map_err(|e| anyhow::anyhow!("{e}"))?;
|
||||
};
|
||||
|
||||
// Merge follower inboxes with explicitly mentioned actor inboxes,
|
||||
// deduplicating by string to avoid delivering the same inbox twice.
|
||||
let follower_inboxes = data
|
||||
.follow_repo
|
||||
.get_accepted_follower_inboxes(local_user_id)
|
||||
.await?;
|
||||
let mut seen = std::collections::HashSet::new();
|
||||
let mut inboxes: Vec<Url> = follower_inboxes
|
||||
.into_iter()
|
||||
.filter_map(|s| Url::parse(&s).ok())
|
||||
.filter(|u| seen.insert(u.to_string()))
|
||||
.collect();
|
||||
for inbox in mentioned_inboxes {
|
||||
if seen.insert(inbox.to_string()) {
|
||||
inboxes.push(inbox);
|
||||
}
|
||||
}
|
||||
if inboxes.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let note_id_str = note["id"].as_str().unwrap_or("");
|
||||
let object_id_str = object["id"].as_str().unwrap_or("");
|
||||
let create_id = Url::parse(&format!(
|
||||
"{}/activities/create/{}",
|
||||
self.base_url,
|
||||
uuid::Uuid::new_v5(&uuid::Uuid::NAMESPACE_URL, note_id_str.as_bytes())
|
||||
))
|
||||
.map_err(|e| anyhow::anyhow!("{e}"))?;
|
||||
let (to, cc) = visibility_addressing(visibility, &local_actor.followers_url);
|
||||
uuid::Uuid::new_v5(&uuid::Uuid::NAMESPACE_URL, object_id_str.as_bytes())
|
||||
))?;
|
||||
let create = CreateActivity {
|
||||
id: create_id,
|
||||
kind: Default::default(),
|
||||
actor: ObjectId::from(local_actor.ap_id.clone()),
|
||||
object: note,
|
||||
to,
|
||||
cc,
|
||||
actor: local_actor.object_id(),
|
||||
object,
|
||||
to: addressing.to,
|
||||
cc: addressing.cc,
|
||||
bto: vec![],
|
||||
bcc: vec![],
|
||||
};
|
||||
let (json, sends, inboxes) = self
|
||||
.prepare_broadcast(&data, &local_actor, inboxes, create)
|
||||
.await?;
|
||||
self.dispatch_deliveries(&data, &local_actor, inboxes, sends, json)
|
||||
|
||||
self.send_activity(&data, &local_actor, inboxes, create)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Fan out an Update(Note) activity to accepted followers and mentioned actors.
|
||||
/// See [`broadcast_create_note`] for `mentioned_inboxes` semantics.
|
||||
pub async fn broadcast_update_note(
|
||||
/// Fan out an Update activity to accepted followers and mentioned actors.
|
||||
/// See [`ActivityPubService::broadcast_create`] for `mentioned_inboxes` semantics.
|
||||
pub async fn broadcast_update(
|
||||
&self,
|
||||
local_user_id: uuid::Uuid,
|
||||
note: serde_json::Value,
|
||||
object: serde_json::Value,
|
||||
visibility: ApVisibility,
|
||||
mentioned_inboxes: Vec<Url>,
|
||||
) -> anyhow::Result<()> {
|
||||
if visibility == ApVisibility::Private {
|
||||
let Some((data, local_actor, inboxes, addressing)) = self
|
||||
.prepare_addressed_broadcast(local_user_id, visibility, mentioned_inboxes)
|
||||
.await?
|
||||
else {
|
||||
return Ok(());
|
||||
}
|
||||
let data = self.federation_config.to_request_data();
|
||||
let local_actor = crate::actors::get_local_actor(local_user_id, &data)
|
||||
.await
|
||||
.map_err(|e| anyhow::anyhow!("{e}"))?;
|
||||
|
||||
let follower_inboxes = data
|
||||
.follow_repo
|
||||
.get_accepted_follower_inboxes(local_user_id)
|
||||
.await?;
|
||||
let mut seen = std::collections::HashSet::new();
|
||||
let mut inboxes: Vec<Url> = follower_inboxes
|
||||
.into_iter()
|
||||
.filter_map(|s| Url::parse(&s).ok())
|
||||
.filter(|u| seen.insert(u.to_string()))
|
||||
.collect();
|
||||
for inbox in mentioned_inboxes {
|
||||
if seen.insert(inbox.to_string()) {
|
||||
inboxes.push(inbox);
|
||||
}
|
||||
}
|
||||
if inboxes.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let (to, cc) = visibility_addressing(visibility, &local_actor.followers_url);
|
||||
let update = crate::activities::UpdateActivity {
|
||||
id: activity_url(&self.base_url).map_err(|e| anyhow::anyhow!("{e}"))?,
|
||||
kind: Default::default(),
|
||||
actor: ObjectId::from(local_actor.ap_id.clone()),
|
||||
object: note,
|
||||
to,
|
||||
cc,
|
||||
};
|
||||
let (json, sends, inboxes) = self
|
||||
.prepare_broadcast(&data, &local_actor, inboxes, update)
|
||||
.await?;
|
||||
self.dispatch_deliveries(&data, &local_actor, inboxes, sends, json)
|
||||
|
||||
let update = UpdateActivity {
|
||||
id: data.url_scheme.activity_url(&self.base_url)?,
|
||||
kind: Default::default(),
|
||||
actor: local_actor.object_id(),
|
||||
object,
|
||||
to: addressing.to,
|
||||
cc: addressing.cc,
|
||||
};
|
||||
|
||||
self.send_activity(&data, &local_actor, inboxes, update)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn broadcast_actor_update(&self, user_id: uuid::Uuid) -> anyhow::Result<()> {
|
||||
let data = self.federation_config.to_request_data();
|
||||
let local_actor = get_local_actor(user_id, &data)
|
||||
.await
|
||||
.map_err(|e| anyhow::anyhow!("{e}"))?;
|
||||
let person = local_actor
|
||||
.clone()
|
||||
.into_json(&data)
|
||||
.await
|
||||
.map_err(|e| anyhow::anyhow!("{e}"))?;
|
||||
let local_actor = get_local_actor(user_id, &data).await?;
|
||||
|
||||
let person = local_actor.clone().into_json(&data).await?;
|
||||
let person_json =
|
||||
serde_json::to_value(WithContext::new(person, crate::urls::actor_ap_context()))?;
|
||||
let update_id = Url::parse(&format!(
|
||||
@@ -370,65 +345,81 @@ impl ActivityPubService {
|
||||
let update = UpdateActivity {
|
||||
id: update_id,
|
||||
kind: Default::default(),
|
||||
actor: ObjectId::from(local_actor.ap_id.clone()),
|
||||
actor: local_actor.object_id(),
|
||||
object: person_json,
|
||||
to: vec![crate::urls::AS_PUBLIC.to_string()],
|
||||
cc: vec![local_actor.followers_url.to_string()],
|
||||
};
|
||||
|
||||
let Some((_, inboxes)) = self.accepted_follower_inboxes(&data, user_id).await? else {
|
||||
tracing::info!(%user_id, "no accepted followers, skipping actor update broadcast");
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
tracing::info!(%user_id, inbox_count = inboxes.len(), "broadcasting actor update");
|
||||
let (json, sends, inboxes) = self
|
||||
.prepare_broadcast(&data, &local_actor, inboxes, update)
|
||||
.await?;
|
||||
self.dispatch_deliveries(&data, &local_actor, inboxes, sends, json)
|
||||
self.send_activity(&data, &local_actor, inboxes, update)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn broadcast_move(
|
||||
&self,
|
||||
user_id: uuid::Uuid,
|
||||
new_actor_url: url::Url,
|
||||
new_actor_url: Url,
|
||||
) -> anyhow::Result<()> {
|
||||
let data = self.federation_config.to_request_data();
|
||||
let local_actor = get_local_actor(user_id, &data)
|
||||
.await
|
||||
.map_err(|e| anyhow::anyhow!("{e}"))?;
|
||||
let local_actor = get_local_actor(user_id, &data).await?;
|
||||
let Some((_, inboxes)) = self.accepted_follower_inboxes(&data, user_id).await? else {
|
||||
tracing::info!(%user_id, "broadcast_move: no accepted followers");
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let move_activity = MoveActivity {
|
||||
id: activity_url(&self.base_url).map_err(|e| anyhow::anyhow!("{e}"))?,
|
||||
id: data.url_scheme.activity_url(&self.base_url)?,
|
||||
kind: Default::default(),
|
||||
actor: ObjectId::from(local_actor.ap_id.clone()),
|
||||
actor: local_actor.object_id(),
|
||||
object: local_actor.ap_id.clone(),
|
||||
target: new_actor_url.clone(),
|
||||
};
|
||||
let (json, sends, inboxes) = self
|
||||
.prepare_broadcast(&data, &local_actor, inboxes, move_activity)
|
||||
.await?;
|
||||
self.dispatch_deliveries(&data, &local_actor, inboxes, sends, json)
|
||||
|
||||
self.send_activity(&data, &local_actor, inboxes, move_activity)
|
||||
.await?;
|
||||
tracing::info!(%user_id, target = %new_actor_url, "broadcast_move: dispatched");
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
/// Broadcast a pre-built activity to all accepted followers.
|
||||
///
|
||||
/// This is the low-level escape hatch for custom activity types that
|
||||
/// k-ap doesn't have a dedicated method for. The `activity` JSON must
|
||||
/// be a complete AP activity with `id`, `type`, `actor`, etc. already set.
|
||||
/// k-ap wraps it in `@context` and handles signing + delivery.
|
||||
pub async fn broadcast_raw_to_followers(
|
||||
&self,
|
||||
local_user_id: uuid::Uuid,
|
||||
activity: serde_json::Value,
|
||||
) -> anyhow::Result<()> {
|
||||
let data = self.federation_config.to_request_data();
|
||||
let Some((local_actor, inboxes)) =
|
||||
self.accepted_follower_inboxes(&data, local_user_id).await?
|
||||
else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
/// Returns `(to, cc)` addressing for the given visibility.
|
||||
/// `Private` is handled before calling this (early return in broadcast methods).
|
||||
pub(crate) fn visibility_addressing(
|
||||
visibility: ApVisibility,
|
||||
followers_url: &Url,
|
||||
) -> (Vec<String>, Vec<String>) {
|
||||
match visibility {
|
||||
ApVisibility::Public => (
|
||||
vec![crate::urls::AS_PUBLIC.to_string()],
|
||||
vec![followers_url.to_string()],
|
||||
),
|
||||
ApVisibility::FollowersOnly => (vec![followers_url.to_string()], vec![]),
|
||||
ApVisibility::Private => (vec![], vec![]),
|
||||
self.send_raw_activity(&data, &local_actor, inboxes, activity)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
fn merge_inboxes(follower_inboxes: Vec<String>, mentioned_inboxes: Vec<Url>) -> Vec<Url> {
|
||||
let mut seen = std::collections::HashSet::new();
|
||||
let mut inboxes: Vec<Url> = follower_inboxes
|
||||
.into_iter()
|
||||
.filter_map(|inbox_str| Url::parse(&inbox_str).ok())
|
||||
.filter(|url| seen.insert(url.to_string()))
|
||||
.collect();
|
||||
for inbox in mentioned_inboxes {
|
||||
if seen.insert(inbox.to_string()) {
|
||||
inboxes.push(inbox);
|
||||
}
|
||||
}
|
||||
inboxes
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user