]> Untitled Git - lemmy.git/blob - crates/apub/src/objects/comment.rs
Merge pull request #1850 from LemmyNet/refactor-apub
[lemmy.git] / crates / apub / src / objects / comment.rs
1 use crate::{
2   activities::verify_person_in_community,
3   context::lemmy_context,
4   fetcher::object_id::ObjectId,
5   migrations::CommentInReplyToMigration,
6   objects::{create_tombstone, person::ApubPerson, post::ApubPost, Source},
7   PostOrComment,
8 };
9 use activitystreams::{
10   base::AnyBase,
11   chrono::NaiveDateTime,
12   object::{kind::NoteType, Tombstone},
13   primitives::OneOrMany,
14   unparsed::Unparsed,
15 };
16 use anyhow::{anyhow, Context};
17 use chrono::{DateTime, FixedOffset};
18 use lemmy_api_common::blocking;
19 use lemmy_apub_lib::{
20   traits::{ApubObject, FromApub, ToApub},
21   values::{MediaTypeHtml, MediaTypeMarkdown, PublicUrl},
22   verify::verify_domains_match,
23 };
24 use lemmy_db_schema::{
25   newtypes::CommentId,
26   source::{
27     comment::{Comment, CommentForm},
28     community::Community,
29     person::Person,
30     post::Post,
31   },
32   traits::Crud,
33   DbPool,
34 };
35 use lemmy_utils::{
36   location_info,
37   utils::{convert_datetime, remove_slurs},
38   LemmyError,
39 };
40 use lemmy_websocket::LemmyContext;
41 use serde::{Deserialize, Serialize};
42 use serde_with::skip_serializing_none;
43 use std::ops::Deref;
44 use url::Url;
45
46 #[skip_serializing_none]
47 #[derive(Clone, Debug, Deserialize, Serialize)]
48 #[serde(rename_all = "camelCase")]
49 pub struct Note {
50   #[serde(rename = "@context")]
51   context: OneOrMany<AnyBase>,
52   r#type: NoteType,
53   id: Url,
54   pub(crate) attributed_to: ObjectId<ApubPerson>,
55   /// Indicates that the object is publicly readable. Unlike [`Post.to`], this one doesn't contain
56   /// the community ID, as it would be incompatible with Pleroma (and we can get the community from
57   /// the post in [`in_reply_to`]).
58   to: PublicUrl,
59   content: String,
60   media_type: MediaTypeHtml,
61   source: Source,
62   in_reply_to: CommentInReplyToMigration,
63   published: DateTime<FixedOffset>,
64   updated: Option<DateTime<FixedOffset>>,
65   #[serde(flatten)]
66   unparsed: Unparsed,
67 }
68
69 impl Note {
70   pub(crate) fn id_unchecked(&self) -> &Url {
71     &self.id
72   }
73   pub(crate) fn id(&self, expected_domain: &Url) -> Result<&Url, LemmyError> {
74     verify_domains_match(&self.id, expected_domain)?;
75     Ok(&self.id)
76   }
77
78   pub(crate) async fn get_parents(
79     &self,
80     context: &LemmyContext,
81     request_counter: &mut i32,
82   ) -> Result<(ApubPost, Option<CommentId>), LemmyError> {
83     match &self.in_reply_to {
84       CommentInReplyToMigration::Old(in_reply_to) => {
85         // This post, or the parent comment might not yet exist on this server yet, fetch them.
86         let post_id = in_reply_to.get(0).context(location_info!())?;
87         let post_id = ObjectId::new(post_id.clone());
88         let post = Box::pin(post_id.dereference(context, request_counter)).await?;
89
90         // The 2nd item, if it exists, is the parent comment apub_id
91         // Nested comments will automatically get fetched recursively
92         let parent_id: Option<CommentId> = match in_reply_to.get(1) {
93           Some(comment_id) => {
94             let comment_id = ObjectId::<ApubComment>::new(comment_id.clone());
95             let parent_comment = Box::pin(comment_id.dereference(context, request_counter)).await?;
96
97             Some(parent_comment.id)
98           }
99           None => None,
100         };
101
102         Ok((post, parent_id))
103       }
104       CommentInReplyToMigration::New(in_reply_to) => {
105         let parent = Box::pin(in_reply_to.dereference(context, request_counter).await?);
106         match parent.deref() {
107           PostOrComment::Post(p) => {
108             // Workaround because I cant figure out how to get the post out of the box (and we dont
109             // want to stackoverflow in a deep comment hierarchy).
110             let post_id = p.id;
111             let post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??;
112             Ok((post.into(), None))
113           }
114           PostOrComment::Comment(c) => {
115             let post_id = c.post_id;
116             let post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??;
117             Ok((post.into(), Some(c.id)))
118           }
119         }
120       }
121     }
122   }
123
124   pub(crate) async fn verify(
125     &self,
126     context: &LemmyContext,
127     request_counter: &mut i32,
128   ) -> Result<(), LemmyError> {
129     let (post, _parent_comment_id) = self.get_parents(context, request_counter).await?;
130     let community_id = post.community_id;
131     let community = blocking(context.pool(), move |conn| {
132       Community::read(conn, community_id)
133     })
134     .await??;
135
136     if post.locked {
137       return Err(anyhow!("Post is locked").into());
138     }
139     verify_domains_match(self.attributed_to.inner(), &self.id)?;
140     verify_person_in_community(
141       &self.attributed_to,
142       &ObjectId::new(community.actor_id),
143       context,
144       request_counter,
145     )
146     .await?;
147     Ok(())
148   }
149 }
150
151 #[derive(Clone, Debug)]
152 pub struct ApubComment(Comment);
153
154 impl Deref for ApubComment {
155   type Target = Comment;
156   fn deref(&self) -> &Self::Target {
157     &self.0
158   }
159 }
160
161 impl From<Comment> for ApubComment {
162   fn from(c: Comment) -> Self {
163     ApubComment { 0: c }
164   }
165 }
166
167 #[async_trait::async_trait(?Send)]
168 impl ApubObject for ApubComment {
169   type DataType = LemmyContext;
170
171   fn last_refreshed_at(&self) -> Option<NaiveDateTime> {
172     None
173   }
174
175   async fn read_from_apub_id(
176     object_id: Url,
177     context: &LemmyContext,
178   ) -> Result<Option<Self>, LemmyError> {
179     Ok(
180       blocking(context.pool(), move |conn| {
181         Comment::read_from_apub_id(conn, object_id)
182       })
183       .await??
184       .map(Into::into),
185     )
186   }
187
188   async fn delete(self, context: &LemmyContext) -> Result<(), LemmyError> {
189     blocking(context.pool(), move |conn| {
190       Comment::update_deleted(conn, self.id, true)
191     })
192     .await??;
193     Ok(())
194   }
195 }
196
197 #[async_trait::async_trait(?Send)]
198 impl ToApub for ApubComment {
199   type ApubType = Note;
200   type TombstoneType = Tombstone;
201   type DataType = DbPool;
202
203   async fn to_apub(&self, pool: &DbPool) -> Result<Note, LemmyError> {
204     let creator_id = self.creator_id;
205     let creator = blocking(pool, move |conn| Person::read(conn, creator_id)).await??;
206
207     let post_id = self.post_id;
208     let post = blocking(pool, move |conn| Post::read(conn, post_id)).await??;
209
210     // Add a vector containing some important info to the "in_reply_to" field
211     // [post_ap_id, Option(parent_comment_ap_id)]
212     let mut in_reply_to_vec = vec![post.ap_id.into_inner()];
213
214     if let Some(parent_id) = self.parent_id {
215       let parent_comment = blocking(pool, move |conn| Comment::read(conn, parent_id)).await??;
216
217       in_reply_to_vec.push(parent_comment.ap_id.into_inner());
218     }
219
220     let note = Note {
221       context: lemmy_context(),
222       r#type: NoteType::Note,
223       id: self.ap_id.to_owned().into_inner(),
224       attributed_to: ObjectId::new(creator.actor_id),
225       to: PublicUrl::Public,
226       content: self.content.clone(),
227       media_type: MediaTypeHtml::Html,
228       source: Source {
229         content: self.content.clone(),
230         media_type: MediaTypeMarkdown::Markdown,
231       },
232       in_reply_to: CommentInReplyToMigration::Old(in_reply_to_vec),
233       published: convert_datetime(self.published),
234       updated: self.updated.map(convert_datetime),
235       unparsed: Default::default(),
236     };
237
238     Ok(note)
239   }
240
241   fn to_tombstone(&self) -> Result<Tombstone, LemmyError> {
242     create_tombstone(
243       self.deleted,
244       self.ap_id.to_owned().into(),
245       self.updated,
246       NoteType::Note,
247     )
248   }
249 }
250
251 #[async_trait::async_trait(?Send)]
252 impl FromApub for ApubComment {
253   type ApubType = Note;
254   type DataType = LemmyContext;
255
256   /// Converts a `Note` to `Comment`.
257   ///
258   /// If the parent community, post and comment(s) are not known locally, these are also fetched.
259   async fn from_apub(
260     note: &Note,
261     context: &LemmyContext,
262     expected_domain: &Url,
263     request_counter: &mut i32,
264   ) -> Result<ApubComment, LemmyError> {
265     let ap_id = Some(note.id(expected_domain)?.clone().into());
266     let creator = note
267       .attributed_to
268       .dereference(context, request_counter)
269       .await?;
270     let (post, parent_comment_id) = note.get_parents(context, request_counter).await?;
271     if post.locked {
272       return Err(anyhow!("Post is locked").into());
273     }
274
275     let content = &note.source.content;
276     let content_slurs_removed = remove_slurs(content, &context.settings().slur_regex());
277
278     let form = CommentForm {
279       creator_id: creator.id,
280       post_id: post.id,
281       parent_id: parent_comment_id,
282       content: content_slurs_removed,
283       removed: None,
284       read: None,
285       published: Some(note.published.naive_local()),
286       updated: note.updated.map(|u| u.to_owned().naive_local()),
287       deleted: None,
288       ap_id,
289       local: Some(false),
290     };
291     let comment = blocking(context.pool(), move |conn| Comment::upsert(conn, &form)).await??;
292     Ok(comment.into())
293   }
294 }