Implement FEP-e232 and allow to add quotes to posts
This commit is contained in:
parent
5b7979b9d4
commit
a685829472
6 changed files with 83 additions and 11 deletions
|
@ -28,6 +28,10 @@ And these additional standards:
|
||||||
|
|
||||||
Activities are implemented in way that is compatible with Pleroma, Mastodon and other popular ActivityPub servers.
|
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
|
## Profile extensions
|
||||||
|
|
||||||
### Cryptocurrency addresses
|
### Cryptocurrency addresses
|
||||||
|
|
|
@ -35,6 +35,9 @@ pub struct Tag {
|
||||||
pub tag_type: String,
|
pub tag_type: String,
|
||||||
|
|
||||||
pub href: Option<String>,
|
pub href: Option<String>,
|
||||||
|
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub media_type: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default, Deserialize, Serialize)]
|
#[derive(Default, Deserialize, Serialize)]
|
||||||
|
|
|
@ -5,7 +5,7 @@ use tokio_postgres::GenericClient;
|
||||||
use crate::activitypub::{
|
use crate::activitypub::{
|
||||||
activity::{create_activity, Activity, Attachment, Tag},
|
activity::{create_activity, Activity, Attachment, Tag},
|
||||||
actors::types::Actor,
|
actors::types::Actor,
|
||||||
constants::{AP_CONTEXT, AP_PUBLIC},
|
constants::{AP_MEDIA_TYPE, AP_CONTEXT, AP_PUBLIC},
|
||||||
deliverer::OutgoingActivity,
|
deliverer::OutgoingActivity,
|
||||||
identifiers::{
|
identifiers::{
|
||||||
local_actor_id,
|
local_actor_id,
|
||||||
|
@ -13,7 +13,7 @@ use crate::activitypub::{
|
||||||
local_actor_subscribers,
|
local_actor_subscribers,
|
||||||
local_object_id,
|
local_object_id,
|
||||||
},
|
},
|
||||||
vocabulary::{CREATE, DOCUMENT, HASHTAG, MENTION, NOTE},
|
vocabulary::{CREATE, DOCUMENT, HASHTAG, LINK, MENTION, NOTE},
|
||||||
};
|
};
|
||||||
use crate::config::Instance;
|
use crate::config::Instance;
|
||||||
use crate::errors::DatabaseError;
|
use crate::errors::DatabaseError;
|
||||||
|
@ -103,6 +103,7 @@ pub fn build_note(
|
||||||
name: Some(tag_name),
|
name: Some(tag_name),
|
||||||
tag_type: MENTION.to_string(),
|
tag_type: MENTION.to_string(),
|
||||||
href: Some(actor_id),
|
href: Some(actor_id),
|
||||||
|
media_type: None,
|
||||||
};
|
};
|
||||||
tags.push(tag);
|
tags.push(tag);
|
||||||
};
|
};
|
||||||
|
@ -112,6 +113,19 @@ pub fn build_note(
|
||||||
name: Some(format!("#{}", tag_name)),
|
name: Some(format!("#{}", tag_name)),
|
||||||
tag_type: HASHTAG.to_string(),
|
tag_type: HASHTAG.to_string(),
|
||||||
href: Some(tag_page_url),
|
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);
|
tags.push(tag);
|
||||||
};
|
};
|
||||||
|
|
|
@ -8,7 +8,7 @@ use uuid::Uuid;
|
||||||
|
|
||||||
use crate::activitypub::{
|
use crate::activitypub::{
|
||||||
activity::{Attachment, Link, Object, Tag},
|
activity::{Attachment, Link, Object, Tag},
|
||||||
constants::AP_PUBLIC,
|
constants::{AP_MEDIA_TYPE, AP_PUBLIC, AS_MEDIA_TYPE},
|
||||||
fetcher::fetchers::fetch_file,
|
fetcher::fetchers::fetch_file,
|
||||||
fetcher::helpers::{
|
fetcher::helpers::{
|
||||||
get_or_import_profile_by_actor_id,
|
get_or_import_profile_by_actor_id,
|
||||||
|
@ -17,7 +17,7 @@ use crate::activitypub::{
|
||||||
},
|
},
|
||||||
identifiers::{parse_local_actor_id, parse_local_object_id},
|
identifiers::{parse_local_actor_id, parse_local_object_id},
|
||||||
receiver::{parse_array, parse_property_value},
|
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::config::Instance;
|
||||||
use crate::errors::{ConversionError, DatabaseError, ValidationError};
|
use crate::errors::{ConversionError, DatabaseError, ValidationError};
|
||||||
|
@ -305,18 +305,33 @@ pub async fn handle_note(
|
||||||
} else {
|
} else {
|
||||||
log::warn!("failed to parse mention {}", tag_name);
|
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 {
|
if let Some(ref object_id) = object.quote_url {
|
||||||
log::warn!("link to object found: {}", object_id);
|
let linked_id = get_internal_post_id(
|
||||||
let quoted_id = get_internal_post_id(
|
|
||||||
db_client,
|
db_client,
|
||||||
&instance.url(),
|
&instance.url(),
|
||||||
object_id,
|
object_id,
|
||||||
redirects,
|
redirects,
|
||||||
).await?;
|
).await?;
|
||||||
links.push(quoted_id);
|
links.push(linked_id);
|
||||||
};
|
};
|
||||||
|
|
||||||
let in_reply_to_id = match object.in_reply_to {
|
let in_reply_to_id = match object.in_reply_to {
|
||||||
|
|
|
@ -146,6 +146,7 @@ pub struct StatusData {
|
||||||
|
|
||||||
// Not supported by Mastodon
|
// Not supported by Mastodon
|
||||||
pub mentions: Option<Vec<Uuid>>,
|
pub mentions: Option<Vec<Uuid>>,
|
||||||
|
pub links: Option<Vec<Uuid>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TryFrom<StatusData> for PostCreateData {
|
impl TryFrom<StatusData> for PostCreateData {
|
||||||
|
@ -169,7 +170,7 @@ impl TryFrom<StatusData> for PostCreateData {
|
||||||
attachments: value.media_ids.unwrap_or(vec![]),
|
attachments: value.media_ids.unwrap_or(vec![]),
|
||||||
mentions: value.mentions.unwrap_or(vec![]),
|
mentions: value.mentions.unwrap_or(vec![]),
|
||||||
tags: vec![],
|
tags: vec![],
|
||||||
links: vec![],
|
links: value.links.unwrap_or(vec![]),
|
||||||
object_id: None,
|
object_id: None,
|
||||||
created_at: Utc::now(),
|
created_at: Utc::now(),
|
||||||
};
|
};
|
||||||
|
|
|
@ -81,8 +81,6 @@ async fn create_status(
|
||||||
.into_iter().map(|profile| profile.id);
|
.into_iter().map(|profile| profile.id);
|
||||||
post_data.mentions.extend(subscribers);
|
post_data.mentions.extend(subscribers);
|
||||||
};
|
};
|
||||||
post_data.mentions.sort();
|
|
||||||
post_data.mentions.dedup();
|
|
||||||
// Hashtags
|
// Hashtags
|
||||||
post_data.tags = find_hashtags(&post_data.content);
|
post_data.tags = find_hashtags(&post_data.content);
|
||||||
post_data.content = replace_hashtags(
|
post_data.content = replace_hashtags(
|
||||||
|
@ -90,6 +88,39 @@ async fn create_status(
|
||||||
&post_data.content,
|
&post_data.content,
|
||||||
&post_data.tags,
|
&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#"<p class="inline-quote">RE: <a href="{0}">{0}</a></p>"#,
|
||||||
|
linked.get_object_id(&instance.url()),
|
||||||
|
);
|
||||||
|
post_data.mentions.push(linked.author.id);
|
||||||
|
vec![linked]
|
||||||
|
},
|
||||||
|
_ => return Err(ValidationError("too many links").into()),
|
||||||
|
};
|
||||||
// Reply validation
|
// 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) = post_data.in_reply_to_id.as_ref() {
|
||||||
let in_reply_to = match get_post_by_id(db_client, in_reply_to_id).await {
|
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 {
|
} else {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
// Remove duplicate mentions
|
||||||
|
post_data.mentions.sort();
|
||||||
|
post_data.mentions.dedup();
|
||||||
// Create post
|
// Create post
|
||||||
let mut post = create_post(db_client, ¤t_user.id, post_data).await?;
|
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| {
|
post.in_reply_to = maybe_in_reply_to.map(|mut in_reply_to| {
|
||||||
in_reply_to.reply_count += 1;
|
in_reply_to.reply_count += 1;
|
||||||
Box::new(in_reply_to)
|
Box::new(in_reply_to)
|
||||||
});
|
});
|
||||||
|
post.linked = linked;
|
||||||
// Federate
|
// 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();
|
.spawn_deliver();
|
||||||
|
|
||||||
let status = Status::from_post(post, &instance.url());
|
let status = Status::from_post(post, &instance.url());
|
||||||
|
|
Loading…
Reference in a new issue