]> Untitled Git - lemmy.git/blobdiff - crates/apub/src/protocol/objects/page.rs
Cache & Optimize Woodpecker CI (#3450)
[lemmy.git] / crates / apub / src / protocol / objects / page.rs
index 1ff12bc293df6bf1a6cd32de542624a1511cad30..f3308b0753078a03bec43d314e37887e2d507d1e 100644 (file)
@@ -1,29 +1,28 @@
 use crate::{
   activities::verify_community_matches,
   fetcher::user_or_community::{PersonOrGroupType, UserOrCommunity},
-  local_instance,
   objects::{community::ApubCommunity, person::ApubPerson, post::ApubPost},
   protocol::{objects::LanguageTag, ImageObject, InCommunity, Source},
 };
 use activitypub_federation::{
-  core::object_id::ObjectId,
-  data::Data,
-  deser::{
+  config::Data,
+  fetch::object_id::ObjectId,
+  kinds::{
+    link::LinkType,
+    object::{DocumentType, ImageType},
+  },
+  protocol::{
     helpers::{deserialize_one_or_many, deserialize_skip_error},
     values::MediaTypeMarkdownOrHtml,
   },
-  traits::{ActivityHandler, ApubObject},
-};
-use activitystreams_kinds::{
-  link::LinkType,
-  object::{DocumentType, ImageType},
+  traits::{ActivityHandler, Object},
 };
 use chrono::{DateTime, FixedOffset};
 use itertools::Itertools;
-use lemmy_api_common::LemmyContext;
+use lemmy_api_common::context::LemmyContext;
 use lemmy_db_schema::newtypes::DbUrl;
-use lemmy_utils::error::LemmyError;
-use serde::{Deserialize, Serialize};
+use lemmy_utils::error::{LemmyError, LemmyErrorType};
+use serde::{de::Error, Deserialize, Deserializer, Serialize};
 use serde_with::skip_serializing_none;
 use url::Url;
 
@@ -46,17 +45,17 @@ pub struct Page {
   pub(crate) attributed_to: AttributedTo,
   #[serde(deserialize_with = "deserialize_one_or_many")]
   pub(crate) to: Vec<Url>,
-  pub(crate) name: String,
+  // If there is inReplyTo field this is actually a comment and must not be parsed
+  #[serde(deserialize_with = "deserialize_not_present", default)]
+  pub(crate) in_reply_to: Option<String>,
 
+  pub(crate) name: Option<String>,
   #[serde(deserialize_with = "deserialize_one_or_many", default)]
   pub(crate) cc: Vec<Url>,
   pub(crate) content: Option<String>,
   pub(crate) media_type: Option<MediaTypeMarkdownOrHtml>,
   #[serde(deserialize_with = "deserialize_skip_error", default)]
   pub(crate) source: Option<Source>,
-  /// deprecated, use attachment field
-  #[serde(deserialize_with = "deserialize_skip_error", default)]
-  pub(crate) url: Option<Url>,
   /// most software uses array type for attachment field, so we do the same. nevertheless, we only
   /// use the first item
   #[serde(default)]
@@ -64,7 +63,6 @@ pub struct Page {
   pub(crate) image: Option<ImageObject>,
   pub(crate) comments_enabled: Option<bool>,
   pub(crate) sensitive: Option<bool>,
-  pub(crate) stickied: Option<bool>,
   pub(crate) published: Option<DateTime<FixedOffset>>,
   pub(crate) updated: Option<DateTime<FixedOffset>>,
   pub(crate) language: Option<LanguageTag>,
@@ -131,31 +129,16 @@ pub(crate) struct AttributedToPeertube {
 }
 
 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.
+  /// Only mods can change the post's locked status. So if it is changed from the default value,
+  /// it is a mod action and needs to be verified as such.
   ///
-  /// Both stickied and locked need to be false on a newly created post (verified in [[CreatePost]].
-  pub(crate) async fn is_mod_action(&self, context: &LemmyContext) -> Result<bool, LemmyError> {
-    let old_post = ObjectId::<ApubPost>::new(self.id.clone())
-      .dereference_local(context)
-      .await;
-
-    let stickied_changed = Page::is_stickied_changed(&old_post, &self.stickied);
-    let locked_changed = Page::is_locked_changed(&old_post, &self.comments_enabled);
-    Ok(stickied_changed || locked_changed)
-  }
-
-  pub(crate) fn is_stickied_changed<E>(
-    old_post: &Result<ApubPost, E>,
-    new_stickied: &Option<bool>,
-  ) -> bool {
-    if let Some(new_stickied) = new_stickied {
-      if let Ok(old_post) = old_post {
-        return new_stickied != &old_post.stickied;
-      }
-    }
-
-    false
+  /// Locked needs to be false on a newly created post (verified in [[CreatePost]].
+  pub(crate) async fn is_mod_action(
+    &self,
+    context: &Data<LemmyContext>,
+  ) -> Result<bool, LemmyError> {
+    let old_post = self.id.clone().dereference_local(context).await;
+    Ok(Page::is_locked_changed(&old_post, &self.comments_enabled))
   }
 
   pub(crate) fn is_locked_changed<E>(
@@ -177,8 +160,8 @@ impl Page {
       AttributedTo::Peertube(p) => p
         .iter()
         .find(|a| a.kind == PersonOrGroupType::Person)
-        .map(|a| ObjectId::<ApubPerson>::new(a.id.clone().into_inner()))
-        .ok_or_else(|| LemmyError::from_message("page does not specify creator person")),
+        .map(|a| ObjectId::<ApubPerson>::from(a.id.clone().into_inner()))
+        .ok_or_else(|| LemmyErrorType::PageDoesNotSpecifyCreator.into()),
     }
   }
 }
@@ -193,7 +176,7 @@ impl Attachment {
 }
 
 // Used for community outbox, so that it can be compatible with Pleroma/Mastodon.
-#[async_trait::async_trait(?Send)]
+#[async_trait::async_trait]
 impl ActivityHandler for Page {
   type DataType = LemmyContext;
   type Error = LemmyError;
@@ -203,62 +186,69 @@ impl ActivityHandler for Page {
   fn actor(&self) -> &Url {
     unimplemented!()
   }
-  async fn verify(
-    &self,
-    data: &Data<Self::DataType>,
-    request_counter: &mut i32,
-  ) -> Result<(), LemmyError> {
-    ApubPost::verify(self, self.id.inner(), data, request_counter).await
+  async fn verify(&self, data: &Data<Self::DataType>) -> Result<(), LemmyError> {
+    ApubPost::verify(self, self.id.inner(), data).await
   }
-  async fn receive(
-    self,
-    data: &Data<Self::DataType>,
-    request_counter: &mut i32,
-  ) -> Result<(), LemmyError> {
-    ApubPost::from_apub(self, data, request_counter).await?;
+  async fn receive(self, data: &Data<Self::DataType>) -> Result<(), LemmyError> {
+    ApubPost::from_json(self, data).await?;
     Ok(())
   }
 }
 
-#[async_trait::async_trait(?Send)]
+#[async_trait::async_trait]
 impl InCommunity for Page {
-  async fn community(
-    &self,
-    context: &LemmyContext,
-    request_counter: &mut i32,
-  ) -> Result<ApubCommunity, LemmyError> {
-    let instance = local_instance(context).await;
+  async fn community(&self, context: &Data<LemmyContext>) -> Result<ApubCommunity, LemmyError> {
     let community = match &self.attributed_to {
       AttributedTo::Lemmy(_) => {
         let mut iter = self.to.iter().merge(self.cc.iter());
         loop {
           if let Some(cid) = iter.next() {
-            let cid = ObjectId::new(cid.clone());
-            if let Ok(c) = cid.dereference(context, instance, request_counter).await {
+            let cid = ObjectId::from(cid.clone());
+            if let Ok(c) = cid.dereference(context).await {
               break c;
             }
           } else {
-            return Err(LemmyError::from_message("No community found in cc"));
+            return Err(LemmyErrorType::NoCommunityFoundInCc)?;
           }
         }
       }
       AttributedTo::Peertube(p) => {
         p.iter()
           .find(|a| a.kind == PersonOrGroupType::Group)
-          .map(|a| ObjectId::<ApubCommunity>::new(a.id.clone().into_inner()))
-          .ok_or_else(|| LemmyError::from_message("page does not specify group"))?
-          .dereference(context, instance, request_counter)
+          .map(|a| ObjectId::<ApubCommunity>::from(a.id.clone().into_inner()))
+          .ok_or(LemmyErrorType::PageDoesNotSpecifyGroup)?
+          .dereference(context)
           .await?
       }
     };
     if let Some(audience) = &self.audience {
-      let audience = audience
-        .dereference(context, instance, request_counter)
-        .await?;
-      verify_community_matches(&audience, community.id)?;
-      Ok(audience)
-    } else {
-      Ok(community)
+      verify_community_matches(audience, community.actor_id.clone())?;
     }
+    Ok(community)
+  }
+}
+
+/// Only allows deserialization if the field is missing or null. If it is present, throws an error.
+pub fn deserialize_not_present<'de, D>(deserializer: D) -> Result<Option<String>, D::Error>
+where
+  D: Deserializer<'de>,
+{
+  let result: Option<String> = Deserialize::deserialize(deserializer)?;
+  match result {
+    None => Ok(None),
+    Some(_) => Err(D::Error::custom("Post must not have inReplyTo property")),
+  }
+}
+
+#[cfg(test)]
+mod tests {
+  #![allow(clippy::unwrap_used)]
+  #![allow(clippy::indexing_slicing)]
+
+  use crate::protocol::{objects::page::Page, tests::test_parse_lemmy_item};
+
+  #[test]
+  fn test_not_parsing_note_as_page() {
+    assert!(test_parse_lemmy_item::<Page>("assets/lemmy/objects/note.json").is_err());
   }
 }