4 Commits

Author SHA1 Message Date
Gabriel
72cda57dd9 feat: add broadcast_create_note, broadcast_update_note, base_url() accessor 2026-05-17 22:56:57 +02:00
Gabriel
7927aec05e gitignore 2026-05-17 22:54:03 +02:00
Gabriel
1021861e2b clean up 2026-05-17 22:53:45 +02:00
Gabriel
fc01619a25 feat: k-ap public API, no ap_ports 2026-05-17 22:31:23 +02:00
4 changed files with 3392 additions and 1 deletions

2
.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
/target
Cargo.lock

3271
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1 +1,28 @@
// placeholder — filled in Task 4 pub mod activities;
pub mod actor_handler;
pub mod actors;
pub mod content;
pub mod data;
pub mod error;
pub mod federation;
pub mod followers_handler;
pub mod inbox;
pub mod nodeinfo;
pub mod outbox;
pub mod repository;
pub mod service;
pub(crate) mod urls;
pub mod user;
pub mod webfinger;
pub use urls::AS_PUBLIC;
pub use activitypub_federation::kinds::object::NoteType;
pub use content::ApObjectHandler;
pub use data::FederationData;
pub use error::Error;
pub use federation::ApFederationConfig;
pub use repository::{
BlockedDomain, FederationRepository, Follower, FollowerStatus, FollowingStatus, RemoteActor,
};
pub use service::ActivityPubService;
pub use user::{ApProfileField, ApUser, ApUserRepository};

View File

@@ -32,6 +32,7 @@ const DELIVERY_INITIAL_DELAY_SECS: u64 = 1;
const HTTP_FETCH_TIMEOUT_SECS: u64 = 30; const HTTP_FETCH_TIMEOUT_SECS: u64 = 30;
const BATCH_FETCH_SLEEP_MS: u64 = 100; const BATCH_FETCH_SLEEP_MS: u64 = 100;
#[allow(dead_code)]
fn content_to_html(text: &str) -> String { fn content_to_html(text: &str) -> String {
let escaped = text let escaped = text
.replace('&', "&") .replace('&', "&")
@@ -172,6 +173,10 @@ impl ActivityPubService {
self.federation_config.to_request_data() self.federation_config.to_request_data()
} }
pub fn base_url(&self) -> &str {
&self.base_url
}
/// Returns `(local_actor, deduplicated_inboxes)` for all accepted followers, /// Returns `(local_actor, deduplicated_inboxes)` for all accepted followers,
/// excluding blocked actors and blocked domains. /// excluding blocked actors and blocked domains.
/// Returns `None` if there are no eligible followers. /// Returns `None` if there are no eligible followers.
@@ -913,6 +918,92 @@ impl ActivityPubService {
Ok(()) Ok(())
} }
/// Fan out a Create(Note) activity to all accepted followers.
/// `note` is the fully-formed Note JSON (including id, type, content, etc.).
/// The activity ID is derived deterministically from the note's `id` field.
pub async fn broadcast_create_note(
&self,
local_user_id: uuid::Uuid,
note: 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(());
};
let note_id_str = note["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 create = crate::activities::CreateActivity {
id: create_id,
kind: Default::default(),
actor: ObjectId::from(local_actor.ap_id.clone()),
object: note,
to: vec![crate::urls::AS_PUBLIC.to_string()],
cc: vec![local_actor.followers_url.to_string()],
bto: vec![],
bcc: vec![],
};
let sends = SendActivityTask::prepare(
&WithContext::new_default(create),
&local_actor,
inboxes,
&data,
)
.await?;
let failures = send_with_retry(sends, &data).await;
if !failures.is_empty() {
tracing::warn!(count = failures.len(), "some Create(Note) deliveries failed");
}
Ok(())
}
/// Fan out an Update(Note) activity to all accepted followers.
/// `note` is the fully-formed Note JSON.
pub async fn broadcast_update_note(
&self,
local_user_id: uuid::Uuid,
note: 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(());
};
let update_id = crate::urls::activity_url(&self.base_url)
.map_err(|e| anyhow::anyhow!("{e}"))?;
let update = crate::activities::UpdateActivity {
id: update_id,
kind: Default::default(),
actor: ObjectId::from(local_actor.ap_id.clone()),
object: note,
to: vec![crate::urls::AS_PUBLIC.to_string()],
cc: vec![local_actor.followers_url.to_string()],
};
let sends = SendActivityTask::prepare(
&WithContext::new_default(update),
&local_actor,
inboxes,
&data,
)
.await?;
let failures = send_with_retry(sends, &data).await;
if !failures.is_empty() {
tracing::warn!(count = failures.len(), "some Update(Note) deliveries failed");
}
Ok(())
}
pub async fn broadcast_actor_update(&self, user_id: uuid::Uuid) -> anyhow::Result<()> { pub async fn broadcast_actor_update(&self, user_id: uuid::Uuid) -> anyhow::Result<()> {
use activitypub_federation::traits::Object; use activitypub_federation::traits::Object;