diff --git a/src/mastodon_api/statuses/types.rs b/src/mastodon_api/statuses/types.rs index c6e0d4e..83e8357 100644 --- a/src/mastodon_api/statuses/types.rs +++ b/src/mastodon_api/statuses/types.rs @@ -2,12 +2,10 @@ use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use uuid::Uuid; -use crate::errors::ValidationError; use crate::mastodon_api::accounts::types::Account; use crate::mastodon_api::media::types::Attachment; -use crate::models::posts::types::{Post, PostCreateData, Visibility}; +use crate::models::posts::types::{Post, Visibility}; use crate::models::profiles::types::DbActorProfile; -use crate::utils::markdown::markdown_lite_to_html; /// https://docs.joinmastodon.org/entities/mention/ #[derive(Serialize)] @@ -151,68 +149,7 @@ pub struct StatusData { pub content_type: String, } -impl TryFrom for PostCreateData { - - type Error = ValidationError; - - fn try_from(status_data: StatusData) -> Result { - let visibility = match status_data.visibility.as_deref() { - Some("public") => Visibility::Public, - Some("direct") => Visibility::Direct, - Some("private") => Visibility::Followers, - Some("subscribers") => Visibility::Subscribers, - Some(_) => return Err(ValidationError("invalid visibility parameter")), - None => Visibility::Public, - }; - let content = match status_data.content_type.as_str() { - "text/html" => status_data.status, - "text/markdown" => { - markdown_lite_to_html(&status_data.status) - .map_err(|_| ValidationError("invalid markdown"))? - }, - _ => return Err(ValidationError("unsupported content type")), - }; - let post_data = Self { - content: content, - in_reply_to_id: status_data.in_reply_to_id, - repost_of_id: None, - visibility: visibility, - attachments: status_data.media_ids.unwrap_or(vec![]), - mentions: status_data.mentions.unwrap_or(vec![]), - tags: vec![], - links: vec![], - object_id: None, - created_at: Utc::now(), - }; - Ok(post_data) - } -} - #[derive(Deserialize)] pub struct TransactionData { pub transaction_id: String, } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_status_data_into_post_data() { - let status_content = "

test

