use crate::{ apub::{ actions::{ apub::require_federation, CreateComment, CreateSubmission, DeleteComment, DeleteSubmission, Noop, UpdateComment, UpdateSubmission, }, ExtendedNote, }, recover, store::Visibility, Action, Context, Error, KeyOwner, MissingImage, RecoverableError, Required, }; use activitystreams::{object::Image, prelude::*, public}; fn find_visibility(note: &ExtendedNote) -> Visibility { if note .to() .and_then(|one_or_many| { one_or_many .iter() .find(|any_base| (*any_base).id() == Some(&public())) }) .is_some() { return Visibility::Public; } if note .cc() .and_then(|one_or_many| { one_or_many .iter() .find(|any_base| (*any_base).id() == Some(&public())) }) .is_some() { return Visibility::Unlisted; } Visibility::Followers } pub(super) fn note( note: &ExtendedNote, key_owner: Option, ctx: &Context, ) -> Result, RecoverableError>, Error> { log::debug!("Ingest Note"); let note_id = note.id_unchecked().req("note id")?; // Double create if ctx.apub.object(note_id)?.is_some() { return Err(Error::Invalid); } let published = note.published().req("published")?; let updated = note.updated(); let attributed_to = note .attributed_to() .req("attributed to")? .as_single_id() .req("attributed to actor id")?; require_federation(key_owner, attributed_to, ctx)?; let profile_id = recover!(attributed_to, ctx.apub.id_for_apub(attributed_to)?); let profile_id = profile_id .profile() .req("attributed to actor id as profile id")?; if let Some(actor_profile) = ctx.store.profiles.by_id(profile_id)? { if actor_profile.is_suspended() { return Err(Error::Invalid); } } else { return Err(Error::Invalid); } if let Some(in_reply_to) = note.in_reply_to() { log::debug!("Reply Note"); let in_reply_to = in_reply_to.as_single_id().req("in reply to id")?; let in_reply_to = recover!(in_reply_to, ctx.apub.id_for_apub(in_reply_to)?); let body = note .content() .req("content")? .as_single_xsd_string() .req("content string")?; if let Some(submission_id) = in_reply_to.submission() { return Ok(Ok(Box::new(CreateComment { note_apub_id: Some(note_id.to_owned()), submission_id, profile_id, comment_id: None, body: body.to_owned(), body_source: None, published: published.into(), }))); } if let Some(comment_id) = in_reply_to.comment() { let submission_id = ctx .store .comments .by_id(comment_id)? .req("comment by id")? .submission_id(); return Ok(Ok(Box::new(CreateComment { note_apub_id: Some(note_id.to_owned()), submission_id, profile_id, comment_id: Some(comment_id), body: body.to_owned(), body_source: None, published: published.into(), }))); } return Err(Error::Invalid); } log::debug!("Standalone Note"); let title = note .summary() .req("summary")? .as_single_xsd_string() .req("summary string")?; let description = note .content() .and_then(|c| c.as_single_xsd_string()) .map(|s| s.to_owned()); let mut missing_files = vec![]; let mut existing_files = vec![]; for attachment in note.attachment().req("attachment")?.iter() { let image = if let Ok(Some(image)) = attachment.clone().extend::() { image } else { continue; }; let id = if let Some(id) = image.id_unchecked() { id } else { continue; }; let url = if let Some(url) = image.url().and_then(|u| u.as_single_id()) { url } else { continue; }; match ctx.apub.id_for_apub(id) { Ok(Some(id)) => { existing_files.push(id.file().req("file id for image url")?); } Ok(None) => { missing_files.push(MissingImage { apub_id: id.to_owned(), image_url: url.to_owned(), }); } _ => (), } } if !missing_files.is_empty() { log::debug!("Files needed before further processing"); return Ok(Err(RecoverableError::MissingImages(missing_files))); } return Ok(Ok(Box::new(CreateSubmission { note_apub_id: Some(note_id.to_owned()), profile_id, title: title.to_owned(), description, published: Some(published.into()), updated: updated.map(|u| u.into()), files: existing_files, visibility: find_visibility(note), local_only: false, logged_in_only: true, sensitive: note.ext_one.sensitive, }))); } pub(super) fn create_note( create: &activitystreams::activity::Create, note_obj: &ExtendedNote, key_owner: Option, ctx: &Context, ) -> Result, RecoverableError>, Error> { log::debug!("Ingest Create Note"); let actor_id = create.actor()?.as_single_id().req("create actor id")?; require_federation(key_owner, actor_id, ctx)?; let attributed_to = note_obj .attributed_to() .req("attributed to")? .as_single_id() .req("attributed to actor id")?; if attributed_to != actor_id { return Err(Error::Invalid); } note(note_obj, None, ctx) } pub(super) fn announce_note( announce: &activitystreams::activity::Announce, note: &ExtendedNote, key_owner: Option, ctx: &Context, ) -> Result, RecoverableError>, Error> { log::debug!("Ingest Announce Note"); let actor_id = announce.actor()?.as_single_id().req("announce actor id")?; require_federation(key_owner, actor_id, ctx)?; let actor_id = recover!(actor_id, ctx.apub.id_for_apub(actor_id)?); let actor_id = actor_id.profile().req("announce actor id as profile id")?; if let Some(actor_profile) = ctx.store.profiles.by_id(actor_id)? { if actor_profile.is_suspended() { return Err(Error::Invalid); } } else { return Err(Error::Invalid); } let note_id = note.id_unchecked().req("note id")?; // enforce that Announce doesn't create a note if ctx.apub.object(note_id)?.is_none() { return Ok(Err(RecoverableError::MissingApub(note_id.to_owned()))); } Ok(Ok(Box::new(Noop))) } pub(super) fn announce_update_note( announce: &activitystreams::activity::Announce, update: &activitystreams::activity::Update, _: &ExtendedNote, key_owner: Option, ctx: &Context, ) -> Result, RecoverableError>, Error> { log::debug!("Ingest Announce Update Note"); let actor_id = announce.actor()?.as_single_id().req("announce actor id")?; require_federation(key_owner, actor_id, ctx)?; let actor_id = recover!(actor_id, ctx.apub.id_for_apub(actor_id)?); let actor_id = actor_id.profile().req("announce actor id as profile id")?; if let Some(actor_profile) = ctx.store.profiles.by_id(actor_id)? { if actor_profile.is_suspended() { return Err(Error::Invalid); } } else { return Err(Error::Invalid); } let update_id = update.id_unchecked().req("update id")?; // enforce that Announce doesn't update a note if ctx.apub.object(update_id)?.is_none() { return Ok(Err(RecoverableError::MissingApub(update_id.to_owned()))); } Ok(Ok(Box::new(Noop))) } pub(super) fn announce_delete_note( announce: &activitystreams::activity::Announce, delete: &activitystreams::activity::Delete, _: &ExtendedNote, key_owner: Option, ctx: &Context, ) -> Result, RecoverableError>, Error> { log::debug!("Ingest Announce Delete Note"); let actor_id = announce.actor()?.as_single_id().req("announce actor id")?; require_federation(key_owner, actor_id, ctx)?; let actor_id = recover!(actor_id, ctx.apub.id_for_apub(actor_id)?); let actor_id = actor_id.profile().req("announce actor id as profile id")?; if let Some(actor_profile) = ctx.store.profiles.by_id(actor_id)? { if actor_profile.is_suspended() { return Err(Error::Invalid); } } else { return Err(Error::Invalid); } let delete_id = delete.id_unchecked().req("delete id")?; // enforce that Announce doesn't delete a note if ctx.apub.object(delete_id)?.is_none() { return Ok(Err(RecoverableError::MissingApub(delete_id.to_owned()))); } Ok(Ok(Box::new(Noop))) } pub(super) fn update_note( update: &activitystreams::activity::Update, note: &ExtendedNote, key_owner: Option, ctx: &Context, ) -> Result, RecoverableError>, Error> { log::debug!("Ingest Update Note"); let update_id = update.id_unchecked().req("update id")?; let actor_id = update.actor()?.as_single_id().req("update actor id")?; require_federation(key_owner, actor_id, ctx)?; let attributed_to = note .attributed_to() .req("attributed to")? .as_single_id() .req("attributed to actor id")?; if actor_id != attributed_to { return Err(Error::Invalid); } let note_id = note.id_unchecked().req("note id")?; let note_id = recover!(note_id, ctx.apub.id_for_apub(note_id)?); let profile_id = recover!(attributed_to, ctx.apub.id_for_apub(attributed_to)?); let profile_id = profile_id .profile() .req("attributed to actor id as profile id")?; if let Some(actor_profile) = ctx.store.profiles.by_id(profile_id)? { if actor_profile.is_suspended() { return Err(Error::Invalid); } } else { return Err(Error::Invalid); } if let Some(comment_id) = note_id.comment() { let comment = ctx.store.comments.by_id(comment_id)?.req("comment by id")?; if comment.profile_id() != profile_id { return Err(Error::Invalid); } let body = note .content() .and_then(|c| c.as_single_xsd_string()) .map(|s| s.to_owned()); return Ok(Ok(Box::new(UpdateComment { update_apub_id: Some(update_id.to_owned()), comment_id, body, body_source: None, }))); } let submission_id = note_id.submission().req("note id as submission id")?; let submission = ctx .store .submissions .by_id(submission_id)? .req("submission by id")?; if submission.profile_id() != profile_id { return Err(Error::Invalid); } let title = note .summary() .and_then(|s| s.as_single_xsd_string()) .map(|s| s.to_owned()); let description = note .content() .and_then(|c| c.as_single_xsd_string()) .map(|s| s.to_owned()); let published = note.published().req("published")?; let updated = note.updated().req("updated")?; let mut missing_files = vec![]; let mut existing_files = vec![]; for attachment in note.attachment().req("attachment")?.iter() { let image = if let Ok(Some(image)) = attachment.clone().extend::() { image } else { continue; }; let id = if let Some(id) = image.id_unchecked() { id } else { continue; }; let url = if let Some(url) = image.url().and_then(|u| u.as_single_id()) { url } else { continue; }; match ctx.apub.id_for_apub(id) { Ok(Some(id)) => { existing_files.push(id.file().req("image url as file id")?); } Ok(None) => { missing_files.push(MissingImage { apub_id: id.to_owned(), image_url: url.to_owned(), }); } _ => (), } } if !missing_files.is_empty() { return Ok(Err(RecoverableError::MissingImages(missing_files))); } Ok(Ok(Box::new(UpdateSubmission { submission_id, title, title_source: None, description, description_source: None, visibility: Some(find_visibility(note)), published: Some(published.into()), updated: Some(updated.into()), removed_files: None, new_files: None, only_files: Some(existing_files), local_only: Some(false), logged_in_only: Some(true), sensitive: Some(note.ext_one.sensitive), }))) } pub(super) fn delete_note( delete: &activitystreams::activity::Delete, note: &ExtendedNote, key_owner: Option, ctx: &Context, ) -> Result, RecoverableError>, Error> { log::debug!("Ingest Delete Note"); let delete_id = delete.id_unchecked().req("delete id")?; let actor_id = delete.actor()?.as_single_id().req("delete actor id")?; require_federation(key_owner, actor_id, ctx)?; let attributed_to = note .attributed_to() .req("attributed to")? .as_single_id() .req("attributed to actor id")?; if actor_id != attributed_to { return Err(Error::Invalid); } let note_object_id = note.id_unchecked().req("note id")?; let note_id = recover!(note_object_id, ctx.apub.id_for_apub(note_object_id)?); let profile_id = recover!(attributed_to, ctx.apub.id_for_apub(attributed_to)?); let profile_id = profile_id .profile() .req("attributed to actor id as profile id")?; if let Some(actor_profile) = ctx.store.profiles.by_id(profile_id)? { if actor_profile.is_suspended() { return Err(Error::Invalid); } } else { return Err(Error::Invalid); } if let Some(comment_id) = note_id.comment() { let comment = ctx.store.comments.by_id(comment_id)?.req("comment by id")?; if comment.profile_id() != profile_id { return Err(Error::Invalid); } return Ok(Ok(Box::new(DeleteComment { delete_apub_id: Some(delete_id.to_owned()), comment_id, }))); } let submission_id = note_id.submission().req("note id as submission id")?; let submission = ctx .store .submissions .by_id(submission_id)? .req("submission by id")?; if submission.profile_id() != profile_id { return Err(Error::Invalid); } Ok(Ok(Box::new(DeleteSubmission { submission_id }))) }