Refactor handle_note() function

This commit is contained in:
silverpill 2023-01-30 23:04:56 +00:00
parent f142bee72b
commit 86beb532e2
5 changed files with 150 additions and 112 deletions

View file

@ -170,7 +170,6 @@ pub async fn import_post(
object_received: Option<Object>, object_received: Option<Object>,
) -> Result<Post, HandlerError> { ) -> Result<Post, HandlerError> {
let instance = config.instance(); let instance = config.instance();
let media_dir = config.media_dir();
if parse_local_object_id(&instance.url(), &object_id).is_ok() { if parse_local_object_id(&instance.url(), &object_id).is_ok() {
return Err(HandlerError::LocalObject); return Err(HandlerError::LocalObject);
}; };
@ -264,9 +263,8 @@ pub async fn import_post(
objects.reverse(); objects.reverse();
for object in objects { for object in objects {
let post = handle_note( let post = handle_note(
config,
db_client, db_client,
&instance,
&media_dir,
object, object,
&redirects, &redirects,
).await?; ).await?;

View file

@ -1,5 +1,4 @@
use std::collections::HashMap; use std::collections::HashMap;
use std::path::Path;
use chrono::Utc; use chrono::Utc;
use serde_json::{Value as JsonValue}; use serde_json::{Value as JsonValue};
@ -18,7 +17,7 @@ use crate::activitypub::{
types::{Attachment, EmojiTag, Link, LinkTag, Object, Tag}, types::{Attachment, EmojiTag, Link, LinkTag, Object, Tag},
vocabulary::*, vocabulary::*,
}; };
use crate::config::{Config, Instance}; use crate::config::Config;
use crate::database::{DatabaseClient, DatabaseError}; use crate::database::{DatabaseClient, DatabaseError};
use crate::errors::{ConversionError, ValidationError}; use crate::errors::{ConversionError, ValidationError};
use crate::models::{ use crate::models::{
@ -56,7 +55,9 @@ use crate::utils::{
}; };
use super::HandlerResult; use super::HandlerResult;
fn get_note_author_id(object: &Object) -> Result<String, ValidationError> { fn get_object_attributed_to(object: &Object)
-> Result<String, ValidationError>
{
let attributed_to = object.attributed_to.as_ref() let attributed_to = object.attributed_to.as_ref()
.ok_or(ValidationError("unattributed note"))?; .ok_or(ValidationError("unattributed note"))?;
let author_id = parse_array(attributed_to) let author_id = parse_array(attributed_to)
@ -82,7 +83,7 @@ fn parse_object_url(value: &JsonValue) -> Result<String, ConversionError> {
Ok(object_url) Ok(object_url)
} }
pub fn get_note_content(object: &Object) -> Result<String, ValidationError> { pub fn get_object_content(object: &Object) -> Result<String, ValidationError> {
let mut content = if let Some(ref content) = object.content { let mut content = if let Some(ref content) = object.content {
if object.media_type == Some("text/markdown".to_string()) { if object.media_type == Some("text/markdown".to_string()) {
format!("<p>{}</p>", content) format!("<p>{}</p>", content)
@ -114,38 +115,6 @@ pub fn get_note_content(object: &Object) -> Result<String, ValidationError> {
Ok(content_safe) Ok(content_safe)
} }
fn get_note_visibility(
author: &DbActorProfile,
primary_audience: Vec<String>,
secondary_audience: Vec<String>,
) -> Visibility {
let audience = [primary_audience, secondary_audience].concat();
// Some servers (e.g. Takahe) use "as" namespace
const PUBLIC_VARIANTS: [&str; 3] = [
AP_PUBLIC,
"as:Public",
"Public",
];
if audience.iter().any(|item| PUBLIC_VARIANTS.contains(&item.as_str())) {
return Visibility::Public;
};
let maybe_followers = author.actor_json.as_ref()
.and_then(|actor| actor.followers.as_ref());
if let Some(followers) = maybe_followers {
if audience.contains(followers) {
return Visibility::Followers;
};
};
let maybe_subscribers = author.actor_json.as_ref()
.and_then(|actor| actor.subscribers.as_ref());
if let Some(subscribers) = maybe_subscribers {
if audience.contains(subscribers) {
return Visibility::Subscribers;
};
};
Visibility::Direct
}
const ATTACHMENT_MAX_SIZE: usize = 20 * 1000 * 1000; // 20 MB const ATTACHMENT_MAX_SIZE: usize = 20 * 1000 * 1000; // 20 MB
fn is_gnu_social_link(author_id: &str, attachment: &Attachment) -> bool { fn is_gnu_social_link(author_id: &str, attachment: &Attachment) -> bool {
@ -162,40 +131,17 @@ fn is_gnu_social_link(author_id: &str, attachment: &Attachment) -> bool {
} }
} }
pub async fn handle_note( async fn get_object_attachments(
db_client: &mut impl DatabaseClient, config: &Config,
instance: &Instance, db_client: &impl DatabaseClient,
media_dir: &Path, object: &Object,
object: Object, author: &DbActorProfile,
redirects: &HashMap<String, String>, ) -> Result<Vec<Uuid>, HandlerError> {
) -> Result<Post, HandlerError> { let instance = config.instance();
match object.object_type.as_str() { let media_dir = config.media_dir();
NOTE => (), let mut attachments = vec![];
ARTICLE | EVENT | QUESTION | PAGE | VIDEO => { if let Some(ref value) = object.attachment {
log::info!("processing object of type {}", object.object_type); let list: Vec<Attachment> = parse_property_value(value)
},
other_type => {
log::warn!("discarding object of type {}", other_type);
return Err(ValidationError("unsupported object type").into());
},
};
let author_id = get_note_author_id(&object)?;
let author = get_or_import_profile_by_actor_id(
db_client,
instance,
media_dir,
&author_id,
).await.map_err(|err| {
log::warn!("failed to import {} ({})", author_id, err);
err
})?;
let content = get_note_content(&object)?;
let created_at = object.published.unwrap_or(Utc::now());
let mut attachments: Vec<Uuid> = Vec::new();
if let Some(value) = object.attachment {
let list: Vec<Attachment> = parse_property_value(&value)
.map_err(|_| ValidationError("invalid attachment property"))?; .map_err(|_| ValidationError("invalid attachment property"))?;
let mut downloaded = vec![]; let mut downloaded = vec![];
for attachment in list { for attachment in list {
@ -209,18 +155,21 @@ pub async fn handle_note(
continue; continue;
}, },
}; };
if is_gnu_social_link(&author_id, &attachment) { if is_gnu_social_link(
&author.actor_id(&instance.url()),
&attachment,
) {
// Don't fetch HTML pages attached by GNU Social // Don't fetch HTML pages attached by GNU Social
continue; continue;
}; };
let attachment_url = attachment.url let attachment_url = attachment.url
.ok_or(ValidationError("attachment URL is missing"))?; .ok_or(ValidationError("attachment URL is missing"))?;
let (file_name, file_size, maybe_media_type) = fetch_file( let (file_name, file_size, maybe_media_type) = fetch_file(
instance, &instance,
&attachment_url, &attachment_url,
attachment.media_type.as_deref(), attachment.media_type.as_deref(),
ATTACHMENT_MAX_SIZE, ATTACHMENT_MAX_SIZE,
media_dir, &media_dir,
).await ).await
.map_err(|err| { .map_err(|err| {
log::warn!("{}", err); log::warn!("{}", err);
@ -245,16 +194,23 @@ pub async fn handle_note(
attachments.push(db_attachment.id); attachments.push(db_attachment.id);
}; };
}; };
if content.is_empty() && attachments.is_empty() { Ok(attachments)
return Err(ValidationError("post is empty").into()); }
};
let mut mentions: Vec<Uuid> = Vec::new(); async fn get_object_tags(
config: &Config,
db_client: &impl DatabaseClient,
object: &Object,
redirects: &HashMap<String, String>,
) -> Result<(Vec<Uuid>, Vec<String>, Vec<Uuid>, Vec<Uuid>), HandlerError> {
let instance = config.instance();
let media_dir = config.media_dir();
let mut mentions = vec![];
let mut hashtags = vec![]; let mut hashtags = vec![];
let mut links = vec![]; let mut links = vec![];
let mut emojis = vec![]; let mut emojis = vec![];
if let Some(value) = object.tag { if let Some(ref value) = object.tag {
let list: Vec<JsonValue> = parse_property_value(&value) let list: Vec<JsonValue> = parse_property_value(value)
.map_err(|_| ValidationError("invalid tag property"))?; .map_err(|_| ValidationError("invalid tag property"))?;
for tag_value in list { for tag_value in list {
let tag_type = tag_value["type"].as_str().unwrap_or(HASHTAG); let tag_type = tag_value["type"].as_str().unwrap_or(HASHTAG);
@ -295,8 +251,8 @@ pub async fn handle_note(
// but also can be actor URL (profile link). // but also can be actor URL (profile link).
match get_or_import_profile_by_actor_id( match get_or_import_profile_by_actor_id(
db_client, db_client,
instance, &instance,
media_dir, &media_dir,
&href, &href,
).await { ).await {
Ok(profile) => { Ok(profile) => {
@ -325,8 +281,8 @@ pub async fn handle_note(
if let Ok(actor_address) = mention_to_address(&tag_name) { if let Ok(actor_address) = mention_to_address(&tag_name) {
let profile = match get_or_import_profile_by_actor_address( let profile = match get_or_import_profile_by_actor_address(
db_client, db_client,
instance, &instance,
media_dir, &media_dir,
&actor_address, &actor_address,
).await { ).await {
Ok(profile) => profile, Ok(profile) => profile,
@ -413,11 +369,11 @@ pub async fn handle_note(
Err(other_error) => return Err(other_error.into()), Err(other_error) => return Err(other_error.into()),
}; };
let (file_name, file_size, maybe_media_type) = match fetch_file( let (file_name, file_size, maybe_media_type) = match fetch_file(
instance, &instance,
&tag.icon.url, &tag.icon.url,
tag.icon.media_type.as_deref(), tag.icon.media_type.as_deref(),
EMOJI_MAX_SIZE, EMOJI_MAX_SIZE,
media_dir, &media_dir,
).await { ).await {
Ok(file) => file, Ok(file) => file,
Err(error) => { Err(error) => {
@ -480,6 +436,87 @@ pub async fn handle_note(
links.push(linked.id); links.push(linked.id);
}; };
}; };
Ok((mentions, hashtags, links, emojis))
}
fn get_object_visibility(
author: &DbActorProfile,
primary_audience: Vec<String>,
secondary_audience: Vec<String>,
) -> Visibility {
let audience = [primary_audience, secondary_audience].concat();
// Some servers (e.g. Takahe) use "as" namespace
const PUBLIC_VARIANTS: [&str; 3] = [
AP_PUBLIC,
"as:Public",
"Public",
];
if audience.iter().any(|item| PUBLIC_VARIANTS.contains(&item.as_str())) {
return Visibility::Public;
};
let actor = author.actor_json.as_ref()
.expect("actor data should be present");
if let Some(ref followers) = actor.followers {
if audience.contains(followers) {
return Visibility::Followers;
};
};
if let Some(ref subscribers) = actor.subscribers {
if audience.contains(subscribers) {
return Visibility::Subscribers;
};
};
Visibility::Direct
}
pub async fn handle_note(
config: &Config,
db_client: &mut impl DatabaseClient,
object: Object,
redirects: &HashMap<String, String>,
) -> Result<Post, HandlerError> {
let instance = config.instance();
let media_dir = config.media_dir();
match object.object_type.as_str() {
NOTE => (),
ARTICLE | EVENT | QUESTION | PAGE | VIDEO => {
log::info!("processing object of type {}", object.object_type);
},
other_type => {
log::warn!("discarding object of type {}", other_type);
return Err(ValidationError("unsupported object type").into());
},
};
let author_id = get_object_attributed_to(&object)?;
let author = get_or_import_profile_by_actor_id(
db_client,
&instance,
&media_dir,
&author_id,
).await.map_err(|err| {
log::warn!("failed to import {} ({})", author_id, err);
err
})?;
let content = get_object_content(&object)?;
let created_at = object.published.unwrap_or(Utc::now());
let attachments = get_object_attachments(
config,
db_client,
&object,
&author,
).await?;
if content.is_empty() && attachments.is_empty() {
return Err(ValidationError("post is empty").into());
};
let (mentions, hashtags, links, emojis) = get_object_tags(
config,
db_client,
&object,
redirects,
).await?;
let in_reply_to_id = match object.in_reply_to { let in_reply_to_id = match object.in_reply_to {
Some(ref object_id) => { Some(ref object_id) => {
@ -507,7 +544,7 @@ pub async fn handle_note(
}, },
None => vec![], None => vec![],
}; };
let visibility = get_note_visibility( let visibility = get_object_visibility(
&author, &author,
primary_audience, primary_audience,
secondary_audience, secondary_audience,
@ -567,29 +604,29 @@ mod tests {
use super::*; use super::*;
#[test] #[test]
fn test_get_note_author_id() { fn test_get_object_attributed_to() {
let object = Object { let object = Object {
object_type: NOTE.to_string(), object_type: NOTE.to_string(),
attributed_to: Some(json!(["https://example.org/1"])), attributed_to: Some(json!(["https://example.org/1"])),
..Default::default() ..Default::default()
}; };
let author_id = get_note_author_id(&object).unwrap(); let author_id = get_object_attributed_to(&object).unwrap();
assert_eq!(author_id, "https://example.org/1"); assert_eq!(author_id, "https://example.org/1");
} }
#[test] #[test]
fn test_get_note_content() { fn test_get_object_content() {
let object = Object { let object = Object {
content: Some("test".to_string()), content: Some("test".to_string()),
object_type: NOTE.to_string(), object_type: NOTE.to_string(),
..Default::default() ..Default::default()
}; };
let content = get_note_content(&object).unwrap(); let content = get_object_content(&object).unwrap();
assert_eq!(content, "test"); assert_eq!(content, "test");
} }
#[test] #[test]
fn test_get_note_content_from_video() { fn test_get_object_content_from_video() {
let object = Object { let object = Object {
name: Some("test-name".to_string()), name: Some("test-name".to_string()),
content: Some("test-content".to_string()), content: Some("test-content".to_string()),
@ -601,7 +638,7 @@ mod tests {
}])), }])),
..Default::default() ..Default::default()
}; };
let content = get_note_content(&object).unwrap(); let content = get_object_content(&object).unwrap();
assert_eq!( assert_eq!(
content, content,
r#"test-content<p><a href="https://example.org/xyz" rel="noopener">https://example.org/xyz</a></p>"#, r#"test-content<p><a href="https://example.org/xyz" rel="noopener">https://example.org/xyz</a></p>"#,
@ -609,11 +646,11 @@ mod tests {
} }
#[test] #[test]
fn test_get_note_visibility_public() { fn test_get_object_visibility_public() {
let author = DbActorProfile::default(); let author = DbActorProfile::default();
let primary_audience = vec![AP_PUBLIC.to_string()]; let primary_audience = vec![AP_PUBLIC.to_string()];
let secondary_audience = vec![]; let secondary_audience = vec![];
let visibility = get_note_visibility( let visibility = get_object_visibility(
&author, &author,
primary_audience, primary_audience,
secondary_audience, secondary_audience,
@ -622,7 +659,7 @@ mod tests {
} }
#[test] #[test]
fn test_get_note_visibility_followers() { fn test_get_object_visibility_followers() {
let author_followers = "https://example.com/users/author/followers"; let author_followers = "https://example.com/users/author/followers";
let author = DbActorProfile { let author = DbActorProfile {
actor_json: Some(Actor { actor_json: Some(Actor {
@ -633,7 +670,7 @@ mod tests {
}; };
let primary_audience = vec![author_followers.to_string()]; let primary_audience = vec![author_followers.to_string()];
let secondary_audience = vec![]; let secondary_audience = vec![];
let visibility = get_note_visibility( let visibility = get_object_visibility(
&author, &author,
primary_audience, primary_audience,
secondary_audience, secondary_audience,
@ -642,7 +679,7 @@ mod tests {
} }
#[test] #[test]
fn test_get_note_visibility_subscribers() { fn test_get_object_visibility_subscribers() {
let author_followers = "https://example.com/users/author/followers"; let author_followers = "https://example.com/users/author/followers";
let author_subscribers = "https://example.com/users/author/subscribers"; let author_subscribers = "https://example.com/users/author/subscribers";
let author = DbActorProfile { let author = DbActorProfile {
@ -655,7 +692,7 @@ mod tests {
}; };
let primary_audience = vec![author_subscribers.to_string()]; let primary_audience = vec![author_subscribers.to_string()];
let secondary_audience = vec![]; let secondary_audience = vec![];
let visibility = get_note_visibility( let visibility = get_object_visibility(
&author, &author,
primary_audience, primary_audience,
secondary_audience, secondary_audience,
@ -664,11 +701,14 @@ mod tests {
} }
#[test] #[test]
fn test_get_note_visibility_direct() { fn test_get_object_visibility_direct() {
let author = DbActorProfile::default(); let author = DbActorProfile {
actor_json: Some(Actor::default()),
..Default::default()
};
let primary_audience = vec!["https://example.com/users/1".to_string()]; let primary_audience = vec!["https://example.com/users/1".to_string()];
let secondary_audience = vec![]; let secondary_audience = vec![];
let visibility = get_note_visibility( let visibility = get_object_visibility(
&author, &author,
primary_audience, primary_audience,
secondary_audience, secondary_audience,

View file

@ -7,7 +7,7 @@ use crate::activitypub::{
helpers::update_remote_profile, helpers::update_remote_profile,
types::Actor, types::Actor,
}, },
handlers::create::get_note_content, handlers::create::get_object_content,
types::Object, types::Object,
vocabulary::{NOTE, PERSON}, vocabulary::{NOTE, PERSON},
}; };
@ -39,7 +39,7 @@ async fn handle_update_note(
Err(DatabaseError::NotFound(_)) => return Ok(None), Err(DatabaseError::NotFound(_)) => return Ok(None),
Err(other_error) => return Err(other_error.into()), Err(other_error) => return Err(other_error.into()),
}; };
let content = get_note_content(&object)?; let content = get_object_content(&object)?;
let updated_at = object.updated.unwrap_or(Utc::now()); let updated_at = object.updated.unwrap_or(Utc::now());
let post_data = PostUpdateData { content, updated_at }; let post_data = PostUpdateData { content, updated_at };
update_post(db_client, &post_id, post_data).await?; update_post(db_client, &post_id, post_data).await?;

View file

@ -19,7 +19,7 @@ use super::types::Status;
pub struct PostContent { pub struct PostContent {
pub content: String, pub content: String,
pub mentions: Vec<Uuid>, pub mentions: Vec<Uuid>,
pub tags: Vec<String>, pub hashtags: Vec<String>,
pub links: Vec<Uuid>, pub links: Vec<Uuid>,
pub linked: Vec<Post>, pub linked: Vec<Post>,
pub emojis: Vec<DbEmoji>, pub emojis: Vec<DbEmoji>,
@ -44,11 +44,11 @@ pub async fn parse_microsyntaxes(
); );
let mentions = mention_map.values().map(|profile| profile.id).collect(); let mentions = mention_map.values().map(|profile| profile.id).collect();
// Hashtags // Hashtags
let tags = find_hashtags(&content); let hashtags = find_hashtags(&content);
content = replace_hashtags( content = replace_hashtags(
&instance.url(), &instance.url(),
&content, &content,
&tags, &hashtags,
); );
// Links // Links
let link_map = find_linked_posts( let link_map = find_linked_posts(
@ -68,7 +68,7 @@ pub async fn parse_microsyntaxes(
&content, &content,
).await?; ).await?;
let emojis = emoji_map.into_values().collect(); let emojis = emoji_map.into_values().collect();
Ok(PostContent { content, mentions, tags, links, linked, emojis }) Ok(PostContent { content, mentions, hashtags, links, linked, emojis })
} }
/// Load related objects and build status for API response /// Load related objects and build status for API response

View file

@ -90,7 +90,7 @@ async fn create_status(
_ => return Err(ValidationError("unsupported content type").into()), _ => return Err(ValidationError("unsupported content type").into()),
}; };
// Parse content // Parse content
let PostContent { mut content, mut mentions, tags, links, linked, emojis } = let PostContent { mut content, mut mentions, hashtags, links, linked, emojis } =
parse_microsyntaxes( parse_microsyntaxes(
db_client, db_client,
&instance, &instance,
@ -167,7 +167,7 @@ async fn create_status(
visibility: visibility, visibility: visibility,
attachments: attachments, attachments: attachments,
mentions: mentions, mentions: mentions,
tags: tags, tags: hashtags,
links: links, links: links,
emojis: emojis, emojis: emojis,
object_id: None, object_id: None,