diff --git a/src/activitypub/activity.rs b/src/activitypub/activity.rs index ab3ff28..2e28184 100644 --- a/src/activitypub/activity.rs +++ b/src/activitypub/activity.rs @@ -179,11 +179,11 @@ pub fn create_note( let mut recipients = vec![AP_PUBLIC.to_string()]; let mut tags = vec![]; for profile in &post.mentions { - let actor_id = profile.actor_id(instance_url).unwrap(); + let actor_id = profile.actor_id(instance_url); if !profile.is_local() { recipients.push(actor_id); }; - let actor_url = profile.actor_url(instance_url).unwrap(); + let actor_url = profile.actor_url(instance_url); let tag = Tag { name: format!("@{}", profile.actor_address(instance_host)), tag_type: MENTION.to_string(), @@ -208,7 +208,7 @@ pub fn create_note( Some(get_object_url(instance_url, &post.id)) } else { // Replying to remote post - let remote_actor_id = post.author.actor_id(instance_url).unwrap(); + let remote_actor_id = post.author.actor_id(instance_url); if !recipients.contains(&remote_actor_id) { recipients.push(remote_actor_id); }; @@ -294,7 +294,7 @@ pub fn create_activity_announce( repost_id: &Uuid, ) -> Activity { let object_id = post.get_object_id(instance_url); - let recipient_id = post.author.actor_id(instance_url).unwrap(); + let recipient_id = post.author.actor_id(instance_url); let activity = create_activity( instance_url, &actor_profile.username, @@ -345,7 +345,7 @@ pub fn create_activity_delete_note( }; let mut recipients = vec![AP_PUBLIC.to_string()]; for profile in &post.mentions { - let actor_id = profile.actor_id(instance_url).unwrap(); + let actor_id = profile.actor_id(instance_url); if !profile.is_local() { recipients.push(actor_id); }; @@ -460,6 +460,7 @@ pub fn create_activity_update_person( #[cfg(test)] mod tests { + use crate::activitypub::actor::Actor; use super::*; const INSTANCE_HOST: &str = "example.com"; @@ -511,10 +512,11 @@ mod tests { let parent_author_actor_url = "https://test.net/@test"; let parent_author = DbActorProfile { acct: parent_author_acct.to_string(), - actor_json: Some(json!({ - "id": parent_author_actor_id, - "url": parent_author_actor_url, - })), + actor_json: Some(Actor { + id: parent_author_actor_id.to_string(), + url: Some(parent_author_actor_url.to_string()), + ..Default::default() + }), ..Default::default() }; let parent = Post { diff --git a/src/activitypub/actor.rs b/src/activitypub/actor.rs index b23fed6..dbd5fa9 100644 --- a/src/activitypub/actor.rs +++ b/src/activitypub/actor.rs @@ -2,8 +2,7 @@ use serde::{Deserialize, Serialize}; use serde_json::{json, Value}; use crate::config::Instance; -use crate::errors::ConversionError; -use crate::models::profiles::types::{DbActorProfile, ExtraField}; +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; @@ -19,7 +18,8 @@ use super::vocabulary::{IMAGE, PERSON, PROPERTY_VALUE, SERVICE}; const W3ID_CONTEXT: &str = "https://w3id.org/security/v1"; -#[derive(Deserialize, Serialize)] +#[derive(Clone, Deserialize, Serialize)] +#[cfg_attr(test, derive(Default))] #[serde(rename_all = "camelCase")] pub struct PublicKey { id: String, @@ -27,7 +27,7 @@ pub struct PublicKey { pub public_key_pem: String, } -#[derive(Deserialize, Serialize)] +#[derive(Clone, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct Image { #[serde(rename = "type")] @@ -35,13 +35,13 @@ pub struct Image { pub url: String, } -#[derive(Deserialize, Serialize)] +#[derive(Clone, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct ActorCapabilities { accepts_chat_messages: Option, } -#[derive(Deserialize, Serialize)] +#[derive(Clone, Deserialize, Serialize)] pub struct ActorProperty { name: String, #[serde(rename = "type")] @@ -49,16 +49,18 @@ pub struct ActorProperty { value: Option, } -#[derive(Deserialize, Serialize)] +// Clone trait is required by FromSql +#[derive(Clone, Deserialize, Serialize)] +#[cfg_attr(test, derive(Default))] #[serde(rename_all = "camelCase")] pub struct Actor { #[serde(rename = "@context")] - context: Option, + pub context: Option, pub id: String, #[serde(rename = "type")] - object_type: String, + pub object_type: String, pub name: Option, @@ -116,20 +118,6 @@ impl Actor { } } -impl DbActorProfile { - pub fn remote_actor(&self) -> Result, ConversionError> { - let actor = match self.actor_json { - Some(ref value) => { - let actor: Actor = serde_json::from_value(value.clone()) - .map_err(|_| ConversionError)?; - Some(actor) - }, - None => None, - }; - Ok(actor) - } -} - pub struct ActorAddress { pub username: String, pub instance: String, diff --git a/src/activitypub/deliverer.rs b/src/activitypub/deliverer.rs index 128435f..9db2741 100644 --- a/src/activitypub/deliverer.rs +++ b/src/activitypub/deliverer.rs @@ -53,7 +53,7 @@ async fn send_activity( .body(activity_json.to_owned()); if instance.is_private { - log::debug!( + log::info!( "private mode: not sending activity to {}", inbox_url, ); diff --git a/src/activitypub/receiver.rs b/src/activitypub/receiver.rs index 68aeb96..b26da66 100644 --- a/src/activitypub/receiver.rs +++ b/src/activitypub/receiver.rs @@ -472,7 +472,7 @@ pub async fn receive_activity( &config.media_dir(), &activity.actor, ).await?; - let source_actor = source_profile.remote_actor().ok().flatten() + let source_actor = source_profile.actor_json .ok_or(HttpError::InternalError)?; let target_actor_id = get_object_id(activity.object)?; let target_username = parse_actor_id(&config.instance_url(), &target_actor_id)?; @@ -543,8 +543,7 @@ pub async fn receive_activity( 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 actor_old = profile.remote_actor() - .map_err(|_| HttpError::InternalError)?.unwrap(); + let actor_old = profile.actor_json.unwrap(); if actor_old.id != actor.id { log::warn!( "actor ID changed from {} to {}", diff --git a/src/http_signatures/verify.rs b/src/http_signatures/verify.rs index 6814ba5..aed2044 100644 --- a/src/http_signatures/verify.rs +++ b/src/http_signatures/verify.rs @@ -134,7 +134,7 @@ pub async fn verify_http_signature( return Err(VerificationError::ActorError(other_error.to_string())); }, }; - let actor = actor_profile.remote_actor().ok().flatten() + let actor = actor_profile.actor_json.as_ref() .ok_or(VerificationError::ActorError("invalid profile".to_string()))?; let public_key = deserialize_public_key(&actor.public_key.public_key_pem)?; @@ -146,8 +146,7 @@ pub async fn verify_http_signature( if !is_valid_signature { return Err(VerificationError::InvalidSignature); } - let signer_id = actor_profile.actor_id(&config.instance_url()) - .map_err(|_| VerificationError::ActorError("invalid profile".to_string()))?; + let signer_id = actor_profile.actor_id(&config.instance_url()); Ok(signer_id) } diff --git a/src/mastodon_api/accounts/views.rs b/src/mastodon_api/accounts/views.rs index 82d22e9..ca189cc 100644 --- a/src/mastodon_api/accounts/views.rs +++ b/src/mastodon_api/accounts/views.rs @@ -157,9 +157,7 @@ async fn update_credentials( let followers = get_followers(db_client, ¤t_user.id).await?; let mut recipients: Vec = Vec::new(); for follower in followers { - let maybe_remote_actor = follower.remote_actor() - .map_err(|_| HttpError::InternalError)?; - if let Some(remote_actor) = maybe_remote_actor { + if let Some(remote_actor) = follower.actor_json { recipients.push(remote_actor); }; }; @@ -203,9 +201,7 @@ async fn follow_account( let db_client = &mut **get_database_client(&db_pool).await?; let current_user = get_current_user(db_client, auth.token()).await?; let target = get_profile_by_id(db_client, &account_id).await?; - let maybe_remote_actor = target.remote_actor() - .map_err(|_| HttpError::InternalError)?; - if let Some(remote_actor) = maybe_remote_actor { + if let Some(remote_actor) = target.actor_json { // Remote follow let request = create_follow_request(db_client, ¤t_user.id, &target.id).await?; let activity = create_activity_follow( @@ -236,9 +232,7 @@ async fn unfollow_account( let db_client = &mut **get_database_client(&db_pool).await?; let current_user = get_current_user(db_client, auth.token()).await?; let target = get_profile_by_id(db_client, &account_id).await?; - let maybe_remote_actor = target.remote_actor() - .map_err(|_| HttpError::InternalError)?; - if let Some(remote_actor) = maybe_remote_actor { + if let Some(remote_actor) = target.actor_json { // Remote follow let follow_request = get_follow_request_by_path( db_client, diff --git a/src/mastodon_api/statuses/helpers.rs b/src/mastodon_api/statuses/helpers.rs index 6d97b8e..fdb9def 100644 --- a/src/mastodon_api/statuses/helpers.rs +++ b/src/mastodon_api/statuses/helpers.rs @@ -21,8 +21,7 @@ pub async fn get_note_audience( audience.extend(post.mentions.clone()); let mut recipients: Vec = Vec::new(); for profile in audience { - let maybe_remote_actor = profile.remote_actor()?; - if let Some(remote_actor) = maybe_remote_actor { + if let Some(remote_actor) = profile.actor_json { recipients.push(remote_actor); }; }; @@ -40,10 +39,9 @@ pub async fn get_like_audience( ) -> Result { let mut recipients: Vec = Vec::new(); let mut primary_recipient = None; - let maybe_remote_author = post.author.remote_actor()?; - if let Some(remote_actor) = maybe_remote_author { + if let Some(remote_actor) = post.author.actor_json.as_ref() { primary_recipient = Some(remote_actor.id.clone()); - recipients.push(remote_actor); + recipients.push(remote_actor.clone()); }; Ok(Audience { recipients, primary_recipient }) } @@ -56,16 +54,14 @@ pub async fn get_announce_audience( let followers = get_followers(db_client, ¤t_user.id).await?; let mut recipients: Vec = Vec::new(); for profile in followers { - let maybe_remote_actor = profile.remote_actor()?; - if let Some(remote_actor) = maybe_remote_actor { + if let Some(remote_actor) = profile.actor_json { recipients.push(remote_actor); }; }; let mut primary_recipient = None; - let maybe_remote_author = post.author.remote_actor()?; - if let Some(remote_actor) = maybe_remote_author { + if let Some(remote_actor) = post.author.actor_json.as_ref() { primary_recipient = Some(remote_actor.id.clone()); - recipients.push(remote_actor); + recipients.push(remote_actor.clone()); }; Ok(Audience { recipients, primary_recipient }) } diff --git a/src/mastodon_api/statuses/types.rs b/src/mastodon_api/statuses/types.rs index b4ecc5e..6eace68 100644 --- a/src/mastodon_api/statuses/types.rs +++ b/src/mastodon_api/statuses/types.rs @@ -22,7 +22,7 @@ impl Mention { id: profile.id.to_string(), username: profile.username.clone(), acct: profile.acct.clone(), - url: profile.actor_id(instance_url).unwrap(), + url: profile.actor_id(instance_url), } } } diff --git a/src/models/posts/mentions.rs b/src/models/posts/mentions.rs index def2d4b..e8a170f 100644 --- a/src/models/posts/mentions.rs +++ b/src/models/posts/mentions.rs @@ -69,7 +69,7 @@ pub fn replace_mentions( if let Some(profile) = mention_map.get(&acct) { // Replace with a link to profile. // Actor URL may differ from actor ID. - let url = profile.actor_url(instance_url).unwrap(); + let url = profile.actor_url(instance_url); return format!( // https://microformats.org/wiki/h-card r#"{}@{}{}"#, @@ -103,7 +103,7 @@ pub fn mention_to_address( #[cfg(test)] mod tests { - use serde_json::json; + use crate::activitypub::actor::Actor; use super::*; const INSTANCE_HOST: &str = "server1.com"; @@ -137,10 +137,11 @@ mod tests { // Remote actor let profile_2 = DbActorProfile { username: "user2".to_string(), - actor_json: Some(json!({ - "id": "https://server2.com/actors/user2", - "url": "https://server2.com/@user2", - })), + actor_json: Some(Actor { + id: "https://server2.com/actors/user2".to_string(), + url: Some("https://server2.com/@user2".to_string()), + ..Default::default() + }), ..Default::default() }; let text = concat!( diff --git a/src/models/profiles/types.rs b/src/models/profiles/types.rs index eff4e74..014b75a 100644 --- a/src/models/profiles/types.rs +++ b/src/models/profiles/types.rs @@ -8,8 +8,9 @@ use serde::{Deserialize, Serialize}; use serde_json::Value; use uuid::Uuid; +use crate::activitypub::actor::Actor; use crate::activitypub::views::get_actor_url; -use crate::errors::{ConversionError, ValidationError}; +use crate::errors::ValidationError; use crate::utils::html::clean_html; use super::validators::{ validate_username, @@ -41,7 +42,7 @@ impl<'a> FromSql<'a> for ExtraFields { let fields: Self = serde_json::from_value(json_value)?; Ok(fields) } - accepts!(JSON,JSONB); + accepts!(JSON, JSONB); } impl ToSql for ExtraFields { @@ -54,6 +55,15 @@ impl ToSql for ExtraFields { to_sql_checked!(); } +impl<'a> FromSql<'a> for Actor { + fn from_sql(ty: &Type, raw: &'a [u8]) -> Result { + let Json(json_value) = Json::::from_sql(ty, raw)?; + let actor: Self = serde_json::from_value(json_value)?; + Ok(actor) + } + accepts!(JSON, JSONB); +} + #[derive(Clone, FromSql)] #[postgres(name = "actor_profile")] pub struct DbActorProfile { @@ -70,7 +80,7 @@ pub struct DbActorProfile { pub following_count: i32, pub post_count: i32, pub created_at: DateTime, - pub actor_json: Option, + pub actor_json: Option, } impl DbActorProfile { @@ -78,22 +88,17 @@ impl DbActorProfile { self.actor_json.is_none() } - pub fn actor_id(&self, instance_url: &str) -> Result { - let actor_id = match self.actor_json { - Some(ref actor_value) => { - actor_value["id"].as_str() - .ok_or(ConversionError)? - .to_string() - }, + pub fn actor_id(&self, instance_url: &str) -> String { + match self.actor_json { + Some(ref actor) => actor.id.clone(), None => get_actor_url(instance_url, &self.username), - }; - Ok(actor_id) + } } - pub fn actor_url(&self, instance_url: &str) -> Result { - if let Some(ref actor_value) = self.actor_json { - if let Some(actor_url) = actor_value["url"].as_str() { - return Ok(actor_url.to_string()); + pub fn actor_url(&self, instance_url: &str) -> String { + if let Some(ref actor) = self.actor_json { + if let Some(ref actor_url) = actor.url { + return actor_url.to_string(); }; }; self.actor_id(instance_url) @@ -189,7 +194,7 @@ impl ProfileUpdateData { #[cfg(test)] mod tests { - use serde_json::json; + use crate::activitypub::actor::Actor; use super::*; const INSTANCE_HOST: &str = "example.com"; @@ -211,7 +216,10 @@ mod tests { fn test_remote_actor_address() { let remote_profile = DbActorProfile { acct: "test@remote.com".to_string(), - actor_json: Some(json!({"id": "https://test"})), + actor_json: Some(Actor { + id: "https://test".to_string(), + ..Default::default() + }), ..Default::default() }; assert_eq!(