This commit is contained in:
@@ -72,7 +72,8 @@ impl Activity for FollowActivity {
|
||||
let _follower = self.actor.dereference(data).await?;
|
||||
let local_actor = self.object.dereference(data).await?;
|
||||
|
||||
if data.federation_repo
|
||||
if data
|
||||
.federation_repo
|
||||
.is_actor_blocked(local_actor.user_id, self.actor.inner().as_str())
|
||||
.await?
|
||||
{
|
||||
@@ -246,7 +247,11 @@ impl Activity for UndoActivity {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let obj_type = self.object.get("type").and_then(|t| t.as_str()).unwrap_or("");
|
||||
let obj_type = self
|
||||
.object
|
||||
.get("type")
|
||||
.and_then(|t| t.as_str())
|
||||
.unwrap_or("");
|
||||
|
||||
match obj_type {
|
||||
"Follow" => {
|
||||
@@ -266,7 +271,8 @@ impl Activity for UndoActivity {
|
||||
tracing::info!(actor = %self.actor.inner(), "unfollowed");
|
||||
}
|
||||
"Add" => {
|
||||
let ap_id_str = self.object
|
||||
let ap_id_str = self
|
||||
.object
|
||||
.get("object")
|
||||
.and_then(|o| o.get("id"))
|
||||
.and_then(|id| id.as_str())
|
||||
|
||||
@@ -222,14 +222,18 @@ impl Object for DbActor {
|
||||
});
|
||||
let profile_url = self.profile_url;
|
||||
let also_known_as: Vec<String> = self.also_known_as.into_iter().collect();
|
||||
let attachment: Vec<ProfileFieldObject> = self.attachment.into_iter().map(|f| ProfileFieldObject {
|
||||
kind: "PropertyValue".to_string(),
|
||||
name: f.name,
|
||||
value: f.value,
|
||||
}).collect();
|
||||
let attachment: Vec<ProfileFieldObject> = self
|
||||
.attachment
|
||||
.into_iter()
|
||||
.map(|f| ProfileFieldObject {
|
||||
kind: "PropertyValue".to_string(),
|
||||
name: f.name,
|
||||
value: f.value,
|
||||
})
|
||||
.collect();
|
||||
|
||||
let shared_inbox = Url::parse(&format!("{}/inbox", data.base_url))
|
||||
.expect("base_url is always valid");
|
||||
let shared_inbox =
|
||||
Url::parse(&format!("{}/inbox", data.base_url)).expect("base_url is always valid");
|
||||
|
||||
Ok(Person {
|
||||
kind: Default::default(),
|
||||
|
||||
@@ -56,9 +56,7 @@ pub async fn nodeinfo_well_known_handler(
|
||||
}))
|
||||
}
|
||||
|
||||
pub async fn nodeinfo_handler(
|
||||
data: Data<FederationData>,
|
||||
) -> Result<Json<NodeInfo>, Error> {
|
||||
pub async fn nodeinfo_handler(data: Data<FederationData>) -> Result<Json<NodeInfo>, Error> {
|
||||
let user_count = data.user_repo.count_users().await.unwrap_or(0);
|
||||
let local_posts = data.object_handler.count_local_posts().await.unwrap_or(0);
|
||||
|
||||
|
||||
@@ -5,9 +5,7 @@ use serde::{Deserialize, Serialize};
|
||||
use url::Url;
|
||||
|
||||
use activitypub_federation::{
|
||||
config::Data,
|
||||
fetch::object_id::ObjectId,
|
||||
kinds::activity::CreateType,
|
||||
config::Data, fetch::object_id::ObjectId, kinds::activity::CreateType,
|
||||
protocol::context::WithContext,
|
||||
};
|
||||
|
||||
@@ -83,8 +81,7 @@ pub async fn outbox_handler(
|
||||
let ordered_items: Vec<serde_json::Value> = items
|
||||
.into_iter()
|
||||
.map(|(ap_id, object, _)| {
|
||||
let create_id =
|
||||
Url::parse(&format!("{}/activity", ap_id)).expect("valid url");
|
||||
let create_id = Url::parse(&format!("{}/activity", ap_id)).expect("valid url");
|
||||
serde_json::to_value(WithContext::new_default(CreateActivity {
|
||||
id: create_id,
|
||||
kind: CreateType::default(),
|
||||
@@ -105,9 +102,7 @@ pub async fn outbox_handler(
|
||||
let next = if has_more {
|
||||
oldest_ts.map(|ts| {
|
||||
// Use RFC 3339 with Z suffix (no + sign) to avoid percent-encoding
|
||||
let ts_str = ts
|
||||
.format("%Y-%m-%dT%H:%M:%S%.3fZ")
|
||||
.to_string();
|
||||
let ts_str = ts.format("%Y-%m-%dT%H:%M:%S%.3fZ").to_string();
|
||||
format!("{}?page=true&before={}", outbox_url, ts_str)
|
||||
})
|
||||
} else {
|
||||
|
||||
@@ -10,18 +10,23 @@ use axum::{Router, routing::get, routing::post};
|
||||
use url::Url;
|
||||
|
||||
use crate::{
|
||||
activities::{AcceptActivity, CreateActivity, FollowActivity, RejectActivity, UndoActivity, UpdateActivity},
|
||||
activities::{
|
||||
AcceptActivity, CreateActivity, FollowActivity, RejectActivity, UndoActivity,
|
||||
UpdateActivity,
|
||||
},
|
||||
actors::{DbActor, get_local_actor},
|
||||
content::ApObjectHandler,
|
||||
data::FederationData,
|
||||
federation::ApFederationConfig,
|
||||
followers_handler::{followers_handler, following_handler},
|
||||
inbox::inbox_handler,
|
||||
nodeinfo::{nodeinfo_handler, nodeinfo_well_known_handler},
|
||||
outbox::outbox_handler,
|
||||
repository::{BlockedDomain, FederationRepository, FollowerStatus, FollowingStatus, RemoteActor},
|
||||
repository::{
|
||||
BlockedDomain, FederationRepository, FollowerStatus, FollowingStatus, RemoteActor,
|
||||
},
|
||||
urls::activity_url,
|
||||
user::ApUserRepository,
|
||||
nodeinfo::{nodeinfo_handler, nodeinfo_well_known_handler},
|
||||
webfinger::webfinger_handler,
|
||||
};
|
||||
|
||||
@@ -35,9 +40,10 @@ fn collect_inboxes(followers: &[crate::repository::Follower]) -> Vec<Url> {
|
||||
.as_deref()
|
||||
.unwrap_or(&f.actor.inbox_url);
|
||||
if seen.insert(inbox_str.to_string())
|
||||
&& let Ok(url) = Url::parse(inbox_str) {
|
||||
inboxes.push(url);
|
||||
}
|
||||
&& let Ok(url) = Url::parse(inbox_str)
|
||||
{
|
||||
inboxes.push(url);
|
||||
}
|
||||
}
|
||||
inboxes
|
||||
}
|
||||
@@ -84,8 +90,13 @@ impl ActivityPubService {
|
||||
event_publisher: Option<Arc<dyn domain::ports::EventPublisher>>,
|
||||
) -> anyhow::Result<Self> {
|
||||
let data = FederationData::new(
|
||||
repo, user_repo, object_handler, base_url.clone(),
|
||||
allow_registration, software_name, event_publisher,
|
||||
repo,
|
||||
user_repo,
|
||||
object_handler,
|
||||
base_url.clone(),
|
||||
allow_registration,
|
||||
software_name,
|
||||
event_publisher,
|
||||
);
|
||||
let federation_config = ApFederationConfig::new(data, debug).await?;
|
||||
Ok(Self {
|
||||
@@ -550,8 +561,8 @@ impl ActivityPubService {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let delete_id = crate::urls::activity_url(&self.base_url)
|
||||
.map_err(|e| anyhow::anyhow!("{e}"))?;
|
||||
let delete_id =
|
||||
crate::urls::activity_url(&self.base_url).map_err(|e| anyhow::anyhow!("{e}"))?;
|
||||
let delete = crate::activities::DeleteActivity {
|
||||
id: delete_id,
|
||||
kind: Default::default(),
|
||||
@@ -627,8 +638,7 @@ impl ActivityPubService {
|
||||
};
|
||||
let add_with_ctx = WithContext::new_default(add);
|
||||
let inboxes = collect_inboxes(&accepted);
|
||||
let sends =
|
||||
SendActivityTask::prepare(&add_with_ctx, &local_actor, inboxes, &data).await?;
|
||||
let sends = SendActivityTask::prepare(&add_with_ctx, &local_actor, inboxes, &data).await?;
|
||||
let failures = send_with_retry(sends, &data).await;
|
||||
if !failures.is_empty() {
|
||||
tracing::warn!(count = failures.len(), "some Add deliveries failed");
|
||||
@@ -678,8 +688,8 @@ impl ActivityPubService {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let undo_id = crate::urls::activity_url(&self.base_url)
|
||||
.map_err(|e| anyhow::anyhow!("{e}"))?;
|
||||
let undo_id =
|
||||
crate::urls::activity_url(&self.base_url).map_err(|e| anyhow::anyhow!("{e}"))?;
|
||||
let undo = crate::activities::UndoActivity {
|
||||
id: undo_id,
|
||||
kind: Default::default(),
|
||||
@@ -692,8 +702,7 @@ impl ActivityPubService {
|
||||
};
|
||||
let undo_with_ctx = WithContext::new_default(undo);
|
||||
let inboxes = collect_inboxes(&accepted);
|
||||
let sends =
|
||||
SendActivityTask::prepare(&undo_with_ctx, &local_actor, inboxes, &data).await?;
|
||||
let sends = SendActivityTask::prepare(&undo_with_ctx, &local_actor, inboxes, &data).await?;
|
||||
let failures = send_with_retry(sends, &data).await;
|
||||
if !failures.is_empty() {
|
||||
tracing::warn!(count = failures.len(), "some Undo(Add) deliveries failed");
|
||||
@@ -778,7 +787,10 @@ impl ActivityPubService {
|
||||
.await
|
||||
.map_err(|e| anyhow::anyhow!("{e}"))?;
|
||||
|
||||
let person = local_actor.clone().into_json(&data).await
|
||||
let person = local_actor
|
||||
.clone()
|
||||
.into_json(&data)
|
||||
.await
|
||||
.map_err(|e| anyhow::anyhow!("{e}"))?;
|
||||
// Wrap with @context so Mastodon's JSON-LD processor can resolve field names.
|
||||
let person_json = serde_json::to_value(&WithContext::new_default(person))?;
|
||||
@@ -831,29 +843,43 @@ impl ActivityPubService {
|
||||
return Err(anyhow::anyhow!(
|
||||
"actor update delivery failed for {} inbox(es): {}",
|
||||
failures.len(),
|
||||
failures.iter().map(|e| e.to_string()).collect::<Vec<_>>().join("; ")
|
||||
failures
|
||||
.iter()
|
||||
.map(|e| e.to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.join("; ")
|
||||
));
|
||||
}
|
||||
tracing::info!(user_id = %user_id, "actor update broadcast complete");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn block_actor(&self, local_user_id: uuid::Uuid, actor_url: &str) -> anyhow::Result<()> {
|
||||
pub async fn block_actor(
|
||||
&self,
|
||||
local_user_id: uuid::Uuid,
|
||||
actor_url: &str,
|
||||
) -> anyhow::Result<()> {
|
||||
let data = self.federation_config.to_request_data();
|
||||
|
||||
data.federation_repo
|
||||
.add_blocked_actor(local_user_id, actor_url)
|
||||
.await?;
|
||||
let _ = data.federation_repo.remove_follower(local_user_id, actor_url).await;
|
||||
let _ = data.federation_repo.remove_following(local_user_id, actor_url).await;
|
||||
let _ = data
|
||||
.federation_repo
|
||||
.remove_follower(local_user_id, actor_url)
|
||||
.await;
|
||||
let _ = data
|
||||
.federation_repo
|
||||
.remove_following(local_user_id, actor_url)
|
||||
.await;
|
||||
|
||||
let local_actor = get_local_actor(local_user_id, &data)
|
||||
.await
|
||||
.map_err(|e| anyhow::anyhow!("{e}"))?;
|
||||
|
||||
if let Ok(Some(remote_actor)) = data.federation_repo.get_remote_actor(actor_url).await {
|
||||
let block_id = crate::urls::activity_url(&self.base_url)
|
||||
.map_err(|e| anyhow::anyhow!("{e}"))?;
|
||||
let block_id =
|
||||
crate::urls::activity_url(&self.base_url).map_err(|e| anyhow::anyhow!("{e}"))?;
|
||||
let block = crate::activities::BlockActivity {
|
||||
id: block_id,
|
||||
kind: Default::default(),
|
||||
@@ -877,16 +903,26 @@ impl ActivityPubService {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn unblock_actor(&self, local_user_id: uuid::Uuid, actor_url: &str) -> anyhow::Result<()> {
|
||||
pub async fn unblock_actor(
|
||||
&self,
|
||||
local_user_id: uuid::Uuid,
|
||||
actor_url: &str,
|
||||
) -> anyhow::Result<()> {
|
||||
let data = self.federation_config.to_request_data();
|
||||
data.federation_repo
|
||||
.remove_blocked_actor(local_user_id, actor_url)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn get_blocked_actors(&self, local_user_id: uuid::Uuid) -> anyhow::Result<Vec<RemoteActor>> {
|
||||
pub async fn get_blocked_actors(
|
||||
&self,
|
||||
local_user_id: uuid::Uuid,
|
||||
) -> anyhow::Result<Vec<RemoteActor>> {
|
||||
let data = self.federation_config.to_request_data();
|
||||
let actor_urls = data.federation_repo.get_blocked_actors(local_user_id).await?;
|
||||
let actor_urls = data
|
||||
.federation_repo
|
||||
.get_blocked_actors(local_user_id)
|
||||
.await?;
|
||||
let mut actors = Vec::new();
|
||||
for url in actor_urls {
|
||||
let actor = match data.federation_repo.get_remote_actor(&url).await {
|
||||
@@ -906,9 +942,15 @@ impl ActivityPubService {
|
||||
Ok(actors)
|
||||
}
|
||||
|
||||
pub async fn add_blocked_domain(&self, domain: &str, reason: Option<&str>) -> anyhow::Result<()> {
|
||||
pub async fn add_blocked_domain(
|
||||
&self,
|
||||
domain: &str,
|
||||
reason: Option<&str>,
|
||||
) -> anyhow::Result<()> {
|
||||
let data = self.federation_config.to_request_data();
|
||||
data.federation_repo.add_blocked_domain(domain, reason).await
|
||||
data.federation_repo
|
||||
.add_blocked_domain(domain, reason)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn remove_blocked_domain(&self, domain: &str) -> anyhow::Result<()> {
|
||||
|
||||
@@ -4,7 +4,10 @@ use super::*;
|
||||
fn person_serializes_with_enriched_fields() {
|
||||
let person = Person {
|
||||
kind: Default::default(),
|
||||
id: "https://example.com/users/1".parse::<url::Url>().unwrap().into(),
|
||||
id: "https://example.com/users/1"
|
||||
.parse::<url::Url>()
|
||||
.unwrap()
|
||||
.into(),
|
||||
preferred_username: "alice".to_string(),
|
||||
inbox: "https://example.com/users/1/inbox".parse().unwrap(),
|
||||
outbox: "https://example.com/users/1/outbox".parse().unwrap(),
|
||||
@@ -39,5 +42,8 @@ fn person_serializes_with_enriched_fields() {
|
||||
assert_eq!(json["manuallyApprovesFollowers"], true);
|
||||
assert!(json.get("updated").is_some());
|
||||
assert!(json.get("endpoints").is_some());
|
||||
assert_eq!(json["endpoints"]["sharedInbox"], "https://example.com/inbox");
|
||||
assert_eq!(
|
||||
json["endpoints"]["sharedInbox"],
|
||||
"https://example.com/inbox"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -9,7 +9,10 @@ fn nodeinfo_well_known_serializes_correctly() {
|
||||
}],
|
||||
};
|
||||
let json = serde_json::to_value(&doc).unwrap();
|
||||
assert_eq!(json["links"][0]["rel"], "http://nodeinfo.diaspora.software/ns/schema/2.0");
|
||||
assert_eq!(
|
||||
json["links"][0]["rel"],
|
||||
"http://nodeinfo.diaspora.software/ns/schema/2.0"
|
||||
);
|
||||
assert_eq!(json["links"][0]["href"], "https://example.com/nodeinfo/2.0");
|
||||
}
|
||||
|
||||
|
||||
@@ -19,8 +19,14 @@ fn make_follower(inbox: &str, shared: Option<&str>) -> Follower {
|
||||
#[test]
|
||||
fn collect_inboxes_deduplicates_shared() {
|
||||
let followers = vec![
|
||||
make_follower("https://mastodon.social/users/a/inbox", Some("https://mastodon.social/inbox")),
|
||||
make_follower("https://mastodon.social/users/b/inbox", Some("https://mastodon.social/inbox")),
|
||||
make_follower(
|
||||
"https://mastodon.social/users/a/inbox",
|
||||
Some("https://mastodon.social/inbox"),
|
||||
),
|
||||
make_follower(
|
||||
"https://mastodon.social/users/b/inbox",
|
||||
Some("https://mastodon.social/inbox"),
|
||||
),
|
||||
make_follower("https://other.instance/users/c/inbox", None),
|
||||
];
|
||||
let inboxes = collect_inboxes(&followers);
|
||||
@@ -32,9 +38,7 @@ fn collect_inboxes_deduplicates_shared() {
|
||||
|
||||
#[test]
|
||||
fn collect_inboxes_falls_back_to_individual_inbox() {
|
||||
let followers = vec![
|
||||
make_follower("https://example.com/users/x/inbox", None),
|
||||
];
|
||||
let followers = vec![make_follower("https://example.com/users/x/inbox", None)];
|
||||
let inboxes = collect_inboxes(&followers);
|
||||
assert_eq!(inboxes.len(), 1);
|
||||
assert_eq!(inboxes[0].as_str(), "https://example.com/users/x/inbox");
|
||||
|
||||
Reference in New Issue
Block a user