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