2 activities::{generate_activity_id, send_activity_to_community},
5 create_apub_tombstone_response,
7 extensions::page_extension::PageExtension,
8 fetcher::{get_or_fetch_and_upsert_community, get_or_fetch_and_upsert_user},
16 use activitystreams::{
18 kind::{CreateType, DeleteType, DislikeType, LikeType, RemoveType, UndoType, UpdateType},
27 object::{kind::PageType, Image, Object, Page, Tombstone},
31 use activitystreams_ext::Ext1;
32 use actix_web::{body::Body, web, HttpResponse};
36 post::{Post, PostForm},
41 use lemmy_structs::blocking;
44 utils::{check_slurs, convert_datetime, remove_slurs},
47 use lemmy_websocket::LemmyContext;
48 use serde::Deserialize;
51 #[derive(Deserialize)]
52 pub struct PostQuery {
56 /// Return the post json over HTTP.
57 pub async fn get_apub_post(
58 info: web::Path<PostQuery>,
59 context: web::Data<LemmyContext>,
60 ) -> Result<HttpResponse<Body>, LemmyError> {
61 let id = info.post_id.parse::<i32>()?;
62 let post = blocking(context.pool(), move |conn| Post::read(conn, id)).await??;
65 Ok(create_apub_response(&post.to_apub(context.pool()).await?))
67 Ok(create_apub_tombstone_response(&post.to_tombstone()?))
71 #[async_trait::async_trait(?Send)]
72 impl ToApub for Post {
73 type Response = PageExt;
75 // Turn a Lemmy post into an ActivityPub page that can be sent out over the network.
76 async fn to_apub(&self, pool: &DbPool) -> Result<PageExt, LemmyError> {
77 let mut page = Page::new();
79 let creator_id = self.creator_id;
80 let creator = blocking(pool, move |conn| User_::read(conn, creator_id)).await??;
82 let community_id = self.community_id;
83 let community = blocking(pool, move |conn| Community::read(conn, community_id)).await??;
86 // Not needed when the Post is embedded in a collection (like for community outbox)
87 // TODO: need to set proper context defining sensitive/commentsEnabled fields
88 // https://git.asonix.dog/Aardwolf/activitystreams/issues/5
89 .set_context(activitystreams::context())
90 .set_id(self.ap_id.parse::<Url>()?)
91 // Use summary field to be consistent with mastodon content warning.
92 // https://mastodon.xyz/@Louisa/103987265222901387.json
93 .set_summary(self.name.to_owned())
94 .set_published(convert_datetime(self.published))
95 .set_to(community.actor_id)
96 .set_attributed_to(creator.actor_id);
98 if let Some(body) = &self.body {
99 page.set_content(body.to_owned());
102 // TODO: hacky code because we get self.url == Some("")
103 // https://github.com/LemmyNet/lemmy/issues/602
104 let url = self.url.as_ref().filter(|u| !u.is_empty());
105 if let Some(u) = url {
106 page.set_url(u.to_owned());
109 let mut page_preview = Page::new();
110 page_preview.set_url(u.to_owned());
112 if let Some(embed_title) = &self.embed_title {
113 page_preview.set_name(embed_title.to_owned());
116 if let Some(embed_description) = &self.embed_description {
117 page_preview.set_summary(embed_description.to_owned());
120 if let Some(embed_html) = &self.embed_html {
121 page_preview.set_content(embed_html.to_owned());
124 page.set_preview(page_preview.into_any_base()?);
127 if let Some(thumbnail_url) = &self.thumbnail_url {
128 let mut image = Image::new();
129 image.set_url(thumbnail_url.to_string());
130 page.set_image(image.into_any_base()?);
133 if let Some(u) = self.updated {
134 page.set_updated(convert_datetime(u));
137 let ext = PageExtension {
138 comments_enabled: !self.locked,
139 sensitive: self.nsfw,
140 stickied: self.stickied,
142 Ok(Ext1::new(page, ext))
145 fn to_tombstone(&self) -> Result<Tombstone, LemmyError> {
146 create_tombstone(self.deleted, &self.ap_id, self.updated, PageType::Page)
151 title: Option<String>,
152 description: Option<String>,
153 html: Option<String>,
156 fn extract_embed_from_apub(
157 page: &Ext1<Object<PageType>, PageExtension>,
158 ) -> Result<EmbedType, LemmyError> {
159 match page.inner.preview() {
161 let preview_page = Page::from_any_base(preview.one().context(location_info!())?.to_owned())?
162 .context(location_info!())?;
163 let title = preview_page
167 .map(|s| s.as_xsd_string())
169 .map(|s| s.to_string());
170 let description = preview_page
172 .map(|s| s.as_single_xsd_string())
174 .map(|s| s.to_string());
175 let html = preview_page
177 .map(|c| c.as_single_xsd_string())
179 .map(|s| s.to_string());
186 None => Ok(EmbedType {
194 #[async_trait::async_trait(?Send)]
195 impl FromApub for PostForm {
196 type ApubType = PageExt;
198 /// Parse an ActivityPub page received from another instance into a Lemmy post.
201 context: &LemmyContext,
202 expected_domain: Option<Url>,
203 ) -> Result<PostForm, LemmyError> {
204 let ext = &page.ext_one;
205 let creator_actor_id = page
209 .context(location_info!())?
210 .as_single_xsd_any_uri()
211 .context(location_info!())?;
213 let creator = get_or_fetch_and_upsert_user(creator_actor_id, context).await?;
215 let community_actor_id = page
219 .context(location_info!())?
220 .as_single_xsd_any_uri()
221 .context(location_info!())?;
223 let community = get_or_fetch_and_upsert_community(community_actor_id, context).await?;
225 let thumbnail_url = match &page.inner.image() {
226 Some(any_image) => Image::from_any_base(
230 .context(location_info!())?
233 .context(location_info!())?
235 .context(location_info!())?
236 .as_single_xsd_any_uri()
237 .map(|u| u.to_string()),
241 let embed = extract_embed_from_apub(page)?;
247 .context(location_info!())?
248 .as_single_xsd_string()
249 .context(location_info!())?
254 .map(|u| u.as_single_xsd_any_uri())
256 .map(|s| s.to_string());
261 .map(|c| c.as_single_xsd_string())
263 .map(|s| s.to_string());
265 let body_slurs_removed = body.map(|b| remove_slurs(&b));
269 body: body_slurs_removed,
270 creator_id: creator.id,
271 community_id: community.id,
273 locked: Some(!ext.comments_enabled),
278 .map(|u| u.to_owned().naive_local()),
283 .map(|u| u.to_owned().naive_local()),
286 stickied: Some(ext.stickied),
287 embed_title: embed.title,
288 embed_description: embed.description,
289 embed_html: embed.html,
291 ap_id: Some(check_actor_domain(page, expected_domain)?),
297 #[async_trait::async_trait(?Send)]
298 impl ApubObjectType for Post {
299 /// Send out information about a newly created post, to the followers of the community.
300 async fn send_create(&self, creator: &User_, context: &LemmyContext) -> Result<(), LemmyError> {
301 let page = self.to_apub(context.pool()).await?;
303 let community_id = self.community_id;
304 let community = blocking(context.pool(), move |conn| {
305 Community::read(conn, community_id)
309 let mut create = Create::new(creator.actor_id.to_owned(), page.into_any_base()?);
311 .set_context(activitystreams::context())
312 .set_id(generate_activity_id(CreateType::Create)?)
314 .set_many_ccs(vec![community.get_followers_url()?]);
316 send_activity_to_community(
319 vec![community.get_shared_inbox_url()?],
327 /// Send out information about an edited post, to the followers of the community.
328 async fn send_update(&self, creator: &User_, context: &LemmyContext) -> Result<(), LemmyError> {
329 let page = self.to_apub(context.pool()).await?;
331 let community_id = self.community_id;
332 let community = blocking(context.pool(), move |conn| {
333 Community::read(conn, community_id)
337 let mut update = Update::new(creator.actor_id.to_owned(), page.into_any_base()?);
339 .set_context(activitystreams::context())
340 .set_id(generate_activity_id(UpdateType::Update)?)
342 .set_many_ccs(vec![community.get_followers_url()?]);
344 send_activity_to_community(
347 vec![community.get_shared_inbox_url()?],
355 async fn send_delete(&self, creator: &User_, context: &LemmyContext) -> Result<(), LemmyError> {
356 let page = self.to_apub(context.pool()).await?;
358 let community_id = self.community_id;
359 let community = blocking(context.pool(), move |conn| {
360 Community::read(conn, community_id)
364 let mut delete = Delete::new(creator.actor_id.to_owned(), page.into_any_base()?);
366 .set_context(activitystreams::context())
367 .set_id(generate_activity_id(DeleteType::Delete)?)
369 .set_many_ccs(vec![community.get_followers_url()?]);
371 send_activity_to_community(
374 vec![community.get_shared_inbox_url()?],
382 async fn send_undo_delete(
385 context: &LemmyContext,
386 ) -> Result<(), LemmyError> {
387 let page = self.to_apub(context.pool()).await?;
389 let community_id = self.community_id;
390 let community = blocking(context.pool(), move |conn| {
391 Community::read(conn, community_id)
395 let mut delete = Delete::new(creator.actor_id.to_owned(), page.into_any_base()?);
397 .set_context(activitystreams::context())
398 .set_id(generate_activity_id(DeleteType::Delete)?)
400 .set_many_ccs(vec![community.get_followers_url()?]);
402 // Undo that fake activity
403 let mut undo = Undo::new(creator.actor_id.to_owned(), delete.into_any_base()?);
405 .set_context(activitystreams::context())
406 .set_id(generate_activity_id(UndoType::Undo)?)
408 .set_many_ccs(vec![community.get_followers_url()?]);
410 send_activity_to_community(
413 vec![community.get_shared_inbox_url()?],
421 async fn send_remove(&self, mod_: &User_, context: &LemmyContext) -> Result<(), LemmyError> {
422 let page = self.to_apub(context.pool()).await?;
424 let community_id = self.community_id;
425 let community = blocking(context.pool(), move |conn| {
426 Community::read(conn, community_id)
430 let mut remove = Remove::new(mod_.actor_id.to_owned(), page.into_any_base()?);
432 .set_context(activitystreams::context())
433 .set_id(generate_activity_id(RemoveType::Remove)?)
435 .set_many_ccs(vec![community.get_followers_url()?]);
437 send_activity_to_community(
440 vec![community.get_shared_inbox_url()?],
448 async fn send_undo_remove(&self, mod_: &User_, context: &LemmyContext) -> Result<(), LemmyError> {
449 let page = self.to_apub(context.pool()).await?;
451 let community_id = self.community_id;
452 let community = blocking(context.pool(), move |conn| {
453 Community::read(conn, community_id)
457 let mut remove = Remove::new(mod_.actor_id.to_owned(), page.into_any_base()?);
459 .set_context(activitystreams::context())
460 .set_id(generate_activity_id(RemoveType::Remove)?)
462 .set_many_ccs(vec![community.get_followers_url()?]);
464 // Undo that fake activity
465 let mut undo = Undo::new(mod_.actor_id.to_owned(), remove.into_any_base()?);
467 .set_context(activitystreams::context())
468 .set_id(generate_activity_id(UndoType::Undo)?)
470 .set_many_ccs(vec![community.get_followers_url()?]);
472 send_activity_to_community(
475 vec![community.get_shared_inbox_url()?],
484 #[async_trait::async_trait(?Send)]
485 impl ApubLikeableType for Post {
486 async fn send_like(&self, creator: &User_, context: &LemmyContext) -> Result<(), LemmyError> {
487 let page = self.to_apub(context.pool()).await?;
489 let community_id = self.community_id;
490 let community = blocking(context.pool(), move |conn| {
491 Community::read(conn, community_id)
495 let mut like = Like::new(creator.actor_id.to_owned(), page.into_any_base()?);
497 .set_context(activitystreams::context())
498 .set_id(generate_activity_id(LikeType::Like)?)
500 .set_many_ccs(vec![community.get_followers_url()?]);
502 send_activity_to_community(
505 vec![community.get_shared_inbox_url()?],
513 async fn send_dislike(&self, creator: &User_, context: &LemmyContext) -> Result<(), LemmyError> {
514 let page = self.to_apub(context.pool()).await?;
516 let community_id = self.community_id;
517 let community = blocking(context.pool(), move |conn| {
518 Community::read(conn, community_id)
522 let mut dislike = Dislike::new(creator.actor_id.to_owned(), page.into_any_base()?);
524 .set_context(activitystreams::context())
525 .set_id(generate_activity_id(DislikeType::Dislike)?)
527 .set_many_ccs(vec![community.get_followers_url()?]);
529 send_activity_to_community(
532 vec![community.get_shared_inbox_url()?],
540 async fn send_undo_like(
543 context: &LemmyContext,
544 ) -> Result<(), LemmyError> {
545 let page = self.to_apub(context.pool()).await?;
547 let community_id = self.community_id;
548 let community = blocking(context.pool(), move |conn| {
549 Community::read(conn, community_id)
553 let mut like = Like::new(creator.actor_id.to_owned(), page.into_any_base()?);
555 .set_context(activitystreams::context())
556 .set_id(generate_activity_id(LikeType::Like)?)
558 .set_many_ccs(vec![community.get_followers_url()?]);
560 // Undo that fake activity
561 let mut undo = Undo::new(creator.actor_id.to_owned(), like.into_any_base()?);
563 .set_context(activitystreams::context())
564 .set_id(generate_activity_id(UndoType::Undo)?)
566 .set_many_ccs(vec![community.get_followers_url()?]);
568 send_activity_to_community(
571 vec![community.get_shared_inbox_url()?],