lemmy/server/src/apub/post.rs
nutomic d4dccd17ae implement ActivitySender actor (#89)
Merge pull request 'Adding unique ap_ids. Fixes #1100' (#90) from unique_ap_ids into activity-sender

Reviewed-on: https://yerbamate.dev/LemmyNet/lemmy/pulls/90

Adding back in on_conflict.

Trying to add back in the on_conflict_do_nothing.

Trying to reduce delay time.

Removing createFakes.

Removing some unit tests.

Adding comment jest timeout.

Fixing tests again.

Fixing tests again.

Merge branch 'activity-sender' into unique_ap_ids_2

Replace actix client with reqwest to speed up federation tests

Trying to fix tests again.

Fixing unit tests.

Fixing some broken unit tests, not done yet.

Adding uniques.

Adding unique ap_ids. Fixes #1100

use proper sql functionality for upsert

added logging

in fetcher, replace post/comment::create with upsert

no need to do an actual update in post/comment::upsert

Merge branch 'main' into activity-sender

implement upsert for user/community

reuse http client

got it working

attempt to use background-jobs crate

rewrite with proper error handling and less boilerplate

remove do_send, dont return errors from activity_sender

WIP: implement ActivitySender actor

Co-authored-by: dessalines <dessalines@noreply.yerbamate.dev>
Co-authored-by: Dessalines <tyhou13@gmx.com>
Co-authored-by: Felix Ableitner <me@nutomic.com>
Reviewed-on: https://yerbamate.dev/LemmyNet/lemmy/pulls/89
2020-08-31 13:48:02 +00:00

580 lines
16 KiB
Rust

use crate::{
api::check_slurs,
apub::{
activities::{generate_activity_id, send_activity_to_community},
check_actor_domain,
create_apub_response,
create_apub_tombstone_response,
create_tombstone,
extensions::page_extension::PageExtension,
fetcher::{get_or_fetch_and_upsert_community, get_or_fetch_and_upsert_user},
ActorType,
ApubLikeableType,
ApubObjectType,
FromApub,
PageExt,
ToApub,
},
blocking,
DbPool,
LemmyContext,
LemmyError,
};
use activitystreams::{
activity::{
kind::{CreateType, DeleteType, DislikeType, LikeType, RemoveType, UndoType, UpdateType},
Create,
Delete,
Dislike,
Like,
Remove,
Undo,
Update,
},
object::{kind::PageType, Image, Object, Page, Tombstone},
prelude::*,
public,
};
use activitystreams_ext::Ext1;
use actix_web::{body::Body, web, HttpResponse};
use anyhow::Context;
use lemmy_db::{
community::Community,
post::{Post, PostForm},
user::User_,
Crud,
};
use lemmy_utils::{convert_datetime, location_info, remove_slurs};
use serde::Deserialize;
use url::Url;
#[derive(Deserialize)]
pub struct PostQuery {
post_id: String,
}
/// Return the post json over HTTP.
pub async fn get_apub_post(
info: web::Path<PostQuery>,
context: web::Data<LemmyContext>,
) -> Result<HttpResponse<Body>, LemmyError> {
let id = info.post_id.parse::<i32>()?;
let post = blocking(context.pool(), move |conn| Post::read(conn, id)).await??;
if !post.deleted {
Ok(create_apub_response(&post.to_apub(context.pool()).await?))
} else {
Ok(create_apub_tombstone_response(&post.to_tombstone()?))
}
}
#[async_trait::async_trait(?Send)]
impl ToApub for Post {
type Response = PageExt;
// Turn a Lemmy post into an ActivityPub page that can be sent out over the network.
async fn to_apub(&self, pool: &DbPool) -> Result<PageExt, LemmyError> {
let mut page = Page::new();
let creator_id = self.creator_id;
let creator = blocking(pool, move |conn| User_::read(conn, creator_id)).await??;
let community_id = self.community_id;
let community = blocking(pool, move |conn| Community::read(conn, community_id)).await??;
page
// Not needed when the Post is embedded in a collection (like for community outbox)
// TODO: need to set proper context defining sensitive/commentsEnabled fields
// https://git.asonix.dog/Aardwolf/activitystreams/issues/5
.set_context(activitystreams::context())
.set_id(self.ap_id.parse::<Url>()?)
// Use summary field to be consistent with mastodon content warning.
// https://mastodon.xyz/@Louisa/103987265222901387.json
.set_summary(self.name.to_owned())
.set_published(convert_datetime(self.published))
.set_to(community.actor_id)
.set_attributed_to(creator.actor_id);
if let Some(body) = &self.body {
page.set_content(body.to_owned());
}
// TODO: hacky code because we get self.url == Some("")
// https://github.com/LemmyNet/lemmy/issues/602
let url = self.url.as_ref().filter(|u| !u.is_empty());
if let Some(u) = url {
page.set_url(u.to_owned());
// Embeds
let mut page_preview = Page::new();
page_preview.set_url(u.to_owned());
if let Some(embed_title) = &self.embed_title {
page_preview.set_name(embed_title.to_owned());
}
if let Some(embed_description) = &self.embed_description {
page_preview.set_summary(embed_description.to_owned());
}
if let Some(embed_html) = &self.embed_html {
page_preview.set_content(embed_html.to_owned());
}
page.set_preview(page_preview.into_any_base()?);
}
if let Some(thumbnail_url) = &self.thumbnail_url {
let mut image = Image::new();
image.set_url(thumbnail_url.to_string());
page.set_image(image.into_any_base()?);
}
if let Some(u) = self.updated {
page.set_updated(convert_datetime(u));
}
let ext = PageExtension {
comments_enabled: !self.locked,
sensitive: self.nsfw,
stickied: self.stickied,
};
Ok(Ext1::new(page, ext))
}
fn to_tombstone(&self) -> Result<Tombstone, LemmyError> {
create_tombstone(self.deleted, &self.ap_id, self.updated, PageType::Page)
}
}
struct EmbedType {
title: Option<String>,
description: Option<String>,
html: Option<String>,
}
fn extract_embed_from_apub(
page: &Ext1<Object<PageType>, PageExtension>,
) -> Result<EmbedType, LemmyError> {
match page.inner.preview() {
Some(preview) => {
let preview_page = Page::from_any_base(preview.one().context(location_info!())?.to_owned())?
.context(location_info!())?;
let title = preview_page
.name()
.map(|n| n.one())
.flatten()
.map(|s| s.as_xsd_string())
.flatten()
.map(|s| s.to_string());
let description = preview_page
.summary()
.map(|s| s.as_single_xsd_string())
.flatten()
.map(|s| s.to_string());
let html = preview_page
.content()
.map(|c| c.as_single_xsd_string())
.flatten()
.map(|s| s.to_string());
Ok(EmbedType {
title,
description,
html,
})
}
None => Ok(EmbedType {
title: None,
description: None,
html: None,
}),
}
}
#[async_trait::async_trait(?Send)]
impl FromApub for PostForm {
type ApubType = PageExt;
/// Parse an ActivityPub page received from another instance into a Lemmy post.
async fn from_apub(
page: &PageExt,
context: &LemmyContext,
expected_domain: Option<Url>,
) -> Result<PostForm, LemmyError> {
let ext = &page.ext_one;
let creator_actor_id = page
.inner
.attributed_to()
.as_ref()
.context(location_info!())?
.as_single_xsd_any_uri()
.context(location_info!())?;
let creator = get_or_fetch_and_upsert_user(creator_actor_id, context).await?;
let community_actor_id = page
.inner
.to()
.as_ref()
.context(location_info!())?
.as_single_xsd_any_uri()
.context(location_info!())?;
let community = get_or_fetch_and_upsert_community(community_actor_id, context).await?;
let thumbnail_url = match &page.inner.image() {
Some(any_image) => Image::from_any_base(
any_image
.to_owned()
.as_one()
.context(location_info!())?
.to_owned(),
)?
.context(location_info!())?
.url()
.context(location_info!())?
.as_single_xsd_any_uri()
.map(|u| u.to_string()),
None => None,
};
let embed = extract_embed_from_apub(page)?;
let name = page
.inner
.summary()
.as_ref()
.context(location_info!())?
.as_single_xsd_string()
.context(location_info!())?
.to_string();
let url = page
.inner
.url()
.as_ref()
.map(|u| u.as_single_xsd_string())
.flatten()
.map(|s| s.to_string());
let body = page
.inner
.content()
.as_ref()
.map(|c| c.as_single_xsd_string())
.flatten()
.map(|s| s.to_string());
check_slurs(&name)?;
let body_slurs_removed = body.map(|b| remove_slurs(&b));
Ok(PostForm {
name,
url,
body: body_slurs_removed,
creator_id: creator.id,
community_id: community.id,
removed: None,
locked: Some(!ext.comments_enabled),
published: page
.inner
.published()
.as_ref()
.map(|u| u.to_owned().naive_local()),
updated: page
.inner
.updated()
.as_ref()
.map(|u| u.to_owned().naive_local()),
deleted: None,
nsfw: ext.sensitive,
stickied: Some(ext.stickied),
embed_title: embed.title,
embed_description: embed.description,
embed_html: embed.html,
thumbnail_url,
ap_id: Some(check_actor_domain(page, expected_domain)?),
local: false,
})
}
}
#[async_trait::async_trait(?Send)]
impl ApubObjectType for Post {
/// Send out information about a newly created post, to the followers of the community.
async fn send_create(&self, creator: &User_, context: &LemmyContext) -> Result<(), LemmyError> {
let page = self.to_apub(context.pool()).await?;
let community_id = self.community_id;
let community = blocking(context.pool(), move |conn| {
Community::read(conn, community_id)
})
.await??;
let mut create = Create::new(creator.actor_id.to_owned(), page.into_any_base()?);
create
.set_context(activitystreams::context())
.set_id(generate_activity_id(CreateType::Create)?)
.set_to(public())
.set_many_ccs(vec![community.get_followers_url()?]);
send_activity_to_community(
creator,
&community,
vec![community.get_shared_inbox_url()?],
create,
context,
)
.await?;
Ok(())
}
/// Send out information about an edited post, to the followers of the community.
async fn send_update(&self, creator: &User_, context: &LemmyContext) -> Result<(), LemmyError> {
let page = self.to_apub(context.pool()).await?;
let community_id = self.community_id;
let community = blocking(context.pool(), move |conn| {
Community::read(conn, community_id)
})
.await??;
let mut update = Update::new(creator.actor_id.to_owned(), page.into_any_base()?);
update
.set_context(activitystreams::context())
.set_id(generate_activity_id(UpdateType::Update)?)
.set_to(public())
.set_many_ccs(vec![community.get_followers_url()?]);
send_activity_to_community(
creator,
&community,
vec![community.get_shared_inbox_url()?],
update,
context,
)
.await?;
Ok(())
}
async fn send_delete(&self, creator: &User_, context: &LemmyContext) -> Result<(), LemmyError> {
let page = self.to_apub(context.pool()).await?;
let community_id = self.community_id;
let community = blocking(context.pool(), move |conn| {
Community::read(conn, community_id)
})
.await??;
let mut delete = Delete::new(creator.actor_id.to_owned(), page.into_any_base()?);
delete
.set_context(activitystreams::context())
.set_id(generate_activity_id(DeleteType::Delete)?)
.set_to(public())
.set_many_ccs(vec![community.get_followers_url()?]);
send_activity_to_community(
creator,
&community,
vec![community.get_shared_inbox_url()?],
delete,
context,
)
.await?;
Ok(())
}
async fn send_undo_delete(
&self,
creator: &User_,
context: &LemmyContext,
) -> Result<(), LemmyError> {
let page = self.to_apub(context.pool()).await?;
let community_id = self.community_id;
let community = blocking(context.pool(), move |conn| {
Community::read(conn, community_id)
})
.await??;
let mut delete = Delete::new(creator.actor_id.to_owned(), page.into_any_base()?);
delete
.set_context(activitystreams::context())
.set_id(generate_activity_id(DeleteType::Delete)?)
.set_to(public())
.set_many_ccs(vec![community.get_followers_url()?]);
// Undo that fake activity
let mut undo = Undo::new(creator.actor_id.to_owned(), delete.into_any_base()?);
undo
.set_context(activitystreams::context())
.set_id(generate_activity_id(UndoType::Undo)?)
.set_to(public())
.set_many_ccs(vec![community.get_followers_url()?]);
send_activity_to_community(
creator,
&community,
vec![community.get_shared_inbox_url()?],
undo,
context,
)
.await?;
Ok(())
}
async fn send_remove(&self, mod_: &User_, context: &LemmyContext) -> Result<(), LemmyError> {
let page = self.to_apub(context.pool()).await?;
let community_id = self.community_id;
let community = blocking(context.pool(), move |conn| {
Community::read(conn, community_id)
})
.await??;
let mut remove = Remove::new(mod_.actor_id.to_owned(), page.into_any_base()?);
remove
.set_context(activitystreams::context())
.set_id(generate_activity_id(RemoveType::Remove)?)
.set_to(public())
.set_many_ccs(vec![community.get_followers_url()?]);
send_activity_to_community(
mod_,
&community,
vec![community.get_shared_inbox_url()?],
remove,
context,
)
.await?;
Ok(())
}
async fn send_undo_remove(&self, mod_: &User_, context: &LemmyContext) -> Result<(), LemmyError> {
let page = self.to_apub(context.pool()).await?;
let community_id = self.community_id;
let community = blocking(context.pool(), move |conn| {
Community::read(conn, community_id)
})
.await??;
let mut remove = Remove::new(mod_.actor_id.to_owned(), page.into_any_base()?);
remove
.set_context(activitystreams::context())
.set_id(generate_activity_id(RemoveType::Remove)?)
.set_to(public())
.set_many_ccs(vec![community.get_followers_url()?]);
// Undo that fake activity
let mut undo = Undo::new(mod_.actor_id.to_owned(), remove.into_any_base()?);
undo
.set_context(activitystreams::context())
.set_id(generate_activity_id(UndoType::Undo)?)
.set_to(public())
.set_many_ccs(vec![community.get_followers_url()?]);
send_activity_to_community(
mod_,
&community,
vec![community.get_shared_inbox_url()?],
undo,
context,
)
.await?;
Ok(())
}
}
#[async_trait::async_trait(?Send)]
impl ApubLikeableType for Post {
async fn send_like(&self, creator: &User_, context: &LemmyContext) -> Result<(), LemmyError> {
let page = self.to_apub(context.pool()).await?;
let community_id = self.community_id;
let community = blocking(context.pool(), move |conn| {
Community::read(conn, community_id)
})
.await??;
let mut like = Like::new(creator.actor_id.to_owned(), page.into_any_base()?);
like
.set_context(activitystreams::context())
.set_id(generate_activity_id(LikeType::Like)?)
.set_to(public())
.set_many_ccs(vec![community.get_followers_url()?]);
send_activity_to_community(
&creator,
&community,
vec![community.get_shared_inbox_url()?],
like,
context,
)
.await?;
Ok(())
}
async fn send_dislike(&self, creator: &User_, context: &LemmyContext) -> Result<(), LemmyError> {
let page = self.to_apub(context.pool()).await?;
let community_id = self.community_id;
let community = blocking(context.pool(), move |conn| {
Community::read(conn, community_id)
})
.await??;
let mut dislike = Dislike::new(creator.actor_id.to_owned(), page.into_any_base()?);
dislike
.set_context(activitystreams::context())
.set_id(generate_activity_id(DislikeType::Dislike)?)
.set_to(public())
.set_many_ccs(vec![community.get_followers_url()?]);
send_activity_to_community(
&creator,
&community,
vec![community.get_shared_inbox_url()?],
dislike,
context,
)
.await?;
Ok(())
}
async fn send_undo_like(
&self,
creator: &User_,
context: &LemmyContext,
) -> Result<(), LemmyError> {
let page = self.to_apub(context.pool()).await?;
let community_id = self.community_id;
let community = blocking(context.pool(), move |conn| {
Community::read(conn, community_id)
})
.await??;
let mut like = Like::new(creator.actor_id.to_owned(), page.into_any_base()?);
like
.set_context(activitystreams::context())
.set_id(generate_activity_id(LikeType::Like)?)
.set_to(public())
.set_many_ccs(vec![community.get_followers_url()?]);
// Undo that fake activity
let mut undo = Undo::new(creator.actor_id.to_owned(), like.into_any_base()?);
undo
.set_context(activitystreams::context())
.set_id(generate_activity_id(UndoType::Undo)?)
.set_to(public())
.set_many_ccs(vec![community.get_followers_url()?]);
send_activity_to_community(
&creator,
&community,
vec![community.get_shared_inbox_url()?],
undo,
context,
)
.await?;
Ok(())
}
}