diff --git a/src/activitypub/activity.rs b/src/activitypub/activity.rs index 1723ba5..e43eaa7 100644 --- a/src/activitypub/activity.rs +++ b/src/activitypub/activity.rs @@ -3,16 +3,13 @@ use serde::{Deserialize, Serialize}; use serde_json::{json, Value}; use uuid::Uuid; -use crate::frontend::get_tag_page_url; -use crate::models::posts::types::{Post, Visibility}; +use crate::models::posts::types::Post; use crate::models::profiles::types::DbActorProfile; -use crate::utils::files::get_file_url; use crate::utils::id::new_uuid; use super::constants::{AP_CONTEXT, AP_PUBLIC}; use super::views::{ get_actor_url, get_followers_url, - get_subscribers_url, get_object_url, }; use super::vocabulary::*; @@ -93,36 +90,6 @@ pub struct Object { pub updated: Option>, } -#[derive(Serialize)] -#[serde(rename_all = "camelCase")] -pub struct Note { - #[serde(rename = "@context")] - context: String, - - id: String, - - #[serde(rename = "type")] - object_type: String, - - #[serde(skip_serializing_if = "Vec::is_empty")] - attachment: Vec, - - attributed_to: String, - - content: String, - - #[serde(skip_serializing_if = "Option::is_none")] - in_reply_to: Option, - - published: DateTime, - - #[serde(skip_serializing_if = "Vec::is_empty")] - tag: Vec, - - to: Vec, - cc: Vec, -} - #[derive(Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct Activity { @@ -164,117 +131,6 @@ pub fn create_activity( } } -pub fn create_note( - instance_host: &str, - instance_url: &str, - post: &Post, -) -> Note { - let object_id = get_object_url( - instance_url, - &post.id, - ); - let actor_id = get_actor_url( - instance_url, - &post.author.username, - ); - let attachments: Vec = post.attachments.iter().map(|db_item| { - let url = get_file_url(instance_url, &db_item.file_name); - let media_type = db_item.media_type.clone(); - Attachment { - name: None, - attachment_type: DOCUMENT.to_string(), - media_type, - url: Some(url), - } - }).collect(); - let mut primary_audience = vec![]; - let mut secondary_audience = vec![]; - let followers_collection_url = - get_followers_url(instance_url, &post.author.username); - let subscribers_collection_url = - get_subscribers_url(instance_url, &post.author.username); - match post.visibility { - Visibility::Public => { - primary_audience.push(AP_PUBLIC.to_string()); - secondary_audience.push(followers_collection_url); - }, - Visibility::Followers => { - primary_audience.push(followers_collection_url); - }, - Visibility::Subscribers => { - primary_audience.push(subscribers_collection_url); - }, - Visibility::Direct => (), - }; - let mut tags = vec![]; - for profile in &post.mentions { - let tag_name = format!("@{}", profile.actor_address(instance_host)); - let actor_id = profile.actor_id(instance_url); - primary_audience.push(actor_id.clone()); - let tag = Tag { - name: Some(tag_name), - tag_type: MENTION.to_string(), - href: Some(actor_id), - }; - tags.push(tag); - }; - for tag_name in &post.tags { - let tag_page_url = get_tag_page_url(instance_url, tag_name); - let tag = Tag { - name: Some(format!("#{}", tag_name)), - tag_type: HASHTAG.to_string(), - href: Some(tag_page_url), - }; - tags.push(tag); - }; - let in_reply_to_object_id = match post.in_reply_to_id { - Some(in_reply_to_id) => { - let in_reply_to = post.in_reply_to.as_ref().unwrap(); - assert_eq!(in_reply_to.id, in_reply_to_id); - let in_reply_to_actor_id = in_reply_to.author.actor_id(instance_url); - if !primary_audience.contains(&in_reply_to_actor_id) { - primary_audience.push(in_reply_to_actor_id); - }; - Some(in_reply_to.get_object_id(instance_url)) - }, - None => None, - }; - Note { - context: AP_CONTEXT.to_string(), - id: object_id, - object_type: NOTE.to_string(), - attachment: attachments, - published: post.created_at, - attributed_to: actor_id, - in_reply_to: in_reply_to_object_id, - content: post.content.clone(), - tag: tags, - to: primary_audience, - cc: secondary_audience, - } -} - -pub fn create_activity_note( - instance_host: &str, - instance_url: &str, - post: &Post, -) -> Activity { - let object = create_note(instance_host, instance_url, post); - let primary_audience = object.to.clone(); - let secondary_audience = object.cc.clone(); - let activity_id = format!("{}/create", object.id); - let activity = create_activity( - instance_url, - &post.author.username, - CREATE, - activity_id, - object, - primary_audience, - secondary_audience, - ); - activity -} - pub fn create_activity_like( instance_url: &str, actor_profile: &DbActorProfile, @@ -417,133 +273,10 @@ pub fn create_activity_accept_follow( #[cfg(test)] mod tests { - use crate::activitypub::actor::Actor; use super::*; - const INSTANCE_HOST: &str = "example.com"; const INSTANCE_URL: &str = "https://example.com"; - #[test] - fn test_create_note() { - let author = DbActorProfile { - username: "author".to_string(), - ..Default::default() - }; - let post = Post { author, ..Default::default() }; - let note = create_note(INSTANCE_HOST, INSTANCE_URL, &post); - - assert_eq!( - note.id, - format!("{}/objects/{}", INSTANCE_URL, post.id), - ); - assert_eq!(note.attachment.len(), 0); - assert_eq!( - note.attributed_to, - format!("{}/users/{}", INSTANCE_URL, post.author.username), - ); - assert_eq!(note.in_reply_to.is_none(), true); - assert_eq!(note.content, post.content); - assert_eq!(note.to, vec![AP_PUBLIC]); - assert_eq!(note.cc, vec![ - get_followers_url(INSTANCE_URL, "author"), - ]); - } - - #[test] - fn test_create_note_followers_only() { - let post = Post { - visibility: Visibility::Followers, - ..Default::default() - }; - let note = create_note(INSTANCE_HOST, INSTANCE_URL, &post); - - assert_eq!(note.to, vec![ - get_followers_url(INSTANCE_URL, &post.author.username), - ]); - assert_eq!(note.cc.is_empty(), true); - } - - #[test] - fn test_create_note_with_local_parent() { - let parent = Post::default(); - let post = Post { - in_reply_to_id: Some(parent.id), - in_reply_to: Some(Box::new(parent.clone())), - ..Default::default() - }; - let note = create_note(INSTANCE_HOST, INSTANCE_URL, &post); - - assert_eq!( - note.in_reply_to.unwrap(), - format!("{}/objects/{}", INSTANCE_URL, parent.id), - ); - assert_eq!(note.to, vec![ - AP_PUBLIC.to_string(), - get_actor_url(INSTANCE_URL, &parent.author.username), - ]); - } - - #[test] - fn test_create_note_with_remote_parent() { - let parent_author_acct = "test@test.net"; - let parent_author_actor_id = "https://test.net/user/test"; - let parent_author_actor_url = "https://test.net/@test"; - let parent_author = DbActorProfile { - acct: parent_author_acct.to_string(), - actor_json: Some(Actor { - id: parent_author_actor_id.to_string(), - url: Some(parent_author_actor_url.to_string()), - ..Default::default() - }), - actor_id: Some(parent_author_actor_id.to_string()), - ..Default::default() - }; - let parent = Post { - author: parent_author.clone(), - object_id: Some("https://test.net/obj/123".to_string()), - ..Default::default() - }; - let post = Post { - in_reply_to_id: Some(parent.id), - in_reply_to: Some(Box::new(parent.clone())), - mentions: vec![parent_author], - ..Default::default() - }; - let note = create_note(INSTANCE_HOST, INSTANCE_URL, &post); - - assert_eq!( - note.in_reply_to.unwrap(), - parent.object_id.unwrap(), - ); - let tags = note.tag; - assert_eq!(tags.len(), 1); - assert_eq!(tags[0].name.as_deref().unwrap(), format!("@{}", parent_author_acct)); - assert_eq!(tags[0].href.as_ref().unwrap(), parent_author_actor_id); - assert_eq!(note.to, vec![AP_PUBLIC, parent_author_actor_id]); - } - - #[test] - fn test_create_activity_create_note() { - let author_username = "author"; - let author = DbActorProfile { - username: author_username.to_string(), - ..Default::default() - }; - let post = Post { author, ..Default::default() }; - let activity = create_activity_note(INSTANCE_HOST, INSTANCE_URL, &post); - - assert_eq!( - activity.id, - format!("{}/objects/{}/create", INSTANCE_URL, post.id), - ); - assert_eq!(activity.activity_type, CREATE); - assert_eq!( - activity.actor, - format!("{}/users/{}", INSTANCE_URL, author_username), - ); - assert_eq!(activity.to.unwrap(), json!([AP_PUBLIC])); - } - #[test] fn test_create_activity_like() { let author = DbActorProfile::default(); diff --git a/src/activitypub/builders/create_note.rs b/src/activitypub/builders/create_note.rs new file mode 100644 index 0000000..125eb52 --- /dev/null +++ b/src/activitypub/builders/create_note.rs @@ -0,0 +1,285 @@ +use chrono::{DateTime, Utc}; +use serde::Serialize; + +use crate::activitypub::{ + activity::{create_activity, Activity, Attachment, Tag}, + constants::{AP_CONTEXT, AP_PUBLIC}, + views::{get_actor_url, get_followers_url, get_object_url, get_subscribers_url}, + vocabulary::{CREATE, DOCUMENT, HASHTAG, MENTION, NOTE}, +}; +use crate::frontend::get_tag_page_url; +use crate::models::posts::types::{Post, Visibility}; +use crate::utils::files::get_file_url; + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +pub struct Note { + #[serde(rename = "@context")] + context: String, + + id: String, + + #[serde(rename = "type")] + object_type: String, + + #[serde(skip_serializing_if = "Vec::is_empty")] + attachment: Vec, + + attributed_to: String, + + content: String, + + #[serde(skip_serializing_if = "Option::is_none")] + in_reply_to: Option, + + published: DateTime, + + #[serde(skip_serializing_if = "Vec::is_empty")] + tag: Vec, + + to: Vec, + cc: Vec, +} + +pub fn build_note( + instance_host: &str, + instance_url: &str, + post: &Post, +) -> Note { + let object_id = get_object_url( + instance_url, + &post.id, + ); + let actor_id = get_actor_url( + instance_url, + &post.author.username, + ); + let attachments: Vec = post.attachments.iter().map(|db_item| { + let url = get_file_url(instance_url, &db_item.file_name); + let media_type = db_item.media_type.clone(); + Attachment { + name: None, + attachment_type: DOCUMENT.to_string(), + media_type, + url: Some(url), + } + }).collect(); + let mut primary_audience = vec![]; + let mut secondary_audience = vec![]; + let followers_collection_url = + get_followers_url(instance_url, &post.author.username); + let subscribers_collection_url = + get_subscribers_url(instance_url, &post.author.username); + match post.visibility { + Visibility::Public => { + primary_audience.push(AP_PUBLIC.to_string()); + secondary_audience.push(followers_collection_url); + }, + Visibility::Followers => { + primary_audience.push(followers_collection_url); + }, + Visibility::Subscribers => { + primary_audience.push(subscribers_collection_url); + }, + Visibility::Direct => (), + }; + let mut tags = vec![]; + for profile in &post.mentions { + let tag_name = format!("@{}", profile.actor_address(instance_host)); + let actor_id = profile.actor_id(instance_url); + primary_audience.push(actor_id.clone()); + let tag = Tag { + name: Some(tag_name), + tag_type: MENTION.to_string(), + href: Some(actor_id), + }; + tags.push(tag); + }; + for tag_name in &post.tags { + let tag_page_url = get_tag_page_url(instance_url, tag_name); + let tag = Tag { + name: Some(format!("#{}", tag_name)), + tag_type: HASHTAG.to_string(), + href: Some(tag_page_url), + }; + tags.push(tag); + }; + let in_reply_to_object_id = match post.in_reply_to_id { + Some(in_reply_to_id) => { + let in_reply_to = post.in_reply_to.as_ref().unwrap(); + assert_eq!(in_reply_to.id, in_reply_to_id); + let in_reply_to_actor_id = in_reply_to.author.actor_id(instance_url); + if !primary_audience.contains(&in_reply_to_actor_id) { + primary_audience.push(in_reply_to_actor_id); + }; + Some(in_reply_to.get_object_id(instance_url)) + }, + None => None, + }; + Note { + context: AP_CONTEXT.to_string(), + id: object_id, + object_type: NOTE.to_string(), + attachment: attachments, + published: post.created_at, + attributed_to: actor_id, + in_reply_to: in_reply_to_object_id, + content: post.content.clone(), + tag: tags, + to: primary_audience, + cc: secondary_audience, + } +} + +pub fn build_create_note( + instance_host: &str, + instance_url: &str, + post: &Post, +) -> Activity { + let object = build_note(instance_host, instance_url, post); + let primary_audience = object.to.clone(); + let secondary_audience = object.cc.clone(); + let activity_id = format!("{}/create", object.id); + let activity = create_activity( + instance_url, + &post.author.username, + CREATE, + activity_id, + object, + primary_audience, + secondary_audience, + ); + activity +} + +#[cfg(test)] +mod tests { + use serde_json::json; + use crate::activitypub::actor::Actor; + use crate::models::profiles::types::DbActorProfile; + use super::*; + + const INSTANCE_HOST: &str = "example.com"; + const INSTANCE_URL: &str = "https://example.com"; + + #[test] + fn test_build_note() { + let author = DbActorProfile { + username: "author".to_string(), + ..Default::default() + }; + let post = Post { author, ..Default::default() }; + let note = build_note(INSTANCE_HOST, INSTANCE_URL, &post); + + assert_eq!( + note.id, + format!("{}/objects/{}", INSTANCE_URL, post.id), + ); + assert_eq!(note.attachment.len(), 0); + assert_eq!( + note.attributed_to, + format!("{}/users/{}", INSTANCE_URL, post.author.username), + ); + assert_eq!(note.in_reply_to.is_none(), true); + assert_eq!(note.content, post.content); + assert_eq!(note.to, vec![AP_PUBLIC]); + assert_eq!(note.cc, vec![ + get_followers_url(INSTANCE_URL, "author"), + ]); + } + + #[test] + fn test_build_note_followers_only() { + let post = Post { + visibility: Visibility::Followers, + ..Default::default() + }; + let note = build_note(INSTANCE_HOST, INSTANCE_URL, &post); + + assert_eq!(note.to, vec![ + get_followers_url(INSTANCE_URL, &post.author.username), + ]); + assert_eq!(note.cc.is_empty(), true); + } + + #[test] + fn test_build_note_with_local_parent() { + let parent = Post::default(); + let post = Post { + in_reply_to_id: Some(parent.id), + in_reply_to: Some(Box::new(parent.clone())), + ..Default::default() + }; + let note = build_note(INSTANCE_HOST, INSTANCE_URL, &post); + + assert_eq!( + note.in_reply_to.unwrap(), + format!("{}/objects/{}", INSTANCE_URL, parent.id), + ); + assert_eq!(note.to, vec![ + AP_PUBLIC.to_string(), + get_actor_url(INSTANCE_URL, &parent.author.username), + ]); + } + + #[test] + fn test_build_note_with_remote_parent() { + let parent_author_acct = "test@test.net"; + let parent_author_actor_id = "https://test.net/user/test"; + let parent_author_actor_url = "https://test.net/@test"; + let parent_author = DbActorProfile { + acct: parent_author_acct.to_string(), + actor_json: Some(Actor { + id: parent_author_actor_id.to_string(), + url: Some(parent_author_actor_url.to_string()), + ..Default::default() + }), + actor_id: Some(parent_author_actor_id.to_string()), + ..Default::default() + }; + let parent = Post { + author: parent_author.clone(), + object_id: Some("https://test.net/obj/123".to_string()), + ..Default::default() + }; + let post = Post { + in_reply_to_id: Some(parent.id), + in_reply_to: Some(Box::new(parent.clone())), + mentions: vec![parent_author], + ..Default::default() + }; + let note = build_note(INSTANCE_HOST, INSTANCE_URL, &post); + + assert_eq!( + note.in_reply_to.unwrap(), + parent.object_id.unwrap(), + ); + let tags = note.tag; + assert_eq!(tags.len(), 1); + assert_eq!(tags[0].name.as_deref().unwrap(), format!("@{}", parent_author_acct)); + assert_eq!(tags[0].href.as_ref().unwrap(), parent_author_actor_id); + assert_eq!(note.to, vec![AP_PUBLIC, parent_author_actor_id]); + } + + #[test] + fn test_build_create_note() { + let author_username = "author"; + let author = DbActorProfile { + username: author_username.to_string(), + ..Default::default() + }; + let post = Post { author, ..Default::default() }; + let activity = build_create_note(INSTANCE_HOST, INSTANCE_URL, &post); + + assert_eq!( + activity.id, + format!("{}/objects/{}/create", INSTANCE_URL, post.id), + ); + assert_eq!(activity.activity_type, CREATE); + assert_eq!( + activity.actor, + format!("{}/users/{}", INSTANCE_URL, author_username), + ); + assert_eq!(activity.to.unwrap(), json!([AP_PUBLIC])); + } +} diff --git a/src/activitypub/builders/mod.rs b/src/activitypub/builders/mod.rs index b440779..0269d32 100644 --- a/src/activitypub/builders/mod.rs +++ b/src/activitypub/builders/mod.rs @@ -1,3 +1,4 @@ +pub mod create_note; pub mod delete_note; pub mod delete_person; pub mod undo_follow; diff --git a/src/activitypub/views.rs b/src/activitypub/views.rs index af05b2a..490c8a8 100644 --- a/src/activitypub/views.rs +++ b/src/activitypub/views.rs @@ -15,7 +15,7 @@ use crate::errors::HttpError; use crate::frontend::{get_post_page_url, get_profile_page_url}; use crate::models::posts::queries::{get_posts_by_author, get_thread}; use crate::models::users::queries::get_user_by_name; -use super::activity::{create_note, create_activity_note}; +use super::builders::create_note::{build_note, build_create_note}; use super::actor::{get_local_actor, get_instance_actor}; use super::collections::{ COLLECTION_PAGE_SIZE, @@ -168,7 +168,7 @@ async fn outbox( }; // Replies are not included so post.in_reply_to // does not need to be populated - let activity = create_activity_note( + let activity = build_create_note( &instance.host(), &instance.url(), post, @@ -312,7 +312,7 @@ pub async fn object_view( }, None => None, }; - let object = create_note( + let object = build_note( &config.instance().host(), &config.instance().url(), &post, diff --git a/src/mastodon_api/statuses/views.rs b/src/mastodon_api/statuses/views.rs index bf7fa83..8809f12 100644 --- a/src/mastodon_api/statuses/views.rs +++ b/src/mastodon_api/statuses/views.rs @@ -6,12 +6,12 @@ use actix_web_httpauth::extractors::bearer::BearerAuth; use uuid::Uuid; use crate::activitypub::activity::{ - create_activity_note, create_activity_like, create_activity_undo_like, create_activity_announce, create_activity_undo_announce, }; +use crate::activitypub::builders::create_note::build_create_note; use crate::activitypub::builders::delete_note::prepare_delete_note; use crate::activitypub::deliverer::deliver_activity; use crate::config::Config; @@ -119,7 +119,7 @@ async fn create_status( Box::new(in_reply_to) }); // Federate - let activity = create_activity_note( + let activity = build_create_note( &instance.host(), &instance.url(), &post,