hyaenidae/profiles/src/apub/actions/apub/note.rs
2021-04-02 12:07:19 -05:00

493 lines
15 KiB
Rust

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<KeyOwner>,
ctx: &Context,
) -> Result<Result<Box<dyn Action>, 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 content = note.content().req("content")?;
let body = 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: Some(body.to_owned()),
body_source: None,
published: published.into(),
updated: updated.map(|u| u.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: Some(body.to_owned()),
body_source: None,
published: published.into(),
updated: updated.map(|u| u.into()),
})));
}
return Err(Error::Invalid);
}
log::debug!("Standalone Note");
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 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, _>() {
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,
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<KeyOwner>,
ctx: &Context,
) -> Result<Result<Box<dyn Action>, 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<KeyOwner>,
ctx: &Context,
) -> Result<Result<Box<dyn Action>, 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<KeyOwner>,
ctx: &Context,
) -> Result<Result<Box<dyn Action>, 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<KeyOwner>,
ctx: &Context,
) -> Result<Result<Box<dyn Action>, 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<KeyOwner>,
ctx: &Context,
) -> Result<Result<Box<dyn Action>, 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 updated = note.updated().req("updated")?;
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,
updated: updated.into(),
})));
}
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 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, _>() {
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: 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<KeyOwner>,
ctx: &Context,
) -> Result<Result<Box<dyn Action>, 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 })))
}