]> Untitled Git - lemmy.git/blob - server/src/apub/post.rs
routes.api: fix get_captcha endpoint (#1135)
[lemmy.git] / server / src / apub / post.rs
1 use crate::{
2   apub::{
3     activities::{generate_activity_id, send_activity_to_community},
4     check_actor_domain,
5     create_apub_response,
6     create_apub_tombstone_response,
7     create_tombstone,
8     extensions::page_extension::PageExtension,
9     fetcher::{get_or_fetch_and_upsert_community, get_or_fetch_and_upsert_user},
10     ActorType,
11     ApubLikeableType,
12     ApubObjectType,
13     FromApub,
14     PageExt,
15     ToApub,
16   },
17   DbPool,
18   LemmyContext,
19 };
20 use activitystreams::{
21   activity::{
22     kind::{CreateType, DeleteType, DislikeType, LikeType, RemoveType, UndoType, UpdateType},
23     Create,
24     Delete,
25     Dislike,
26     Like,
27     Remove,
28     Undo,
29     Update,
30   },
31   object::{kind::PageType, Image, Object, Page, Tombstone},
32   prelude::*,
33   public,
34 };
35 use activitystreams_ext::Ext1;
36 use actix_web::{body::Body, web, HttpResponse};
37 use anyhow::Context;
38 use lemmy_api_structs::blocking;
39 use lemmy_db::{
40   community::Community,
41   post::{Post, PostForm},
42   user::User_,
43   Crud,
44 };
45 use lemmy_utils::{
46   location_info,
47   utils::{check_slurs, convert_datetime, remove_slurs},
48   LemmyError,
49 };
50 use serde::Deserialize;
51 use url::Url;
52
53 #[derive(Deserialize)]
54 pub struct PostQuery {
55   post_id: String,
56 }
57
58 /// Return the post json over HTTP.
59 pub async fn get_apub_post(
60   info: web::Path<PostQuery>,
61   context: web::Data<LemmyContext>,
62 ) -> Result<HttpResponse<Body>, LemmyError> {
63   let id = info.post_id.parse::<i32>()?;
64   let post = blocking(context.pool(), move |conn| Post::read(conn, id)).await??;
65
66   if !post.deleted {
67     Ok(create_apub_response(&post.to_apub(context.pool()).await?))
68   } else {
69     Ok(create_apub_tombstone_response(&post.to_tombstone()?))
70   }
71 }
72
73 #[async_trait::async_trait(?Send)]
74 impl ToApub for Post {
75   type Response = PageExt;
76
77   // Turn a Lemmy post into an ActivityPub page that can be sent out over the network.
78   async fn to_apub(&self, pool: &DbPool) -> Result<PageExt, LemmyError> {
79     let mut page = Page::new();
80
81     let creator_id = self.creator_id;
82     let creator = blocking(pool, move |conn| User_::read(conn, creator_id)).await??;
83
84     let community_id = self.community_id;
85     let community = blocking(pool, move |conn| Community::read(conn, community_id)).await??;
86
87     page
88       // Not needed when the Post is embedded in a collection (like for community outbox)
89       // TODO: need to set proper context defining sensitive/commentsEnabled fields
90       // https://git.asonix.dog/Aardwolf/activitystreams/issues/5
91       .set_context(activitystreams::context())
92       .set_id(self.ap_id.parse::<Url>()?)
93       // Use summary field to be consistent with mastodon content warning.
94       // https://mastodon.xyz/@Louisa/103987265222901387.json
95       .set_summary(self.name.to_owned())
96       .set_published(convert_datetime(self.published))
97       .set_to(community.actor_id)
98       .set_attributed_to(creator.actor_id);
99
100     if let Some(body) = &self.body {
101       page.set_content(body.to_owned());
102     }
103
104     // TODO: hacky code because we get self.url == Some("")
105     // https://github.com/LemmyNet/lemmy/issues/602
106     let url = self.url.as_ref().filter(|u| !u.is_empty());
107     if let Some(u) = url {
108       page.set_url(u.to_owned());
109
110       // Embeds
111       let mut page_preview = Page::new();
112       page_preview.set_url(u.to_owned());
113
114       if let Some(embed_title) = &self.embed_title {
115         page_preview.set_name(embed_title.to_owned());
116       }
117
118       if let Some(embed_description) = &self.embed_description {
119         page_preview.set_summary(embed_description.to_owned());
120       }
121
122       if let Some(embed_html) = &self.embed_html {
123         page_preview.set_content(embed_html.to_owned());
124       }
125
126       page.set_preview(page_preview.into_any_base()?);
127     }
128
129     if let Some(thumbnail_url) = &self.thumbnail_url {
130       let mut image = Image::new();
131       image.set_url(thumbnail_url.to_string());
132       page.set_image(image.into_any_base()?);
133     }
134
135     if let Some(u) = self.updated {
136       page.set_updated(convert_datetime(u));
137     }
138
139     let ext = PageExtension {
140       comments_enabled: !self.locked,
141       sensitive: self.nsfw,
142       stickied: self.stickied,
143     };
144     Ok(Ext1::new(page, ext))
145   }
146
147   fn to_tombstone(&self) -> Result<Tombstone, LemmyError> {
148     create_tombstone(self.deleted, &self.ap_id, self.updated, PageType::Page)
149   }
150 }
151
152 struct EmbedType {
153   title: Option<String>,
154   description: Option<String>,
155   html: Option<String>,
156 }
157
158 fn extract_embed_from_apub(
159   page: &Ext1<Object<PageType>, PageExtension>,
160 ) -> Result<EmbedType, LemmyError> {
161   match page.inner.preview() {
162     Some(preview) => {
163       let preview_page = Page::from_any_base(preview.one().context(location_info!())?.to_owned())?
164         .context(location_info!())?;
165       let title = preview_page
166         .name()
167         .map(|n| n.one())
168         .flatten()
169         .map(|s| s.as_xsd_string())
170         .flatten()
171         .map(|s| s.to_string());
172       let description = preview_page
173         .summary()
174         .map(|s| s.as_single_xsd_string())
175         .flatten()
176         .map(|s| s.to_string());
177       let html = preview_page
178         .content()
179         .map(|c| c.as_single_xsd_string())
180         .flatten()
181         .map(|s| s.to_string());
182       Ok(EmbedType {
183         title,
184         description,
185         html,
186       })
187     }
188     None => Ok(EmbedType {
189       title: None,
190       description: None,
191       html: None,
192     }),
193   }
194 }
195
196 #[async_trait::async_trait(?Send)]
197 impl FromApub for PostForm {
198   type ApubType = PageExt;
199
200   /// Parse an ActivityPub page received from another instance into a Lemmy post.
201   async fn from_apub(
202     page: &PageExt,
203     context: &LemmyContext,
204     expected_domain: Option<Url>,
205   ) -> Result<PostForm, LemmyError> {
206     let ext = &page.ext_one;
207     let creator_actor_id = page
208       .inner
209       .attributed_to()
210       .as_ref()
211       .context(location_info!())?
212       .as_single_xsd_any_uri()
213       .context(location_info!())?;
214
215     let creator = get_or_fetch_and_upsert_user(creator_actor_id, context).await?;
216
217     let community_actor_id = page
218       .inner
219       .to()
220       .as_ref()
221       .context(location_info!())?
222       .as_single_xsd_any_uri()
223       .context(location_info!())?;
224
225     let community = get_or_fetch_and_upsert_community(community_actor_id, context).await?;
226
227     let thumbnail_url = match &page.inner.image() {
228       Some(any_image) => Image::from_any_base(
229         any_image
230           .to_owned()
231           .as_one()
232           .context(location_info!())?
233           .to_owned(),
234       )?
235       .context(location_info!())?
236       .url()
237       .context(location_info!())?
238       .as_single_xsd_any_uri()
239       .map(|u| u.to_string()),
240       None => None,
241     };
242
243     let embed = extract_embed_from_apub(page)?;
244
245     let name = page
246       .inner
247       .summary()
248       .as_ref()
249       .context(location_info!())?
250       .as_single_xsd_string()
251       .context(location_info!())?
252       .to_string();
253     let url = page
254       .inner
255       .url()
256       .as_ref()
257       .map(|u| u.as_single_xsd_string())
258       .flatten()
259       .map(|s| s.to_string());
260     let body = page
261       .inner
262       .content()
263       .as_ref()
264       .map(|c| c.as_single_xsd_string())
265       .flatten()
266       .map(|s| s.to_string());
267     check_slurs(&name)?;
268     let body_slurs_removed = body.map(|b| remove_slurs(&b));
269     Ok(PostForm {
270       name,
271       url,
272       body: body_slurs_removed,
273       creator_id: creator.id,
274       community_id: community.id,
275       removed: None,
276       locked: Some(!ext.comments_enabled),
277       published: page
278         .inner
279         .published()
280         .as_ref()
281         .map(|u| u.to_owned().naive_local()),
282       updated: page
283         .inner
284         .updated()
285         .as_ref()
286         .map(|u| u.to_owned().naive_local()),
287       deleted: None,
288       nsfw: ext.sensitive,
289       stickied: Some(ext.stickied),
290       embed_title: embed.title,
291       embed_description: embed.description,
292       embed_html: embed.html,
293       thumbnail_url,
294       ap_id: Some(check_actor_domain(page, expected_domain)?),
295       local: false,
296     })
297   }
298 }
299
300 #[async_trait::async_trait(?Send)]
301 impl ApubObjectType for Post {
302   /// Send out information about a newly created post, to the followers of the community.
303   async fn send_create(&self, creator: &User_, context: &LemmyContext) -> Result<(), LemmyError> {
304     let page = self.to_apub(context.pool()).await?;
305
306     let community_id = self.community_id;
307     let community = blocking(context.pool(), move |conn| {
308       Community::read(conn, community_id)
309     })
310     .await??;
311
312     let mut create = Create::new(creator.actor_id.to_owned(), page.into_any_base()?);
313     create
314       .set_context(activitystreams::context())
315       .set_id(generate_activity_id(CreateType::Create)?)
316       .set_to(public())
317       .set_many_ccs(vec![community.get_followers_url()?]);
318
319     send_activity_to_community(
320       creator,
321       &community,
322       vec![community.get_shared_inbox_url()?],
323       create,
324       context,
325     )
326     .await?;
327     Ok(())
328   }
329
330   /// Send out information about an edited post, to the followers of the community.
331   async fn send_update(&self, creator: &User_, context: &LemmyContext) -> Result<(), LemmyError> {
332     let page = self.to_apub(context.pool()).await?;
333
334     let community_id = self.community_id;
335     let community = blocking(context.pool(), move |conn| {
336       Community::read(conn, community_id)
337     })
338     .await??;
339
340     let mut update = Update::new(creator.actor_id.to_owned(), page.into_any_base()?);
341     update
342       .set_context(activitystreams::context())
343       .set_id(generate_activity_id(UpdateType::Update)?)
344       .set_to(public())
345       .set_many_ccs(vec![community.get_followers_url()?]);
346
347     send_activity_to_community(
348       creator,
349       &community,
350       vec![community.get_shared_inbox_url()?],
351       update,
352       context,
353     )
354     .await?;
355     Ok(())
356   }
357
358   async fn send_delete(&self, creator: &User_, context: &LemmyContext) -> Result<(), LemmyError> {
359     let page = self.to_apub(context.pool()).await?;
360
361     let community_id = self.community_id;
362     let community = blocking(context.pool(), move |conn| {
363       Community::read(conn, community_id)
364     })
365     .await??;
366
367     let mut delete = Delete::new(creator.actor_id.to_owned(), page.into_any_base()?);
368     delete
369       .set_context(activitystreams::context())
370       .set_id(generate_activity_id(DeleteType::Delete)?)
371       .set_to(public())
372       .set_many_ccs(vec![community.get_followers_url()?]);
373
374     send_activity_to_community(
375       creator,
376       &community,
377       vec![community.get_shared_inbox_url()?],
378       delete,
379       context,
380     )
381     .await?;
382     Ok(())
383   }
384
385   async fn send_undo_delete(
386     &self,
387     creator: &User_,
388     context: &LemmyContext,
389   ) -> Result<(), LemmyError> {
390     let page = self.to_apub(context.pool()).await?;
391
392     let community_id = self.community_id;
393     let community = blocking(context.pool(), move |conn| {
394       Community::read(conn, community_id)
395     })
396     .await??;
397
398     let mut delete = Delete::new(creator.actor_id.to_owned(), page.into_any_base()?);
399     delete
400       .set_context(activitystreams::context())
401       .set_id(generate_activity_id(DeleteType::Delete)?)
402       .set_to(public())
403       .set_many_ccs(vec![community.get_followers_url()?]);
404
405     // Undo that fake activity
406     let mut undo = Undo::new(creator.actor_id.to_owned(), delete.into_any_base()?);
407     undo
408       .set_context(activitystreams::context())
409       .set_id(generate_activity_id(UndoType::Undo)?)
410       .set_to(public())
411       .set_many_ccs(vec![community.get_followers_url()?]);
412
413     send_activity_to_community(
414       creator,
415       &community,
416       vec![community.get_shared_inbox_url()?],
417       undo,
418       context,
419     )
420     .await?;
421     Ok(())
422   }
423
424   async fn send_remove(&self, mod_: &User_, context: &LemmyContext) -> Result<(), LemmyError> {
425     let page = self.to_apub(context.pool()).await?;
426
427     let community_id = self.community_id;
428     let community = blocking(context.pool(), move |conn| {
429       Community::read(conn, community_id)
430     })
431     .await??;
432
433     let mut remove = Remove::new(mod_.actor_id.to_owned(), page.into_any_base()?);
434     remove
435       .set_context(activitystreams::context())
436       .set_id(generate_activity_id(RemoveType::Remove)?)
437       .set_to(public())
438       .set_many_ccs(vec![community.get_followers_url()?]);
439
440     send_activity_to_community(
441       mod_,
442       &community,
443       vec![community.get_shared_inbox_url()?],
444       remove,
445       context,
446     )
447     .await?;
448     Ok(())
449   }
450
451   async fn send_undo_remove(&self, mod_: &User_, context: &LemmyContext) -> Result<(), LemmyError> {
452     let page = self.to_apub(context.pool()).await?;
453
454     let community_id = self.community_id;
455     let community = blocking(context.pool(), move |conn| {
456       Community::read(conn, community_id)
457     })
458     .await??;
459
460     let mut remove = Remove::new(mod_.actor_id.to_owned(), page.into_any_base()?);
461     remove
462       .set_context(activitystreams::context())
463       .set_id(generate_activity_id(RemoveType::Remove)?)
464       .set_to(public())
465       .set_many_ccs(vec![community.get_followers_url()?]);
466
467     // Undo that fake activity
468     let mut undo = Undo::new(mod_.actor_id.to_owned(), remove.into_any_base()?);
469     undo
470       .set_context(activitystreams::context())
471       .set_id(generate_activity_id(UndoType::Undo)?)
472       .set_to(public())
473       .set_many_ccs(vec![community.get_followers_url()?]);
474
475     send_activity_to_community(
476       mod_,
477       &community,
478       vec![community.get_shared_inbox_url()?],
479       undo,
480       context,
481     )
482     .await?;
483     Ok(())
484   }
485 }
486
487 #[async_trait::async_trait(?Send)]
488 impl ApubLikeableType for Post {
489   async fn send_like(&self, creator: &User_, context: &LemmyContext) -> Result<(), LemmyError> {
490     let page = self.to_apub(context.pool()).await?;
491
492     let community_id = self.community_id;
493     let community = blocking(context.pool(), move |conn| {
494       Community::read(conn, community_id)
495     })
496     .await??;
497
498     let mut like = Like::new(creator.actor_id.to_owned(), page.into_any_base()?);
499     like
500       .set_context(activitystreams::context())
501       .set_id(generate_activity_id(LikeType::Like)?)
502       .set_to(public())
503       .set_many_ccs(vec![community.get_followers_url()?]);
504
505     send_activity_to_community(
506       &creator,
507       &community,
508       vec![community.get_shared_inbox_url()?],
509       like,
510       context,
511     )
512     .await?;
513     Ok(())
514   }
515
516   async fn send_dislike(&self, creator: &User_, context: &LemmyContext) -> Result<(), LemmyError> {
517     let page = self.to_apub(context.pool()).await?;
518
519     let community_id = self.community_id;
520     let community = blocking(context.pool(), move |conn| {
521       Community::read(conn, community_id)
522     })
523     .await??;
524
525     let mut dislike = Dislike::new(creator.actor_id.to_owned(), page.into_any_base()?);
526     dislike
527       .set_context(activitystreams::context())
528       .set_id(generate_activity_id(DislikeType::Dislike)?)
529       .set_to(public())
530       .set_many_ccs(vec![community.get_followers_url()?]);
531
532     send_activity_to_community(
533       &creator,
534       &community,
535       vec![community.get_shared_inbox_url()?],
536       dislike,
537       context,
538     )
539     .await?;
540     Ok(())
541   }
542
543   async fn send_undo_like(
544     &self,
545     creator: &User_,
546     context: &LemmyContext,
547   ) -> Result<(), LemmyError> {
548     let page = self.to_apub(context.pool()).await?;
549
550     let community_id = self.community_id;
551     let community = blocking(context.pool(), move |conn| {
552       Community::read(conn, community_id)
553     })
554     .await??;
555
556     let mut like = Like::new(creator.actor_id.to_owned(), page.into_any_base()?);
557     like
558       .set_context(activitystreams::context())
559       .set_id(generate_activity_id(LikeType::Like)?)
560       .set_to(public())
561       .set_many_ccs(vec![community.get_followers_url()?]);
562
563     // Undo that fake activity
564     let mut undo = Undo::new(creator.actor_id.to_owned(), like.into_any_base()?);
565     undo
566       .set_context(activitystreams::context())
567       .set_id(generate_activity_id(UndoType::Undo)?)
568       .set_to(public())
569       .set_many_ccs(vec![community.get_followers_url()?]);
570
571     send_activity_to_community(
572       &creator,
573       &community,
574       vec![community.get_shared_inbox_url()?],
575       undo,
576       context,
577     )
578     .await?;
579     Ok(())
580   }
581 }