3 activities::{generate_activity_id, send_activity_to_community},
6 create_apub_tombstone_response,
8 extensions::page_extension::PageExtension,
9 fetcher::{get_or_fetch_and_upsert_community, get_or_fetch_and_upsert_user},
20 use activitystreams::{
22 kind::{CreateType, DeleteType, DislikeType, LikeType, RemoveType, UndoType, UpdateType},
31 object::{kind::PageType, Image, Object, Page, Tombstone},
35 use activitystreams_ext::Ext1;
36 use actix_web::{body::Body, web, HttpResponse};
38 use lemmy_api_structs::blocking;
41 post::{Post, PostForm},
47 utils::{check_slurs, convert_datetime, remove_slurs},
50 use serde::Deserialize;
53 #[derive(Deserialize)]
54 pub struct PostQuery {
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??;
67 Ok(create_apub_response(&post.to_apub(context.pool()).await?))
69 Ok(create_apub_tombstone_response(&post.to_tombstone()?))
73 #[async_trait::async_trait(?Send)]
74 impl ToApub for Post {
75 type Response = PageExt;
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();
81 let creator_id = self.creator_id;
82 let creator = blocking(pool, move |conn| User_::read(conn, creator_id)).await??;
84 let community_id = self.community_id;
85 let community = blocking(pool, move |conn| Community::read(conn, community_id)).await??;
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);
100 if let Some(body) = &self.body {
101 page.set_content(body.to_owned());
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());
111 let mut page_preview = Page::new();
112 page_preview.set_url(u.to_owned());
114 if let Some(embed_title) = &self.embed_title {
115 page_preview.set_name(embed_title.to_owned());
118 if let Some(embed_description) = &self.embed_description {
119 page_preview.set_summary(embed_description.to_owned());
122 if let Some(embed_html) = &self.embed_html {
123 page_preview.set_content(embed_html.to_owned());
126 page.set_preview(page_preview.into_any_base()?);
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()?);
135 if let Some(u) = self.updated {
136 page.set_updated(convert_datetime(u));
139 let ext = PageExtension {
140 comments_enabled: !self.locked,
141 sensitive: self.nsfw,
142 stickied: self.stickied,
144 Ok(Ext1::new(page, ext))
147 fn to_tombstone(&self) -> Result<Tombstone, LemmyError> {
148 create_tombstone(self.deleted, &self.ap_id, self.updated, PageType::Page)
153 title: Option<String>,
154 description: Option<String>,
155 html: Option<String>,
158 fn extract_embed_from_apub(
159 page: &Ext1<Object<PageType>, PageExtension>,
160 ) -> Result<EmbedType, LemmyError> {
161 match page.inner.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
169 .map(|s| s.as_xsd_string())
171 .map(|s| s.to_string());
172 let description = preview_page
174 .map(|s| s.as_single_xsd_string())
176 .map(|s| s.to_string());
177 let html = preview_page
179 .map(|c| c.as_single_xsd_string())
181 .map(|s| s.to_string());
188 None => Ok(EmbedType {
196 #[async_trait::async_trait(?Send)]
197 impl FromApub for PostForm {
198 type ApubType = PageExt;
200 /// Parse an ActivityPub page received from another instance into a Lemmy post.
203 context: &LemmyContext,
204 expected_domain: Option<Url>,
205 ) -> Result<PostForm, LemmyError> {
206 let ext = &page.ext_one;
207 let creator_actor_id = page
211 .context(location_info!())?
212 .as_single_xsd_any_uri()
213 .context(location_info!())?;
215 let creator = get_or_fetch_and_upsert_user(creator_actor_id, context).await?;
217 let community_actor_id = page
221 .context(location_info!())?
222 .as_single_xsd_any_uri()
223 .context(location_info!())?;
225 let community = get_or_fetch_and_upsert_community(community_actor_id, context).await?;
227 let thumbnail_url = match &page.inner.image() {
228 Some(any_image) => Image::from_any_base(
232 .context(location_info!())?
235 .context(location_info!())?
237 .context(location_info!())?
238 .as_single_xsd_any_uri()
239 .map(|u| u.to_string()),
243 let embed = extract_embed_from_apub(page)?;
249 .context(location_info!())?
250 .as_single_xsd_string()
251 .context(location_info!())?
257 .map(|u| u.as_single_xsd_string())
259 .map(|s| s.to_string());
264 .map(|c| c.as_single_xsd_string())
266 .map(|s| s.to_string());
268 let body_slurs_removed = body.map(|b| remove_slurs(&b));
272 body: body_slurs_removed,
273 creator_id: creator.id,
274 community_id: community.id,
276 locked: Some(!ext.comments_enabled),
281 .map(|u| u.to_owned().naive_local()),
286 .map(|u| u.to_owned().naive_local()),
289 stickied: Some(ext.stickied),
290 embed_title: embed.title,
291 embed_description: embed.description,
292 embed_html: embed.html,
294 ap_id: Some(check_actor_domain(page, expected_domain)?),
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?;
306 let community_id = self.community_id;
307 let community = blocking(context.pool(), move |conn| {
308 Community::read(conn, community_id)
312 let mut create = Create::new(creator.actor_id.to_owned(), page.into_any_base()?);
314 .set_context(activitystreams::context())
315 .set_id(generate_activity_id(CreateType::Create)?)
317 .set_many_ccs(vec![community.get_followers_url()?]);
319 send_activity_to_community(
322 vec![community.get_shared_inbox_url()?],
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?;
334 let community_id = self.community_id;
335 let community = blocking(context.pool(), move |conn| {
336 Community::read(conn, community_id)
340 let mut update = Update::new(creator.actor_id.to_owned(), page.into_any_base()?);
342 .set_context(activitystreams::context())
343 .set_id(generate_activity_id(UpdateType::Update)?)
345 .set_many_ccs(vec![community.get_followers_url()?]);
347 send_activity_to_community(
350 vec![community.get_shared_inbox_url()?],
358 async fn send_delete(&self, creator: &User_, context: &LemmyContext) -> Result<(), LemmyError> {
359 let page = self.to_apub(context.pool()).await?;
361 let community_id = self.community_id;
362 let community = blocking(context.pool(), move |conn| {
363 Community::read(conn, community_id)
367 let mut delete = Delete::new(creator.actor_id.to_owned(), page.into_any_base()?);
369 .set_context(activitystreams::context())
370 .set_id(generate_activity_id(DeleteType::Delete)?)
372 .set_many_ccs(vec![community.get_followers_url()?]);
374 send_activity_to_community(
377 vec![community.get_shared_inbox_url()?],
385 async fn send_undo_delete(
388 context: &LemmyContext,
389 ) -> Result<(), LemmyError> {
390 let page = self.to_apub(context.pool()).await?;
392 let community_id = self.community_id;
393 let community = blocking(context.pool(), move |conn| {
394 Community::read(conn, community_id)
398 let mut delete = Delete::new(creator.actor_id.to_owned(), page.into_any_base()?);
400 .set_context(activitystreams::context())
401 .set_id(generate_activity_id(DeleteType::Delete)?)
403 .set_many_ccs(vec![community.get_followers_url()?]);
405 // Undo that fake activity
406 let mut undo = Undo::new(creator.actor_id.to_owned(), delete.into_any_base()?);
408 .set_context(activitystreams::context())
409 .set_id(generate_activity_id(UndoType::Undo)?)
411 .set_many_ccs(vec![community.get_followers_url()?]);
413 send_activity_to_community(
416 vec![community.get_shared_inbox_url()?],
424 async fn send_remove(&self, mod_: &User_, context: &LemmyContext) -> Result<(), LemmyError> {
425 let page = self.to_apub(context.pool()).await?;
427 let community_id = self.community_id;
428 let community = blocking(context.pool(), move |conn| {
429 Community::read(conn, community_id)
433 let mut remove = Remove::new(mod_.actor_id.to_owned(), page.into_any_base()?);
435 .set_context(activitystreams::context())
436 .set_id(generate_activity_id(RemoveType::Remove)?)
438 .set_many_ccs(vec![community.get_followers_url()?]);
440 send_activity_to_community(
443 vec![community.get_shared_inbox_url()?],
451 async fn send_undo_remove(&self, mod_: &User_, context: &LemmyContext) -> Result<(), LemmyError> {
452 let page = self.to_apub(context.pool()).await?;
454 let community_id = self.community_id;
455 let community = blocking(context.pool(), move |conn| {
456 Community::read(conn, community_id)
460 let mut remove = Remove::new(mod_.actor_id.to_owned(), page.into_any_base()?);
462 .set_context(activitystreams::context())
463 .set_id(generate_activity_id(RemoveType::Remove)?)
465 .set_many_ccs(vec![community.get_followers_url()?]);
467 // Undo that fake activity
468 let mut undo = Undo::new(mod_.actor_id.to_owned(), remove.into_any_base()?);
470 .set_context(activitystreams::context())
471 .set_id(generate_activity_id(UndoType::Undo)?)
473 .set_many_ccs(vec![community.get_followers_url()?]);
475 send_activity_to_community(
478 vec![community.get_shared_inbox_url()?],
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?;
492 let community_id = self.community_id;
493 let community = blocking(context.pool(), move |conn| {
494 Community::read(conn, community_id)
498 let mut like = Like::new(creator.actor_id.to_owned(), page.into_any_base()?);
500 .set_context(activitystreams::context())
501 .set_id(generate_activity_id(LikeType::Like)?)
503 .set_many_ccs(vec![community.get_followers_url()?]);
505 send_activity_to_community(
508 vec![community.get_shared_inbox_url()?],
516 async fn send_dislike(&self, creator: &User_, context: &LemmyContext) -> Result<(), LemmyError> {
517 let page = self.to_apub(context.pool()).await?;
519 let community_id = self.community_id;
520 let community = blocking(context.pool(), move |conn| {
521 Community::read(conn, community_id)
525 let mut dislike = Dislike::new(creator.actor_id.to_owned(), page.into_any_base()?);
527 .set_context(activitystreams::context())
528 .set_id(generate_activity_id(DislikeType::Dislike)?)
530 .set_many_ccs(vec![community.get_followers_url()?]);
532 send_activity_to_community(
535 vec![community.get_shared_inbox_url()?],
543 async fn send_undo_like(
546 context: &LemmyContext,
547 ) -> Result<(), LemmyError> {
548 let page = self.to_apub(context.pool()).await?;
550 let community_id = self.community_id;
551 let community = blocking(context.pool(), move |conn| {
552 Community::read(conn, community_id)
556 let mut like = Like::new(creator.actor_id.to_owned(), page.into_any_base()?);
558 .set_context(activitystreams::context())
559 .set_id(generate_activity_id(LikeType::Like)?)
561 .set_many_ccs(vec![community.get_followers_url()?]);
563 // Undo that fake activity
564 let mut undo = Undo::new(creator.actor_id.to_owned(), like.into_any_base()?);
566 .set_context(activitystreams::context())
567 .set_id(generate_activity_id(UndoType::Undo)?)
569 .set_many_ccs(vec![community.get_followers_url()?]);
571 send_activity_to_community(
574 vec![community.get_shared_inbox_url()?],