From a685829472e3af0c6ea816ffc23a159d4e57e883 Mon Sep 17 00:00:00 2001 From: silverpill Date: Sat, 1 Oct 2022 11:08:56 +0000 Subject: [PATCH] Implement FEP-e232 and allow to add quotes to posts --- FEDERATION.md | 4 +++ src/activitypub/activity.rs | 3 ++ src/activitypub/builders/create_note.rs | 18 +++++++++-- src/activitypub/handlers/create_note.rs | 25 ++++++++++++--- src/mastodon_api/statuses/types.rs | 3 +- src/mastodon_api/statuses/views.rs | 41 +++++++++++++++++++++++-- 6 files changed, 83 insertions(+), 11 deletions(-) diff --git a/FEDERATION.md b/FEDERATION.md index d192aef..f7335d0 100644 --- a/FEDERATION.md +++ b/FEDERATION.md @@ -28,6 +28,10 @@ And these additional standards: Activities are implemented in way that is compatible with Pleroma, Mastodon and other popular ActivityPub servers. +## Supported FEPs + +- [FEP-e232](https://codeberg.org/fediverse/fep/src/branch/main/feps/fep-e232.md) + ## Profile extensions ### Cryptocurrency addresses diff --git a/src/activitypub/activity.rs b/src/activitypub/activity.rs index cd2ee8a..58dd35d 100644 --- a/src/activitypub/activity.rs +++ b/src/activitypub/activity.rs @@ -35,6 +35,9 @@ pub struct Tag { pub tag_type: String, pub href: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub media_type: Option, } #[derive(Default, Deserialize, Serialize)] diff --git a/src/activitypub/builders/create_note.rs b/src/activitypub/builders/create_note.rs index 53c3279..478c885 100644 --- a/src/activitypub/builders/create_note.rs +++ b/src/activitypub/builders/create_note.rs @@ -5,7 +5,7 @@ use tokio_postgres::GenericClient; use crate::activitypub::{ activity::{create_activity, Activity, Attachment, Tag}, actors::types::Actor, - constants::{AP_CONTEXT, AP_PUBLIC}, + constants::{AP_MEDIA_TYPE, AP_CONTEXT, AP_PUBLIC}, deliverer::OutgoingActivity, identifiers::{ local_actor_id, @@ -13,7 +13,7 @@ use crate::activitypub::{ local_actor_subscribers, local_object_id, }, - vocabulary::{CREATE, DOCUMENT, HASHTAG, MENTION, NOTE}, + vocabulary::{CREATE, DOCUMENT, HASHTAG, LINK, MENTION, NOTE}, }; use crate::config::Instance; use crate::errors::DatabaseError; @@ -103,6 +103,7 @@ pub fn build_note( name: Some(tag_name), tag_type: MENTION.to_string(), href: Some(actor_id), + media_type: None, }; tags.push(tag); }; @@ -112,6 +113,19 @@ pub fn build_note( name: Some(format!("#{}", tag_name)), tag_type: HASHTAG.to_string(), href: Some(tag_page_url), + media_type: None, + }; + tags.push(tag); + }; + assert_eq!(post.links.len(), post.linked.len()); + for linked in &post.linked { + // Build FEP-e232 object link + let link_href = linked.get_object_id(instance_url); + let tag = Tag { + name: Some(format!("RE: {}", link_href)), + tag_type: LINK.to_string(), + href: Some(link_href), + media_type: Some(AP_MEDIA_TYPE.to_string()), }; tags.push(tag); }; diff --git a/src/activitypub/handlers/create_note.rs b/src/activitypub/handlers/create_note.rs index 3ac0a53..2236351 100644 --- a/src/activitypub/handlers/create_note.rs +++ b/src/activitypub/handlers/create_note.rs @@ -8,7 +8,7 @@ use uuid::Uuid; use crate::activitypub::{ activity::{Attachment, Link, Object, Tag}, - constants::AP_PUBLIC, + constants::{AP_MEDIA_TYPE, AP_PUBLIC, AS_MEDIA_TYPE}, fetcher::fetchers::fetch_file, fetcher::helpers::{ get_or_import_profile_by_actor_id, @@ -17,7 +17,7 @@ use crate::activitypub::{ }, identifiers::{parse_local_actor_id, parse_local_object_id}, receiver::{parse_array, parse_property_value}, - vocabulary::{DOCUMENT, HASHTAG, IMAGE, MENTION, NOTE}, + vocabulary::{DOCUMENT, HASHTAG, IMAGE, LINK, MENTION, NOTE}, }; use crate::config::Instance; use crate::errors::{ConversionError, DatabaseError, ValidationError}; @@ -305,18 +305,33 @@ pub async fn handle_note( } else { log::warn!("failed to parse mention {}", tag_name); }; + } else if tag.tag_type == LINK { + if tag.media_type != Some(AP_MEDIA_TYPE.to_string()) && + tag.media_type != Some(AS_MEDIA_TYPE.to_string()) + { + // Unknown media type + continue; + }; + if let Some(href) = tag.href { + let linked_id = get_internal_post_id( + db_client, + &instance.url(), + &href, + redirects, + ).await?; + links.push(linked_id); + }; }; }; }; if let Some(ref object_id) = object.quote_url { - log::warn!("link to object found: {}", object_id); - let quoted_id = get_internal_post_id( + let linked_id = get_internal_post_id( db_client, &instance.url(), object_id, redirects, ).await?; - links.push(quoted_id); + links.push(linked_id); }; let in_reply_to_id = match object.in_reply_to { diff --git a/src/mastodon_api/statuses/types.rs b/src/mastodon_api/statuses/types.rs index ef22e39..0d36040 100644 --- a/src/mastodon_api/statuses/types.rs +++ b/src/mastodon_api/statuses/types.rs @@ -146,6 +146,7 @@ pub struct StatusData { // Not supported by Mastodon pub mentions: Option>, + pub links: Option>, } impl TryFrom for PostCreateData { @@ -169,7 +170,7 @@ impl TryFrom for PostCreateData { attachments: value.media_ids.unwrap_or(vec![]), mentions: value.mentions.unwrap_or(vec![]), tags: vec![], - links: vec![], + links: value.links.unwrap_or(vec![]), object_id: None, created_at: Utc::now(), }; diff --git a/src/mastodon_api/statuses/views.rs b/src/mastodon_api/statuses/views.rs index b716020..810eca5 100644 --- a/src/mastodon_api/statuses/views.rs +++ b/src/mastodon_api/statuses/views.rs @@ -81,8 +81,6 @@ async fn create_status( .into_iter().map(|profile| profile.id); post_data.mentions.extend(subscribers); }; - post_data.mentions.sort(); - post_data.mentions.dedup(); // Hashtags post_data.tags = find_hashtags(&post_data.content); post_data.content = replace_hashtags( @@ -90,6 +88,39 @@ async fn create_status( &post_data.content, &post_data.tags, ); + // Links + let linked = match &post_data.links[..] { + [] => vec![], + [linked_id] => { + if post_data.in_reply_to_id.is_some() { + return Err(ValidationError("can't add links to reply").into()); + }; + if post_data.visibility != Visibility::Public { + return Err(ValidationError("can't add links to non-public posts").into()); + }; + let linked = match get_post_by_id(db_client, linked_id).await { + Ok(post) => post, + Err(DatabaseError::NotFound(_)) => { + return Err(ValidationError("referenced post does't exist").into()); + }, + Err(other_error) => return Err(other_error.into()), + }; + if linked.repost_of_id.is_some() { + return Err(ValidationError("can't reference repost").into()); + }; + if linked.visibility != Visibility::Public { + return Err(ValidationError("can't reference non-public post").into()); + }; + // Append inline quote and add author to mentions + post_data.content += &format!( + r#"

RE: {0}

"#, + linked.get_object_id(&instance.url()), + ); + post_data.mentions.push(linked.author.id); + vec![linked] + }, + _ => return Err(ValidationError("too many links").into()), + }; // Reply validation let maybe_in_reply_to = if let Some(in_reply_to_id) = post_data.in_reply_to_id.as_ref() { let in_reply_to = match get_post_by_id(db_client, in_reply_to_id).await { @@ -118,14 +149,18 @@ async fn create_status( } else { None }; + // Remove duplicate mentions + post_data.mentions.sort(); + post_data.mentions.dedup(); // Create post 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; Box::new(in_reply_to) }); + post.linked = linked; // Federate - prepare_create_note(db_client, config.instance(), ¤t_user, &post).await? + prepare_create_note(db_client, instance.clone(), ¤t_user, &post).await? .spawn_deliver(); let status = Status::from_post(post, &instance.url());