]> Untitled Git - lemmy.git/blob - crates/apub/src/objects/post.rs
7c79ff88b8e77f5229db26151d954c375c8c7266
[lemmy.git] / crates / apub / src / objects / post.rs
1 use crate::{
2   activities::extract_community,
3   extensions::context::lemmy_context,
4   fetcher::person::get_or_fetch_and_upsert_person,
5   objects::{create_tombstone, FromApub, MediaTypeHtml, MediaTypeMarkdown, Source, ToApub},
6 };
7 use activitystreams::{
8   base::AnyBase,
9   object::{
10     kind::{ImageType, PageType},
11     Tombstone,
12   },
13   primitives::OneOrMany,
14   public,
15   unparsed::Unparsed,
16 };
17 use chrono::{DateTime, FixedOffset};
18 use lemmy_api_common::blocking;
19 use lemmy_apub_lib::verify_domains_match;
20 use lemmy_db_queries::{ApubObject, Crud, DbPool};
21 use lemmy_db_schema::{
22   self,
23   source::{
24     community::Community,
25     person::Person,
26     post::{Post, PostForm},
27   },
28 };
29 use lemmy_utils::{
30   request::fetch_iframely_and_pictrs_data,
31   utils::{check_slurs, convert_datetime, markdown_to_html, remove_slurs},
32   LemmyError,
33 };
34 use lemmy_websocket::LemmyContext;
35 use url::Url;
36
37 #[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
38 #[serde(rename_all = "camelCase")]
39 pub struct Page {
40   #[serde(rename = "@context")]
41   context: OneOrMany<AnyBase>,
42   r#type: PageType,
43   pub(crate) id: Url,
44   pub(crate) attributed_to: Url,
45   to: [Url; 2],
46   name: String,
47   content: Option<String>,
48   media_type: MediaTypeHtml,
49   source: Option<Source>,
50   url: Option<Url>,
51   image: Option<ImageObject>,
52   pub(crate) comments_enabled: Option<bool>,
53   sensitive: Option<bool>,
54   pub(crate) stickied: Option<bool>,
55   published: DateTime<FixedOffset>,
56   updated: Option<DateTime<FixedOffset>>,
57
58   // unparsed fields
59   #[serde(flatten)]
60   unparsed: Unparsed,
61 }
62
63 #[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
64 #[serde(rename_all = "camelCase")]
65 pub struct ImageObject {
66   content: ImageType,
67   url: Url,
68 }
69
70 impl Page {
71   /// Only mods can change the post's stickied/locked status. So if either of these is changed from
72   /// the current value, it is a mod action and needs to be verified as such.
73   ///
74   /// Both stickied and locked need to be false on a newly created post (verified in [[CreatePost]].
75   pub(crate) async fn is_mod_action(&self, pool: &DbPool) -> Result<bool, LemmyError> {
76     let post_id = self.id.clone();
77     let old_post = blocking(pool, move |conn| {
78       Post::read_from_apub_id(conn, &post_id.into())
79     })
80     .await?;
81
82     let is_mod_action = if let Ok(old_post) = old_post {
83       self.stickied != Some(old_post.stickied) || self.comments_enabled != Some(!old_post.locked)
84     } else {
85       false
86     };
87     Ok(is_mod_action)
88   }
89
90   pub(crate) async fn verify(
91     &self,
92     _context: &LemmyContext,
93     _request_counter: &mut i32,
94   ) -> Result<(), LemmyError> {
95     check_slurs(&self.name)?;
96     verify_domains_match(&self.attributed_to, &self.id)?;
97     Ok(())
98   }
99 }
100
101 #[async_trait::async_trait(?Send)]
102 impl ToApub for Post {
103   type ApubType = Page;
104
105   // Turn a Lemmy post into an ActivityPub page that can be sent out over the network.
106   async fn to_apub(&self, pool: &DbPool) -> Result<Page, LemmyError> {
107     let creator_id = self.creator_id;
108     let creator = blocking(pool, move |conn| Person::read(conn, creator_id)).await??;
109     let community_id = self.community_id;
110     let community = blocking(pool, move |conn| Community::read(conn, community_id)).await??;
111
112     let source = self.body.clone().map(|body| Source {
113       content: body,
114       media_type: MediaTypeMarkdown::Markdown,
115     });
116     let image = self.thumbnail_url.clone().map(|thumb| ImageObject {
117       content: ImageType::Image,
118       url: thumb.into(),
119     });
120
121     let page = Page {
122       context: lemmy_context(),
123       r#type: PageType::Page,
124       id: self.ap_id.clone().into(),
125       attributed_to: creator.actor_id.into(),
126       to: [community.actor_id.into(), public()],
127       name: self.name.clone(),
128       content: self.body.as_ref().map(|b| markdown_to_html(b)),
129       media_type: MediaTypeHtml::Markdown,
130       source,
131       url: self.url.clone().map(|u| u.into()),
132       image,
133       comments_enabled: Some(!self.locked),
134       sensitive: Some(self.nsfw),
135       stickied: Some(self.stickied),
136       published: convert_datetime(self.published),
137       updated: self.updated.map(convert_datetime),
138       unparsed: Default::default(),
139     };
140     Ok(page)
141   }
142
143   fn to_tombstone(&self) -> Result<Tombstone, LemmyError> {
144     create_tombstone(
145       self.deleted,
146       self.ap_id.to_owned().into(),
147       self.updated,
148       PageType::Page,
149     )
150   }
151 }
152
153 #[async_trait::async_trait(?Send)]
154 impl FromApub for Post {
155   type ApubType = Page;
156
157   async fn from_apub(
158     page: &Page,
159     context: &LemmyContext,
160     _expected_domain: Url,
161     request_counter: &mut i32,
162     _mod_action_allowed: bool,
163   ) -> Result<Post, LemmyError> {
164     let creator =
165       get_or_fetch_and_upsert_person(&page.attributed_to, context, request_counter).await?;
166     let community = extract_community(&page.to, context, request_counter).await?;
167
168     let thumbnail_url: Option<Url> = page.image.clone().map(|i| i.url);
169     let (iframely_title, iframely_description, iframely_html, pictrs_thumbnail) =
170       if let Some(url) = &page.url {
171         fetch_iframely_and_pictrs_data(context.client(), Some(url)).await
172       } else {
173         (None, None, None, thumbnail_url)
174       };
175
176     let body_slurs_removed = page.source.as_ref().map(|s| remove_slurs(&s.content));
177     let form = PostForm {
178       name: page.name.clone(),
179       url: page.url.clone().map(|u| u.into()),
180       body: body_slurs_removed,
181       creator_id: creator.id,
182       community_id: community.id,
183       removed: None,
184       locked: page.comments_enabled.map(|e| !e),
185       published: Some(page.published.naive_local()),
186       updated: page.updated.map(|u| u.naive_local()),
187       deleted: None,
188       nsfw: page.sensitive,
189       stickied: page.stickied,
190       embed_title: iframely_title,
191       embed_description: iframely_description,
192       embed_html: iframely_html,
193       thumbnail_url: pictrs_thumbnail.map(|u| u.into()),
194       ap_id: Some(page.id.clone().into()),
195       local: Some(false),
196     };
197     Ok(blocking(context.pool(), move |conn| Post::upsert(conn, &form)).await??)
198   }
199 }