Compare commits
3 Commits
v0.1.3
...
13111c10b9
| Author | SHA1 | Date | |
|---|---|---|---|
| 13111c10b9 | |||
| 2e3b6d5cd4 | |||
| bc857b2c08 |
2
Cargo.lock
generated
2
Cargo.lock
generated
@@ -1368,7 +1368,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "k-ap"
|
||||
version = "0.1.0"
|
||||
version = "0.1.4"
|
||||
dependencies = [
|
||||
"activitypub_federation",
|
||||
"anyhow",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "k-ap"
|
||||
version = "0.1.0"
|
||||
version = "0.1.5"
|
||||
edition = "2024"
|
||||
description = "Generic ActivityPub protocol layer"
|
||||
license = "MIT"
|
||||
|
||||
@@ -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},
|
||||
};
|
||||
@@ -19,6 +18,7 @@ use crate::user::ApProfileField;
|
||||
pub struct DbActor {
|
||||
pub user_id: uuid::Uuid,
|
||||
pub username: String,
|
||||
pub display_name: Option<String>,
|
||||
pub public_key_pem: String,
|
||||
pub private_key_pem: Option<String>,
|
||||
pub inbox_url: Url,
|
||||
@@ -57,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<DbActor>,
|
||||
#[serde(default)]
|
||||
preferred_username: String,
|
||||
inbox: Url,
|
||||
outbox: Url,
|
||||
followers: Url,
|
||||
following: Url,
|
||||
public_key: PublicKey,
|
||||
#[serde(default)]
|
||||
outbox: Option<Url>,
|
||||
#[serde(default)]
|
||||
followers: Option<Url>,
|
||||
#[serde(default)]
|
||||
following: Option<Url>,
|
||||
pub public_key: PublicKey,
|
||||
#[serde(default)]
|
||||
name: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
summary: Option<String>,
|
||||
@@ -78,6 +99,7 @@ pub struct Person {
|
||||
url: Option<Url>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
discoverable: Option<bool>,
|
||||
#[serde(default)]
|
||||
manually_approves_followers: bool,
|
||||
#[serde(skip_serializing_if = "Option::is_none", default)]
|
||||
updated: Option<DateTime<Utc>>,
|
||||
@@ -152,6 +174,7 @@ pub async fn get_local_actor(
|
||||
Ok(DbActor {
|
||||
user_id,
|
||||
username: user.username,
|
||||
display_name: None,
|
||||
public_key_pem: public_key,
|
||||
private_key_pem: Some(private_key),
|
||||
inbox_url,
|
||||
@@ -219,6 +242,7 @@ impl Object for DbActor {
|
||||
Ok(Some(DbActor {
|
||||
user_id,
|
||||
username: user.username,
|
||||
display_name: None,
|
||||
public_key_pem: public_key,
|
||||
private_key_pem: private_key,
|
||||
inbox_url,
|
||||
@@ -272,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(),
|
||||
@@ -308,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?;
|
||||
|
||||
@@ -320,13 +344,17 @@ 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,
|
||||
username: json.preferred_username.clone(),
|
||||
display_name: json.name.clone(),
|
||||
public_key_pem: json.public_key.public_key_pem,
|
||||
private_key_pem: None,
|
||||
inbox_url,
|
||||
|
||||
@@ -25,4 +25,4 @@ pub use repository::{
|
||||
BlockedDomain, FederationRepository, Follower, FollowerStatus, FollowingStatus, RemoteActor,
|
||||
};
|
||||
pub use service::ActivityPubService;
|
||||
pub use user::{ApProfileField, ApUser, ApUserRepository};
|
||||
pub use user::{ApProfileField, ApUser, ApUserRepository, LookedUpActor};
|
||||
|
||||
@@ -330,6 +330,33 @@ impl ActivityPubService {
|
||||
Ok(serde_json::to_string(&WithContext::new_default(person))?)
|
||||
}
|
||||
|
||||
/// Resolve a `@user@domain` handle to actor data using a signed HTTP request.
|
||||
/// Unlike a plain unauthenticated fetch, this works with instances (e.g. Threads)
|
||||
/// that require HTTP signatures before returning full actor JSON.
|
||||
pub async fn lookup_actor_by_handle(
|
||||
&self,
|
||||
handle: &str,
|
||||
) -> anyhow::Result<crate::user::LookedUpActor> {
|
||||
let data = self.federation_config.to_request_data();
|
||||
let actor = Self::webfinger_https(handle, &data).await?;
|
||||
let domain = actor.ap_id.host_str().unwrap_or("").to_string();
|
||||
let handle = format!("{}@{}", actor.username, domain);
|
||||
Ok(crate::user::LookedUpActor {
|
||||
handle,
|
||||
display_name: actor.display_name,
|
||||
bio: actor.bio,
|
||||
avatar_url: actor.avatar_url,
|
||||
banner_url: actor.banner_url,
|
||||
ap_url: actor.ap_id,
|
||||
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,
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns the ActivityPub router compatible with any outer state `S`.
|
||||
/// Handlers only use `Data<FederationData>` injected by the middleware layer,
|
||||
/// so the router is independent of the application state type.
|
||||
|
||||
18
src/user.rs
18
src/user.rs
@@ -7,6 +7,24 @@ pub struct ApProfileField {
|
||||
pub value: String,
|
||||
}
|
||||
|
||||
/// Resolved actor data returned by [`crate::service::ActivityPubService::lookup_actor_by_handle`].
|
||||
/// Fetched via a signed HTTP request so strict instances (e.g. Threads) return full data.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct LookedUpActor {
|
||||
pub handle: String,
|
||||
pub display_name: Option<String>,
|
||||
pub bio: Option<String>,
|
||||
pub avatar_url: Option<Url>,
|
||||
pub banner_url: Option<Url>,
|
||||
pub ap_url: Url,
|
||||
pub outbox_url: Option<Url>,
|
||||
pub followers_url: Option<Url>,
|
||||
pub following_url: Option<Url>,
|
||||
pub also_known_as: Option<String>,
|
||||
pub profile_url: Option<Url>,
|
||||
pub attachment: Vec<ApProfileField>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ApUser {
|
||||
pub id: uuid::Uuid,
|
||||
|
||||
Reference in New Issue
Block a user