Rewrite apub post (de)serialization using structs (ref #1657)

This commit is contained in:
Felix Ableitner 2021-07-28 00:18:50 +02:00
parent 15a11c13a2
commit ff265c7ebc
36 changed files with 517 additions and 389 deletions

View file

@ -9,7 +9,7 @@ use lemmy_api_common::{
mark_post_as_read, mark_post_as_read,
post::*, post::*,
}; };
use lemmy_apub::{ApubLikeableType, ApubObjectType}; use lemmy_apub::{activities::post::update::UpdatePost, ApubLikeableType};
use lemmy_db_queries::{source::post::Post_, Crud, Likeable, Saveable}; use lemmy_db_queries::{source::post::Post_, Crud, Likeable, Saveable};
use lemmy_db_schema::source::{moderator::*, post::*}; use lemmy_db_schema::source::{moderator::*, post::*};
use lemmy_db_views::post_view::PostView; use lemmy_db_views::post_view::PostView;
@ -140,9 +140,7 @@ impl Perform for LockPost {
blocking(context.pool(), move |conn| ModLockPost::create(conn, &form)).await??; blocking(context.pool(), move |conn| ModLockPost::create(conn, &form)).await??;
// apub updates // apub updates
updated_post UpdatePost::send(&updated_post, &local_user_view.person, context).await?;
.send_update(&local_user_view.person, context)
.await?;
// Refetch the post // Refetch the post
let post_id = data.post_id; let post_id = data.post_id;
@ -214,9 +212,7 @@ impl Perform for StickyPost {
// Apub updates // Apub updates
// TODO stickied should pry work like locked for ease of use // TODO stickied should pry work like locked for ease of use
updated_post UpdatePost::send(&updated_post, &local_user_view.person, context).await?;
.send_update(&local_user_view.person, context)
.await?;
// Refetch the post // Refetch the post
let post_id = data.post_id; let post_id = data.post_id;

View file

@ -7,7 +7,7 @@ use lemmy_api_common::{
mark_post_as_read, mark_post_as_read,
post::*, post::*,
}; };
use lemmy_apub::{generate_apub_endpoint, ApubLikeableType, ApubObjectType, EndpointType}; use lemmy_apub::{generate_apub_endpoint, ApubLikeableType, EndpointType};
use lemmy_db_queries::{source::post::Post_, Crud, Likeable}; use lemmy_db_queries::{source::post::Post_, Crud, Likeable};
use lemmy_db_schema::source::post::*; use lemmy_db_schema::source::post::*;
use lemmy_db_views::post_view::PostView; use lemmy_db_views::post_view::PostView;
@ -82,8 +82,11 @@ impl PerformCrud for CreatePost {
.await? .await?
.map_err(|_| ApiError::err("couldnt_create_post"))?; .map_err(|_| ApiError::err("couldnt_create_post"))?;
updated_post lemmy_apub::activities::post::create::CreatePost::send(
.send_create(&local_user_view.person, context) &updated_post,
&local_user_view.person,
context,
)
.await?; .await?;
// They like their own post by default // They like their own post by default

View file

@ -1,7 +1,7 @@
use crate::PerformCrud; use crate::PerformCrud;
use actix_web::web::Data; use actix_web::web::Data;
use lemmy_api_common::{blocking, check_community_ban, get_local_user_view_from_jwt, post::*}; use lemmy_api_common::{blocking, check_community_ban, get_local_user_view_from_jwt, post::*};
use lemmy_apub::ApubObjectType; use lemmy_apub::activities::post::update::UpdatePost;
use lemmy_db_queries::{source::post::Post_, Crud, DeleteableOrRemoveable}; use lemmy_db_queries::{source::post::Post_, Crud, DeleteableOrRemoveable};
use lemmy_db_schema::{naive_now, source::post::*}; use lemmy_db_schema::{naive_now, source::post::*};
use lemmy_db_views::post_view::PostView; use lemmy_db_views::post_view::PostView;
@ -89,9 +89,7 @@ impl PerformCrud for EditPost {
}; };
// Send apub update // Send apub update
updated_post UpdatePost::send(&updated_post, &local_user_view.person, context).await?;
.send_update(&local_user_view.person, context)
.await?;
let post_id = data.post_id; let post_id = data.post_id;
let mut post_view = blocking(context.pool(), move |conn| { let mut post_view = blocking(context.pool(), move |conn| {

View file

@ -1,10 +1,12 @@
use crate::{ use crate::{
activities::{ activities::{
comment::{get_notif_recipients, send_websocket_message}, comment::{get_notif_recipients, send_websocket_message},
extract_community,
verify_activity, verify_activity,
verify_person_in_community, verify_person_in_community,
}, },
objects::FromApub, objects::FromApub,
ActorType,
NoteExt, NoteExt,
}; };
use activitystreams::{activity::kind::CreateType, base::BaseExt}; use activitystreams::{activity::kind::CreateType, base::BaseExt};
@ -33,8 +35,16 @@ impl ActivityHandler for CreateComment {
context: &LemmyContext, context: &LemmyContext,
request_counter: &mut i32, request_counter: &mut i32,
) -> Result<(), LemmyError> { ) -> Result<(), LemmyError> {
let community = extract_community(&self.cc, context, request_counter).await?;
verify_activity(self.common())?; verify_activity(self.common())?;
verify_person_in_community(&self.common.actor, &self.cc, context, request_counter).await?; verify_person_in_community(
&self.common.actor,
&community.actor_id(),
context,
request_counter,
)
.await?;
verify_domains_match_opt(&self.common.actor, self.object.id_unchecked())?; verify_domains_match_opt(&self.common.actor, self.object.id_unchecked())?;
// TODO: should add a check that the correct community is in cc (probably needs changes to // TODO: should add a check that the correct community is in cc (probably needs changes to
// comment deserialization) // comment deserialization)

View file

@ -1,10 +1,12 @@
use crate::{ use crate::{
activities::{ activities::{
comment::{get_notif_recipients, send_websocket_message}, comment::{get_notif_recipients, send_websocket_message},
extract_community,
verify_activity, verify_activity,
verify_person_in_community, verify_person_in_community,
}, },
objects::FromApub, objects::FromApub,
ActorType,
NoteExt, NoteExt,
}; };
use activitystreams::{activity::kind::UpdateType, base::BaseExt}; use activitystreams::{activity::kind::UpdateType, base::BaseExt};
@ -33,8 +35,16 @@ impl ActivityHandler for UpdateComment {
context: &LemmyContext, context: &LemmyContext,
request_counter: &mut i32, request_counter: &mut i32,
) -> Result<(), LemmyError> { ) -> Result<(), LemmyError> {
let community = extract_community(&self.cc, context, request_counter).await?;
verify_activity(self.common())?; verify_activity(self.common())?;
verify_person_in_community(&self.common.actor, &self.cc, context, request_counter).await?; verify_person_in_community(
&self.common.actor,
&community.actor_id(),
context,
request_counter,
)
.await?;
verify_domains_match_opt(&self.common.actor, self.object.id_unchecked())?; verify_domains_match_opt(&self.common.actor, self.object.id_unchecked())?;
Ok(()) Ok(())
} }

View file

@ -38,7 +38,7 @@ impl ActivityHandler for AddMod {
request_counter: &mut i32, request_counter: &mut i32,
) -> Result<(), LemmyError> { ) -> Result<(), LemmyError> {
verify_activity(self.common())?; verify_activity(self.common())?;
verify_person_in_community(&self.common.actor, &self.cc, context, request_counter).await?; verify_person_in_community(&self.common.actor, &self.cc[0], context, request_counter).await?;
verify_mod_action(&self.common.actor, self.cc[0].clone(), context).await?; verify_mod_action(&self.common.actor, self.cc[0].clone(), context).await?;
verify_add_remove_moderator_target(&self.target, self.cc[0].clone())?; verify_add_remove_moderator_target(&self.target, self.cc[0].clone())?;
Ok(()) Ok(())

View file

@ -4,12 +4,14 @@ use crate::{
community::{ community::{
add_mod::AddMod, add_mod::AddMod,
block_user::BlockUserFromCommunity, block_user::BlockUserFromCommunity,
list_community_follower_inboxes,
undo_block_user::UndoBlockUserFromCommunity, undo_block_user::UndoBlockUserFromCommunity,
}, },
deletion::{ deletion::{
delete::DeletePostCommentOrCommunity, delete::DeletePostCommentOrCommunity,
undo_delete::UndoDeletePostCommentOrCommunity, undo_delete::UndoDeletePostCommentOrCommunity,
}, },
generate_activity_id,
post::{create::CreatePost, update::UpdatePost}, post::{create::CreatePost, update::UpdatePost},
removal::{ removal::{
remove::RemovePostCommentCommunityOrMod, remove::RemovePostCommentCommunityOrMod,
@ -24,11 +26,16 @@ use crate::{
undo_like::UndoLikePostOrComment, undo_like::UndoLikePostOrComment,
}, },
}, },
activity_queue::send_activity_new,
extensions::context::lemmy_context,
http::is_activity_already_known, http::is_activity_already_known,
insert_activity, insert_activity,
ActorType,
CommunityType,
}; };
use activitystreams::activity::kind::AnnounceType; use activitystreams::activity::kind::AnnounceType;
use lemmy_apub_lib::{ActivityCommonFields, ActivityHandler, PublicUrl}; use lemmy_apub_lib::{ActivityCommonFields, ActivityHandler, PublicUrl};
use lemmy_db_schema::source::community::Community;
use lemmy_utils::LemmyError; use lemmy_utils::LemmyError;
use lemmy_websocket::LemmyContext; use lemmy_websocket::LemmyContext;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -66,6 +73,38 @@ pub struct AnnounceActivity {
common: ActivityCommonFields, common: ActivityCommonFields,
} }
impl AnnounceActivity {
pub async fn send(
object: AnnouncableActivities,
community: &Community,
additional_inboxes: Vec<Url>,
context: &LemmyContext,
) -> Result<(), LemmyError> {
let announce = AnnounceActivity {
to: PublicUrl::Public,
object,
cc: vec![community.followers_url()],
kind: AnnounceType::Announce,
common: ActivityCommonFields {
context: lemmy_context()?.into(),
id: generate_activity_id(AnnounceType::Announce)?,
actor: community.actor_id(),
unparsed: Default::default(),
},
};
let inboxes = list_community_follower_inboxes(community, additional_inboxes, context).await?;
send_activity_new(
context,
&announce,
&announce.common.id,
community,
inboxes,
false,
)
.await
}
}
#[async_trait::async_trait(?Send)] #[async_trait::async_trait(?Send)]
impl ActivityHandler for AnnounceActivity { impl ActivityHandler for AnnounceActivity {
async fn verify( async fn verify(

View file

@ -36,7 +36,7 @@ impl ActivityHandler for BlockUserFromCommunity {
request_counter: &mut i32, request_counter: &mut i32,
) -> Result<(), LemmyError> { ) -> Result<(), LemmyError> {
verify_activity(self.common())?; verify_activity(self.common())?;
verify_person_in_community(&self.common.actor, &self.cc, context, request_counter).await?; verify_person_in_community(&self.common.actor, &self.cc[0], context, request_counter).await?;
verify_mod_action(&self.common.actor, self.cc[0].clone(), context).await?; verify_mod_action(&self.common.actor, self.cc[0].clone(), context).await?;
Ok(()) Ok(())
} }

View file

@ -1,8 +1,11 @@
use crate::{check_is_apub_id_valid, CommunityType};
use itertools::Itertools;
use lemmy_api_common::{blocking, community::CommunityResponse}; use lemmy_api_common::{blocking, community::CommunityResponse};
use lemmy_db_schema::CommunityId; use lemmy_db_schema::{source::community::Community, CommunityId};
use lemmy_db_views_actor::community_view::CommunityView; use lemmy_db_views_actor::community_view::CommunityView;
use lemmy_utils::LemmyError; use lemmy_utils::{settings::structs::Settings, LemmyError};
use lemmy_websocket::{messages::SendCommunityRoomMessage, LemmyContext}; use lemmy_websocket::{messages::SendCommunityRoomMessage, LemmyContext};
use url::Url;
pub mod add_mod; pub mod add_mod;
pub mod announce; pub mod announce;
@ -33,3 +36,23 @@ pub(crate) async fn send_websocket_message<
Ok(()) Ok(())
} }
async fn list_community_follower_inboxes(
community: &Community,
additional_inboxes: Vec<Url>,
context: &LemmyContext,
) -> Result<Vec<Url>, LemmyError> {
Ok(
vec![
community.get_follower_inboxes(context.pool()).await?,
additional_inboxes,
]
.iter()
.flatten()
.unique()
.filter(|inbox| inbox.host_str() != Some(&Settings::get().hostname()))
.filter(|inbox| check_is_apub_id_valid(inbox, false).is_ok())
.map(|inbox| inbox.to_owned())
.collect(),
)
}

View file

@ -36,7 +36,7 @@ impl ActivityHandler for UndoBlockUserFromCommunity {
request_counter: &mut i32, request_counter: &mut i32,
) -> Result<(), LemmyError> { ) -> Result<(), LemmyError> {
verify_activity(self.common())?; verify_activity(self.common())?;
verify_person_in_community(&self.common.actor, &self.cc, context, request_counter).await?; verify_person_in_community(&self.common.actor, &self.cc[0], context, request_counter).await?;
verify_mod_action(&self.common.actor, self.cc[0].clone(), context).await?; verify_mod_action(&self.common.actor, self.cc[0].clone(), context).await?;
self.object.verify(context, request_counter).await?; self.object.verify(context, request_counter).await?;
Ok(()) Ok(())

View file

@ -39,7 +39,7 @@ impl ActivityHandler for UpdateCommunity {
request_counter: &mut i32, request_counter: &mut i32,
) -> Result<(), LemmyError> { ) -> Result<(), LemmyError> {
verify_activity(self.common())?; verify_activity(self.common())?;
verify_person_in_community(&self.common.actor, &self.cc, context, request_counter).await?; verify_person_in_community(&self.common.actor, &self.cc[0], context, request_counter).await?;
verify_mod_action(&self.common.actor, self.cc[0].clone(), context).await?; verify_mod_action(&self.common.actor, self.cc[0].clone(), context).await?;
Ok(()) Ok(())
} }

View file

@ -64,7 +64,8 @@ impl ActivityHandler for DeletePostCommentOrCommunity {
} }
// deleting a post or comment // deleting a post or comment
else { else {
verify_person_in_community(&self.common().actor, &self.cc, context, request_counter).await?; verify_person_in_community(&self.common().actor, &self.cc[0], context, request_counter)
.await?;
let object_creator = let object_creator =
get_post_or_comment_actor_id(&self.object, context, request_counter).await?; get_post_or_comment_actor_id(&self.object, context, request_counter).await?;
verify_urls_match(&self.common.actor, &object_creator)?; verify_urls_match(&self.common.actor, &object_creator)?;
@ -83,7 +84,7 @@ impl ActivityHandler for DeletePostCommentOrCommunity {
if let Ok(community) = object_community { if let Ok(community) = object_community {
if community.local { if community.local {
// repeat these checks just to be sure // repeat these checks just to be sure
verify_person_in_community(&self.common().actor, &self.cc, context, request_counter) verify_person_in_community(&self.common().actor, &self.cc[0], context, request_counter)
.await?; .await?;
verify_mod_action(&self.common.actor, self.object.clone(), context).await?; verify_mod_action(&self.common.actor, self.object.clone(), context).await?;
let mod_ = let mod_ =

View file

@ -54,7 +54,8 @@ impl ActivityHandler for UndoDeletePostCommentOrCommunity {
} }
// restoring a post or comment // restoring a post or comment
else { else {
verify_person_in_community(&self.common().actor, &self.cc, context, request_counter).await?; verify_person_in_community(&self.common().actor, &self.cc[0], context, request_counter)
.await?;
verify_urls_match(&self.common.actor, &self.object.common().actor)?; verify_urls_match(&self.common.actor, &self.object.common().actor)?;
} }
Ok(()) Ok(())
@ -71,7 +72,7 @@ impl ActivityHandler for UndoDeletePostCommentOrCommunity {
if let Ok(community) = object_community { if let Ok(community) = object_community {
if community.local { if community.local {
// repeat these checks just to be sure // repeat these checks just to be sure
verify_person_in_community(&self.common().actor, &self.cc, context, request_counter) verify_person_in_community(&self.common().actor, &self.cc[0], context, request_counter)
.await?; .await?;
verify_mod_action(&self.common.actor, self.object.object.clone(), context).await?; verify_mod_action(&self.common.actor, self.object.object.clone(), context).await?;
let mod_ = let mod_ =

View file

@ -13,9 +13,10 @@ use lemmy_db_schema::{
DbUrl, DbUrl,
}; };
use lemmy_db_views_actor::community_view::CommunityView; use lemmy_db_views_actor::community_view::CommunityView;
use lemmy_utils::LemmyError; use lemmy_utils::{settings::structs::Settings, LemmyError};
use lemmy_websocket::LemmyContext; use lemmy_websocket::LemmyContext;
use url::Url; use url::{ParseError, Url};
use uuid::Uuid;
pub mod comment; pub mod comment;
pub mod community; pub mod community;
@ -41,27 +42,34 @@ async fn verify_person(
Ok(()) Ok(())
} }
/// Fetches the person and community to verify their type, then checks if person is banned from site pub(crate) async fn extract_community(
/// or community.
async fn verify_person_in_community(
person_id: &Url,
cc: &[Url], cc: &[Url],
context: &LemmyContext, context: &LemmyContext,
request_counter: &mut i32, request_counter: &mut i32,
) -> Result<Community, LemmyError> { ) -> Result<Community, LemmyError> {
let person = get_or_fetch_and_upsert_person(person_id, context, request_counter).await?;
let mut cc_iter = cc.iter(); let mut cc_iter = cc.iter();
let community: Community = loop { loop {
if let Some(cid) = cc_iter.next() { if let Some(cid) = cc_iter.next() {
if let Ok(c) = get_or_fetch_and_upsert_community(cid, context, request_counter).await { if let Ok(c) = get_or_fetch_and_upsert_community(cid, context, request_counter).await {
break c; break Ok(c);
} }
} else { } else {
return Err(anyhow!("No community found in cc").into()); return Err(anyhow!("No community found in cc").into());
} }
}; }
check_community_or_site_ban(&person, community.id, context.pool()).await?; }
Ok(community)
/// Fetches the person and community to verify their type, then checks if person is banned from site
/// or community.
async fn verify_person_in_community(
person_id: &Url,
community_id: &Url,
context: &LemmyContext,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
let community = get_or_fetch_and_upsert_community(community_id, context, request_counter).await?;
let person = get_or_fetch_and_upsert_person(person_id, context, request_counter).await?;
check_community_or_site_ban(&person, community.id, context.pool()).await
} }
/// Simply check that the url actually refers to a valid group. /// Simply check that the url actually refers to a valid group.
@ -80,13 +88,16 @@ fn verify_activity(common: &ActivityCommonFields) -> Result<(), LemmyError> {
Ok(()) Ok(())
} }
async fn verify_mod_action( /// Verify that the actor is a community mod. This check is only run if the community is local,
/// because in case of remote communities, admins can also perform mod actions. As admin status
/// is not federated, we cant verify their actions remotely.
pub(crate) async fn verify_mod_action(
actor_id: &Url, actor_id: &Url,
activity_cc: Url, community: Url,
context: &LemmyContext, context: &LemmyContext,
) -> Result<(), LemmyError> { ) -> Result<(), LemmyError> {
let community = blocking(context.pool(), move |conn| { let community = blocking(context.pool(), move |conn| {
Community::read_from_apub_id(conn, &activity_cc.into()) Community::read_from_apub_id(conn, &community.into())
}) })
.await??; .await??;
@ -120,3 +131,18 @@ fn verify_add_remove_moderator_target(target: &Url, community: Url) -> Result<()
} }
Ok(()) Ok(())
} }
/// Generate a unique ID for an activity, in the format:
/// `http(s)://example.com/receive/create/202daf0a-1489-45df-8d2e-c8a3173fed36`
fn generate_activity_id<T>(kind: T) -> Result<Url, ParseError>
where
T: ToString,
{
let id = format!(
"{}/activities/{}/{}",
Settings::get().get_protocol_and_hostname(),
kind.to_string().to_lowercase(),
Uuid::new_v4()
);
Url::parse(&id)
}

View file

@ -1,13 +1,30 @@
use crate::{ use crate::{
activities::{post::send_websocket_message, verify_activity, verify_person_in_community}, activities::{
community::announce::AnnouncableActivities,
extract_community,
generate_activity_id,
post::send_websocket_message,
verify_activity,
verify_person_in_community,
},
activity_queue::send_to_community_new,
extensions::context::lemmy_context,
fetcher::person::get_or_fetch_and_upsert_person, fetcher::person::get_or_fetch_and_upsert_person,
objects::FromApub, objects::{post::Page, FromApub, ToApub},
ActorType, ActorType,
PageExt,
}; };
use activitystreams::{activity::kind::CreateType, base::BaseExt}; use activitystreams::activity::kind::CreateType;
use lemmy_apub_lib::{verify_domains_match_opt, ActivityCommonFields, ActivityHandler, PublicUrl}; use anyhow::anyhow;
use lemmy_db_schema::source::post::Post; use lemmy_api_common::blocking;
use lemmy_apub_lib::{
verify_domains_match,
verify_urls_match,
ActivityCommonFields,
ActivityHandler,
PublicUrl,
};
use lemmy_db_queries::Crud;
use lemmy_db_schema::source::{community::Community, person::Person, post::Post};
use lemmy_utils::LemmyError; use lemmy_utils::LemmyError;
use lemmy_websocket::{LemmyContext, UserOperationCrud}; use lemmy_websocket::{LemmyContext, UserOperationCrud};
use url::Url; use url::Url;
@ -16,14 +33,40 @@ use url::Url;
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct CreatePost { pub struct CreatePost {
to: PublicUrl, to: PublicUrl,
object: PageExt, object: Page,
cc: Vec<Url>, cc: [Url; 1],
#[serde(rename = "type")] r#type: CreateType,
kind: CreateType,
#[serde(flatten)] #[serde(flatten)]
common: ActivityCommonFields, common: ActivityCommonFields,
} }
impl CreatePost {
pub async fn send(post: &Post, actor: &Person, context: &LemmyContext) -> Result<(), LemmyError> {
let community_id = post.community_id;
let community = blocking(context.pool(), move |conn| {
Community::read(conn, community_id)
})
.await??;
let id = generate_activity_id(CreateType::Create)?;
let create = CreatePost {
to: PublicUrl::Public,
object: post.to_apub(context.pool()).await?,
cc: [community.actor_id()],
r#type: Default::default(),
common: ActivityCommonFields {
context: lemmy_context()?.into(),
id: id.clone(),
actor: actor.actor_id(),
unparsed: Default::default(),
},
};
let activity = AnnouncableActivities::CreatePost(create);
send_to_community_new(activity, &id, actor, &community, vec![], context).await
}
}
#[async_trait::async_trait(?Send)] #[async_trait::async_trait(?Send)]
impl ActivityHandler for CreatePost { impl ActivityHandler for CreatePost {
async fn verify( async fn verify(
@ -31,9 +74,23 @@ impl ActivityHandler for CreatePost {
context: &LemmyContext, context: &LemmyContext,
request_counter: &mut i32, request_counter: &mut i32,
) -> Result<(), LemmyError> { ) -> Result<(), LemmyError> {
let community = extract_community(&self.cc, context, request_counter).await?;
let community_id = &community.actor_id();
verify_activity(self.common())?; verify_activity(self.common())?;
verify_person_in_community(&self.common.actor, &self.cc, context, request_counter).await?; verify_person_in_community(&self.common.actor, community_id, context, request_counter).await?;
verify_domains_match_opt(&self.common.actor, self.object.id_unchecked())?; verify_domains_match(&self.common.actor, &self.object.id)?;
verify_urls_match(&self.common.actor, &self.object.attributed_to)?;
// Check that the post isnt locked or stickied, as that isnt possible for newly created posts.
// However, when fetching a remote post we generate a new create activity with the current
// locked/stickied value, so this check may fail. So only check if its a local community,
// because then we will definitely receive all create and update activities separately.
let is_stickied_or_locked =
self.object.stickied == Some(true) || self.object.comments_enabled == Some(false);
if community.local && is_stickied_or_locked {
return Err(anyhow!("New post cannot be stickied or locked").into());
}
self.object.verify(context, request_counter).await?;
Ok(()) Ok(())
} }

View file

@ -1,24 +1,24 @@
use crate::{ use crate::{
activities::{ activities::{
community::announce::AnnouncableActivities,
generate_activity_id,
post::send_websocket_message, post::send_websocket_message,
verify_activity, verify_activity,
verify_mod_action, verify_mod_action,
verify_person_in_community, verify_person_in_community,
}, },
objects::{FromApub, FromApubToForm}, activity_queue::send_to_community_new,
extensions::context::lemmy_context,
fetcher::community::get_or_fetch_and_upsert_community,
objects::{post::Page, FromApub, ToApub},
ActorType, ActorType,
PageExt,
}; };
use activitystreams::{activity::kind::UpdateType, base::BaseExt}; use activitystreams::activity::kind::UpdateType;
use anyhow::Context;
use lemmy_api_common::blocking; use lemmy_api_common::blocking;
use lemmy_apub_lib::{verify_domains_match_opt, ActivityCommonFields, ActivityHandler, PublicUrl}; use lemmy_apub_lib::{verify_urls_match, ActivityCommonFields, ActivityHandler, PublicUrl};
use lemmy_db_queries::ApubObject; use lemmy_db_queries::Crud;
use lemmy_db_schema::{ use lemmy_db_schema::source::{community::Community, person::Person, post::Post};
source::post::{Post, PostForm}, use lemmy_utils::LemmyError;
DbUrl,
};
use lemmy_utils::{location_info, LemmyError};
use lemmy_websocket::{LemmyContext, UserOperationCrud}; use lemmy_websocket::{LemmyContext, UserOperationCrud};
use url::Url; use url::Url;
@ -26,14 +26,39 @@ use url::Url;
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct UpdatePost { pub struct UpdatePost {
to: PublicUrl, to: PublicUrl,
object: PageExt, object: Page,
cc: Vec<Url>, cc: [Url; 1],
#[serde(rename = "type")] r#type: UpdateType,
kind: UpdateType,
#[serde(flatten)] #[serde(flatten)]
common: ActivityCommonFields, common: ActivityCommonFields,
} }
impl UpdatePost {
pub async fn send(post: &Post, actor: &Person, context: &LemmyContext) -> Result<(), LemmyError> {
let community_id = post.community_id;
let community = blocking(context.pool(), move |conn| {
Community::read(conn, community_id)
})
.await??;
let id = generate_activity_id(UpdateType::Update)?;
let update = UpdatePost {
to: PublicUrl::Public,
object: post.to_apub(context.pool()).await?,
cc: [community.actor_id()],
r#type: Default::default(),
common: ActivityCommonFields {
context: lemmy_context()?.into(),
id: id.clone(),
actor: actor.actor_id(),
unparsed: Default::default(),
},
};
let activity = AnnouncableActivities::UpdatePost(update);
send_to_community_new(activity, &id, actor, &community, vec![], context).await
}
}
#[async_trait::async_trait(?Send)] #[async_trait::async_trait(?Send)]
impl ActivityHandler for UpdatePost { impl ActivityHandler for UpdatePost {
async fn verify( async fn verify(
@ -41,34 +66,19 @@ impl ActivityHandler for UpdatePost {
context: &LemmyContext, context: &LemmyContext,
request_counter: &mut i32, request_counter: &mut i32,
) -> Result<(), LemmyError> { ) -> Result<(), LemmyError> {
let community_id = get_or_fetch_and_upsert_community(&self.cc[0], context, request_counter)
.await?
.actor_id();
let is_mod_action = self.object.is_mod_action(context.pool()).await?;
verify_activity(self.common())?; verify_activity(self.common())?;
let community = verify_person_in_community(&self.common.actor, &community_id, context, request_counter).await?;
verify_person_in_community(&self.common.actor, &self.cc, context, request_counter).await?; if is_mod_action {
verify_mod_action(&self.common.actor, community_id, context).await?;
let temp_post = PostForm::from_apub( } else {
&self.object, verify_urls_match(&self.common.actor, &self.object.attributed_to)?;
context,
self.common.actor.clone(),
request_counter,
true,
)
.await?;
let post_id: DbUrl = temp_post.ap_id.context(location_info!())?;
let old_post = blocking(context.pool(), move |conn| {
Post::read_from_apub_id(conn, &post_id)
})
.await??;
let stickied = temp_post.stickied.context(location_info!())?;
let locked = temp_post.locked.context(location_info!())?;
// community mod changed locked/sticky status
if (stickied != old_post.stickied) || (locked != old_post.locked) {
verify_mod_action(&self.common.actor, community.actor_id(), context).await?;
} }
// user edited their own post self.object.verify(context, request_counter).await?;
else {
verify_domains_match_opt(&self.common.actor, self.object.id_unchecked())?;
}
Ok(()) Ok(())
} }

View file

@ -64,13 +64,13 @@ impl ActivityHandler for RemovePostCommentCommunityOrMod {
} }
// removing community mod // removing community mod
else if let Some(target) = &self.target { else if let Some(target) = &self.target {
verify_person_in_community(&self.common.actor, &self.cc, context, request_counter).await?; verify_person_in_community(&self.common.actor, &self.cc[0], context, request_counter).await?;
verify_mod_action(&self.common.actor, self.cc[0].clone(), context).await?; verify_mod_action(&self.common.actor, self.cc[0].clone(), context).await?;
verify_add_remove_moderator_target(target, self.cc[0].clone())?; verify_add_remove_moderator_target(target, self.cc[0].clone())?;
} }
// removing a post or comment // removing a post or comment
else { else {
verify_person_in_community(&self.common.actor, &self.cc, context, request_counter).await?; verify_person_in_community(&self.common.actor, &self.cc[0], context, request_counter).await?;
verify_mod_action(&self.common.actor, self.cc[0].clone(), context).await?; verify_mod_action(&self.common.actor, self.cc[0].clone(), context).await?;
} }
Ok(()) Ok(())

View file

@ -52,7 +52,7 @@ impl ActivityHandler for UndoRemovePostCommentOrCommunity {
} }
// removing a post or comment // removing a post or comment
else { else {
verify_person_in_community(&self.common.actor, &self.cc, context, request_counter).await?; verify_person_in_community(&self.common.actor, &self.cc[0], context, request_counter).await?;
verify_mod_action(&self.common.actor, self.cc[0].clone(), context).await?; verify_mod_action(&self.common.actor, self.cc[0].clone(), context).await?;
} }
self.object.verify(context, request_counter).await?; self.object.verify(context, request_counter).await?;

View file

@ -1,5 +1,5 @@
use crate::{ use crate::{
activities::send::generate_activity_id, activities::generate_activity_id,
activity_queue::{send_comment_mentions, send_to_community}, activity_queue::{send_comment_mentions, send_to_community},
extensions::context::lemmy_context, extensions::context::lemmy_context,
fetcher::person::get_or_fetch_and_upsert_person, fetcher::person::get_or_fetch_and_upsert_person,

View file

@ -1,5 +1,5 @@
use crate::{ use crate::{
activities::send::generate_activity_id, activities::generate_activity_id,
activity_queue::{send_activity_single_dest, send_to_community, send_to_community_followers}, activity_queue::{send_activity_single_dest, send_to_community, send_to_community_followers},
check_is_apub_id_valid, check_is_apub_id_valid,
extensions::context::lemmy_context, extensions::context::lemmy_context,

View file

@ -1,24 +1,5 @@
use lemmy_utils::settings::structs::Settings;
use url::{ParseError, Url};
use uuid::Uuid;
pub(crate) mod comment; pub(crate) mod comment;
pub(crate) mod community; pub(crate) mod community;
pub(crate) mod person; pub(crate) mod person;
pub(crate) mod post; pub(crate) mod post;
pub(crate) mod private_message; pub(crate) mod private_message;
/// Generate a unique ID for an activity, in the format:
/// `http(s)://example.com/receive/create/202daf0a-1489-45df-8d2e-c8a3173fed36`
fn generate_activity_id<T>(kind: T) -> Result<Url, ParseError>
where
T: ToString,
{
let id = format!(
"{}/activities/{}/{}",
Settings::get().get_protocol_and_hostname(),
kind.to_string().to_lowercase(),
Uuid::new_v4()
);
Url::parse(&id)
}

View file

@ -1,5 +1,5 @@
use crate::{ use crate::{
activities::send::generate_activity_id, activities::generate_activity_id,
activity_queue::send_activity_single_dest, activity_queue::send_activity_single_dest,
extensions::context::lemmy_context, extensions::context::lemmy_context,
ActorType, ActorType,

View file

@ -1,22 +1,19 @@
use crate::{ use crate::{
activities::send::generate_activity_id, activities::generate_activity_id,
activity_queue::send_to_community, activity_queue::send_to_community,
extensions::context::lemmy_context, extensions::context::lemmy_context,
objects::ToApub,
ActorType, ActorType,
ApubLikeableType, ApubLikeableType,
ApubObjectType, ApubObjectType,
}; };
use activitystreams::{ use activitystreams::{
activity::{ activity::{
kind::{CreateType, DeleteType, DislikeType, LikeType, RemoveType, UndoType, UpdateType}, kind::{DeleteType, DislikeType, LikeType, RemoveType, UndoType},
Create,
Delete, Delete,
Dislike, Dislike,
Like, Like,
Remove, Remove,
Undo, Undo,
Update,
}, },
prelude::*, prelude::*,
public, public,
@ -29,52 +26,20 @@ use lemmy_websocket::LemmyContext;
#[async_trait::async_trait(?Send)] #[async_trait::async_trait(?Send)]
impl ApubObjectType for Post { impl ApubObjectType for Post {
/// Send out information about a newly created post, to the followers of the community. async fn send_create(
async fn send_create(&self, creator: &Person, context: &LemmyContext) -> Result<(), LemmyError> { &self,
let page = self.to_apub(context.pool()).await?; _creator: &Person,
_context: &LemmyContext,
let community_id = self.community_id; ) -> Result<(), LemmyError> {
let community = blocking(context.pool(), move |conn| { unimplemented!()
Community::read(conn, community_id)
})
.await??;
let mut create = Create::new(
creator.actor_id.to_owned().into_inner(),
page.into_any_base()?,
);
create
.set_many_contexts(lemmy_context()?)
.set_id(generate_activity_id(CreateType::Create)?)
.set_to(public())
.set_many_ccs(vec![community.actor_id()]);
send_to_community(create, creator, &community, None, context).await?;
Ok(())
} }
/// Send out information about an edited post, to the followers of the community. async fn send_update(
async fn send_update(&self, creator: &Person, context: &LemmyContext) -> Result<(), LemmyError> { &self,
let page = self.to_apub(context.pool()).await?; _creator: &Person,
_context: &LemmyContext,
let community_id = self.community_id; ) -> Result<(), LemmyError> {
let community = blocking(context.pool(), move |conn| { unimplemented!()
Community::read(conn, community_id)
})
.await??;
let mut update = Update::new(
creator.actor_id.to_owned().into_inner(),
page.into_any_base()?,
);
update
.set_many_contexts(lemmy_context()?)
.set_id(generate_activity_id(UpdateType::Update)?)
.set_to(public())
.set_many_ccs(vec![community.actor_id()]);
send_to_community(update, creator, &community, None, context).await?;
Ok(())
} }
async fn send_delete(&self, creator: &Person, context: &LemmyContext) -> Result<(), LemmyError> { async fn send_delete(&self, creator: &Person, context: &LemmyContext) -> Result<(), LemmyError> {

View file

@ -1,5 +1,5 @@
use crate::{ use crate::{
activities::send::generate_activity_id, activities::generate_activity_id,
activity_queue::send_activity_single_dest, activity_queue::send_activity_single_dest,
extensions::context::lemmy_context, extensions::context::lemmy_context,
objects::ToApub, objects::ToApub,

View file

@ -29,7 +29,7 @@ impl ActivityHandler for DislikePostOrComment {
request_counter: &mut i32, request_counter: &mut i32,
) -> Result<(), LemmyError> { ) -> Result<(), LemmyError> {
verify_activity(self.common())?; verify_activity(self.common())?;
verify_person_in_community(&self.common.actor, &self.cc, context, request_counter).await?; verify_person_in_community(&self.common.actor, &self.cc[0], context, request_counter).await?;
Ok(()) Ok(())
} }

View file

@ -29,7 +29,7 @@ impl ActivityHandler for LikePostOrComment {
request_counter: &mut i32, request_counter: &mut i32,
) -> Result<(), LemmyError> { ) -> Result<(), LemmyError> {
verify_activity(self.common())?; verify_activity(self.common())?;
verify_person_in_community(&self.common.actor, &self.cc, context, request_counter).await?; verify_person_in_community(&self.common.actor, &self.cc[0], context, request_counter).await?;
Ok(()) Ok(())
} }

View file

@ -29,7 +29,7 @@ impl ActivityHandler for UndoDislikePostOrComment {
request_counter: &mut i32, request_counter: &mut i32,
) -> Result<(), LemmyError> { ) -> Result<(), LemmyError> {
verify_activity(self.common())?; verify_activity(self.common())?;
verify_person_in_community(&self.common.actor, &self.cc, context, request_counter).await?; verify_person_in_community(&self.common.actor, &self.cc[0], context, request_counter).await?;
verify_urls_match(&self.common.actor, &self.object.common().actor)?; verify_urls_match(&self.common.actor, &self.object.common().actor)?;
self.object.verify(context, request_counter).await?; self.object.verify(context, request_counter).await?;
Ok(()) Ok(())

View file

@ -29,7 +29,7 @@ impl ActivityHandler for UndoLikePostOrComment {
request_counter: &mut i32, request_counter: &mut i32,
) -> Result<(), LemmyError> { ) -> Result<(), LemmyError> {
verify_activity(self.common())?; verify_activity(self.common())?;
verify_person_in_community(&self.common.actor, &self.cc, context, request_counter).await?; verify_person_in_community(&self.common.actor, &self.cc[0], context, request_counter).await?;
verify_urls_match(&self.common.actor, &self.object.common().actor)?; verify_urls_match(&self.common.actor, &self.object.common().actor)?;
self.object.verify(context, request_counter).await?; self.object.verify(context, request_counter).await?;
Ok(()) Ok(())

View file

@ -1,4 +1,5 @@
use crate::{ use crate::{
activities::community::announce::{AnnouncableActivities, AnnounceActivity},
check_is_apub_id_valid, check_is_apub_id_valid,
extensions::signatures::sign_and_send, extensions::signatures::sign_and_send,
insert_activity, insert_activity,
@ -24,7 +25,7 @@ use itertools::Itertools;
use lemmy_db_schema::source::{community::Community, person::Person}; use lemmy_db_schema::source::{community::Community, person::Person};
use lemmy_utils::{location_info, settings::structs::Settings, LemmyError}; use lemmy_utils::{location_info, settings::structs::Settings, LemmyError};
use lemmy_websocket::LemmyContext; use lemmy_websocket::LemmyContext;
use log::{debug, warn}; use log::{debug, info, warn};
use reqwest::Client; use reqwest::Client;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::{collections::BTreeMap, env, fmt::Debug, future::Future, pin::Pin}; use std::{collections::BTreeMap, env, fmt::Debug, future::Future, pin::Pin};
@ -171,6 +172,80 @@ where
Ok(()) Ok(())
} }
pub(crate) async fn send_to_community_new(
activity: AnnouncableActivities,
activity_id: &Url,
actor: &dyn ActorType,
community: &Community,
additional_inboxes: Vec<Url>,
context: &LemmyContext,
) -> Result<(), LemmyError> {
// if this is a local community, we need to do an announce from the community instead
if community.local {
insert_activity(activity_id, activity.clone(), true, false, context.pool()).await?;
AnnounceActivity::send(activity, community, additional_inboxes, context).await?;
} else {
let mut inboxes = additional_inboxes;
inboxes.push(community.get_shared_inbox_or_inbox_url());
send_activity_new(context, &activity, activity_id, actor, inboxes, false).await?;
}
Ok(())
}
pub(crate) async fn send_activity_new<T>(
context: &LemmyContext,
activity: &T,
activity_id: &Url,
actor: &dyn ActorType,
inboxes: Vec<Url>,
sensitive: bool,
) -> Result<(), LemmyError>
where
T: Serialize,
{
if !Settings::get().federation().enabled || inboxes.is_empty() {
return Ok(());
}
info!("Sending activity {}", activity_id.to_string());
// Don't send anything to ourselves
// TODO: this should be a debug assert
let hostname = Settings::get().get_hostname_without_port()?;
let inboxes: Vec<&Url> = inboxes
.iter()
.filter(|i| i.domain().expect("valid inbox url") != hostname)
.collect();
let serialised_activity = serde_json::to_string(&activity)?;
insert_activity(
activity_id,
serialised_activity.clone(),
true,
sensitive,
context.pool(),
)
.await?;
for i in inboxes {
let message = SendActivityTask {
activity: serialised_activity.to_owned(),
inbox: i.to_owned(),
actor_id: actor.actor_id(),
private_key: actor.private_key().context(location_info!())?,
};
if env::var("LEMMY_TEST_SEND_SYNC").is_ok() {
do_send(message, &Client::default()).await?;
} else {
context.activity_queue.queue::<SendActivityTask>(message)?;
}
}
Ok(())
}
/// Create new `SendActivityTasks`, which will deliver the given activity to inboxes, as well as /// Create new `SendActivityTasks`, which will deliver the given activity to inboxes, as well as
/// handling signing and retrying failed deliveres. /// handling signing and retrying failed deliveres.
/// ///

View file

@ -1,5 +1,4 @@
pub mod context; pub mod context;
pub(crate) mod group_extension; pub(crate) mod group_extension;
pub(crate) mod page_extension;
pub(crate) mod person_extension; pub(crate) mod person_extension;
pub mod signatures; pub mod signatures;

View file

@ -1,36 +0,0 @@
use activitystreams::unparsed::UnparsedMutExt;
use activitystreams_ext::UnparsedExtension;
use serde::{Deserialize, Serialize};
/// Activitystreams extension to allow (de)serializing additional Post fields
/// `comemnts_enabled` (called 'locked' in Lemmy),
/// `sensitive` (called 'nsfw') and `stickied`.
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct PageExtension {
pub comments_enabled: Option<bool>,
pub sensitive: Option<bool>,
pub stickied: Option<bool>,
}
impl<U> UnparsedExtension<U> for PageExtension
where
U: UnparsedMutExt,
{
type Error = serde_json::Error;
fn try_from_unparsed(unparsed_mut: &mut U) -> Result<Self, Self::Error> {
Ok(PageExtension {
comments_enabled: unparsed_mut.remove("commentsEnabled")?,
sensitive: unparsed_mut.remove("sensitive")?,
stickied: unparsed_mut.remove("stickied")?,
})
}
fn try_into_unparsed(self, unparsed_mut: &mut U) -> Result<(), Self::Error> {
unparsed_mut.insert("commentsEnabled", self.comments_enabled)?;
unparsed_mut.insert("sensitive", self.sensitive)?;
unparsed_mut.insert("stickied", self.stickied)?;
Ok(())
}
}

View file

@ -1,8 +1,7 @@
use crate::{ use crate::{
fetcher::fetch::fetch_remote_object, fetcher::fetch::fetch_remote_object,
objects::FromApub, objects::{post::Page, FromApub},
NoteExt, NoteExt,
PageExt,
PostOrComment, PostOrComment,
}; };
use anyhow::anyhow; use anyhow::anyhow;
@ -35,7 +34,7 @@ pub async fn get_or_fetch_and_insert_post(
Err(NotFound {}) => { Err(NotFound {}) => {
debug!("Fetching and creating remote post: {}", post_ap_id); debug!("Fetching and creating remote post: {}", post_ap_id);
let page = let page =
fetch_remote_object::<PageExt>(context.client(), post_ap_id, recursion_counter).await?; fetch_remote_object::<Page>(context.client(), post_ap_id, recursion_counter).await?;
let post = Post::from_apub( let post = Post::from_apub(
&page, &page,
context, context,

View file

@ -6,11 +6,10 @@ use crate::{
is_deleted, is_deleted,
}, },
find_object_by_id, find_object_by_id,
objects::FromApub, objects::{post::Page, FromApub},
GroupExt, GroupExt,
NoteExt, NoteExt,
Object, Object,
PageExt,
PersonExt, PersonExt,
}; };
use activitystreams::base::BaseExt; use activitystreams::base::BaseExt;
@ -46,7 +45,7 @@ use url::Url;
enum SearchAcceptedObjects { enum SearchAcceptedObjects {
Person(Box<PersonExt>), Person(Box<PersonExt>),
Group(Box<GroupExt>), Group(Box<GroupExt>),
Page(Box<PageExt>), Page(Box<Page>),
Comment(Box<NoteExt>), Comment(Box<NoteExt>),
} }

View file

@ -11,7 +11,6 @@ pub mod objects;
use crate::{ use crate::{
extensions::{ extensions::{
group_extension::GroupExtension, group_extension::GroupExtension,
page_extension::PageExtension,
person_extension::PersonExtension, person_extension::PersonExtension,
signatures::{PublicKey, PublicKeyExtension}, signatures::{PublicKey, PublicKeyExtension},
}, },
@ -21,9 +20,9 @@ use activitystreams::{
activity::Follow, activity::Follow,
actor, actor,
base::AnyBase, base::AnyBase,
object::{ApObject, AsObject, Note, ObjectExt, Page}, object::{ApObject, AsObject, Note, ObjectExt},
}; };
use activitystreams_ext::{Ext1, Ext2}; use activitystreams_ext::Ext2;
use anyhow::{anyhow, Context}; use anyhow::{anyhow, Context};
use diesel::NotFound; use diesel::NotFound;
use lemmy_api_common::blocking; use lemmy_api_common::blocking;
@ -54,8 +53,6 @@ pub type GroupExt =
type PersonExt = type PersonExt =
Ext2<actor::ApActor<ApObject<actor::Actor<UserTypes>>>, PersonExtension, PublicKeyExtension>; Ext2<actor::ApActor<ApObject<actor::Actor<UserTypes>>>, PersonExtension, PublicKeyExtension>;
pub type SiteExt = actor::ApActor<ApObject<actor::Service>>; pub type SiteExt = actor::ApActor<ApObject<actor::Service>>;
/// Activitystreams type for post
pub type PageExt = Ext1<ApObject<Page>, PageExtension>;
pub type NoteExt = ApObject<Note>; pub type NoteExt = ApObject<Note>;
#[derive(Clone, Copy, Debug, serde::Deserialize, serde::Serialize, PartialEq)] #[derive(Clone, Copy, Debug, serde::Deserialize, serde::Serialize, PartialEq)]

View file

@ -1,30 +1,23 @@
use crate::{ use crate::{
check_is_apub_id_valid, activities::extract_community,
extensions::{context::lemmy_context, page_extension::PageExtension}, extensions::context::lemmy_context,
fetcher::person::get_or_fetch_and_upsert_person, fetcher::person::get_or_fetch_and_upsert_person,
get_community_from_to_or_cc, objects::{create_tombstone, FromApub, ToApub},
objects::{
check_object_domain,
check_object_for_community_or_site_ban,
create_tombstone,
get_object_from_apub,
get_source_markdown_value,
set_content_and_source,
FromApub,
FromApubToForm,
ToApub,
},
PageExt,
}; };
use activitystreams::{ use activitystreams::{
object::{kind::PageType, ApObject, Image, Page, Tombstone}, base::AnyBase,
prelude::*, object::{
kind::{ImageType, PageType},
Tombstone,
},
primitives::OneOrMany,
public, public,
unparsed::Unparsed,
}; };
use activitystreams_ext::Ext1; use chrono::{DateTime, FixedOffset};
use anyhow::Context;
use lemmy_api_common::blocking; use lemmy_api_common::blocking;
use lemmy_db_queries::{Crud, DbPool}; use lemmy_apub_lib::verify_domains_match;
use lemmy_db_queries::{ApubObject, Crud, DbPool};
use lemmy_db_schema::{ use lemmy_db_schema::{
self, self,
source::{ source::{
@ -34,9 +27,8 @@ use lemmy_db_schema::{
}, },
}; };
use lemmy_utils::{ use lemmy_utils::{
location_info,
request::fetch_iframely_and_pictrs_data, request::fetch_iframely_and_pictrs_data,
utils::{check_slurs, convert_datetime, remove_slurs}, utils::{check_slurs, convert_datetime, markdown_to_html, remove_slurs},
LemmyError, LemmyError,
}; };
use lemmy_websocket::LemmyContext; use lemmy_websocket::LemmyContext;
@ -44,56 +36,44 @@ use url::Url;
#[async_trait::async_trait(?Send)] #[async_trait::async_trait(?Send)]
impl ToApub for Post { impl ToApub for Post {
type ApubType = PageExt; type ApubType = Page;
// Turn a Lemmy post into an ActivityPub page that can be sent out over the network. // Turn a Lemmy post into an ActivityPub page that can be sent out over the network.
async fn to_apub(&self, pool: &DbPool) -> Result<PageExt, LemmyError> { async fn to_apub(&self, pool: &DbPool) -> Result<Page, LemmyError> {
let mut page = ApObject::new(Page::new());
let creator_id = self.creator_id; let creator_id = self.creator_id;
let creator = blocking(pool, move |conn| Person::read(conn, creator_id)).await??; let creator = blocking(pool, move |conn| Person::read(conn, creator_id)).await??;
let community_id = self.community_id; let community_id = self.community_id;
let community = blocking(pool, move |conn| Community::read(conn, community_id)).await??; let community = blocking(pool, move |conn| Community::read(conn, community_id)).await??;
page let source = self.body.clone().map(|body| Source {
// Not needed when the Post is embedded in a collection (like for community outbox) content: body,
// TODO: need to set proper context defining sensitive/commentsEnabled fields media_type: MediaTypeMarkdown::Markdown,
// https://git.asonix.dog/Aardwolf/activitystreams/issues/5 });
.set_many_contexts(lemmy_context()?) let image = self.thumbnail_url.clone().map(|thumb| ImageObject {
.set_id(self.ap_id.to_owned().into_inner()) content: ImageType::Image,
.set_name(self.name.to_owned()) url: thumb.into(),
// `summary` field for compatibility with lemmy v0.9.9 and older, });
// TODO: remove this after some time
.set_summary(self.name.to_owned())
.set_published(convert_datetime(self.published))
.set_many_tos(vec![community.actor_id.into_inner(), public()])
.set_attributed_to(creator.actor_id.into_inner());
if let Some(body) = &self.body { let page = Page {
set_content_and_source(&mut page, body)?; context: lemmy_context()?.into(),
} r#type: PageType::Page,
id: self.ap_id.clone().into(),
if let Some(url) = &self.url { attributed_to: creator.actor_id.into(),
page.set_url::<Url>(url.to_owned().into()); to: [community.actor_id.into(), public()],
} name: self.name.clone(),
content: self.body.as_ref().map(|b| markdown_to_html(b)),
if let Some(thumbnail_url) = &self.thumbnail_url { media_type: MediaTypeHtml::Markdown,
let mut image = Image::new(); source,
image.set_url::<Url>(thumbnail_url.to_owned().into()); url: self.url.clone().map(|u| u.into()),
page.set_image(image.into_any_base()?); image,
}
if let Some(u) = self.updated {
page.set_updated(convert_datetime(u));
}
let ext = PageExtension {
comments_enabled: Some(!self.locked), comments_enabled: Some(!self.locked),
sensitive: Some(self.nsfw), sensitive: Some(self.nsfw),
stickied: Some(self.stickied), stickied: Some(self.stickied),
published: convert_datetime(self.published),
updated: self.updated.map(convert_datetime),
unparsed: Default::default(),
}; };
Ok(Ext1::new(page, ext)) Ok(page)
} }
fn to_tombstone(&self) -> Result<Tombstone, LemmyError> { fn to_tombstone(&self) -> Result<Tombstone, LemmyError> {
@ -106,138 +86,133 @@ impl ToApub for Post {
} }
} }
#[async_trait::async_trait(?Send)] #[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
impl FromApub for Post { pub enum MediaTypeMarkdown {
type ApubType = PageExt; #[serde(rename = "text/markdown")]
Markdown,
}
/// Converts a `PageExt` to `PostForm`. #[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
pub enum MediaTypeHtml {
#[serde(rename = "text/html")]
Markdown,
}
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Source {
content: String,
media_type: MediaTypeMarkdown,
}
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ImageObject {
content: ImageType,
url: Url,
}
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Page {
#[serde(rename = "@context")]
context: OneOrMany<AnyBase>,
r#type: PageType,
pub(crate) id: Url,
pub(crate) attributed_to: Url,
to: [Url; 2],
name: String,
content: Option<String>,
media_type: MediaTypeHtml,
source: Option<Source>,
url: Option<Url>,
image: Option<ImageObject>,
pub(crate) comments_enabled: Option<bool>,
sensitive: Option<bool>,
pub(crate) stickied: Option<bool>,
published: DateTime<FixedOffset>,
updated: Option<DateTime<FixedOffset>>,
// unparsed fields
#[serde(flatten)]
unparsed: Unparsed,
}
impl Page {
/// Only mods can change the post's stickied/locked status. So if either of these is changed from
/// the current value, it is a mod action and needs to be verified as such.
/// ///
/// If the post's community or creator are not known locally, these are also fetched. /// Both stickied and locked need to be false on a newly created post (verified in [[CreatePost]].
async fn from_apub( pub(crate) async fn is_mod_action(&self, pool: &DbPool) -> Result<bool, LemmyError> {
page: &PageExt, let post_id = self.id.clone();
context: &LemmyContext, let old_post = blocking(pool, move |conn| {
expected_domain: Url, Post::read_from_apub_id(conn, &post_id.into())
request_counter: &mut i32, })
mod_action_allowed: bool,
) -> Result<Post, LemmyError> {
let post: Post = get_object_from_apub(
page,
context,
expected_domain,
request_counter,
mod_action_allowed,
)
.await?; .await?;
check_object_for_community_or_site_ban(page, post.community_id, context, request_counter)
.await?; let is_mod_action = if let Ok(old_post) = old_post {
Ok(post) self.stickied != Some(old_post.stickied) || self.comments_enabled != Some(!old_post.locked)
} else {
false
};
Ok(is_mod_action)
}
pub(crate) async fn verify(
&self,
_context: &LemmyContext,
_request_counter: &mut i32,
) -> Result<(), LemmyError> {
check_slurs(&self.name)?;
verify_domains_match(&self.attributed_to, &self.id)?;
Ok(())
} }
} }
#[async_trait::async_trait(?Send)] #[async_trait::async_trait(?Send)]
impl FromApubToForm<PageExt> for PostForm { impl FromApub for Post {
type ApubType = Page;
async fn from_apub( async fn from_apub(
page: &PageExt, page: &Page,
context: &LemmyContext, context: &LemmyContext,
expected_domain: Url, _expected_domain: Url,
request_counter: &mut i32, request_counter: &mut i32,
mod_action_allowed: bool, _mod_action_allowed: bool,
) -> Result<PostForm, LemmyError> { ) -> Result<Post, LemmyError> {
let community = get_community_from_to_or_cc(page, context, request_counter).await?;
let ap_id = if mod_action_allowed {
let id = page.id_unchecked().context(location_info!())?;
check_is_apub_id_valid(id, community.local)?;
id.to_owned().into()
} else {
check_object_domain(page, expected_domain, community.local)?
};
let ext = &page.ext_one;
let creator_actor_id = page
.inner
.attributed_to()
.as_ref()
.context(location_info!())?
.as_single_xsd_any_uri()
.context(location_info!())?;
let creator = let creator =
get_or_fetch_and_upsert_person(creator_actor_id, context, request_counter).await?; get_or_fetch_and_upsert_person(&page.attributed_to, context, request_counter).await?;
let community = extract_community(&page.to, context, request_counter).await?;
let thumbnail_url: Option<Url> = match &page.inner.image() {
Some(any_image) => Image::from_any_base(
any_image
.to_owned()
.as_one()
.context(location_info!())?
.to_owned(),
)?
.context(location_info!())?
.url()
.context(location_info!())?
.as_single_xsd_any_uri()
.map(|url| url.to_owned()),
None => None,
};
let url = page
.inner
.url()
.map(|u| u.as_single_xsd_any_uri())
.flatten()
.map(|u| u.to_owned());
let thumbnail_url: Option<Url> = page.image.clone().map(|i| i.url);
let (iframely_title, iframely_description, iframely_html, pictrs_thumbnail) = let (iframely_title, iframely_description, iframely_html, pictrs_thumbnail) =
if let Some(url) = &url { if let Some(url) = &page.url {
fetch_iframely_and_pictrs_data(context.client(), Some(url)).await fetch_iframely_and_pictrs_data(context.client(), Some(url)).await
} else { } else {
(None, None, None, thumbnail_url) (None, None, None, thumbnail_url)
}; };
let name = page let body_slurs_removed = page.source.as_ref().map(|s| remove_slurs(&s.content));
.inner let form = PostForm {
.name() name: page.name.clone(),
// The following is for compatibility with lemmy v0.9.9 and older url: page.url.clone().map(|u| u.into()),
// TODO: remove it after some time (along with the map above)
.or_else(|| page.inner.summary())
.context(location_info!())?
.as_single_xsd_string()
.context(location_info!())?
.to_string();
let body = get_source_markdown_value(page)?;
// TODO: expected_domain is wrong in this case, because it simply takes the domain of the actor
// maybe we need to take id_unchecked() if the activity is from community to user?
// why did this work before? -> i dont think it did?
// -> try to make expected_domain optional and set it null if it is a mod action
check_slurs(&name)?;
let body_slurs_removed = body.map(|b| remove_slurs(&b));
Ok(PostForm {
name,
url: url.map(|u| u.into()),
body: body_slurs_removed, body: body_slurs_removed,
creator_id: creator.id, creator_id: creator.id,
community_id: community.id, community_id: community.id,
removed: None, removed: None,
locked: ext.comments_enabled.map(|e| !e), locked: page.comments_enabled.map(|e| !e),
published: page published: Some(page.published.naive_local()),
.inner updated: page.updated.map(|u| u.naive_local()),
.published()
.as_ref()
.map(|u| u.to_owned().naive_local()),
updated: page
.inner
.updated()
.as_ref()
.map(|u| u.to_owned().naive_local()),
deleted: None, deleted: None,
nsfw: ext.sensitive, nsfw: page.sensitive,
stickied: ext.stickied, stickied: page.stickied,
embed_title: iframely_title, embed_title: iframely_title,
embed_description: iframely_description, embed_description: iframely_description,
embed_html: iframely_html, embed_html: iframely_html,
thumbnail_url: pictrs_thumbnail.map(|u| u.into()), thumbnail_url: pictrs_thumbnail.map(|u| u.into()),
ap_id: Some(ap_id), ap_id: Some(page.id.clone().into()),
local: Some(false), local: Some(false),
}) };
Ok(blocking(context.pool(), move |conn| Post::upsert(conn, &form)).await??)
} }
} }

View file

@ -20,7 +20,7 @@ pub enum PublicUrl {
pub struct ActivityCommonFields { pub struct ActivityCommonFields {
#[serde(rename = "@context")] #[serde(rename = "@context")]
pub context: OneOrMany<AnyBase>, pub context: OneOrMany<AnyBase>,
id: Url, pub id: Url,
pub actor: Url, pub actor: Url,
// unparsed fields // unparsed fields