From 2e3b6d5cd4d254324b4105ad51d60f4b49cefb66 Mon Sep 17 00:00:00 2001 From: Gabriel Kaszewski Date: Wed, 27 May 2026 22:37:49 +0200 Subject: [PATCH] fix: accept optional outbox/followers/following and any AP actor type Person struct now deserializes gracefully when outbox, followers, or following are absent (Threads omits them for some actors). Accepts Service/Application/Organization/Group in addition to Person. manually_approves_followers defaults to false when absent. --- Cargo.lock | 2 +- src/actors.rs | 50 +++++++++++++++++++++++++++++++++++++------------- src/service.rs | 6 +++--- src/user.rs | 6 +++--- 4 files changed, 44 insertions(+), 20 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index feceec6..2b57535 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1368,7 +1368,7 @@ dependencies = [ [[package]] name = "k-ap" -version = "0.1.0" +version = "0.1.4" dependencies = [ "activitypub_federation", "anyhow", diff --git a/src/actors.rs b/src/actors.rs index e282334..f211356 100644 --- a/src/actors.rs +++ b/src/actors.rs @@ -2,7 +2,6 @@ use activitypub_federation::{ config::Data, fetch::object_id::ObjectId, http_signatures::generate_actor_keypair, - kinds::actor::PersonType, protocol::{public_key::PublicKey, verification::verify_domains_match}, traits::{Actor, Object}, }; @@ -58,18 +57,39 @@ pub struct ProfileFieldObject { pub value: String, } +/// Accepts any AP actor type on inbound JSON; always serializes as "Person" for local actors. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum ApActorType { + Person, + Service, + Application, + Organization, + Group, +} + +impl Default for ApActorType { + fn default() -> Self { + Self::Person + } +} + #[derive(Debug, Clone, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct Person { #[serde(rename = "type")] - kind: PersonType, + kind: ApActorType, id: ObjectId, + #[serde(default)] preferred_username: String, inbox: Url, - outbox: Url, - followers: Url, - following: Url, - public_key: PublicKey, + #[serde(default)] + outbox: Option, + #[serde(default)] + followers: Option, + #[serde(default)] + following: Option, + pub public_key: PublicKey, + #[serde(default)] name: Option, #[serde(skip_serializing_if = "Option::is_none")] summary: Option, @@ -79,6 +99,7 @@ pub struct Person { url: Option, #[serde(skip_serializing_if = "Option::is_none")] discoverable: Option, + #[serde(default)] manually_approves_followers: bool, #[serde(skip_serializing_if = "Option::is_none", default)] updated: Option>, @@ -275,9 +296,9 @@ impl Object for DbActor { id: self.ap_id.clone().into(), preferred_username: self.username.clone(), inbox: self.inbox_url.clone(), - outbox: self.outbox_url.clone(), - followers: self.followers_url.clone(), - following: self.following_url.clone(), + outbox: Some(self.outbox_url.clone()), + followers: Some(self.followers_url.clone()), + following: Some(self.following_url.clone()), public_key, name: Some(self.username.clone()), summary: self.bio.clone(), @@ -311,7 +332,7 @@ impl Object for DbActor { shared_inbox_url, display_name: json.name.clone(), avatar_url: json.icon.as_ref().map(|i| i.url.to_string()), - outbox_url: Some(json.outbox.to_string()), + outbox_url: json.outbox.as_ref().map(|u| u.to_string()), }; data.federation_repo.upsert_remote_actor(actor).await?; @@ -323,9 +344,12 @@ impl Object for DbActor { .endpoints .as_ref() .and_then(|e| Url::parse(e.shared_inbox.as_str()).ok()); - let outbox_url = json.outbox.clone(); - let followers_url = json.followers.clone(); - let following_url = json.following.clone(); + let fallback = |suffix: &str| { + Url::parse(&format!("{}{}", ap_id, suffix)).unwrap_or_else(|_| ap_id.clone()) + }; + let outbox_url = json.outbox.clone().unwrap_or_else(|| fallback("/outbox")); + let followers_url = json.followers.clone().unwrap_or_else(|| fallback("/followers")); + let following_url = json.following.clone().unwrap_or_else(|| fallback("/following")); Ok(DbActor { user_id, diff --git a/src/service.rs b/src/service.rs index 1d9a7b1..2cbe11a 100644 --- a/src/service.rs +++ b/src/service.rs @@ -348,9 +348,9 @@ impl ActivityPubService { avatar_url: actor.avatar_url, banner_url: actor.banner_url, ap_url: actor.ap_id, - outbox_url: actor.outbox_url, - followers_url: actor.followers_url, - following_url: actor.following_url, + outbox_url: Some(actor.outbox_url), + followers_url: Some(actor.followers_url), + following_url: Some(actor.following_url), also_known_as: actor.also_known_as, profile_url: actor.profile_url, attachment: actor.attachment, diff --git a/src/user.rs b/src/user.rs index 0dcc9b4..6447c34 100644 --- a/src/user.rs +++ b/src/user.rs @@ -17,9 +17,9 @@ pub struct LookedUpActor { pub avatar_url: Option, pub banner_url: Option, pub ap_url: Url, - pub outbox_url: Url, - pub followers_url: Url, - pub following_url: Url, + pub outbox_url: Option, + pub followers_url: Option, + pub following_url: Option, pub also_known_as: Option, pub profile_url: Option, pub attachment: Vec,