]> Untitled Git - lemmy.git/commitdiff
Rewrite apub post (de)serialization using structs (ref #1657)
authorFelix Ableitner <me@nutomic.com>
Tue, 27 Jul 2021 22:18:50 +0000 (00:18 +0200)
committerFelix Ableitner <me@nutomic.com>
Fri, 30 Jul 2021 21:16:32 +0000 (23:16 +0200)
36 files changed:
crates/api/src/post.rs
crates/api_crud/src/post/create.rs
crates/api_crud/src/post/update.rs
crates/apub/src/activities/comment/create.rs
crates/apub/src/activities/comment/update.rs
crates/apub/src/activities/community/add_mod.rs
crates/apub/src/activities/community/announce.rs
crates/apub/src/activities/community/block_user.rs
crates/apub/src/activities/community/mod.rs
crates/apub/src/activities/community/undo_block_user.rs
crates/apub/src/activities/community/update.rs
crates/apub/src/activities/deletion/delete.rs
crates/apub/src/activities/deletion/undo_delete.rs
crates/apub/src/activities/mod.rs
crates/apub/src/activities/post/create.rs
crates/apub/src/activities/post/update.rs
crates/apub/src/activities/removal/remove.rs
crates/apub/src/activities/removal/undo_remove.rs
crates/apub/src/activities/send/comment.rs
crates/apub/src/activities/send/community.rs
crates/apub/src/activities/send/mod.rs
crates/apub/src/activities/send/person.rs
crates/apub/src/activities/send/post.rs
crates/apub/src/activities/send/private_message.rs
crates/apub/src/activities/voting/dislike.rs
crates/apub/src/activities/voting/like.rs
crates/apub/src/activities/voting/undo_dislike.rs
crates/apub/src/activities/voting/undo_like.rs
crates/apub/src/activity_queue.rs
crates/apub/src/extensions/mod.rs
crates/apub/src/extensions/page_extension.rs [deleted file]
crates/apub/src/fetcher/objects.rs
crates/apub/src/fetcher/search.rs
crates/apub/src/lib.rs
crates/apub/src/objects/post.rs
crates/apub_lib/src/lib.rs

index 4a55b7e05b876a06f27a5db07bfe2410ec317ec6..84e0c849e0dde837f92e2af6af732a6167a61081 100644 (file)
@@ -9,7 +9,7 @@ use lemmy_api_common::{
   mark_post_as_read,
   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_schema::source::{moderator::*, post::*};
 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??;
 
     // apub updates
-    updated_post
-      .send_update(&local_user_view.person, context)
-      .await?;
+    UpdatePost::send(&updated_post, &local_user_view.person, context).await?;
 
     // Refetch the post
     let post_id = data.post_id;
@@ -214,9 +212,7 @@ impl Perform for StickyPost {
 
     // Apub updates
     // TODO stickied should pry work like locked for ease of use
-    updated_post
-      .send_update(&local_user_view.person, context)
-      .await?;
+    UpdatePost::send(&updated_post, &local_user_view.person, context).await?;
 
     // Refetch the post
     let post_id = data.post_id;
index dc99f3ecf4f81052bbbe203691e6d7512b9442ac..89f71d939d7d5b5082d6936247593420cb2a8a2e 100644 (file)
@@ -7,7 +7,7 @@ use lemmy_api_common::{
   mark_post_as_read,
   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_schema::source::post::*;
 use lemmy_db_views::post_view::PostView;
@@ -82,9 +82,12 @@ impl PerformCrud for CreatePost {
     .await?
     .map_err(|_| ApiError::err("couldnt_create_post"))?;
 
-    updated_post
-      .send_create(&local_user_view.person, context)
-      .await?;
+    lemmy_apub::activities::post::create::CreatePost::send(
+      &updated_post,
+      &local_user_view.person,
+      context,
+    )
+    .await?;
 
     // They like their own post by default
     let person_id = local_user_view.person.id;
index 5166f21278d34a4710f7a7451a3439afa61a02e4..6b2780376e3e81d3080aa2fef8f6b406931a599e 100644 (file)
@@ -1,7 +1,7 @@
 use crate::PerformCrud;
 use actix_web::web::Data;
 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_schema::{naive_now, source::post::*};
 use lemmy_db_views::post_view::PostView;
@@ -89,9 +89,7 @@ impl PerformCrud for EditPost {
     };
 
     // Send apub update
-    updated_post
-      .send_update(&local_user_view.person, context)
-      .await?;
+    UpdatePost::send(&updated_post, &local_user_view.person, context).await?;
 
     let post_id = data.post_id;
     let mut post_view = blocking(context.pool(), move |conn| {
index 84dab107132b3b92d0c7fdbc8627158a7d8dd24c..b385533597a0d5cf0f32bf2c4077b386f4ee4f00 100644 (file)
@@ -1,10 +1,12 @@
 use crate::{
   activities::{
     comment::{get_notif_recipients, send_websocket_message},
+    extract_community,
     verify_activity,
     verify_person_in_community,
   },
   objects::FromApub,
+  ActorType,
   NoteExt,
 };
 use activitystreams::{activity::kind::CreateType, base::BaseExt};
@@ -33,8 +35,16 @@ impl ActivityHandler for CreateComment {
     context: &LemmyContext,
     request_counter: &mut i32,
   ) -> Result<(), LemmyError> {
+    let community = extract_community(&self.cc, context, request_counter).await?;
+
     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())?;
     // TODO: should add a check that the correct community is in cc (probably needs changes to
     //       comment deserialization)
index 142656f506e41dce0f4a9725a0363629244f3a71..5e785229f4739b78b530aa4a7a9ebce33cf1e052 100644 (file)
@@ -1,10 +1,12 @@
 use crate::{
   activities::{
     comment::{get_notif_recipients, send_websocket_message},
+    extract_community,
     verify_activity,
     verify_person_in_community,
   },
   objects::FromApub,
+  ActorType,
   NoteExt,
 };
 use activitystreams::{activity::kind::UpdateType, base::BaseExt};
@@ -33,8 +35,16 @@ impl ActivityHandler for UpdateComment {
     context: &LemmyContext,
     request_counter: &mut i32,
   ) -> Result<(), LemmyError> {
+    let community = extract_community(&self.cc, context, request_counter).await?;
+
     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())?;
     Ok(())
   }
index 2785856c131e53919fd59906623d763d335c6b60..fd22b978a6496614446533b4ff103575d617e39e 100644 (file)
@@ -38,7 +38,7 @@ impl ActivityHandler for AddMod {
     request_counter: &mut i32,
   ) -> Result<(), LemmyError> {
     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_add_remove_moderator_target(&self.target, self.cc[0].clone())?;
     Ok(())
index 301ccc64840574e0bcce1a9e839b96677cfa7700..0bdb8bc06dd883abb029cee780601ca8b3c1db18 100644 (file)
@@ -4,12 +4,14 @@ use crate::{
     community::{
       add_mod::AddMod,
       block_user::BlockUserFromCommunity,
+      list_community_follower_inboxes,
       undo_block_user::UndoBlockUserFromCommunity,
     },
     deletion::{
       delete::DeletePostCommentOrCommunity,
       undo_delete::UndoDeletePostCommentOrCommunity,
     },
+    generate_activity_id,
     post::{create::CreatePost, update::UpdatePost},
     removal::{
       remove::RemovePostCommentCommunityOrMod,
@@ -24,11 +26,16 @@ use crate::{
       undo_like::UndoLikePostOrComment,
     },
   },
+  activity_queue::send_activity_new,
+  extensions::context::lemmy_context,
   http::is_activity_already_known,
   insert_activity,
+  ActorType,
+  CommunityType,
 };
 use activitystreams::activity::kind::AnnounceType;
 use lemmy_apub_lib::{ActivityCommonFields, ActivityHandler, PublicUrl};
+use lemmy_db_schema::source::community::Community;
 use lemmy_utils::LemmyError;
 use lemmy_websocket::LemmyContext;
 use serde::{Deserialize, Serialize};
@@ -66,6 +73,38 @@ pub struct AnnounceActivity {
   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)]
 impl ActivityHandler for AnnounceActivity {
   async fn verify(
index c20652b02c85f61c6b584a4894564735cfe416d5..1c5f0eaf58a081c8fb4b51a9e91ef49ad1485f49 100644 (file)
@@ -36,7 +36,7 @@ impl ActivityHandler for BlockUserFromCommunity {
     request_counter: &mut i32,
   ) -> Result<(), LemmyError> {
     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?;
     Ok(())
   }
index 81152d926e4d19f12e4b93358992bca7ea44c1c8..62b39c6e8ce0a087955730240fec5f04fccbc71c 100644 (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_db_schema::CommunityId;
+use lemmy_db_schema::{source::community::Community, CommunityId};
 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 url::Url;
 
 pub mod add_mod;
 pub mod announce;
@@ -33,3 +36,23 @@ pub(crate) async fn send_websocket_message<
 
   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(),
+  )
+}
index 025c498a6d4fff01908b7b8916d6856457166bd0..d44fe266905c5d6ef5b938add6b521bf5fbbafd8 100644 (file)
@@ -36,7 +36,7 @@ impl ActivityHandler for UndoBlockUserFromCommunity {
     request_counter: &mut i32,
   ) -> Result<(), LemmyError> {
     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?;
     self.object.verify(context, request_counter).await?;
     Ok(())
index cfee29da74399c381383926783f7fba472ebd110..ce1854969bd4d8220cbcb6111b046e76453c7d3d 100644 (file)
@@ -39,7 +39,7 @@ impl ActivityHandler for UpdateCommunity {
     request_counter: &mut i32,
   ) -> Result<(), LemmyError> {
     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?;
     Ok(())
   }
index f0f4185a7b744d8e109dabed9b9c3c3db48f0fbe..f7e7fe5c2b6765b3e19bdbf9a9c676f40e9c7a0b 100644 (file)
@@ -64,7 +64,8 @@ impl ActivityHandler for DeletePostCommentOrCommunity {
     }
     // deleting a post or comment
     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 =
         get_post_or_comment_actor_id(&self.object, context, request_counter).await?;
       verify_urls_match(&self.common.actor, &object_creator)?;
@@ -83,7 +84,7 @@ impl ActivityHandler for DeletePostCommentOrCommunity {
     if let Ok(community) = object_community {
       if community.local {
         // 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?;
         verify_mod_action(&self.common.actor, self.object.clone(), context).await?;
         let mod_ =
index 1de5ca587ead823f4e331b99c1a74ed36030a7bf..94e44d7acd969862298b649d754d8492719b75a5 100644 (file)
@@ -54,7 +54,8 @@ impl ActivityHandler for UndoDeletePostCommentOrCommunity {
     }
     // restoring a post or comment
     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)?;
     }
     Ok(())
@@ -71,7 +72,7 @@ impl ActivityHandler for UndoDeletePostCommentOrCommunity {
     if let Ok(community) = object_community {
       if community.local {
         // 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?;
         verify_mod_action(&self.common.actor, self.object.object.clone(), context).await?;
         let mod_ =
index d9f3dcddef0843a7fe433598b7ab475a66b3f582..af133167f1ee6223394835d825c38124b681dd0a 100644 (file)
@@ -13,9 +13,10 @@ use lemmy_db_schema::{
   DbUrl,
 };
 use lemmy_db_views_actor::community_view::CommunityView;
-use lemmy_utils::LemmyError;
+use lemmy_utils::{settings::structs::Settings, LemmyError};
 use lemmy_websocket::LemmyContext;
-use url::Url;
+use url::{ParseError, Url};
+use uuid::Uuid;
 
 pub mod comment;
 pub mod community;
@@ -41,27 +42,34 @@ async fn verify_person(
   Ok(())
 }
 
-/// 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,
+pub(crate) async fn extract_community(
   cc: &[Url],
   context: &LemmyContext,
   request_counter: &mut i32,
 ) -> Result<Community, LemmyError> {
-  let person = get_or_fetch_and_upsert_person(person_id, context, request_counter).await?;
   let mut cc_iter = cc.iter();
-  let community: Community = loop {
+  loop {
     if let Some(cid) = cc_iter.next() {
       if let Ok(c) = get_or_fetch_and_upsert_community(cid, context, request_counter).await {
-        break c;
+        break Ok(c);
       }
     } else {
       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.
@@ -80,13 +88,16 @@ fn verify_activity(common: &ActivityCommonFields) -> Result<(), LemmyError> {
   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,
-  activity_cc: Url,
+  community: Url,
   context: &LemmyContext,
 ) -> Result<(), LemmyError> {
   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??;
 
@@ -120,3 +131,18 @@ fn verify_add_remove_moderator_target(target: &Url, community: Url) -> Result<()
   }
   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)
+}
index a2ccf4ce5c418b09a063b54c2ae8a843fc8d8337..909a6148dad584f287b08907dbc15557ecb472c4 100644 (file)
@@ -1,13 +1,30 @@
 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,
-  objects::FromApub,
+  objects::{post::Page, FromApub, ToApub},
   ActorType,
-  PageExt,
 };
-use activitystreams::{activity::kind::CreateType, base::BaseExt};
-use lemmy_apub_lib::{verify_domains_match_opt, ActivityCommonFields, ActivityHandler, PublicUrl};
-use lemmy_db_schema::source::post::Post;
+use activitystreams::activity::kind::CreateType;
+use anyhow::anyhow;
+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_websocket::{LemmyContext, UserOperationCrud};
 use url::Url;
@@ -16,14 +33,40 @@ use url::Url;
 #[serde(rename_all = "camelCase")]
 pub struct CreatePost {
   to: PublicUrl,
-  object: PageExt,
-  cc: Vec<Url>,
-  #[serde(rename = "type")]
-  kind: CreateType,
+  object: Page,
+  cc: [Url; 1],
+  r#type: CreateType,
   #[serde(flatten)]
   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)]
 impl ActivityHandler for CreatePost {
   async fn verify(
@@ -31,9 +74,23 @@ impl ActivityHandler for CreatePost {
     context: &LemmyContext,
     request_counter: &mut i32,
   ) -> Result<(), LemmyError> {
+    let community = extract_community(&self.cc, context, request_counter).await?;
+    let community_id = &community.actor_id();
+
     verify_activity(self.common())?;
-    verify_person_in_community(&self.common.actor, &self.cc, context, request_counter).await?;
-    verify_domains_match_opt(&self.common.actor, self.object.id_unchecked())?;
+    verify_person_in_community(&self.common.actor, community_id, context, request_counter).await?;
+    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(())
   }
 
index 13456dcd8ce761ae20733a63abbd33c060eb44ca..a8cec9e73705300310c9e6026fabc22c4be5c035 100644 (file)
@@ -1,24 +1,24 @@
 use crate::{
   activities::{
+    community::announce::AnnouncableActivities,
+    generate_activity_id,
     post::send_websocket_message,
     verify_activity,
     verify_mod_action,
     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,
-  PageExt,
 };
-use activitystreams::{activity::kind::UpdateType, base::BaseExt};
-use anyhow::Context;
+use activitystreams::activity::kind::UpdateType;
 use lemmy_api_common::blocking;
-use lemmy_apub_lib::{verify_domains_match_opt, ActivityCommonFields, ActivityHandler, PublicUrl};
-use lemmy_db_queries::ApubObject;
-use lemmy_db_schema::{
-  source::post::{Post, PostForm},
-  DbUrl,
-};
-use lemmy_utils::{location_info, LemmyError};
+use lemmy_apub_lib::{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_websocket::{LemmyContext, UserOperationCrud};
 use url::Url;
 
@@ -26,14 +26,39 @@ use url::Url;
 #[serde(rename_all = "camelCase")]
 pub struct UpdatePost {
   to: PublicUrl,
-  object: PageExt,
-  cc: Vec<Url>,
-  #[serde(rename = "type")]
-  kind: UpdateType,
+  object: Page,
+  cc: [Url; 1],
+  r#type: UpdateType,
   #[serde(flatten)]
   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)]
 impl ActivityHandler for UpdatePost {
   async fn verify(
@@ -41,34 +66,19 @@ impl ActivityHandler for UpdatePost {
     context: &LemmyContext,
     request_counter: &mut i32,
   ) -> Result<(), LemmyError> {
-    verify_activity(self.common())?;
-    let community =
-      verify_person_in_community(&self.common.actor, &self.cc, context, request_counter).await?;
+    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?;
 
-    let temp_post = PostForm::from_apub(
-      &self.object,
-      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
-    else {
-      verify_domains_match_opt(&self.common.actor, self.object.id_unchecked())?;
+    verify_activity(self.common())?;
+    verify_person_in_community(&self.common.actor, &community_id, context, request_counter).await?;
+    if is_mod_action {
+      verify_mod_action(&self.common.actor, community_id, context).await?;
+    } else {
+      verify_urls_match(&self.common.actor, &self.object.attributed_to)?;
     }
-
+    self.object.verify(context, request_counter).await?;
     Ok(())
   }
 
index a30f23327ffedaaade7a4e49e931b849eca56e7b..7e62ab7bdb258ebea7db6d861c193adc5edae88f 100644 (file)
@@ -64,13 +64,13 @@ impl ActivityHandler for RemovePostCommentCommunityOrMod {
     }
     // removing community mod
     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_add_remove_moderator_target(target, self.cc[0].clone())?;
     }
     // removing a post or comment
     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?;
     }
     Ok(())
index 997d527d8de9e108ead71553da5546f61b54c298..ca77a31da0e76968b00ac242fe3f8b8297f8f6b6 100644 (file)
@@ -52,7 +52,7 @@ impl ActivityHandler for UndoRemovePostCommentOrCommunity {
     }
     // removing a post or comment
     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?;
     }
     self.object.verify(context, request_counter).await?;
index b93f9e1c9b90d2350194321746cffc766f8e495f..be24dd07a42db348e2883ccd2bb6953b7d6cb4c6 100644 (file)
@@ -1,5 +1,5 @@
 use crate::{
-  activities::send::generate_activity_id,
+  activities::generate_activity_id,
   activity_queue::{send_comment_mentions, send_to_community},
   extensions::context::lemmy_context,
   fetcher::person::get_or_fetch_and_upsert_person,
index bf7514797591f8f6914850fced3764b5b3449ea7..96cb058a484a49cf4adaf207ea09397559ec27b6 100644 (file)
@@ -1,5 +1,5 @@
 use crate::{
-  activities::send::generate_activity_id,
+  activities::generate_activity_id,
   activity_queue::{send_activity_single_dest, send_to_community, send_to_community_followers},
   check_is_apub_id_valid,
   extensions::context::lemmy_context,
index 10dd8a26398b73fbf4ec48c9efc4b1e67d6f42c7..65135bddaca339269d6b2f719b5294fa82d14e95 100644 (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 community;
 pub(crate) mod person;
 pub(crate) mod post;
 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)
-}
index b1fc0cd2bf2e85cae6fba2d29e947b6dd2d5490b..a5e792512f7e90b7e39bdd80430734bf4a4b3636 100644 (file)
@@ -1,5 +1,5 @@
 use crate::{
-  activities::send::generate_activity_id,
+  activities::generate_activity_id,
   activity_queue::send_activity_single_dest,
   extensions::context::lemmy_context,
   ActorType,
index c51d6f2d6b27eb3b0671e44888f03542b51dd0d2..db96621b0176c9a01648728ab167798e25aa36e7 100644 (file)
@@ -1,22 +1,19 @@
 use crate::{
-  activities::send::generate_activity_id,
+  activities::generate_activity_id,
   activity_queue::send_to_community,
   extensions::context::lemmy_context,
-  objects::ToApub,
   ActorType,
   ApubLikeableType,
   ApubObjectType,
 };
 use activitystreams::{
   activity::{
-    kind::{CreateType, DeleteType, DislikeType, LikeType, RemoveType, UndoType, UpdateType},
-    Create,
+    kind::{DeleteType, DislikeType, LikeType, RemoveType, UndoType},
     Delete,
     Dislike,
     Like,
     Remove,
     Undo,
-    Update,
   },
   prelude::*,
   public,
@@ -29,52 +26,20 @@ use lemmy_websocket::LemmyContext;
 
 #[async_trait::async_trait(?Send)]
 impl ApubObjectType for Post {
-  /// Send out information about a newly created post, to the followers of the community.
-  async fn send_create(&self, creator: &Person, context: &LemmyContext) -> Result<(), LemmyError> {
-    let page = self.to_apub(context.pool()).await?;
-
-    let community_id = self.community_id;
-    let community = blocking(context.pool(), move |conn| {
-      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(())
+  async fn send_create(
+    &self,
+    _creator: &Person,
+    _context: &LemmyContext,
+  ) -> Result<(), LemmyError> {
+    unimplemented!()
   }
 
-  /// Send out information about an edited post, to the followers of the community.
-  async fn send_update(&self, creator: &Person, context: &LemmyContext) -> Result<(), LemmyError> {
-    let page = self.to_apub(context.pool()).await?;
-
-    let community_id = self.community_id;
-    let community = blocking(context.pool(), move |conn| {
-      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_update(
+    &self,
+    _creator: &Person,
+    _context: &LemmyContext,
+  ) -> Result<(), LemmyError> {
+    unimplemented!()
   }
 
   async fn send_delete(&self, creator: &Person, context: &LemmyContext) -> Result<(), LemmyError> {
index e5a30585b9a4447eb5cb602984139b651d75078c..d208b25d43ad5f0a1e5df14c3d6877a6f95f1239 100644 (file)
@@ -1,5 +1,5 @@
 use crate::{
-  activities::send::generate_activity_id,
+  activities::generate_activity_id,
   activity_queue::send_activity_single_dest,
   extensions::context::lemmy_context,
   objects::ToApub,
index 18d72f39422ecb291a17de1a2e5d49386394d440..b34b2d10b403625419e78d5e5c2b4a3f54327b7d 100644 (file)
@@ -29,7 +29,7 @@ impl ActivityHandler for DislikePostOrComment {
     request_counter: &mut i32,
   ) -> Result<(), LemmyError> {
     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(())
   }
 
index ca899d3d3742d0aabd9681ca8985a47ec94025e1..50a4d44d48a0b54f7b619893fcf2593c731834e6 100644 (file)
@@ -29,7 +29,7 @@ impl ActivityHandler for LikePostOrComment {
     request_counter: &mut i32,
   ) -> Result<(), LemmyError> {
     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(())
   }
 
index 11871e79168cbe3c09d0e90f12275cebb0a3ab53..5ba3b47f93ddc8792cc706bf131d2abb23537bc2 100644 (file)
@@ -29,7 +29,7 @@ impl ActivityHandler for UndoDislikePostOrComment {
     request_counter: &mut i32,
   ) -> Result<(), LemmyError> {
     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)?;
     self.object.verify(context, request_counter).await?;
     Ok(())
index 07c3c4709d049d5d80fb6787553bef64dbeb276b..2de03f4ba19a091403f3173ed5a8c819893b13da 100644 (file)
@@ -29,7 +29,7 @@ impl ActivityHandler for UndoLikePostOrComment {
     request_counter: &mut i32,
   ) -> Result<(), LemmyError> {
     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)?;
     self.object.verify(context, request_counter).await?;
     Ok(())
index 22b88d148b40d13a07a1fe63001d4f36b799114a..f4f8ce90a62e5941d83a00dde78447c9054fcbee 100644 (file)
@@ -1,4 +1,5 @@
 use crate::{
+  activities::community::announce::{AnnouncableActivities, AnnounceActivity},
   check_is_apub_id_valid,
   extensions::signatures::sign_and_send,
   insert_activity,
@@ -24,7 +25,7 @@ use itertools::Itertools;
 use lemmy_db_schema::source::{community::Community, person::Person};
 use lemmy_utils::{location_info, settings::structs::Settings, LemmyError};
 use lemmy_websocket::LemmyContext;
-use log::{debug, warn};
+use log::{debug, info, warn};
 use reqwest::Client;
 use serde::{Deserialize, Serialize};
 use std::{collections::BTreeMap, env, fmt::Debug, future::Future, pin::Pin};
@@ -171,6 +172,80 @@ where
   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
 /// handling signing and retrying failed deliveres.
 ///
index 19e37894d616bf5afbeb2fe422f24fdcb537e2ab..781e89e6ec5541147730426a9b4df1f0eabba328 100644 (file)
@@ -1,5 +1,4 @@
 pub mod context;
 pub(crate) mod group_extension;
-pub(crate) mod page_extension;
 pub(crate) mod person_extension;
 pub mod signatures;
diff --git a/crates/apub/src/extensions/page_extension.rs b/crates/apub/src/extensions/page_extension.rs
deleted file mode 100644 (file)
index 752fa2b..0000000
+++ /dev/null
@@ -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(())
-  }
-}
index af8a59f764233bcd8ec7e912342798f99949aa69..fd94f6499575a02df3ce1fedb6e61e301bbc8325 100644 (file)
@@ -1,8 +1,7 @@
 use crate::{
   fetcher::fetch::fetch_remote_object,
-  objects::FromApub,
+  objects::{post::Page, FromApub},
   NoteExt,
-  PageExt,
   PostOrComment,
 };
 use anyhow::anyhow;
@@ -35,7 +34,7 @@ pub async fn get_or_fetch_and_insert_post(
     Err(NotFound {}) => {
       debug!("Fetching and creating remote post: {}", post_ap_id);
       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(
         &page,
         context,
index 5a09fd4307532e09d71b66d02592b07a4715b360..69076c15132ad533b407c8410975f1363defb8db 100644 (file)
@@ -6,11 +6,10 @@ use crate::{
     is_deleted,
   },
   find_object_by_id,
-  objects::FromApub,
+  objects::{post::Page, FromApub},
   GroupExt,
   NoteExt,
   Object,
-  PageExt,
   PersonExt,
 };
 use activitystreams::base::BaseExt;
@@ -46,7 +45,7 @@ use url::Url;
 enum SearchAcceptedObjects {
   Person(Box<PersonExt>),
   Group(Box<GroupExt>),
-  Page(Box<PageExt>),
+  Page(Box<Page>),
   Comment(Box<NoteExt>),
 }
 
index 1b1e3d37f690a1a53a6b92c0487757c53e3f0342..94da33cc6145bf1c07ece417e75a8a80852a09a6 100644 (file)
@@ -11,7 +11,6 @@ pub mod objects;
 use crate::{
   extensions::{
     group_extension::GroupExtension,
-    page_extension::PageExtension,
     person_extension::PersonExtension,
     signatures::{PublicKey, PublicKeyExtension},
   },
@@ -21,9 +20,9 @@ use activitystreams::{
   activity::Follow,
   actor,
   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 diesel::NotFound;
 use lemmy_api_common::blocking;
@@ -54,8 +53,6 @@ pub type GroupExt =
 type PersonExt =
   Ext2<actor::ApActor<ApObject<actor::Actor<UserTypes>>>, PersonExtension, PublicKeyExtension>;
 pub type SiteExt = actor::ApActor<ApObject<actor::Service>>;
-/// Activitystreams type for post
-pub type PageExt = Ext1<ApObject<Page>, PageExtension>;
 pub type NoteExt = ApObject<Note>;
 
 #[derive(Clone, Copy, Debug, serde::Deserialize, serde::Serialize, PartialEq)]
index 19bfe8aabae631405bf32a71b2286bef86f68a39..ace6b913121fd73329f340b1c6a0d2040f51f769 100644 (file)
@@ -1,30 +1,23 @@
 use crate::{
-  check_is_apub_id_valid,
-  extensions::{context::lemmy_context, page_extension::PageExtension},
+  activities::extract_community,
+  extensions::context::lemmy_context,
   fetcher::person::get_or_fetch_and_upsert_person,
-  get_community_from_to_or_cc,
-  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,
+  objects::{create_tombstone, FromApub, ToApub},
 };
 use activitystreams::{
-  object::{kind::PageType, ApObject, Image, Page, Tombstone},
-  prelude::*,
+  base::AnyBase,
+  object::{
+    kind::{ImageType, PageType},
+    Tombstone,
+  },
+  primitives::OneOrMany,
   public,
+  unparsed::Unparsed,
 };
-use activitystreams_ext::Ext1;
-use anyhow::Context;
+use chrono::{DateTime, FixedOffset};
 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::{
   self,
   source::{
@@ -34,9 +27,8 @@ use lemmy_db_schema::{
   },
 };
 use lemmy_utils::{
-  location_info,
   request::fetch_iframely_and_pictrs_data,
-  utils::{check_slurs, convert_datetime, remove_slurs},
+  utils::{check_slurs, convert_datetime, markdown_to_html, remove_slurs},
   LemmyError,
 };
 use lemmy_websocket::LemmyContext;
@@ -44,56 +36,44 @@ use url::Url;
 
 #[async_trait::async_trait(?Send)]
 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.
-  async fn to_apub(&self, pool: &DbPool) -> Result<PageExt, LemmyError> {
-    let mut page = ApObject::new(Page::new());
-
+  async fn to_apub(&self, pool: &DbPool) -> Result<Page, LemmyError> {
     let creator_id = self.creator_id;
     let creator = blocking(pool, move |conn| Person::read(conn, creator_id)).await??;
-
     let community_id = self.community_id;
     let community = blocking(pool, move |conn| Community::read(conn, community_id)).await??;
 
-    page
-      // Not needed when the Post is embedded in a collection (like for community outbox)
-      // TODO: need to set proper context defining sensitive/commentsEnabled fields
-      // https://git.asonix.dog/Aardwolf/activitystreams/issues/5
-      .set_many_contexts(lemmy_context()?)
-      .set_id(self.ap_id.to_owned().into_inner())
-      .set_name(self.name.to_owned())
-      // `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 {
-      set_content_and_source(&mut page, body)?;
-    }
-
-    if let Some(url) = &self.url {
-      page.set_url::<Url>(url.to_owned().into());
-    }
-
-    if let Some(thumbnail_url) = &self.thumbnail_url {
-      let mut image = Image::new();
-      image.set_url::<Url>(thumbnail_url.to_owned().into());
-      page.set_image(image.into_any_base()?);
-    }
-
-    if let Some(u) = self.updated {
-      page.set_updated(convert_datetime(u));
-    }
-
-    let ext = PageExtension {
+    let source = self.body.clone().map(|body| Source {
+      content: body,
+      media_type: MediaTypeMarkdown::Markdown,
+    });
+    let image = self.thumbnail_url.clone().map(|thumb| ImageObject {
+      content: ImageType::Image,
+      url: thumb.into(),
+    });
+
+    let page = Page {
+      context: lemmy_context()?.into(),
+      r#type: PageType::Page,
+      id: self.ap_id.clone().into(),
+      attributed_to: creator.actor_id.into(),
+      to: [community.actor_id.into(), public()],
+      name: self.name.clone(),
+      content: self.body.as_ref().map(|b| markdown_to_html(b)),
+      media_type: MediaTypeHtml::Markdown,
+      source,
+      url: self.url.clone().map(|u| u.into()),
+      image,
       comments_enabled: Some(!self.locked),
       sensitive: Some(self.nsfw),
       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> {
@@ -106,138 +86,133 @@ impl ToApub for Post {
   }
 }
 
-#[async_trait::async_trait(?Send)]
-impl FromApub for Post {
-  type ApubType = PageExt;
+#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
+pub enum MediaTypeMarkdown {
+  #[serde(rename = "text/markdown")]
+  Markdown,
+}
+
+#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
+pub enum MediaTypeHtml {
+  #[serde(rename = "text/html")]
+  Markdown,
+}
 
-  /// Converts a `PageExt` to `PostForm`.
+#[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.
-  async fn from_apub(
-    page: &PageExt,
-    context: &LemmyContext,
-    expected_domain: Url,
-    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,
-    )
+  /// Both stickied and locked need to be false on a newly created post (verified in [[CreatePost]].
+  pub(crate) async fn is_mod_action(&self, pool: &DbPool) -> Result<bool, LemmyError> {
+    let post_id = self.id.clone();
+    let old_post = blocking(pool, move |conn| {
+      Post::read_from_apub_id(conn, &post_id.into())
+    })
     .await?;
-    check_object_for_community_or_site_ban(page, post.community_id, context, request_counter)
-      .await?;
-    Ok(post)
+
+    let is_mod_action = if let Ok(old_post) = old_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)]
-impl FromApubToForm<PageExt> for PostForm {
+impl FromApub for Post {
+  type ApubType = Page;
+
   async fn from_apub(
-    page: &PageExt,
+    page: &Page,
     context: &LemmyContext,
-    expected_domain: Url,
+    _expected_domain: Url,
     request_counter: &mut i32,
-    mod_action_allowed: bool,
-  ) -> Result<PostForm, 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!())?;
-
+    _mod_action_allowed: bool,
+  ) -> Result<Post, LemmyError> {
     let creator =
-      get_or_fetch_and_upsert_person(creator_actor_id, 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());
+      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> = page.image.clone().map(|i| i.url);
     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
       } else {
         (None, None, None, thumbnail_url)
       };
 
-    let name = page
-      .inner
-      .name()
-      // The following is for compatibility with lemmy v0.9.9 and older
-      // 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()),
+    let body_slurs_removed = page.source.as_ref().map(|s| remove_slurs(&s.content));
+    let form = PostForm {
+      name: page.name.clone(),
+      url: page.url.clone().map(|u| u.into()),
       body: body_slurs_removed,
       creator_id: creator.id,
       community_id: community.id,
       removed: None,
-      locked: ext.comments_enabled.map(|e| !e),
-      published: page
-        .inner
-        .published()
-        .as_ref()
-        .map(|u| u.to_owned().naive_local()),
-      updated: page
-        .inner
-        .updated()
-        .as_ref()
-        .map(|u| u.to_owned().naive_local()),
+      locked: page.comments_enabled.map(|e| !e),
+      published: Some(page.published.naive_local()),
+      updated: page.updated.map(|u| u.naive_local()),
       deleted: None,
-      nsfw: ext.sensitive,
-      stickied: ext.stickied,
+      nsfw: page.sensitive,
+      stickied: page.stickied,
       embed_title: iframely_title,
       embed_description: iframely_description,
       embed_html: iframely_html,
       thumbnail_url: pictrs_thumbnail.map(|u| u.into()),
-      ap_id: Some(ap_id),
+      ap_id: Some(page.id.clone().into()),
       local: Some(false),
-    })
+    };
+    Ok(blocking(context.pool(), move |conn| Post::upsert(conn, &form)).await??)
   }
 }
index 66bba9f43de200f50a2bf69e3c1bc56cbf31dcf6..8156457211d136b20ff26c721f2eb715791897a9 100644 (file)
@@ -20,7 +20,7 @@ pub enum PublicUrl {
 pub struct ActivityCommonFields {
   #[serde(rename = "@context")]
   pub context: OneOrMany<AnyBase>,
-  id: Url,
+  pub id: Url,
   pub actor: Url,
 
   // unparsed fields