diff --git a/src/activitypub/actor.rs b/src/activitypub/actor.rs index 1fa786f..7829ece 100644 --- a/src/activitypub/actor.rs +++ b/src/activitypub/actor.rs @@ -2,7 +2,7 @@ use serde::{Deserialize, Serialize}; use serde_json::{json, Value}; use crate::config::Config; -use crate::errors::HttpError; +use crate::models::profiles::types::ExtraField; use crate::models::users::types::User; use crate::utils::crypto::{deserialize_private_key, get_public_key_pem}; use crate::utils::files::get_file_url; @@ -14,7 +14,7 @@ use super::views::{ get_followers_url, get_following_url, }; -use super::vocabulary::{PERSON, IMAGE}; +use super::vocabulary::{PERSON, IMAGE, PROPERTY_VALUE}; const W3ID_CONTEXT: &str = "https://w3id.org/security/v1"; @@ -40,6 +40,14 @@ pub struct ActorCapabilities { accepts_chat_messages: Option, } +#[derive(Deserialize, Serialize)] +pub struct ActorProperty { + name: String, + #[serde(rename = "type")] + object_type: String, + value: String, +} + #[derive(Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct Actor { @@ -71,12 +79,31 @@ pub struct Actor { #[serde(skip_serializing_if = "Option::is_none")] pub summary: Option, + + pub attachment: Option>, +} + +impl Actor { + /// Parse 'attachment' into ExtraField vector + pub fn extra_fields(&self) -> Vec { + match &self.attachment { + Some(properties) => { + properties.iter() + .map(|prop| ExtraField { + name: prop.name.clone(), + value: prop.value.clone(), + }) + .collect() + }, + None => vec![], + } + } } pub fn get_actor_object( config: &Config, user: &User, -) -> Result { +) -> Result { let username = &user.profile.username; let id = get_actor_url(&config.instance_url(), &username); let inbox = get_inbox_url(&config.instance_url(), &username); @@ -84,15 +111,16 @@ pub fn get_actor_object( let followers = get_followers_url(&config.instance_url(), &username); let following = get_following_url(&config.instance_url(), &username); - let private_key = deserialize_private_key(&user.private_key) - .map_err(|_| HttpError::InternalError)?; - let public_key_pem = get_public_key_pem(&private_key) - .map_err(|_| HttpError::InternalError)?; + let private_key = deserialize_private_key(&user.private_key)?; + let public_key_pem = get_public_key_pem(&private_key)?; let public_key = PublicKey { id: format!("{}#main-key", id), owner: id.clone(), public_key_pem: public_key_pem, }; + let capabilities = ActorCapabilities { + accepts_chat_messages: Some(false), + }; let avatar = match &user.profile.avatar_file_name { Some(file_name) => { let image = Image { @@ -113,9 +141,15 @@ pub fn get_actor_object( }, None => None, }; - let capabilities = ActorCapabilities { - accepts_chat_messages: Some(false), - }; + let properties = user.profile.extra_fields.clone() + .unpack().into_iter() + .map(|field| { + ActorProperty { + object_type: PROPERTY_VALUE.to_string(), + name: field.name, + value: field.value, + } + }).collect(); let actor = Actor { context: Some(json!([ AP_CONTEXT.to_string(), @@ -134,6 +168,7 @@ pub fn get_actor_object( icon: avatar, image: banner, summary: None, + attachment: Some(properties), }; Ok(actor) } diff --git a/src/activitypub/fetcher.rs b/src/activitypub/fetcher.rs index b5fae86..c31e758 100644 --- a/src/activitypub/fetcher.rs +++ b/src/activitypub/fetcher.rs @@ -92,6 +92,7 @@ pub async fn fetch_profile_by_actor_id( let actor_value: Value = serde_json::from_str(&actor_json)?; let actor: Actor = serde_json::from_value(actor_value.clone())?; let (avatar, banner) = fetch_avatar_and_banner(&actor, media_dir).await?; + let extra_fields = actor.extra_fields(); let actor_address = format!( "{}@{}", actor.preferred_username, @@ -102,8 +103,9 @@ pub async fn fetch_profile_by_actor_id( display_name: Some(actor.name), acct: actor_address, bio: actor.summary, - avatar: avatar, - banner: banner, + avatar, + banner, + extra_fields, actor: Some(actor_value), }; Ok(profile_data) diff --git a/src/activitypub/receiver.rs b/src/activitypub/receiver.rs index 7cc3492..e0adaf9 100644 --- a/src/activitypub/receiver.rs +++ b/src/activitypub/receiver.rs @@ -146,13 +146,14 @@ pub async fn receive_activity( let profile = get_profile_by_actor_id(db_client, &actor.id).await?; let (avatar, banner) = fetch_avatar_and_banner(&actor, &config.media_dir()).await .map_err(|_| ValidationError("failed to fetch image"))?; + let extra_fields = actor.extra_fields(); let mut profile_data = ProfileUpdateData { display_name: Some(actor.name), bio: actor.summary.clone(), bio_source: actor.summary, avatar, banner, - extra_fields: vec![], + extra_fields, }; profile_data.clean()?; update_profile(db_client, &profile.id, profile_data).await?; diff --git a/src/activitypub/views.rs b/src/activitypub/views.rs index d2a1f9c..9f416e6 100644 --- a/src/activitypub/views.rs +++ b/src/activitypub/views.rs @@ -47,7 +47,8 @@ async fn get_actor( ) -> Result { let db_client = &**get_database_client(&db_pool).await?; let user = get_user_by_name(db_client, &username).await?; - let actor = get_actor_object(&config, &user)?; + let actor = get_actor_object(&config, &user) + .map_err(|_| HttpError::InternalError)?; let response = HttpResponse::Ok() .content_type(ACTIVITY_CONTENT_TYPE) .json(actor); diff --git a/src/activitypub/vocabulary.rs b/src/activitypub/vocabulary.rs index a2409a6..f3c68d7 100644 --- a/src/activitypub/vocabulary.rs +++ b/src/activitypub/vocabulary.rs @@ -12,3 +12,4 @@ pub const PERSON: &str = "Person"; pub const DOCUMENT: &str = "Document"; pub const IMAGE: &str = "Image"; pub const NOTE: &str = "Note"; +pub const PROPERTY_VALUE: &str = "PropertyValue"; diff --git a/src/models/profiles/queries.rs b/src/models/profiles/queries.rs index a6fdbc7..1a88041 100644 --- a/src/models/profiles/queries.rs +++ b/src/models/profiles/queries.rs @@ -15,14 +15,15 @@ pub async fn create_profile( profile_data: &ProfileCreateData, ) -> Result { let profile_id = Uuid::new_v4(); + let extra_fields = ExtraFields(profile_data.extra_fields.clone()); let result = db_client.query_one( " INSERT INTO actor_profile ( id, username, display_name, acct, bio, bio_source, - avatar_file_name, banner_file_name, + avatar_file_name, banner_file_name, extra_fields, actor_json ) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) RETURNING actor_profile ", &[ @@ -34,6 +35,7 @@ pub async fn create_profile( &profile_data.bio, &profile_data.avatar, &profile_data.banner, + &extra_fields, &profile_data.actor, ], ).await; diff --git a/src/models/profiles/types.rs b/src/models/profiles/types.rs index 6b37006..eed6bed 100644 --- a/src/models/profiles/types.rs +++ b/src/models/profiles/types.rs @@ -74,6 +74,7 @@ pub struct ProfileCreateData { pub bio: Option, pub avatar: Option, pub banner: Option, + pub extra_fields: Vec, pub actor: Option, } diff --git a/src/models/users/queries.rs b/src/models/users/queries.rs index 2a20169..305f874 100644 --- a/src/models/users/queries.rs +++ b/src/models/users/queries.rs @@ -145,6 +145,7 @@ pub async fn create_user( bio: None, avatar: None, banner: None, + extra_fields: vec![], actor: None, }; let profile = create_profile(&transaction, &profile_data).await?;