493 lines
15 KiB
Rust
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 })))
|
|
}
|