Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1949fce620 | |||
| 699258f830 | |||
| 9412a9739a | |||
| 13111c10b9 | |||
| 2e3b6d5cd4 |
2
Cargo.lock
generated
2
Cargo.lock
generated
@@ -1368,7 +1368,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "k-ap"
|
name = "k-ap"
|
||||||
version = "0.1.0"
|
version = "0.1.7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"activitypub_federation",
|
"activitypub_federation",
|
||||||
"anyhow",
|
"anyhow",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "k-ap"
|
name = "k-ap"
|
||||||
version = "0.1.4"
|
version = "0.1.8"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
description = "Generic ActivityPub protocol layer"
|
description = "Generic ActivityPub protocol layer"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
|
|||||||
@@ -65,12 +65,20 @@ impl Activity for FollowActivity {
|
|||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
if target_domain != data.domain {
|
if target_domain == data.domain {
|
||||||
return Err(Error::bad_request(anyhow::anyhow!(
|
return Ok(());
|
||||||
"follow target is not a local actor"
|
|
||||||
)));
|
|
||||||
}
|
}
|
||||||
Ok(())
|
// Domain mismatch — still accept if the UUID resolves to a local user.
|
||||||
|
// This handles domain migrations where remote servers have cached the old actor URL.
|
||||||
|
if let Some(uuid) = crate::urls::extract_user_id_from_url(target_url) {
|
||||||
|
if data.user_repo.find_by_id(uuid).await.ok().flatten().is_some() {
|
||||||
|
tracing::debug!(target = %target_url, local_domain = %data.domain, "accepting follow for migrated actor URL");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(Error::bad_request(anyhow::anyhow!(
|
||||||
|
"follow target is not a local actor"
|
||||||
|
)))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn receive(self, data: &Data<Self::DataType>) -> Result<(), Self::Error> {
|
async fn receive(self, data: &Data<Self::DataType>) -> Result<(), Self::Error> {
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ use activitypub_federation::{
|
|||||||
config::Data,
|
config::Data,
|
||||||
fetch::object_id::ObjectId,
|
fetch::object_id::ObjectId,
|
||||||
http_signatures::generate_actor_keypair,
|
http_signatures::generate_actor_keypair,
|
||||||
kinds::actor::PersonType,
|
|
||||||
protocol::{public_key::PublicKey, verification::verify_domains_match},
|
protocol::{public_key::PublicKey, verification::verify_domains_match},
|
||||||
traits::{Actor, Object},
|
traits::{Actor, Object},
|
||||||
};
|
};
|
||||||
@@ -58,18 +57,39 @@ pub struct ProfileFieldObject {
|
|||||||
pub value: String,
|
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)]
|
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct Person {
|
pub struct Person {
|
||||||
#[serde(rename = "type")]
|
#[serde(rename = "type")]
|
||||||
kind: PersonType,
|
kind: ApActorType,
|
||||||
id: ObjectId<DbActor>,
|
id: ObjectId<DbActor>,
|
||||||
|
#[serde(default)]
|
||||||
preferred_username: String,
|
preferred_username: String,
|
||||||
inbox: Url,
|
inbox: Url,
|
||||||
outbox: Url,
|
#[serde(default)]
|
||||||
followers: Url,
|
outbox: Option<Url>,
|
||||||
following: Url,
|
#[serde(default)]
|
||||||
public_key: PublicKey,
|
followers: Option<Url>,
|
||||||
|
#[serde(default)]
|
||||||
|
following: Option<Url>,
|
||||||
|
pub public_key: PublicKey,
|
||||||
|
#[serde(default)]
|
||||||
name: Option<String>,
|
name: Option<String>,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
summary: Option<String>,
|
summary: Option<String>,
|
||||||
@@ -79,6 +99,7 @@ pub struct Person {
|
|||||||
url: Option<Url>,
|
url: Option<Url>,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
discoverable: Option<bool>,
|
discoverable: Option<bool>,
|
||||||
|
#[serde(default)]
|
||||||
manually_approves_followers: bool,
|
manually_approves_followers: bool,
|
||||||
#[serde(skip_serializing_if = "Option::is_none", default)]
|
#[serde(skip_serializing_if = "Option::is_none", default)]
|
||||||
updated: Option<DateTime<Utc>>,
|
updated: Option<DateTime<Utc>>,
|
||||||
@@ -172,6 +193,11 @@ pub async fn get_local_actor(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn apex_domain(url: &Url) -> String {
|
||||||
|
let host = url.host_str().unwrap_or("");
|
||||||
|
host.strip_prefix("www.").unwrap_or(host).to_owned()
|
||||||
|
}
|
||||||
|
|
||||||
#[async_trait::async_trait]
|
#[async_trait::async_trait]
|
||||||
impl Object for DbActor {
|
impl Object for DbActor {
|
||||||
type DataType = FederationData;
|
type DataType = FederationData;
|
||||||
@@ -275,9 +301,9 @@ impl Object for DbActor {
|
|||||||
id: self.ap_id.clone().into(),
|
id: self.ap_id.clone().into(),
|
||||||
preferred_username: self.username.clone(),
|
preferred_username: self.username.clone(),
|
||||||
inbox: self.inbox_url.clone(),
|
inbox: self.inbox_url.clone(),
|
||||||
outbox: self.outbox_url.clone(),
|
outbox: Some(self.outbox_url.clone()),
|
||||||
followers: self.followers_url.clone(),
|
followers: Some(self.followers_url.clone()),
|
||||||
following: self.following_url.clone(),
|
following: Some(self.following_url.clone()),
|
||||||
public_key,
|
public_key,
|
||||||
name: Some(self.username.clone()),
|
name: Some(self.username.clone()),
|
||||||
summary: self.bio.clone(),
|
summary: self.bio.clone(),
|
||||||
@@ -298,11 +324,26 @@ impl Object for DbActor {
|
|||||||
expected_domain: &Url,
|
expected_domain: &Url,
|
||||||
_data: &Data<Self::DataType>,
|
_data: &Data<Self::DataType>,
|
||||||
) -> Result<(), Self::Error> {
|
) -> Result<(), Self::Error> {
|
||||||
verify_domains_match(json.id.inner(), expected_domain)?;
|
if verify_domains_match(json.id.inner(), expected_domain).is_ok() {
|
||||||
Ok(())
|
return Ok(());
|
||||||
|
}
|
||||||
|
if apex_domain(json.id.inner()) == apex_domain(expected_domain) {
|
||||||
|
tracing::debug!(
|
||||||
|
actor_id = %json.id.inner(),
|
||||||
|
expected = %expected_domain,
|
||||||
|
"domain verified via www-apex equivalence"
|
||||||
|
);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
verify_domains_match(json.id.inner(), expected_domain).map_err(Error::from)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn from_json(json: Self::Kind, data: &Data<Self::DataType>) -> Result<Self, Self::Error> {
|
async fn from_json(json: Self::Kind, data: &Data<Self::DataType>) -> Result<Self, Self::Error> {
|
||||||
|
tracing::debug!(
|
||||||
|
actor_id = %json.id.inner(),
|
||||||
|
username = %json.preferred_username,
|
||||||
|
"ingesting remote actor"
|
||||||
|
);
|
||||||
let shared_inbox_url = json.endpoints.as_ref().map(|e| e.shared_inbox.to_string());
|
let shared_inbox_url = json.endpoints.as_ref().map(|e| e.shared_inbox.to_string());
|
||||||
let actor = RemoteActor {
|
let actor = RemoteActor {
|
||||||
url: json.id.inner().to_string(),
|
url: json.id.inner().to_string(),
|
||||||
@@ -311,7 +352,7 @@ impl Object for DbActor {
|
|||||||
shared_inbox_url,
|
shared_inbox_url,
|
||||||
display_name: json.name.clone(),
|
display_name: json.name.clone(),
|
||||||
avatar_url: json.icon.as_ref().map(|i| i.url.to_string()),
|
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?;
|
data.federation_repo.upsert_remote_actor(actor).await?;
|
||||||
|
|
||||||
@@ -323,9 +364,12 @@ impl Object for DbActor {
|
|||||||
.endpoints
|
.endpoints
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.and_then(|e| Url::parse(e.shared_inbox.as_str()).ok());
|
.and_then(|e| Url::parse(e.shared_inbox.as_str()).ok());
|
||||||
let outbox_url = json.outbox.clone();
|
let fallback = |suffix: &str| {
|
||||||
let followers_url = json.followers.clone();
|
Url::parse(&format!("{}{}", ap_id, suffix)).unwrap_or_else(|_| ap_id.clone())
|
||||||
let following_url = json.following.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 {
|
Ok(DbActor {
|
||||||
user_id,
|
user_id,
|
||||||
|
|||||||
@@ -337,10 +337,13 @@ impl ActivityPubService {
|
|||||||
&self,
|
&self,
|
||||||
handle: &str,
|
handle: &str,
|
||||||
) -> anyhow::Result<crate::user::LookedUpActor> {
|
) -> anyhow::Result<crate::user::LookedUpActor> {
|
||||||
|
tracing::info!(handle, "looking up remote actor");
|
||||||
let data = self.federation_config.to_request_data();
|
let data = self.federation_config.to_request_data();
|
||||||
let actor = Self::webfinger_https(handle, &data).await?;
|
let actor = Self::webfinger_https(handle, &data).await
|
||||||
|
.inspect_err(|e| tracing::warn!(handle, error = %e, "actor lookup failed"))?;
|
||||||
let domain = actor.ap_id.host_str().unwrap_or("").to_string();
|
let domain = actor.ap_id.host_str().unwrap_or("").to_string();
|
||||||
let handle = format!("{}@{}", actor.username, domain);
|
let handle = format!("{}@{}", actor.username, domain);
|
||||||
|
tracing::info!(handle, ap_url = %actor.ap_id, "remote actor resolved");
|
||||||
Ok(crate::user::LookedUpActor {
|
Ok(crate::user::LookedUpActor {
|
||||||
handle,
|
handle,
|
||||||
display_name: actor.display_name,
|
display_name: actor.display_name,
|
||||||
@@ -348,9 +351,9 @@ impl ActivityPubService {
|
|||||||
avatar_url: actor.avatar_url,
|
avatar_url: actor.avatar_url,
|
||||||
banner_url: actor.banner_url,
|
banner_url: actor.banner_url,
|
||||||
ap_url: actor.ap_id,
|
ap_url: actor.ap_id,
|
||||||
outbox_url: actor.outbox_url,
|
outbox_url: Some(actor.outbox_url),
|
||||||
followers_url: actor.followers_url,
|
followers_url: Some(actor.followers_url),
|
||||||
following_url: actor.following_url,
|
following_url: Some(actor.following_url),
|
||||||
also_known_as: actor.also_known_as,
|
also_known_as: actor.also_known_as,
|
||||||
profile_url: actor.profile_url,
|
profile_url: actor.profile_url,
|
||||||
attachment: actor.attachment,
|
attachment: actor.attachment,
|
||||||
@@ -597,6 +600,7 @@ impl ActivityPubService {
|
|||||||
"https://{}/.well-known/webfinger?resource=acct:{}@{}",
|
"https://{}/.well-known/webfinger?resource=acct:{}@{}",
|
||||||
domain_str, user, domain_str
|
domain_str, user, domain_str
|
||||||
);
|
);
|
||||||
|
tracing::debug!(handle, wf_url, "resolving webfinger");
|
||||||
let wf: serde_json::Value = reqwest::Client::new()
|
let wf: serde_json::Value = reqwest::Client::new()
|
||||||
.get(&wf_url)
|
.get(&wf_url)
|
||||||
.header("Accept", "application/jrd+json, application/json")
|
.header("Accept", "application/jrd+json, application/json")
|
||||||
@@ -615,6 +619,7 @@ impl ActivityPubService {
|
|||||||
.and_then(|l| l["href"].as_str())
|
.and_then(|l| l["href"].as_str())
|
||||||
.ok_or_else(|| anyhow::anyhow!("no self link in WebFinger response"))?
|
.ok_or_else(|| anyhow::anyhow!("no self link in WebFinger response"))?
|
||||||
.to_owned();
|
.to_owned();
|
||||||
|
tracing::debug!(handle, self_href, "webfinger resolved, fetching actor with signature");
|
||||||
let self_url = url::Url::parse(&self_href)?;
|
let self_url = url::Url::parse(&self_href)?;
|
||||||
let actor: DbActor = ObjectId::from(self_url)
|
let actor: DbActor = ObjectId::from(self_url)
|
||||||
.dereference(data)
|
.dereference(data)
|
||||||
|
|||||||
@@ -17,9 +17,9 @@ pub struct LookedUpActor {
|
|||||||
pub avatar_url: Option<Url>,
|
pub avatar_url: Option<Url>,
|
||||||
pub banner_url: Option<Url>,
|
pub banner_url: Option<Url>,
|
||||||
pub ap_url: Url,
|
pub ap_url: Url,
|
||||||
pub outbox_url: Url,
|
pub outbox_url: Option<Url>,
|
||||||
pub followers_url: Url,
|
pub followers_url: Option<Url>,
|
||||||
pub following_url: Url,
|
pub following_url: Option<Url>,
|
||||||
pub also_known_as: Option<String>,
|
pub also_known_as: Option<String>,
|
||||||
pub profile_url: Option<Url>,
|
pub profile_url: Option<Url>,
|
||||||
pub attachment: Vec<ApProfileField>,
|
pub attachment: Vec<ApProfileField>,
|
||||||
|
|||||||
Reference in New Issue
Block a user