"; - let status_data = StatusData { - status: status_content.to_string(), - media_ids: None, - in_reply_to_id: None, - visibility: Some("public".to_string()), - mentions: None, - content_type: "text/html".to_string(), - }; - let post_data = PostCreateData::try_from(status_data).unwrap(); - assert_eq!(post_data.content, status_content); - assert_eq!(post_data.visibility, Visibility::Public); - assert_eq!(post_data.attachments, vec![]); - assert_eq!(post_data.mentions, vec![]); - assert_eq!(post_data.links, vec![]); - } -} diff --git a/src/mastodon_api/statuses/views.rs b/src/mastodon_api/statuses/views.rs index 7e57cc3..c982353 100644 --- a/src/mastodon_api/statuses/views.rs +++ b/src/mastodon_api/statuses/views.rs @@ -1,6 +1,7 @@ /// https://docs.joinmastodon.org/methods/statuses/ use actix_web::{delete, get, post, web, HttpResponse, Scope}; use actix_web_httpauth::extractors::bearer::BearerAuth; +use chrono::Utc; use uuid::Uuid; use crate::activitypub::builders::{ @@ -39,7 +40,10 @@ use crate::models::reactions::queries::{ delete_reaction, }; use crate::models::relationships::queries::get_subscribers; -use crate::utils::currencies::Currency; +use crate::utils::{ + currencies::Currency, + markdown::markdown_lite_to_html, +}; use super::helpers::{ build_status, build_status_list, @@ -56,58 +60,78 @@ async fn create_status( let db_client = &mut **get_database_client(&db_pool).await?; let current_user = get_current_user(db_client, auth.token()).await?; let instance = config.instance(); - let mut post_data = PostCreateData::try_from(status_data.into_inner())?; + let status_data = status_data.into_inner(); + let visibility = match status_data.visibility.as_deref() { + Some("public") => Visibility::Public, + Some("direct") => Visibility::Direct, + Some("private") => Visibility::Followers, + Some("subscribers") => Visibility::Subscribers, + Some(_) => return Err(ValidationError("invalid visibility parameter").into()), + None => Visibility::Public, + }; + let mut content = match status_data.content_type.as_str() { + "text/html" => status_data.status, + "text/markdown" => { + markdown_lite_to_html(&status_data.status) + .map_err(|_| ValidationError("invalid markdown"))? + }, + _ => return Err(ValidationError("unsupported content type").into()), + }; + let mut mentions = status_data.mentions.unwrap_or(vec![]); // Mentions let mention_map = find_mentioned_profiles( db_client, &instance.hostname(), - &post_data.content, + &content, ).await?; - post_data.content = replace_mentions( + content = replace_mentions( &mention_map, &instance.hostname(), &instance.url(), - &post_data.content, + &content, ); - post_data.mentions.extend(mention_map.values() - .map(|profile| profile.id)); - if post_data.visibility == Visibility::Subscribers { + mentions.extend(mention_map.values().map(|profile| profile.id)); + // Hashtags + let tags = find_hashtags(&content); + content = replace_hashtags( + &instance.url(), + &content, + &tags, + ); + // Links + let link_map = find_linked_posts( + db_client, + &instance.url(), + &content, + ).await?; + content = replace_object_links( + &link_map, + &content, + ); + let links: Vec<_> = link_map.values().map(|post| post.id).collect(); + let linked = link_map.into_values().collect(); + // Clean content + content = clean_content(&content)?; + + if visibility == Visibility::Subscribers { // Mention all subscribers. // This makes post accessible only to active subscribers // and is required for sending activities to subscribers // on other instances. let subscribers = get_subscribers(db_client, ¤t_user.id).await? .into_iter().map(|profile| profile.id); - post_data.mentions.extend(subscribers); + mentions.extend(subscribers); }; - // Hashtags - post_data.tags = find_hashtags(&post_data.content); - post_data.content = replace_hashtags( - &instance.url(), - &post_data.content, - &post_data.tags, - ); - // Links - let link_map = find_linked_posts( - db_client, - &instance.url(), - &post_data.content, - ).await?; - post_data.content = replace_object_links( - &link_map, - &post_data.content, - ); - post_data.links.extend(link_map.values().map(|post| post.id)); - let linked = link_map.into_values().collect(); - // Clean content - post_data.content = clean_content(&post_data.content)?; + // Remove duplicate mentions + mentions.sort(); + mentions.dedup(); // Links validation - if post_data.links.len() > 0 && post_data.visibility != Visibility::Public { + if links.len() > 0 && visibility != Visibility::Public { return Err(ValidationError("can't add links to non-public posts").into()); }; // Reply validation - let maybe_in_reply_to = if let Some(in_reply_to_id) = post_data.in_reply_to_id.as_ref() { + let maybe_in_reply_to = if let Some(in_reply_to_id) = status_data.in_reply_to_id.as_ref() { let in_reply_to = match get_post_by_id(db_client, in_reply_to_id).await { Ok(post) => post, Err(DatabaseError::NotFound(_)) => { @@ -119,14 +143,14 @@ async fn create_status( return Err(ValidationError("can't reply to repost").into()); }; if in_reply_to.visibility != Visibility::Public && - post_data.visibility != Visibility::Direct { + visibility != Visibility::Direct { return Err(ValidationError("reply must have direct visibility").into()); }; - if post_data.visibility != Visibility::Public { + if visibility != Visibility::Public { let mut in_reply_to_audience: Vec<_> = in_reply_to.mentions.iter() .map(|profile| profile.id).collect(); in_reply_to_audience.push(in_reply_to.author.id); - if !post_data.mentions.iter().all(|id| in_reply_to_audience.contains(id)) { + if !mentions.iter().all(|id| in_reply_to_audience.contains(id)) { return Err(ValidationError("audience can't be expanded").into()); }; }; @@ -134,11 +158,20 @@ async fn create_status( } else { None }; - // Remove duplicate mentions - post_data.mentions.sort(); - post_data.mentions.dedup(); // Create post + let post_data = PostCreateData { + content: content, + in_reply_to_id: status_data.in_reply_to_id, + repost_of_id: None, + visibility: visibility, + attachments: status_data.media_ids.unwrap_or(vec![]), + mentions: mentions, + tags: tags, + links: links, + object_id: None, + created_at: Utc::now(), + }; let mut post = create_post(db_client, ¤t_user.id, post_data).await?; post.in_reply_to = maybe_in_reply_to.map(|mut in_reply_to| { in_reply_to.reply_count += 1;