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};
40 post::{Post, PostForm},
44 use lemmy_structs::blocking;
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!())?
256 .map(|u| u.as_single_xsd_any_uri())
258 .map(|s| s.to_string());
263 .map(|c| c.as_single_xsd_string())
265 .map(|s| s.to_string());
267 let body_slurs_removed = body.map(|b| remove_slurs(&b));
271 body: body_slurs_removed,
272 creator_id: creator.id,
273 community_id: community.id,
275 locked: Some(!ext.comments_enabled),
280 .map(|u| u.to_owned().naive_local()),
285 .map(|u| u.to_owned().naive_local()),
288 stickied: Some(ext.stickied),
289 embed_title: embed.title,
290 embed_description: embed.description,
291 embed_html: embed.html,
293 ap_id: Some(check_actor_domain(page, expected_domain)?),
299 #[async_trait::async_trait(?Send)]
300 impl ApubObjectType for Post {
301 /// Send out information about a newly created post, to the followers of the community.
302 async fn send_create(&self, creator: &User_, context: &LemmyContext) -> Result<(), LemmyError> {
303 let page = self.to_apub(context.pool()).await?;
305 let community_id = self.community_id;
306 let community = blocking(context.pool(), move |conn| {
307 Community::read(conn, community_id)
311 let mut create = Create::new(creator.actor_id.to_owned(), page.into_any_base()?);
313 .set_context(activitystreams::context())
314 .set_id(generate_activity_id(CreateType::Create)?)
316 .set_many_ccs(vec![community.get_followers_url()?]);
318 send_activity_to_community(
321 vec![community.get_shared_inbox_url()?],
329 /// Send out information about an edited post, to the followers of the community.
330 async fn send_update(&self, creator: &User_, context: &LemmyContext) -> Result<(), LemmyError> {
331 let page = self.to_apub(context.pool()).await?;
333 let community_id = self.community_id;
334 let community = blocking(context.pool(), move |conn| {
335 Community::read(conn, community_id)
339 let mut update = Update::new(creator.actor_id.to_owned(), page.into_any_base()?);
341 .set_context(activitystreams::context())
342 .set_id(generate_activity_id(UpdateType::Update)?)
344 .set_many_ccs(vec![community.get_followers_url()?]);
346 send_activity_to_community(
349 vec![community.get_shared_inbox_url()?],
357 async fn send_delete(&self, creator: &User_, context: &LemmyContext) -> Result<(), LemmyError> {
358 let page = self.to_apub(context.pool()).await?;
360 let community_id = self.community_id;
361 let community = blocking(context.pool(), move |conn| {
362 Community::read(conn, community_id)
366 let mut delete = Delete::new(creator.actor_id.to_owned(), page.into_any_base()?);
368 .set_context(activitystreams::context())
369 .set_id(generate_activity_id(DeleteType::Delete)?)
371 .set_many_ccs(vec![community.get_followers_url()?]);
373 send_activity_to_community(
376 vec![community.get_shared_inbox_url()?],
384 async fn send_undo_delete(
387 context: &LemmyContext,
388 ) -> Result<(), LemmyError> {
389 let page = self.to_apub(context.pool()).await?;
391 let community_id = self.community_id;
392 let community = blocking(context.pool(), move |conn| {
393 Community::read(conn, community_id)
397 let mut delete = Delete::new(creator.actor_id.to_owned(), page.into_any_base()?);
399 .set_context(activitystreams::context())
400 .set_id(generate_activity_id(DeleteType::Delete)?)
402 .set_many_ccs(vec![community.get_followers_url()?]);
404 // Undo that fake activity
405 let mut undo = Undo::new(creator.actor_id.to_owned(), delete.into_any_base()?);
407 .set_context(activitystreams::context())
408 .set_id(generate_activity_id(UndoType::Undo)?)
410 .set_many_ccs(vec![community.get_followers_url()?]);
412 send_activity_to_community(
415 vec![community.get_shared_inbox_url()?],
423 async fn send_remove(&self, mod_: &User_, context: &LemmyContext) -> Result<(), LemmyError> {
424 let page = self.to_apub(context.pool()).await?;
426 let community_id = self.community_id;
427 let community = blocking(context.pool(), move |conn| {
428 Community::read(conn, community_id)
432 let mut remove = Remove::new(mod_.actor_id.to_owned(), page.into_any_base()?);
434 .set_context(activitystreams::context())
435 .set_id(generate_activity_id(RemoveType::Remove)?)
437 .set_many_ccs(vec![community.get_followers_url()?]);
439 send_activity_to_community(
442 vec![community.get_shared_inbox_url()?],
450 async fn send_undo_remove(&self, mod_: &User_, context: &LemmyContext) -> Result<(), LemmyError> {
451 let page = self.to_apub(context.pool()).await?;
453 let community_id = self.community_id;
454 let community = blocking(context.pool(), move |conn| {
455 Community::read(conn, community_id)
459 let mut remove = Remove::new(mod_.actor_id.to_owned(), page.into_any_base()?);
461 .set_context(activitystreams::context())
462 .set_id(generate_activity_id(RemoveType::Remove)?)
464 .set_many_ccs(vec![community.get_followers_url()?]);
466 // Undo that fake activity
467 let mut undo = Undo::new(mod_.actor_id.to_owned(), remove.into_any_base()?);
469 .set_context(activitystreams::context())
470 .set_id(generate_activity_id(UndoType::Undo)?)
472 .set_many_ccs(vec![community.get_followers_url()?]);
474 send_activity_to_community(
477 vec![community.get_shared_inbox_url()?],
486 #[async_trait::async_trait(?Send)]
487 impl ApubLikeableType for Post {
488 async fn send_like(&self, creator: &User_, context: &LemmyContext) -> Result<(), LemmyError> {
489 let page = self.to_apub(context.pool()).await?;
491 let community_id = self.community_id;
492 let community = blocking(context.pool(), move |conn| {
493 Community::read(conn, community_id)
497 let mut like = Like::new(creator.actor_id.to_owned(), page.into_any_base()?);
499 .set_context(activitystreams::context())
500 .set_id(generate_activity_id(LikeType::Like)?)
502 .set_many_ccs(vec![community.get_followers_url()?]);
504 send_activity_to_community(
507 vec![community.get_shared_inbox_url()?],
515 async fn send_dislike(&self, creator: &User_, context: &LemmyContext) -> Result<(), LemmyError> {
516 let page = self.to_apub(context.pool()).await?;
518 let community_id = self.community_id;
519 let community = blocking(context.pool(), move |conn| {
520 Community::read(conn, community_id)
524 let mut dislike = Dislike::new(creator.actor_id.to_owned(), page.into_any_base()?);
526 .set_context(activitystreams::context())
527 .set_id(generate_activity_id(DislikeType::Dislike)?)
529 .set_many_ccs(vec![community.get_followers_url()?]);
531 send_activity_to_community(
534 vec![community.get_shared_inbox_url()?],
542 async fn send_undo_like(
545 context: &LemmyContext,
546 ) -> Result<(), LemmyError> {
547 let page = self.to_apub(context.pool()).await?;
549 let community_id = self.community_id;
550 let community = blocking(context.pool(), move |conn| {
551 Community::read(conn, community_id)
555 let mut like = Like::new(creator.actor_id.to_owned(), page.into_any_base()?);
557 .set_context(activitystreams::context())
558 .set_id(generate_activity_id(LikeType::Like)?)
560 .set_many_ccs(vec![community.get_followers_url()?]);
562 // Undo that fake activity
563 let mut undo = Undo::new(creator.actor_id.to_owned(), like.into_any_base()?);
565 .set_context(activitystreams::context())
566 .set_id(generate_activity_id(UndoType::Undo)?)
568 .set_many_ccs(vec![community.get_followers_url()?]);
570 send_activity_to_community(
573 vec![community.get_shared_inbox_url()?],