diff --git a/src/activitypub/fetcher/helpers.rs b/src/activitypub/fetcher/helpers.rs index 24f8012..650e8b2 100644 --- a/src/activitypub/fetcher/helpers.rs +++ b/src/activitypub/fetcher/helpers.rs @@ -254,7 +254,7 @@ pub async fn import_post( // Fetch parent object on next iteration queue.push(object_id.to_owned()); }; - for object_id in get_object_links(&object)? { + for object_id in get_object_links(&object) { // Fetch linked objects after fetching current thread queue.insert(0, object_id); }; diff --git a/src/activitypub/handlers/create.rs b/src/activitypub/handlers/create.rs index f79441e..adeb08a 100644 --- a/src/activitypub/handlers/create.rs +++ b/src/activitypub/handlers/create.rs @@ -199,30 +199,26 @@ pub async fn get_object_attachments( pub fn get_object_links( object: &Object, -) -> Result, HandlerError> { +) -> Vec { let mut links = vec![]; - if let Some(ref value) = object.tag { - let list: Vec = parse_property_value(value) - .map_err(|_| ValidationError("invalid tag property"))?; - for tag_value in list { - let tag_type = tag_value["type"].as_str().unwrap_or(HASHTAG); - if tag_type == LINK { - let tag: LinkTag = match serde_json::from_value(tag_value) { - Ok(tag) => tag, - Err(_) => { - log::warn!("invalid link tag"); - continue; - }, - }; - if tag.media_type != AP_MEDIA_TYPE && - tag.media_type != AS_MEDIA_TYPE - { - // Unknown media type + for tag_value in object.tag.clone() { + let tag_type = tag_value["type"].as_str().unwrap_or(HASHTAG); + if tag_type == LINK { + let tag: LinkTag = match serde_json::from_value(tag_value) { + Ok(tag) => tag, + Err(_) => { + log::warn!("invalid link tag"); continue; - }; - if !links.contains(&tag.href) { - links.push(tag.href); - }; + }, + }; + if tag.media_type != AP_MEDIA_TYPE && + tag.media_type != AS_MEDIA_TYPE + { + // Unknown media type + continue; + }; + if !links.contains(&tag.href) { + links.push(tag.href); }; }; }; @@ -231,7 +227,7 @@ pub fn get_object_links( links.push(object_id.to_owned()); }; }; - Ok(links) + links } pub async fn get_object_tags( @@ -246,220 +242,216 @@ pub async fn get_object_tags( let mut hashtags = vec![]; let mut links = vec![]; let mut emojis = vec![]; - if let Some(ref value) = object.tag { - let list: Vec = parse_property_value(value) - .map_err(|_| ValidationError("invalid tag property"))?; - for tag_value in list { - let tag_type = tag_value["type"].as_str().unwrap_or(HASHTAG); - if tag_type == HASHTAG { - let tag: Tag = match serde_json::from_value(tag_value) { - Ok(tag) => tag, - Err(_) => { - log::warn!("invalid hashtag"); - continue; - }, - }; - if let Some(tag_name) = tag.name { - // Ignore invalid tags - if let Ok(tag_name) = normalize_hashtag(&tag_name) { - if !hashtags.contains(&tag_name) { - hashtags.push(tag_name); - }; + for tag_value in object.tag.clone() { + let tag_type = tag_value["type"].as_str().unwrap_or(HASHTAG); + if tag_type == HASHTAG { + let tag: Tag = match serde_json::from_value(tag_value) { + Ok(tag) => tag, + Err(_) => { + log::warn!("invalid hashtag"); + continue; + }, + }; + if let Some(tag_name) = tag.name { + // Ignore invalid tags + if let Ok(tag_name) = normalize_hashtag(&tag_name) { + if !hashtags.contains(&tag_name) { + hashtags.push(tag_name); }; }; - } else if tag_type == MENTION { - let tag: Tag = match serde_json::from_value(tag_value) { - Ok(tag) => tag, - Err(_) => { - log::warn!("invalid mention"); - continue; - }, - }; - // Try to find profile by actor ID. - if let Some(href) = tag.href { - if let Ok(username) = parse_local_actor_id(&instance.url(), &href) { - let user = get_user_by_name(db_client, &username).await?; - if !mentions.contains(&user.id) { - mentions.push(user.id); - }; - continue; + }; + } else if tag_type == MENTION { + let tag: Tag = match serde_json::from_value(tag_value) { + Ok(tag) => tag, + Err(_) => { + log::warn!("invalid mention"); + continue; + }, + }; + // Try to find profile by actor ID. + if let Some(href) = tag.href { + if let Ok(username) = parse_local_actor_id(&instance.url(), &href) { + let user = get_user_by_name(db_client, &username).await?; + if !mentions.contains(&user.id) { + mentions.push(user.id); }; - // NOTE: `href` attribute is usually actor ID - // but also can be actor URL (profile link). - match get_or_import_profile_by_actor_id( - db_client, - &instance, - &media_dir, - &href, - ).await { - Ok(profile) => { - if !mentions.contains(&profile.id) { - mentions.push(profile.id); - }; - continue; - }, - Err(error) => { - log::warn!( - "failed to find mentioned profile by ID {}: {}", - href, - error, - ); - }, - }; - }; - // Try to find profile by actor address - let tag_name = match tag.name { - Some(name) => name, - None => { - log::warn!("failed to parse mention"); - continue; - }, - }; - if let Ok(actor_address) = mention_to_address(&tag_name) { - let profile = match get_or_import_profile_by_actor_address( - db_client, - &instance, - &media_dir, - &actor_address, - ).await { - Ok(profile) => profile, - Err(error @ ( - HandlerError::FetchError(_) | - HandlerError::DatabaseError(DatabaseError::NotFound(_)) - )) => { - // Ignore mention if fetcher fails - // Ignore mention if local address is not valid - log::warn!( - "failed to find mentioned profile {}: {}", - actor_address, - error, - ); - continue; - }, - Err(other_error) => return Err(other_error), - }; - if !mentions.contains(&profile.id) { - mentions.push(profile.id); - }; - } else { - log::warn!("failed to parse mention {}", tag_name); - }; - } else if tag_type == LINK { - let tag: LinkTag = match serde_json::from_value(tag_value) { - Ok(tag) => tag, - Err(_) => { - log::warn!("invalid link tag"); - continue; - }, - }; - if tag.media_type != AP_MEDIA_TYPE && - tag.media_type != AS_MEDIA_TYPE - { - // Unknown media type continue; }; - let href = redirects.get(&tag.href).unwrap_or(&tag.href); - let linked = get_post_by_object_id( + // NOTE: `href` attribute is usually actor ID + // but also can be actor URL (profile link). + match get_or_import_profile_by_actor_id( db_client, - &instance.url(), - href, - ).await?; - if !links.contains(&linked.id) { - links.push(linked.id); - }; - } else if tag_type == EMOJI { - let tag: EmojiTag = match serde_json::from_value(tag_value) { - Ok(tag) => tag, - Err(error) => { - log::warn!("invalid emoji tag: {}", error); - continue; - }, - }; - if emojis.len() >= EMOJIS_MAX_NUM { - log::warn!("too many emojis"); - continue; - }; - let tag_name = tag.name.trim_matches(':'); - if validate_emoji_name(tag_name).is_err() { - log::warn!("invalid emoji name"); - continue; - }; - let maybe_emoji_id = match get_emoji_by_remote_object_id( - db_client, - &tag.id, - ).await { - Ok(emoji) => { - if emoji.updated_at >= tag.updated { - // Emoji already exists and is up to date - if !emojis.contains(&emoji.id) { - emojis.push(emoji.id); - }; - continue; - }; - if emoji.emoji_name != tag_name { - log::warn!("emoji name can't be changed"); - continue; - }; - Some(emoji.id) - }, - Err(DatabaseError::NotFound("emoji")) => None, - Err(other_error) => return Err(other_error.into()), - }; - let (file_name, file_size, maybe_media_type) = match fetch_file( &instance, - &tag.icon.url, - tag.icon.media_type.as_deref(), - EMOJI_MAX_SIZE, &media_dir, + &href, ).await { - Ok(file) => file, - Err(error) => { - log::warn!("failed to fetch emoji: {}", error); + Ok(profile) => { + if !mentions.contains(&profile.id) { + mentions.push(profile.id); + }; continue; }, - }; - let media_type = match maybe_media_type { - Some(media_type) if EMOJI_MEDIA_TYPES.contains(&media_type.as_str()) => { - media_type - }, - _ => { + Err(error) => { log::warn!( - "unexpected emoji media type: {:?}", - maybe_media_type, + "failed to find mentioned profile by ID {}: {}", + href, + error, + ); + }, + }; + }; + // Try to find profile by actor address + let tag_name = match tag.name { + Some(name) => name, + None => { + log::warn!("failed to parse mention"); + continue; + }, + }; + if let Ok(actor_address) = mention_to_address(&tag_name) { + let profile = match get_or_import_profile_by_actor_address( + db_client, + &instance, + &media_dir, + &actor_address, + ).await { + Ok(profile) => profile, + Err(error @ ( + HandlerError::FetchError(_) | + HandlerError::DatabaseError(DatabaseError::NotFound(_)) + )) => { + // Ignore mention if fetcher fails + // Ignore mention if local address is not valid + log::warn!( + "failed to find mentioned profile {}: {}", + actor_address, + error, ); continue; }, + Err(other_error) => return Err(other_error), }; - log::info!("downloaded emoji {}", tag.icon.url); - let image = EmojiImage { file_name, file_size, media_type }; - let emoji = if let Some(emoji_id) = maybe_emoji_id { - update_emoji( - db_client, - &emoji_id, - image, - &tag.updated, - ).await? - } else { - let hostname = get_hostname(&tag.id) - .map_err(|_| ValidationError("invalid emoji ID"))?; - create_emoji( - db_client, - tag_name, - Some(&hostname), - image, - Some(&tag.id), - &tag.updated, - ).await? - }; - if !emojis.contains(&emoji.id) { - emojis.push(emoji.id); + if !mentions.contains(&profile.id) { + mentions.push(profile.id); }; } else { - log::warn!( - "skipping tag of type {}", - tag_type, - ); + log::warn!("failed to parse mention {}", tag_name); }; + } else if tag_type == LINK { + let tag: LinkTag = match serde_json::from_value(tag_value) { + Ok(tag) => tag, + Err(_) => { + log::warn!("invalid link tag"); + continue; + }, + }; + if tag.media_type != AP_MEDIA_TYPE && + tag.media_type != AS_MEDIA_TYPE + { + // Unknown media type + continue; + }; + let href = redirects.get(&tag.href).unwrap_or(&tag.href); + let linked = get_post_by_object_id( + db_client, + &instance.url(), + href, + ).await?; + if !links.contains(&linked.id) { + links.push(linked.id); + }; + } else if tag_type == EMOJI { + let tag: EmojiTag = match serde_json::from_value(tag_value) { + Ok(tag) => tag, + Err(error) => { + log::warn!("invalid emoji tag: {}", error); + continue; + }, + }; + if emojis.len() >= EMOJIS_MAX_NUM { + log::warn!("too many emojis"); + continue; + }; + let tag_name = tag.name.trim_matches(':'); + if validate_emoji_name(tag_name).is_err() { + log::warn!("invalid emoji name"); + continue; + }; + let maybe_emoji_id = match get_emoji_by_remote_object_id( + db_client, + &tag.id, + ).await { + Ok(emoji) => { + if emoji.updated_at >= tag.updated { + // Emoji already exists and is up to date + if !emojis.contains(&emoji.id) { + emojis.push(emoji.id); + }; + continue; + }; + if emoji.emoji_name != tag_name { + log::warn!("emoji name can't be changed"); + continue; + }; + Some(emoji.id) + }, + Err(DatabaseError::NotFound("emoji")) => None, + Err(other_error) => return Err(other_error.into()), + }; + let (file_name, file_size, maybe_media_type) = match fetch_file( + &instance, + &tag.icon.url, + tag.icon.media_type.as_deref(), + EMOJI_MAX_SIZE, + &media_dir, + ).await { + Ok(file) => file, + Err(error) => { + log::warn!("failed to fetch emoji: {}", error); + continue; + }, + }; + let media_type = match maybe_media_type { + Some(media_type) if EMOJI_MEDIA_TYPES.contains(&media_type.as_str()) => { + media_type + }, + _ => { + log::warn!( + "unexpected emoji media type: {:?}", + maybe_media_type, + ); + continue; + }, + }; + log::info!("downloaded emoji {}", tag.icon.url); + let image = EmojiImage { file_name, file_size, media_type }; + let emoji = if let Some(emoji_id) = maybe_emoji_id { + update_emoji( + db_client, + &emoji_id, + image, + &tag.updated, + ).await? + } else { + let hostname = get_hostname(&tag.id) + .map_err(|_| ValidationError("invalid emoji ID"))?; + create_emoji( + db_client, + tag_name, + Some(&hostname), + image, + Some(&tag.id), + &tag.updated, + ).await? + }; + if !emojis.contains(&emoji.id) { + emojis.push(emoji.id); + }; + } else { + log::warn!( + "skipping tag of type {}", + tag_type, + ); }; }; if let Some(ref object_id) = object.quote_url { diff --git a/src/activitypub/types.rs b/src/activitypub/types.rs index c4150bb..f8b8588 100644 --- a/src/activitypub/types.rs +++ b/src/activitypub/types.rs @@ -1,7 +1,12 @@ use std::collections::HashMap; use chrono::{DateTime, Utc}; -use serde::{Deserialize, Serialize}; +use serde::{ + Deserialize, + Deserializer, + Serialize, + de::{Error as DeserializerError}, +}; use serde_json::Value; use super::constants::{ @@ -10,6 +15,7 @@ use super::constants::{ W3ID_DATA_INTEGRITY_CONTEXT, W3ID_SECURITY_CONTEXT, }; +use super::receiver::parse_property_value; use super::vocabulary::HASHTAG; #[derive(Deserialize, Serialize)] @@ -82,6 +88,20 @@ pub struct EmojiTag { pub updated: DateTime, } +fn deserialize_value_array<'de, D>( + deserializer: D, +) -> Result, D::Error> + where D: Deserializer<'de> +{ + let maybe_value: Option = Option::deserialize(deserializer)?; + let values = if let Some(value) = maybe_value { + parse_property_value(&value).map_err(DeserializerError::custom)? + } else { + vec![] + }; + Ok(values) +} + #[derive(Deserialize)] #[cfg_attr(test, derive(Default))] #[serde(rename_all = "camelCase")] @@ -102,7 +122,13 @@ pub struct Object { pub in_reply_to: Option, pub content: Option, pub quote_url: Option, - pub tag: Option, + + #[serde( + default, + deserialize_with = "deserialize_value_array", + )] + pub tag: Vec, + pub to: Option, pub updated: Option>, pub url: Option,