]> 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 5ffb4869b5ab8900852c89fc0d25590699e89c72..f3308b0753078a03bec43d314e37887e2d507d1e 100644 (file)
@@ -1,25 +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, Source},
+  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},
+  traits::{ActivityHandler, Object},
 };
-use activitystreams_kinds::{link::LinkType, object::ImageType};
 use chrono::{DateTime, FixedOffset};
 use itertools::Itertools;
+use lemmy_api_common::context::LemmyContext;
 use lemmy_db_schema::newtypes::DbUrl;
-use lemmy_utils::error::LemmyError;
-use lemmy_websocket::LemmyContext;
-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;
 
@@ -29,6 +32,7 @@ pub enum PageType {
   Article,
   Note,
   Video,
+  Event,
 }
 
 #[skip_serializing_none]
@@ -41,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)]
@@ -59,10 +63,10 @@ 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>,
+  pub(crate) audience: Option<ObjectId<ApubCommunity>>,
 }
 
 #[derive(Clone, Debug, Deserialize, Serialize)]
@@ -80,11 +84,33 @@ pub(crate) struct Image {
   pub(crate) url: Url,
 }
 
+#[derive(Clone, Debug, Deserialize, Serialize)]
+#[serde(rename_all = "camelCase")]
+pub(crate) struct Document {
+  #[serde(rename = "type")]
+  pub(crate) kind: DocumentType,
+  pub(crate) url: Url,
+}
+
 #[derive(Clone, Debug, Deserialize, Serialize)]
 #[serde(untagged)]
 pub(crate) enum Attachment {
   Link(Link),
   Image(Image),
+  Document(Document),
+}
+
+impl Attachment {
+  pub(crate) fn url(self) -> Url {
+    match self {
+      // url as sent by Lemmy (new)
+      Attachment::Link(l) => l.href,
+      // image sent by lotide
+      Attachment::Image(i) => i.url,
+      // sent by mobilizon
+      Attachment::Document(d) => d.url,
+    }
+  }
 }
 
 #[derive(Clone, Debug, Deserialize, Serialize)]
@@ -103,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>(
@@ -143,47 +154,14 @@ impl Page {
     false
   }
 
-  pub(crate) async fn extract_community(
-    &self,
-    context: &LemmyContext,
-    request_counter: &mut i32,
-  ) -> Result<ApubCommunity, LemmyError> {
-    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, local_instance(context).await, request_counter)
-              .await
-            {
-              break Ok(c);
-            }
-          } else {
-            return Err(LemmyError::from_message("No community found in cc"));
-          }
-        }
-      }
-      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, local_instance(context).await, request_counter)
-          .await
-      }
-    }
-  }
-
   pub(crate) fn creator(&self) -> Result<ObjectId<ApubPerson>, LemmyError> {
     match &self.attributed_to {
       AttributedTo::Lemmy(l) => Ok(l.clone()),
       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()),
     }
   }
 }
@@ -198,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;
@@ -208,19 +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]
+impl InCommunity for Page {
+  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::from(cid.clone());
+            if let Ok(c) = cid.dereference(context).await {
+              break c;
+            }
+          } else {
+            return Err(LemmyErrorType::NoCommunityFoundInCc)?;
+          }
+        }
+      }
+      AttributedTo::Peertube(p) => {
+        p.iter()
+          .find(|a| a.kind == PersonOrGroupType::Group)
+          .map(|a| ObjectId::<ApubCommunity>::from(a.id.clone().into_inner()))
+          .ok_or(LemmyErrorType::PageDoesNotSpecifyGroup)?
+          .dereference(context)
+          .await?
+      }
+    };
+    if let Some(audience) = &self.audience {
+      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());
+  }
+}