]> Untitled Git - lemmy.git/blob - crates/apub/src/objects/post.rs
Simplify config using macros (#1686)
[lemmy.git] / crates / apub / src / objects / post.rs
1 use crate::{
2   activities::{extract_community, verify_person_in_community},
3   extensions::context::lemmy_context,
4   fetcher::person::get_or_fetch_and_upsert_person,
5   objects::{create_tombstone, FromApub, Source, ToApub},
6   ActorType,
7 };
8 use activitystreams::{
9   base::AnyBase,
10   object::{
11     kind::{ImageType, PageType},
12     Tombstone,
13   },
14   primitives::OneOrMany,
15   public,
16   unparsed::Unparsed,
17 };
18 use chrono::{DateTime, FixedOffset};
19 use lemmy_api_common::blocking;
20 use lemmy_apub_lib::{
21   values::{MediaTypeHtml, MediaTypeMarkdown},
22   verify_domains_match,
23 };
24 use lemmy_db_queries::{ApubObject, Crud, DbPool};
25 use lemmy_db_schema::{
26   self,
27   source::{
28     community::Community,
29     person::Person,
30     post::{Post, PostForm},
31   },
32 };
33 use lemmy_utils::{
34   request::fetch_iframely_and_pictrs_data,
35   utils::{check_slurs, convert_datetime, markdown_to_html, remove_slurs},
36   LemmyError,
37 };
38 use lemmy_websocket::LemmyContext;
39 use serde::{Deserialize, Serialize};
40 use url::Url;
41
42 #[derive(Clone, Debug, Deserialize, Serialize)]
43 #[serde(rename_all = "camelCase")]
44 pub struct Page {
45   #[serde(rename = "@context")]
46   context: OneOrMany<AnyBase>,
47   r#type: PageType,
48   pub(crate) id: Url,
49   pub(crate) attributed_to: Url,
50   to: [Url; 2],
51   name: String,
52   content: Option<String>,
53   media_type: MediaTypeHtml,
54   source: Option<Source>,
55   url: Option<Url>,
56   image: Option<ImageObject>,
57   pub(crate) comments_enabled: Option<bool>,
58   sensitive: Option<bool>,
59   pub(crate) stickied: Option<bool>,
60   published: DateTime<FixedOffset>,
61   updated: Option<DateTime<FixedOffset>>,
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     let community = extract_community(&self.to, context, request_counter).await?;
99
100     check_slurs(&self.name)?;
101     verify_domains_match(&self.attributed_to, &self.id)?;
102     verify_person_in_community(
103       &self.attributed_to,
104       &community.actor_id(),
105       context,
106       request_counter,
107     )
108     .await?;
109     Ok(())
110   }
111 }
112
113 #[async_trait::async_trait(?Send)]
114 impl ToApub for Post {
115   type ApubType = Page;
116
117   // Turn a Lemmy post into an ActivityPub page that can be sent out over the network.
118   async fn to_apub(&self, pool: &DbPool) -> Result<Page, LemmyError> {
119     let creator_id = self.creator_id;
120     let creator = blocking(pool, move |conn| Person::read(conn, creator_id)).await??;
121     let community_id = self.community_id;
122     let community = blocking(pool, move |conn| Community::read(conn, community_id)).await??;
123
124     let source = self.body.clone().map(|body| Source {
125       content: body,
126       media_type: MediaTypeMarkdown::Markdown,
127     });
128     let image = self.thumbnail_url.clone().map(|thumb| ImageObject {
129       content: ImageType::Image,
130       url: thumb.into(),
131     });
132
133     let page = Page {
134       context: lemmy_context(),
135       r#type: PageType::Page,
136       id: self.ap_id.clone().into(),
137       attributed_to: creator.actor_id.into(),
138       to: [community.actor_id.into(), public()],
139       name: self.name.clone(),
140       content: self.body.as_ref().map(|b| markdown_to_html(b)),
141       media_type: MediaTypeHtml::Html,
142       source,
143       url: self.url.clone().map(|u| u.into()),
144       image,
145       comments_enabled: Some(!self.locked),
146       sensitive: Some(self.nsfw),
147       stickied: Some(self.stickied),
148       published: convert_datetime(self.published),
149       updated: self.updated.map(convert_datetime),
150       unparsed: Default::default(),
151     };
152     Ok(page)
153   }
154
155   fn to_tombstone(&self) -> Result<Tombstone, LemmyError> {
156     create_tombstone(
157       self.deleted,
158       self.ap_id.to_owned().into(),
159       self.updated,
160       PageType::Page,
161     )
162   }
163 }
164
165 #[async_trait::async_trait(?Send)]
166 impl FromApub for Post {
167   type ApubType = Page;
168
169   async fn from_apub(
170     page: &Page,
171     context: &LemmyContext,
172     _expected_domain: Url,
173     request_counter: &mut i32,
174     _mod_action_allowed: bool,
175   ) -> Result<Post, LemmyError> {
176     let creator =
177       get_or_fetch_and_upsert_person(&page.attributed_to, context, request_counter).await?;
178     let community = extract_community(&page.to, context, request_counter).await?;
179
180     let thumbnail_url: Option<Url> = page.image.clone().map(|i| i.url);
181     let (iframely_response, pictrs_thumbnail) = if let Some(url) = &page.url {
182       fetch_iframely_and_pictrs_data(context.client(), Some(url)).await?
183     } else {
184       (None, thumbnail_url)
185     };
186     let (embed_title, embed_description, embed_html) = iframely_response
187       .map(|u| (u.title, u.description, u.html))
188       .unwrap_or((None, None, None));
189
190     let body_slurs_removed = page.source.as_ref().map(|s| remove_slurs(&s.content));
191     let form = PostForm {
192       name: page.name.clone(),
193       url: page.url.clone().map(|u| u.into()),
194       body: body_slurs_removed,
195       creator_id: creator.id,
196       community_id: community.id,
197       removed: None,
198       locked: page.comments_enabled.map(|e| !e),
199       published: Some(page.published.naive_local()),
200       updated: page.updated.map(|u| u.naive_local()),
201       deleted: None,
202       nsfw: page.sensitive,
203       stickied: page.stickied,
204       embed_title,
205       embed_description,
206       embed_html,
207       thumbnail_url: pictrs_thumbnail.map(|u| u.into()),
208       ap_id: Some(page.id.clone().into()),
209       local: Some(false),
210     };
211     Ok(blocking(context.pool(), move |conn| Post::upsert(conn, &form)).await??)
212   }
213 }