use super::attachments::{ attach_extra_field, attach_identity_proof, attach_payment_option, parse_extra_field, parse_identity_proof, parse_payment_option, }; use crate::activitypub::types::build_default_context; use crate::activitypub::{ identifiers::{ local_actor_id, local_actor_key_id, local_instance_actor_id, LocalActorCollection, }, types::deserialize_value_array, vocabulary::{IDENTITY_PROOF, IMAGE, LINK, PERSON, PROPERTY_VALUE, SERVICE}, }; use crate::errors::ValidationError; use crate::media::get_file_url; use crate::webfinger::types::ActorAddress; use fedimovies_config::Instance; use fedimovies_models::{ profiles::types::{DbActor, DbActorPublicKey, ExtraField, IdentityProof, PaymentOption}, users::types::User, }; use fedimovies_utils::{ crypto_rsa::{deserialize_private_key, get_public_key_pem}, urls::get_hostname, }; use serde::{de::Error as DeserializerError, Deserialize, Deserializer, Serialize}; use serde_json::{json, Value}; #[derive(Deserialize, Serialize)] #[cfg_attr(test, derive(Default))] #[serde(rename_all = "camelCase")] pub struct PublicKey { id: String, owner: String, pub public_key_pem: String, } #[derive(Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct ActorImage { #[serde(rename = "type")] object_type: String, pub url: String, pub media_type: Option, } #[derive(Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct ActorAttachment { pub name: String, #[serde(rename = "type")] pub object_type: String, #[serde(skip_serializing_if = "Option::is_none")] pub value: Option, #[serde(skip_serializing_if = "Option::is_none")] pub href: Option, #[serde(skip_serializing_if = "Option::is_none")] pub signature_algorithm: Option, #[serde(skip_serializing_if = "Option::is_none")] pub signature_value: Option, } // Some implementations use empty object instead of null fn deserialize_image_opt<'de, D>(deserializer: D) -> Result, D::Error> where D: Deserializer<'de>, { let maybe_value: Option = Option::deserialize(deserializer)?; let maybe_image = if let Some(value) = maybe_value { let is_empty_object = value.as_object().map(|map| map.is_empty()).unwrap_or(false); if is_empty_object { None } else { let image = ActorImage::deserialize(value).map_err(DeserializerError::custom)?; Some(image) } } else { None }; Ok(maybe_image) } #[derive(Deserialize, Serialize)] #[cfg_attr(test, derive(Default))] #[serde(rename_all = "camelCase")] pub struct Actor { #[serde(rename = "@context")] pub context: Option, pub id: String, #[serde(rename = "type")] pub object_type: String, pub name: Option, pub preferred_username: String, pub inbox: String, pub outbox: String, #[serde(default)] pub bot: bool, #[serde(skip_serializing_if = "Option::is_none")] pub followers: Option, #[serde(skip_serializing_if = "Option::is_none")] pub following: Option, #[serde(skip_serializing_if = "Option::is_none")] pub subscribers: Option, pub public_key: PublicKey, #[serde( default, deserialize_with = "deserialize_image_opt", skip_serializing_if = "Option::is_none" )] pub icon: Option, #[serde(skip_serializing_if = "Option::is_none")] pub image: Option, #[serde(skip_serializing_if = "Option::is_none")] pub summary: Option, #[serde(skip_serializing_if = "Option::is_none")] pub also_known_as: Option, #[serde( default, deserialize_with = "deserialize_value_array", skip_serializing_if = "Vec::is_empty" )] pub attachment: Vec, #[serde(default)] pub manually_approves_followers: bool, #[serde( default, deserialize_with = "deserialize_value_array", skip_serializing_if = "Vec::is_empty" )] pub tag: Vec, #[serde(skip_serializing_if = "Option::is_none")] pub url: Option, } impl Actor { pub fn address(&self) -> Result { let hostname = get_hostname(&self.id).map_err(|_| ValidationError("invalid actor ID".to_string()))?; let actor_address = ActorAddress { username: self.preferred_username.clone(), hostname: hostname, }; Ok(actor_address) } pub fn into_db_actor(self) -> DbActor { DbActor { object_type: self.object_type, id: self.id, inbox: self.inbox, outbox: self.outbox, followers: self.followers, subscribers: self.subscribers, url: self.url, public_key: DbActorPublicKey { id: self.public_key.id, owner: self.public_key.owner, public_key_pem: self.public_key.public_key_pem, }, } } pub fn parse_attachments(&self) -> (Vec, Vec, Vec) { let mut identity_proofs = vec![]; let mut payment_options = vec![]; let mut extra_fields = vec![]; let log_error = |attachment_type: &str, error| { log::warn!( "ignoring actor attachment of type {}: {}", attachment_type, error, ); }; for attachment_value in self.attachment.iter() { let attachment_type = attachment_value["type"].as_str().unwrap_or("Unknown"); let attachment = match serde_json::from_value(attachment_value.clone()) { Ok(attachment) => attachment, Err(_) => { log_error( attachment_type, ValidationError("invalid attachment".to_string()), ); continue; } }; match attachment_type { IDENTITY_PROOF => { match parse_identity_proof(&self.id, &attachment) { Ok(proof) => identity_proofs.push(proof), Err(error) => log_error(attachment_type, error), }; } LINK => { match parse_payment_option(&attachment) { Ok(option) => payment_options.push(option), Err(error) => log_error(attachment_type, error), }; } PROPERTY_VALUE => { match parse_extra_field(&attachment) { Ok(field) => extra_fields.push(field), Err(error) => log_error(attachment_type, error), }; } _ => { log_error( attachment_type, ValidationError("unsupported attachment type".to_string()), ); } }; } (identity_proofs, payment_options, extra_fields) } } pub type ActorKeyError = rsa::pkcs8::Error; pub fn get_local_actor(user: &User, instance_url: &str) -> Result { let username = &user.profile.username; let actor_id = local_actor_id(instance_url, username); let inbox = LocalActorCollection::Inbox.of(&actor_id); let outbox = LocalActorCollection::Outbox.of(&actor_id); let followers = LocalActorCollection::Followers.of(&actor_id); let following = LocalActorCollection::Following.of(&actor_id); let subscribers = LocalActorCollection::Subscribers.of(&actor_id); let private_key = deserialize_private_key(&user.private_key)?; let public_key_pem = get_public_key_pem(&private_key)?; let public_key = PublicKey { id: local_actor_key_id(&actor_id), owner: actor_id.clone(), public_key_pem: public_key_pem, }; let avatar = match &user.profile.avatar { Some(image) => { let actor_image = ActorImage { object_type: IMAGE.to_string(), url: get_file_url(instance_url, &image.file_name), media_type: image.media_type.clone(), }; Some(actor_image) } None => None, }; let banner = match &user.profile.banner { Some(image) => { let actor_image = ActorImage { object_type: IMAGE.to_string(), url: get_file_url(instance_url, &image.file_name), media_type: image.media_type.clone(), }; Some(actor_image) } None => None, }; let mut attachments = vec![]; for proof in user.profile.identity_proofs.clone().into_inner() { let attachment = attach_identity_proof(proof); let attachment_value = serde_json::to_value(attachment).expect("attachment should be serializable"); attachments.push(attachment_value); } for payment_option in user.profile.payment_options.clone().into_inner() { let attachment = attach_payment_option(instance_url, &user.profile.username, payment_option); let attachment_value = serde_json::to_value(attachment).expect("attachment should be serializable"); attachments.push(attachment_value); } for field in user.profile.extra_fields.clone().into_inner() { let attachment = attach_extra_field(field); let attachment_value = serde_json::to_value(attachment).expect("attachment should be serializable"); attachments.push(attachment_value); } let aliases = user.profile.aliases.clone().into_actor_ids(); let actor = Actor { context: Some(build_default_context()), id: actor_id.clone(), object_type: PERSON.to_string(), name: user.profile.display_name.clone(), preferred_username: username.to_string(), inbox, outbox, bot: true, followers: Some(followers), following: Some(following), subscribers: Some(subscribers), public_key, icon: avatar, image: banner, summary: user.profile.bio.clone(), also_known_as: Some(json!(aliases)), attachment: attachments, manually_approves_followers: false, tag: vec![], url: Some(actor_id), }; Ok(actor) } pub fn get_instance_actor(instance: &Instance) -> Result { let actor_id = local_instance_actor_id(&instance.url()); let actor_inbox = LocalActorCollection::Inbox.of(&actor_id); let actor_outbox = LocalActorCollection::Outbox.of(&actor_id); let public_key_pem = get_public_key_pem(&instance.actor_key)?; let public_key = PublicKey { id: local_actor_key_id(&actor_id), owner: actor_id.clone(), public_key_pem: public_key_pem, }; let actor = Actor { context: Some(build_default_context()), id: actor_id, object_type: SERVICE.to_string(), name: Some(instance.hostname()), preferred_username: instance.hostname(), inbox: actor_inbox, outbox: actor_outbox, bot: true, followers: None, following: None, subscribers: None, public_key, icon: None, image: None, summary: None, also_known_as: None, attachment: vec![], manually_approves_followers: false, tag: vec![], url: None, }; Ok(actor) } #[cfg(test)] mod tests { use super::*; use fedimovies_models::profiles::types::DbActorProfile; use fedimovies_utils::crypto_rsa::{generate_weak_rsa_key, serialize_private_key}; const INSTANCE_HOSTNAME: &str = "example.com"; const INSTANCE_URL: &str = "https://example.com"; #[test] fn test_get_actor_address() { let actor = Actor { id: "https://test.org/users/1".to_string(), preferred_username: "test".to_string(), ..Default::default() }; let actor_address = actor.address().unwrap(); assert_eq!(actor_address.acct(INSTANCE_HOSTNAME), "test@test.org"); } #[test] fn test_local_actor() { let private_key = generate_weak_rsa_key().unwrap(); let private_key_pem = serialize_private_key(&private_key).unwrap(); let profile = DbActorProfile { username: "testuser".to_string(), bio: Some("testbio".to_string()), ..Default::default() }; let user = User { private_key: private_key_pem, profile, ..Default::default() }; let actor = get_local_actor(&user, INSTANCE_URL).unwrap(); assert_eq!(actor.id, "https://example.com/users/testuser"); assert_eq!(actor.preferred_username, user.profile.username); assert_eq!(actor.inbox, "https://example.com/users/testuser/inbox"); assert_eq!(actor.outbox, "https://example.com/users/testuser/outbox"); assert_eq!( actor.followers.unwrap(), "https://example.com/users/testuser/followers", ); assert_eq!( actor.following.unwrap(), "https://example.com/users/testuser/following", ); assert_eq!( actor.subscribers.unwrap(), "https://example.com/users/testuser/subscribers", ); assert_eq!( actor.public_key.id, "https://example.com/users/testuser#main-key", ); assert_eq!(actor.attachment.len(), 0); assert_eq!(actor.summary, user.profile.bio); } #[test] fn test_instance_actor() { let instance_url = "https://example.com/"; let instance = Instance::for_test(instance_url); let actor = get_instance_actor(&instance).unwrap(); assert_eq!(actor.id, "https://example.com/actor"); assert_eq!(actor.object_type, "Service"); assert_eq!(actor.preferred_username, "example.com"); assert_eq!(actor.inbox, "https://example.com/actor/inbox"); assert_eq!(actor.outbox, "https://example.com/actor/outbox"); assert_eq!(actor.public_key.id, "https://example.com/actor#main-key"); } }