Compare commits
5 Commits
1949fce620
...
v0.1.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
767b1e69d4 | ||
|
|
72cda57dd9 | ||
|
|
7927aec05e | ||
|
|
1021861e2b | ||
|
|
fc01619a25 |
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
/target
|
||||||
|
Cargo.lock
|
||||||
3271
Cargo.lock
generated
Normal file
3271
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
29
src/lib.rs
29
src/lib.rs
@@ -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};
|
||||||
|
|||||||
183
src/service.rs
183
src/service.rs
@@ -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.
|
||||||
@@ -219,6 +224,98 @@ impl ActivityPubService {
|
|||||||
Ok(Some((local_actor, collect_inboxes(&accepted))))
|
Ok(Some((local_actor, collect_inboxes(&accepted))))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Build an OrderedCollection or OrderedCollectionPage JSON for the local
|
||||||
|
/// user's followers list. Pass `page = None` for the root collection.
|
||||||
|
pub async fn followers_collection_json(
|
||||||
|
&self,
|
||||||
|
user_id: uuid::Uuid,
|
||||||
|
page: Option<u32>,
|
||||||
|
) -> anyhow::Result<String> {
|
||||||
|
const AP_CONTEXT: &str = "https://www.w3.org/ns/activitystreams";
|
||||||
|
const PAGE_SIZE: usize = 20;
|
||||||
|
let data = self.federation_config.to_request_data();
|
||||||
|
let collection_id = format!("{}/users/{}/followers", self.base_url, user_id);
|
||||||
|
let total = data.federation_repo.count_followers(user_id).await?;
|
||||||
|
let obj = if let Some(p) = page {
|
||||||
|
let p = p.max(1);
|
||||||
|
let offset = (p.saturating_sub(1) as usize) * PAGE_SIZE;
|
||||||
|
let followers = data
|
||||||
|
.federation_repo
|
||||||
|
.get_followers_page(user_id, offset as u32, PAGE_SIZE)
|
||||||
|
.await?;
|
||||||
|
let has_next = offset + followers.len() < total;
|
||||||
|
let items: Vec<String> = followers.into_iter().map(|f| f.actor.url).collect();
|
||||||
|
let mut obj = serde_json::json!({
|
||||||
|
"@context": AP_CONTEXT,
|
||||||
|
"type": "OrderedCollectionPage",
|
||||||
|
"id": format!("{}?page={}", collection_id, p),
|
||||||
|
"partOf": collection_id,
|
||||||
|
"totalItems": total,
|
||||||
|
"orderedItems": items,
|
||||||
|
});
|
||||||
|
if has_next {
|
||||||
|
obj["next"] =
|
||||||
|
serde_json::json!(format!("{}?page={}", collection_id, p + 1));
|
||||||
|
}
|
||||||
|
obj
|
||||||
|
} else {
|
||||||
|
serde_json::json!({
|
||||||
|
"@context": AP_CONTEXT,
|
||||||
|
"type": "OrderedCollection",
|
||||||
|
"id": collection_id,
|
||||||
|
"totalItems": total,
|
||||||
|
"first": format!("{}?page=1", collection_id),
|
||||||
|
})
|
||||||
|
};
|
||||||
|
Ok(serde_json::to_string(&obj)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build an OrderedCollection or OrderedCollectionPage JSON for the local
|
||||||
|
/// user's following list. Pass `page = None` for the root collection.
|
||||||
|
pub async fn following_collection_json(
|
||||||
|
&self,
|
||||||
|
user_id: uuid::Uuid,
|
||||||
|
page: Option<u32>,
|
||||||
|
) -> anyhow::Result<String> {
|
||||||
|
const AP_CONTEXT: &str = "https://www.w3.org/ns/activitystreams";
|
||||||
|
const PAGE_SIZE: usize = 20;
|
||||||
|
let data = self.federation_config.to_request_data();
|
||||||
|
let collection_id = format!("{}/users/{}/following", self.base_url, user_id);
|
||||||
|
let total = data.federation_repo.count_following(user_id).await?;
|
||||||
|
let obj = if let Some(p) = page {
|
||||||
|
let p = p.max(1);
|
||||||
|
let offset = (p.saturating_sub(1) as usize) * PAGE_SIZE;
|
||||||
|
let following = data
|
||||||
|
.federation_repo
|
||||||
|
.get_following_page(user_id, offset as u32, PAGE_SIZE)
|
||||||
|
.await?;
|
||||||
|
let has_next = offset + following.len() < total;
|
||||||
|
let items: Vec<String> = following.into_iter().map(|a| a.url).collect();
|
||||||
|
let mut obj = serde_json::json!({
|
||||||
|
"@context": AP_CONTEXT,
|
||||||
|
"type": "OrderedCollectionPage",
|
||||||
|
"id": format!("{}?page={}", collection_id, p),
|
||||||
|
"partOf": collection_id,
|
||||||
|
"totalItems": total,
|
||||||
|
"orderedItems": items,
|
||||||
|
});
|
||||||
|
if has_next {
|
||||||
|
obj["next"] =
|
||||||
|
serde_json::json!(format!("{}?page={}", collection_id, p + 1));
|
||||||
|
}
|
||||||
|
obj
|
||||||
|
} else {
|
||||||
|
serde_json::json!({
|
||||||
|
"@context": AP_CONTEXT,
|
||||||
|
"type": "OrderedCollection",
|
||||||
|
"id": collection_id,
|
||||||
|
"totalItems": total,
|
||||||
|
"first": format!("{}?page=1", collection_id),
|
||||||
|
})
|
||||||
|
};
|
||||||
|
Ok(serde_json::to_string(&obj)?)
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn actor_json(&self, user_id_str: &str) -> anyhow::Result<String> {
|
pub async fn actor_json(&self, user_id_str: &str) -> anyhow::Result<String> {
|
||||||
use activitypub_federation::traits::Object;
|
use activitypub_federation::traits::Object;
|
||||||
let uuid = uuid::Uuid::parse_str(user_id_str)?;
|
let uuid = uuid::Uuid::parse_str(user_id_str)?;
|
||||||
@@ -913,6 +1010,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;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user