]> Untitled Git - lemmy.git/blob - lemmy_apub/src/inbox/receive_for_community.rs
Merge remote-tracking branch 'origin/split-db-workspace' into move_views_to_diesel_split
[lemmy.git] / lemmy_apub / src / inbox / receive_for_community.rs
1 use crate::{
2   activities::receive::{
3     comment::{
4       receive_create_comment,
5       receive_delete_comment,
6       receive_dislike_comment,
7       receive_like_comment,
8       receive_remove_comment,
9       receive_update_comment,
10     },
11     comment_undo::{
12       receive_undo_delete_comment,
13       receive_undo_dislike_comment,
14       receive_undo_like_comment,
15       receive_undo_remove_comment,
16     },
17     post::{
18       receive_create_post,
19       receive_delete_post,
20       receive_dislike_post,
21       receive_like_post,
22       receive_remove_post,
23       receive_update_post,
24     },
25     post_undo::{
26       receive_undo_delete_post,
27       receive_undo_dislike_post,
28       receive_undo_like_post,
29       receive_undo_remove_post,
30     },
31     receive_unhandled_activity,
32     verify_activity_domains_valid,
33   },
34   fetcher::{get_or_fetch_and_insert_comment, get_or_fetch_and_insert_post},
35   inbox::is_addressed_to_public,
36 };
37 use activitystreams::{
38   activity::{Create, Delete, Dislike, Like, Remove, Undo, Update},
39   base::AnyBase,
40   prelude::*,
41 };
42 use anyhow::Context;
43 use diesel::result::Error::NotFound;
44 use lemmy_db::{ApubObject, Crud};
45 use lemmy_db_schema::source::{comment::Comment, post::Post, site::Site};
46 use lemmy_structs::blocking;
47 use lemmy_utils::{location_info, LemmyError};
48 use lemmy_websocket::LemmyContext;
49 use url::Url;
50
51 /// This file is for post/comment activities received by the community, and for post/comment
52 ///       activities announced by the community and received by the user.
53
54 /// A post or comment being created
55 pub(in crate::inbox) async fn receive_create_for_community(
56   context: &LemmyContext,
57   activity: AnyBase,
58   expected_domain: &Url,
59   request_counter: &mut i32,
60 ) -> Result<(), LemmyError> {
61   let create = Create::from_any_base(activity)?.context(location_info!())?;
62   verify_activity_domains_valid(&create, &expected_domain, true)?;
63   is_addressed_to_public(&create)?;
64
65   match create.object().as_single_kind_str() {
66     Some("Page") => receive_create_post(create, context, request_counter).await,
67     Some("Note") => receive_create_comment(create, context, request_counter).await,
68     _ => receive_unhandled_activity(create),
69   }
70 }
71
72 /// A post or comment being edited
73 pub(in crate::inbox) async fn receive_update_for_community(
74   context: &LemmyContext,
75   activity: AnyBase,
76   expected_domain: &Url,
77   request_counter: &mut i32,
78 ) -> Result<(), LemmyError> {
79   let update = Update::from_any_base(activity)?.context(location_info!())?;
80   verify_activity_domains_valid(&update, &expected_domain, true)?;
81   is_addressed_to_public(&update)?;
82
83   match update.object().as_single_kind_str() {
84     Some("Page") => receive_update_post(update, context, request_counter).await,
85     Some("Note") => receive_update_comment(update, context, request_counter).await,
86     _ => receive_unhandled_activity(update),
87   }
88 }
89
90 /// A post or comment being upvoted
91 pub(in crate::inbox) async fn receive_like_for_community(
92   context: &LemmyContext,
93   activity: AnyBase,
94   expected_domain: &Url,
95   request_counter: &mut i32,
96 ) -> Result<(), LemmyError> {
97   let like = Like::from_any_base(activity)?.context(location_info!())?;
98   verify_activity_domains_valid(&like, &expected_domain, false)?;
99   is_addressed_to_public(&like)?;
100
101   let object_id = get_like_object_id(&like)?;
102   match fetch_post_or_comment_by_id(&object_id, context, request_counter).await? {
103     PostOrComment::Post(post) => receive_like_post(like, post, context, request_counter).await,
104     PostOrComment::Comment(comment) => {
105       receive_like_comment(like, comment, context, request_counter).await
106     }
107   }
108 }
109
110 /// A post or comment being downvoted
111 pub(in crate::inbox) async fn receive_dislike_for_community(
112   context: &LemmyContext,
113   activity: AnyBase,
114   expected_domain: &Url,
115   request_counter: &mut i32,
116 ) -> Result<(), LemmyError> {
117   let enable_downvotes = blocking(context.pool(), move |conn| {
118     Site::read(conn, 1).map(|s| s.enable_downvotes)
119   })
120   .await??;
121   if !enable_downvotes {
122     return Ok(());
123   }
124
125   let dislike = Dislike::from_any_base(activity)?.context(location_info!())?;
126   verify_activity_domains_valid(&dislike, &expected_domain, false)?;
127   is_addressed_to_public(&dislike)?;
128
129   let object_id = get_like_object_id(&dislike)?;
130   match fetch_post_or_comment_by_id(&object_id, context, request_counter).await? {
131     PostOrComment::Post(post) => {
132       receive_dislike_post(dislike, post, context, request_counter).await
133     }
134     PostOrComment::Comment(comment) => {
135       receive_dislike_comment(dislike, comment, context, request_counter).await
136     }
137   }
138 }
139
140 /// A post or comment being deleted by its creator
141 pub(in crate::inbox) async fn receive_delete_for_community(
142   context: &LemmyContext,
143   activity: AnyBase,
144   expected_domain: &Url,
145 ) -> Result<(), LemmyError> {
146   let delete = Delete::from_any_base(activity)?.context(location_info!())?;
147   verify_activity_domains_valid(&delete, &expected_domain, true)?;
148   is_addressed_to_public(&delete)?;
149
150   let object = delete
151     .object()
152     .to_owned()
153     .single_xsd_any_uri()
154     .context(location_info!())?;
155
156   match find_post_or_comment_by_id(context, object).await {
157     Ok(PostOrComment::Post(p)) => receive_delete_post(context, p).await,
158     Ok(PostOrComment::Comment(c)) => receive_delete_comment(context, c).await,
159     // if we dont have the object, no need to do anything
160     Err(_) => Ok(()),
161   }
162 }
163
164 /// A post or comment being removed by a mod/admin
165 pub(in crate::inbox) async fn receive_remove_for_community(
166   context: &LemmyContext,
167   activity: AnyBase,
168   expected_domain: &Url,
169 ) -> Result<(), LemmyError> {
170   let remove = Remove::from_any_base(activity)?.context(location_info!())?;
171   verify_activity_domains_valid(&remove, &expected_domain, false)?;
172   is_addressed_to_public(&remove)?;
173
174   let cc = remove
175     .cc()
176     .map(|c| c.as_many())
177     .flatten()
178     .context(location_info!())?;
179   let community_id = cc
180     .first()
181     .map(|c| c.as_xsd_any_uri())
182     .flatten()
183     .context(location_info!())?;
184
185   let object = remove
186     .object()
187     .to_owned()
188     .single_xsd_any_uri()
189     .context(location_info!())?;
190
191   // Ensure that remove activity comes from the same domain as the community
192   remove.id(community_id.domain().context(location_info!())?)?;
193
194   match find_post_or_comment_by_id(context, object).await {
195     Ok(PostOrComment::Post(p)) => receive_remove_post(context, remove, p).await,
196     Ok(PostOrComment::Comment(c)) => receive_remove_comment(context, remove, c).await,
197     // if we dont have the object, no need to do anything
198     Err(_) => Ok(()),
199   }
200 }
201
202 /// A post/comment action being reverted (either a delete, remove, upvote or downvote)
203 pub(in crate::inbox) async fn receive_undo_for_community(
204   context: &LemmyContext,
205   activity: AnyBase,
206   expected_domain: &Url,
207   request_counter: &mut i32,
208 ) -> Result<(), LemmyError> {
209   let undo = Undo::from_any_base(activity)?.context(location_info!())?;
210   verify_activity_domains_valid(&undo, &expected_domain.to_owned(), true)?;
211   is_addressed_to_public(&undo)?;
212
213   match undo.object().as_single_kind_str() {
214     Some("Delete") => receive_undo_delete_for_community(context, undo, expected_domain).await,
215     Some("Remove") => receive_undo_remove_for_community(context, undo, expected_domain).await,
216     Some("Like") => {
217       receive_undo_like_for_community(context, undo, expected_domain, request_counter).await
218     }
219     Some("Dislike") => {
220       receive_undo_dislike_for_community(context, undo, expected_domain, request_counter).await
221     }
222     _ => receive_unhandled_activity(undo),
223   }
224 }
225
226 /// A post or comment deletion being reverted
227 pub(in crate::inbox) async fn receive_undo_delete_for_community(
228   context: &LemmyContext,
229   undo: Undo,
230   expected_domain: &Url,
231 ) -> Result<(), LemmyError> {
232   let delete = Delete::from_any_base(undo.object().to_owned().one().context(location_info!())?)?
233     .context(location_info!())?;
234   verify_activity_domains_valid(&delete, &expected_domain, true)?;
235   is_addressed_to_public(&delete)?;
236
237   let object = delete
238     .object()
239     .to_owned()
240     .single_xsd_any_uri()
241     .context(location_info!())?;
242   match find_post_or_comment_by_id(context, object).await {
243     Ok(PostOrComment::Post(p)) => receive_undo_delete_post(context, p).await,
244     Ok(PostOrComment::Comment(c)) => receive_undo_delete_comment(context, c).await,
245     // if we dont have the object, no need to do anything
246     Err(_) => Ok(()),
247   }
248 }
249
250 /// A post or comment removal being reverted
251 pub(in crate::inbox) async fn receive_undo_remove_for_community(
252   context: &LemmyContext,
253   undo: Undo,
254   expected_domain: &Url,
255 ) -> Result<(), LemmyError> {
256   let remove = Remove::from_any_base(undo.object().to_owned().one().context(location_info!())?)?
257     .context(location_info!())?;
258   verify_activity_domains_valid(&remove, &expected_domain, false)?;
259   is_addressed_to_public(&remove)?;
260
261   let object = remove
262     .object()
263     .to_owned()
264     .single_xsd_any_uri()
265     .context(location_info!())?;
266   match find_post_or_comment_by_id(context, object).await {
267     Ok(PostOrComment::Post(p)) => receive_undo_remove_post(context, p).await,
268     Ok(PostOrComment::Comment(c)) => receive_undo_remove_comment(context, c).await,
269     // if we dont have the object, no need to do anything
270     Err(_) => Ok(()),
271   }
272 }
273
274 /// A post or comment upvote being reverted
275 pub(in crate::inbox) async fn receive_undo_like_for_community(
276   context: &LemmyContext,
277   undo: Undo,
278   expected_domain: &Url,
279   request_counter: &mut i32,
280 ) -> Result<(), LemmyError> {
281   let like = Like::from_any_base(undo.object().to_owned().one().context(location_info!())?)?
282     .context(location_info!())?;
283   verify_activity_domains_valid(&like, &expected_domain, false)?;
284   is_addressed_to_public(&like)?;
285
286   let object_id = get_like_object_id(&like)?;
287   match fetch_post_or_comment_by_id(&object_id, context, request_counter).await? {
288     PostOrComment::Post(post) => {
289       receive_undo_like_post(&like, post, context, request_counter).await
290     }
291     PostOrComment::Comment(comment) => {
292       receive_undo_like_comment(&like, comment, context, request_counter).await
293     }
294   }
295 }
296
297 /// A post or comment downvote being reverted
298 pub(in crate::inbox) async fn receive_undo_dislike_for_community(
299   context: &LemmyContext,
300   undo: Undo,
301   expected_domain: &Url,
302   request_counter: &mut i32,
303 ) -> Result<(), LemmyError> {
304   let dislike = Dislike::from_any_base(undo.object().to_owned().one().context(location_info!())?)?
305     .context(location_info!())?;
306   verify_activity_domains_valid(&dislike, &expected_domain, false)?;
307   is_addressed_to_public(&dislike)?;
308
309   let object_id = get_like_object_id(&dislike)?;
310   match fetch_post_or_comment_by_id(&object_id, context, request_counter).await? {
311     PostOrComment::Post(post) => {
312       receive_undo_dislike_post(&dislike, post, context, request_counter).await
313     }
314     PostOrComment::Comment(comment) => {
315       receive_undo_dislike_comment(&dislike, comment, context, request_counter).await
316     }
317   }
318 }
319
320 enum PostOrComment {
321   Comment(Comment),
322   Post(Post),
323 }
324
325 /// Tries to find a post or comment in the local database, without any network requests.
326 /// This is used to handle deletions and removals, because in case we dont have the object, we can
327 /// simply ignore the activity.
328 async fn find_post_or_comment_by_id(
329   context: &LemmyContext,
330   apub_id: Url,
331 ) -> Result<PostOrComment, LemmyError> {
332   let ap_id = apub_id.to_string();
333   let post = blocking(context.pool(), move |conn| {
334     Post::read_from_apub_id(conn, &ap_id)
335   })
336   .await?;
337   if let Ok(p) = post {
338     return Ok(PostOrComment::Post(p));
339   }
340
341   let ap_id = apub_id.to_string();
342   let comment = blocking(context.pool(), move |conn| {
343     Comment::read_from_apub_id(conn, &ap_id)
344   })
345   .await?;
346   if let Ok(c) = comment {
347     return Ok(PostOrComment::Comment(c));
348   }
349
350   Err(NotFound.into())
351 }
352
353 async fn fetch_post_or_comment_by_id(
354   apub_id: &Url,
355   context: &LemmyContext,
356   request_counter: &mut i32,
357 ) -> Result<PostOrComment, LemmyError> {
358   if let Ok(post) = get_or_fetch_and_insert_post(apub_id, context, request_counter).await {
359     return Ok(PostOrComment::Post(post));
360   }
361
362   if let Ok(comment) = get_or_fetch_and_insert_comment(apub_id, context, request_counter).await {
363     return Ok(PostOrComment::Comment(comment));
364   }
365
366   Err(NotFound.into())
367 }
368
369 fn get_like_object_id<Activity>(like_or_dislike: &Activity) -> Result<Url, LemmyError>
370 where
371   Activity: ActorAndObjectRefExt,
372 {
373   // TODO: For backwards compatibility with older Lemmy versions where like.object contains a full
374   //       post/comment. This can be removed after some time, using
375   //       `activity.oject().as_single_xsd_any_uri()` instead.
376   let object = like_or_dislike.object();
377   if let Some(xsd_uri) = object.as_single_xsd_any_uri() {
378     Ok(xsd_uri.to_owned())
379   } else {
380     Ok(
381       object
382         .to_owned()
383         .one()
384         .context(location_info!())?
385         .id()
386         .context(location_info!())?
387         .to_owned(),
388     )
389   }
390 }