From: Nutomic Date: Sat, 17 Jul 2021 16:08:46 +0000 (+0000) Subject: Apub inbox rewrite (#1652) X-Git-Url: http://these/git/%7B%60%24%7BwebArchiveUrl%7D/%22%7B%7D/readmes/%7B%7D/%22%7Burl%7D/%7Biframely.url%7D?a=commitdiff_plain;h=c7de1fcf24185c7f72161bf7d3536ccb7c4e96c5;p=lemmy.git Apub inbox rewrite (#1652) * start to implement apub inbox routing lib * got something that almost works * it compiles! * implemented some more * move library code to separate crate (most of it) * convert private message handlers * convert all comment receivers (except undo comment) * convert post receiver * add verify trait * convert community receivers * add cc field for all activities which i forgot before * convert inbox functions, add missing checks * convert undo like/dislike receivers * convert undo_delete and undo_remove receivers * move block/unblock activities * convert remaining activity receivers * reimplement http signature verification and other checks * also use actor type for routing, VerifyActivity and SendActivity traits * cleanup and restructure apub_receive code * wip: try to fix activity routing * implement a (very bad) derive macro for activityhandler * working activity routing! * rework pm verify(), fix tests and confirm manually also remove inbox username check which was broken * rework following verify(), fix tests and test manually * fix post/comment create/update, rework voting * Rewrite remove/delete post/comment, fix tests, test manually * Rework and fix (un)block user, announce, update post * some code cleanup * rework delete/remove activity receivers (still quite messy) * rewrite, test and fix add/remove mod, update community handlers * add docs for ActivityHandler derive macro * dont try to compile macro comments --- diff --git a/Cargo.lock b/Cargo.lock index d6b98150..31492222 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -988,6 +988,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "212d0f5754cb6769937f4501cc0e67f4f4483c8d2c3e1e922ee9edbe4ab4c7c0" +[[package]] +name = "dissimilar" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc4b29f4b9bb94bf267d57269fd0706d343a160937108e9619fe380645428abb" + [[package]] name = "either" version = "1.6.1" @@ -1261,6 +1267,12 @@ version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0e4075386626662786ddb0ec9081e7c7eeb1ba31951f447ca780ef9f5d568189" +[[package]] +name = "glob" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" + [[package]] name = "h2" version = "0.3.3" @@ -1736,6 +1748,30 @@ dependencies = [ "uuid", ] +[[package]] +name = "lemmy_apub_lib" +version = "0.1.0" +dependencies = [ + "activitystreams", + "activitystreams-ext", + "async-trait", + "lemmy_apub_lib_derive", + "lemmy_utils", + "lemmy_websocket", + "serde", + "url", +] + +[[package]] +name = "lemmy_apub_lib_derive" +version = "0.1.0" +dependencies = [ + "proc-macro2 1.0.27", + "quote 1.0.9", + "syn 1.0.73", + "trybuild", +] + [[package]] name = "lemmy_apub_receive" version = "0.1.0" @@ -1760,6 +1796,7 @@ dependencies = [ "itertools", "lemmy_api_common", "lemmy_apub", + "lemmy_apub_lib", "lemmy_db_queries", "lemmy_db_schema", "lemmy_db_views", @@ -3361,6 +3398,15 @@ dependencies = [ "tokio", ] +[[package]] +name = "toml" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31142970826733df8241ef35dc040ef98c679ab14d7c3e54d827099b3acecaa" +dependencies = [ + "serde", +] + [[package]] name = "tower-service" version = "0.3.1" @@ -3393,6 +3439,21 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" +[[package]] +name = "trybuild" +version = "1.0.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1768998d9a3b179411618e377dbb134c58a88cda284b0aa71c42c40660127d46" +dependencies = [ + "dissimilar", + "glob", + "lazy_static", + "serde", + "serde_json", + "termcolor", + "toml", +] + [[package]] name = "twoway" version = "0.2.2" diff --git a/Cargo.toml b/Cargo.toml index 03028579..2352a7d6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,6 +14,8 @@ members = [ "crates/api", "crates/api_crud", "crates/api_common", + "crates/apub_lib", + "crates/apub_lib_derive", "crates/apub", "crates/apub_receive", "crates/utils", diff --git a/api_tests/prepare-drone-federation-test.sh b/api_tests/prepare-drone-federation-test.sh index ae9c1293..c2c7805a 100755 --- a/api_tests/prepare-drone-federation-test.sh +++ b/api_tests/prepare-drone-federation-test.sh @@ -3,6 +3,7 @@ set -e export LEMMY_TEST_SEND_SYNC=1 export RUST_BACKTRACE=1 +export RUST_LOG="warn,lemmy_server=debug,lemmy_api=debug,lemmy_api_common=debug,lemmy_api_crud=debug,lemmy_apub=debug,lemmy_apub_receive=debug,lemmy_db_queries=debug,lemmy_db_schema=debug,lemmy_db_views=debug,lemmy_db_views_actor=debug,lemmy_db_views_moderator=debug,lemmy_routes=debug,lemmy_utils=debug,lemmy_websocket=debug" for INSTANCE in lemmy_alpha lemmy_beta lemmy_gamma lemmy_delta lemmy_epsilon; do psql "${LEMMY_DATABASE_URL}/lemmy" -c "DROP DATABASE IF EXISTS $INSTANCE" diff --git a/crates/apub/src/activities/send/community.rs b/crates/apub/src/activities/send/community.rs index 378c5bd6..bf751479 100644 --- a/crates/apub/src/activities/send/community.rs +++ b/crates/apub/src/activities/send/community.rs @@ -49,7 +49,6 @@ use lemmy_utils::{location_info, settings::structs::Settings, LemmyError}; use lemmy_websocket::LemmyContext; use url::Url; -#[async_trait::async_trait(?Send)] impl ActorType for Community { fn is_local(&self) -> bool { self.local @@ -57,6 +56,9 @@ impl ActorType for Community { fn actor_id(&self) -> Url { self.actor_id.to_owned().into_inner() } + fn name(&self) -> String { + self.name.clone() + } fn public_key(&self) -> Option { self.public_key.to_owned() } diff --git a/crates/apub/src/activities/send/person.rs b/crates/apub/src/activities/send/person.rs index 1e72a857..b1fc0cd2 100644 --- a/crates/apub/src/activities/send/person.rs +++ b/crates/apub/src/activities/send/person.rs @@ -24,7 +24,6 @@ use lemmy_utils::LemmyError; use lemmy_websocket::LemmyContext; use url::Url; -#[async_trait::async_trait(?Send)] impl ActorType for Person { fn is_local(&self) -> bool { self.local @@ -32,6 +31,9 @@ impl ActorType for Person { fn actor_id(&self) -> Url { self.actor_id.to_owned().into_inner() } + fn name(&self) -> String { + self.name.clone() + } fn public_key(&self) -> Option { self.public_key.to_owned() diff --git a/crates/apub/src/activity_queue.rs b/crates/apub/src/activity_queue.rs index 7016e0ca..22b88d14 100644 --- a/crates/apub/src/activity_queue.rs +++ b/crates/apub/src/activity_queue.rs @@ -49,7 +49,7 @@ where if check_is_apub_id_valid(&inbox, false).is_ok() { debug!( "Sending activity {:?} to {}", - &activity.id_unchecked(), + &activity.id_unchecked().map(ToString::to_string), &inbox ); send_activity_internal(context, activity, creator, vec![inbox], true, true).await?; @@ -88,7 +88,7 @@ where .collect(); debug!( "Sending activity {:?} to followers of {}", - &activity.id_unchecked().map(|i| i.to_string()), + &activity.id_unchecked().map(ToString::to_string), &community.actor_id ); @@ -127,7 +127,7 @@ where check_is_apub_id_valid(&inbox, false)?; debug!( "Sending activity {:?} to community {}", - &activity.id_unchecked(), + &activity.id_unchecked().map(ToString::to_string), &community.actor_id ); // dont send to object_actor here, as that is responsibility of the community itself diff --git a/crates/apub/src/extensions/signatures.rs b/crates/apub/src/extensions/signatures.rs index 47b8e5a2..be323d51 100644 --- a/crates/apub/src/extensions/signatures.rs +++ b/crates/apub/src/extensions/signatures.rs @@ -1,12 +1,11 @@ -use crate::ActorType; use activitystreams::unparsed::UnparsedMutExt; use activitystreams_ext::UnparsedExtension; use actix_web::HttpRequest; -use anyhow::{anyhow, Context}; +use anyhow::anyhow; use http::{header::HeaderName, HeaderMap, HeaderValue}; use http_signature_normalization_actix::Config as ConfigActix; use http_signature_normalization_reqwest::prelude::{Config, SignExt}; -use lemmy_utils::{location_info, LemmyError}; +use lemmy_utils::LemmyError; use log::debug; use openssl::{ hash::MessageDigest, @@ -65,8 +64,7 @@ pub(crate) async fn sign_and_send( } /// Verifies the HTTP signature on an incoming inbox request. -pub fn verify_signature(request: &HttpRequest, actor: &dyn ActorType) -> Result<(), LemmyError> { - let public_key = actor.public_key().context(location_info!())?; +pub fn verify_signature(request: &HttpRequest, public_key: &str) -> Result<(), LemmyError> { let verified = CONFIG2 .begin_verify( request.method(), diff --git a/crates/apub/src/fetcher/objects.rs b/crates/apub/src/fetcher/objects.rs index 41bc075a..af8a59f7 100644 --- a/crates/apub/src/fetcher/objects.rs +++ b/crates/apub/src/fetcher/objects.rs @@ -1,4 +1,10 @@ -use crate::{fetcher::fetch::fetch_remote_object, objects::FromApub, NoteExt, PageExt}; +use crate::{ + fetcher::fetch::fetch_remote_object, + objects::FromApub, + NoteExt, + PageExt, + PostOrComment, +}; use anyhow::anyhow; use diesel::result::Error::NotFound; use lemmy_api_common::blocking; @@ -89,3 +95,19 @@ pub async fn get_or_fetch_and_insert_comment( Err(e) => Err(e.into()), } } + +pub async fn get_or_fetch_and_insert_post_or_comment( + ap_id: &Url, + context: &LemmyContext, + recursion_counter: &mut i32, +) -> Result { + Ok( + match get_or_fetch_and_insert_post(ap_id, context, recursion_counter).await { + Ok(p) => PostOrComment::Post(Box::new(p)), + Err(_) => { + let c = get_or_fetch_and_insert_comment(ap_id, context, recursion_counter).await?; + PostOrComment::Comment(Box::new(c)) + } + }, + ) +} diff --git a/crates/apub/src/lib.rs b/crates/apub/src/lib.rs index fe63d4d5..6bb97675 100644 --- a/crates/apub/src/lib.rs +++ b/crates/apub/src/lib.rs @@ -52,6 +52,7 @@ pub type GroupExt = /// Activitystreams type for person type PersonExt = Ext2>>, PersonExtension, PublicKeyExtension>; +pub type SiteExt = actor::ApActor>; /// Activitystreams type for post pub type PageExt = Ext1, PageExtension>; pub type NoteExt = ApObject; @@ -170,10 +171,10 @@ pub trait ApubLikeableType { /// Common methods provided by ActivityPub actors (community and person). Not all methods are /// implemented by all actors. -#[async_trait::async_trait(?Send)] pub trait ActorType { fn is_local(&self) -> bool; fn actor_id(&self) -> Url; + fn name(&self) -> String; // TODO: every actor should have a public key, so this shouldnt be an option (needs to be fixed in db) fn public_key(&self) -> Option; diff --git a/crates/apub/src/objects/comment.rs b/crates/apub/src/objects/comment.rs index 361da462..7b181eff 100644 --- a/crates/apub/src/objects/comment.rs +++ b/crates/apub/src/objects/comment.rs @@ -123,17 +123,7 @@ impl FromApub for Comment { let post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??; check_object_for_community_or_site_ban(note, post.community_id, context, request_counter) .await?; - if post.locked { - // This is not very efficient because a comment gets inserted just to be deleted right - // afterwards, but it seems to be the easiest way to implement it. - blocking(context.pool(), move |conn| { - Comment::delete(conn, comment.id) - }) - .await??; - Err(anyhow!("Post is locked").into()) - } else { - Ok(comment) - } + Ok(comment) } } @@ -174,6 +164,9 @@ impl FromApubToForm for CommentForm { request_counter, )) .await?; + if post.locked { + return Err(anyhow!("Post is locked").into()); + } // The 2nd item, if it exists, is the parent comment apub_id // For deeply nested comments, FromApub automatically gets called recursively diff --git a/crates/apub_lib/Cargo.toml b/crates/apub_lib/Cargo.toml new file mode 100644 index 00000000..327670b5 --- /dev/null +++ b/crates/apub_lib/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "lemmy_apub_lib" +version = "0.1.0" +edition = "2018" + +[dependencies] +lemmy_utils = { path = "../utils" } +lemmy_websocket = { path = "../websocket" } +lemmy_apub_lib_derive = { path = "../apub_lib_derive" } +activitystreams = "0.7.0-alpha.11" +activitystreams-ext = "0.1.0-alpha.2" +serde = { version = "1.0.123", features = ["derive"] } +async-trait = "0.1.42" +url = { version = "2.2.1", features = ["serde"] } diff --git a/crates/apub_lib/src/lib.rs b/crates/apub_lib/src/lib.rs new file mode 100644 index 00000000..66bba9f4 --- /dev/null +++ b/crates/apub_lib/src/lib.rs @@ -0,0 +1,72 @@ +use activitystreams::{ + base::AnyBase, + error::DomainError, + primitives::OneOrMany, + unparsed::Unparsed, +}; +pub use lemmy_apub_lib_derive::*; +use lemmy_utils::LemmyError; +use lemmy_websocket::LemmyContext; +use url::Url; + +#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] +pub enum PublicUrl { + #[serde(rename = "https://www.w3.org/ns/activitystreams#Public")] + Public, +} + +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ActivityCommonFields { + #[serde(rename = "@context")] + pub context: OneOrMany, + id: Url, + pub actor: Url, + + // unparsed fields + #[serde(flatten)] + pub unparsed: Unparsed, +} + +impl ActivityCommonFields { + pub fn id_unchecked(&self) -> &Url { + &self.id + } +} + +#[async_trait::async_trait(?Send)] +pub trait ActivityHandler { + async fn verify( + &self, + context: &LemmyContext, + request_counter: &mut i32, + ) -> Result<(), LemmyError>; + + async fn receive( + &self, + context: &LemmyContext, + request_counter: &mut i32, + ) -> Result<(), LemmyError>; + fn common(&self) -> &ActivityCommonFields; +} + +pub fn verify_domains_match(a: &Url, b: &Url) -> Result<(), LemmyError> { + if a.domain() != b.domain() { + return Err(DomainError.into()); + } + Ok(()) +} + +pub fn verify_domains_match_opt(a: &Url, b: Option<&Url>) -> Result<(), LemmyError> { + if let Some(b2) = b { + return verify_domains_match(a, b2); + } + Ok(()) +} + +pub fn verify_urls_match(a: &Url, b: &Url) -> Result<(), LemmyError> { + if a != b { + return Err(DomainError.into()); + } + Ok(()) +} diff --git a/crates/apub_lib_derive/Cargo.toml b/crates/apub_lib_derive/Cargo.toml new file mode 100644 index 00000000..293a15b8 --- /dev/null +++ b/crates/apub_lib_derive/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "lemmy_apub_lib_derive" +version = "0.1.0" +edition = "2018" + +[lib] +proc-macro = true + +[dev-dependencies] +trybuild = { version = "1.0", features = ["diff"] } + +[dependencies] +proc-macro2 = "1.0" +syn = "1.0" +quote = "1.0" \ No newline at end of file diff --git a/crates/apub_lib_derive/src/lib.rs b/crates/apub_lib_derive/src/lib.rs new file mode 100644 index 00000000..f8750680 --- /dev/null +++ b/crates/apub_lib_derive/src/lib.rs @@ -0,0 +1,149 @@ +use proc_macro2::TokenStream; +use quote::quote; +use syn::{parse_macro_input, Data, DeriveInput}; + +/// Generates implementation ActivityHandler for an enum, which looks like the following (handling +/// all enum variants). +/// +/// Based on this code: +/// ```ignore +/// #[derive(serde::Deserialize, serde::Serialize, ActivityHandler)] +/// #[serde(untagged)] +/// pub enum PersonInboxActivities { +/// CreateNote(CreateNote), +/// UpdateNote(UpdateNote), +/// ``` +/// It will generate this: +/// ```ignore +/// impl ActivityHandler for PersonInboxActivities { +/// +/// async fn verify( +/// &self, +/// context: &LemmyContext, +/// request_counter: &mut i32, +/// ) -> Result<(), LemmyError> { +/// match self { +/// PersonInboxActivities::CreateNote(a) => a.verify(context, request_counter).await, +/// PersonInboxActivities::UpdateNote(a) => a.verify(context, request_counter).await, +/// } +/// } +/// +/// async fn receive( +/// &self, +/// context: &LemmyContext, +/// request_counter: &mut i32, +/// ) -> Result<(), LemmyError> { +/// match self { +/// PersonInboxActivities::CreateNote(a) => a.receive(context, request_counter).await, +/// PersonInboxActivities::UpdateNote(a) => a.receive(context, request_counter).await, +/// } +/// } +/// fn common(&self) -> &ActivityCommonFields { +/// match self { +/// PersonInboxActivities::CreateNote(a) => a.common(), +/// PersonInboxActivities::UpdateNote(a) => a.common(), +/// } +/// } +/// +/// ``` +/// +/// TODO: consider replacing this macro with https://crates.io/crates/typetag crate, though it +/// doesnt support untagged enums which we need for apub. +#[proc_macro_derive(ActivityHandler)] +pub fn derive_activity_handler(input: proc_macro::TokenStream) -> proc_macro::TokenStream { + // Parse the input tokens into a syntax tree. + let input = parse_macro_input!(input as DeriveInput); + + // Used in the quasi-quotation below as `#name`. + let name = input.ident; + + let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl(); + + let input_enum = if let Data::Enum(d) = input.data { + d + } else { + unimplemented!() + }; + + let impl_verify = input_enum + .variants + .iter() + .map(|variant| variant_impl_verify(&name, variant)); + let impl_receive = input_enum + .variants + .iter() + .map(|variant| variant_impl_receive(&name, variant)); + let impl_common = input_enum + .variants + .iter() + .map(|variant| variant_impl_common(&name, variant)); + + // The generated impl. + let expanded = quote! { + #[async_trait::async_trait(?Send)] + impl #impl_generics lemmy_apub_lib::ActivityHandler for #name #ty_generics #where_clause { + async fn verify( + &self, + context: &LemmyContext, + request_counter: &mut i32, + ) -> Result<(), LemmyError> { + match self { + #(#impl_verify)* + } + } + async fn receive( + &self, + context: &LemmyContext, + request_counter: &mut i32, + ) -> Result<(), LemmyError> { + match self { + #(#impl_receive)* + } + } + fn common(&self) -> &ActivityCommonFields { + match self { + #(#impl_common)* + } + } + } + }; + + // Hand the output tokens back to the compiler. + proc_macro::TokenStream::from(expanded) +} + +fn variant_impl_common(name: &syn::Ident, variant: &syn::Variant) -> TokenStream { + let id = &variant.ident; + match &variant.fields { + syn::Fields::Unnamed(_) => { + quote! { + #name::#id(a) => a.common(), + } + } + _ => unimplemented!(), + } +} + +fn variant_impl_verify(name: &syn::Ident, variant: &syn::Variant) -> TokenStream { + let id = &variant.ident; + match &variant.fields { + syn::Fields::Unnamed(_) => { + quote! { + #name::#id(a) => a.verify(context, request_counter).await, + } + } + _ => unimplemented!(), + } +} + +fn variant_impl_receive(name: &syn::Ident, variant: &syn::Variant) -> TokenStream { + let id = &variant.ident; + match &variant.fields { + syn::Fields::Unnamed(_) => { + quote! { + #name::#id(a) => a.receive(context, request_counter).await, + } + } + _ => unimplemented!(), + } +} diff --git a/crates/apub_receive/Cargo.toml b/crates/apub_receive/Cargo.toml index e62f8e49..ca21787c 100644 --- a/crates/apub_receive/Cargo.toml +++ b/crates/apub_receive/Cargo.toml @@ -5,6 +5,7 @@ edition = "2018" [dependencies] lemmy_utils = { path = "../utils" } +lemmy_apub_lib = { path = "../apub_lib" } lemmy_apub = { path = "../apub" } lemmy_db_queries = { path = "../db_queries" } lemmy_db_schema = { path = "../db_schema" } diff --git a/crates/apub_receive/src/activities/comment/create.rs b/crates/apub_receive/src/activities/comment/create.rs new file mode 100644 index 00000000..7ce5d09a --- /dev/null +++ b/crates/apub_receive/src/activities/comment/create.rs @@ -0,0 +1,68 @@ +use crate::activities::{ + comment::{get_notif_recipients, send_websocket_message}, + verify_activity, + verify_person_in_community, +}; +use activitystreams::{activity::kind::CreateType, base::BaseExt}; +use lemmy_apub::{objects::FromApub, NoteExt}; +use lemmy_apub_lib::{verify_domains_match_opt, ActivityCommonFields, ActivityHandler, PublicUrl}; +use lemmy_db_schema::source::comment::Comment; +use lemmy_utils::LemmyError; +use lemmy_websocket::{LemmyContext, UserOperationCrud}; +use url::Url; + +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CreateComment { + to: PublicUrl, + object: NoteExt, + cc: Vec, + #[serde(rename = "type")] + kind: CreateType, + #[serde(flatten)] + common: ActivityCommonFields, +} + +#[async_trait::async_trait(?Send)] +impl ActivityHandler for CreateComment { + async fn verify( + &self, + context: &LemmyContext, + request_counter: &mut i32, + ) -> Result<(), LemmyError> { + verify_activity(self.common())?; + verify_person_in_community(&self.common.actor, &self.cc, context, request_counter).await?; + verify_domains_match_opt(&self.common.actor, self.object.id_unchecked())?; + // TODO: should add a check that the correct community is in cc (probably needs changes to + // comment deserialization) + Ok(()) + } + + async fn receive( + &self, + context: &LemmyContext, + request_counter: &mut i32, + ) -> Result<(), LemmyError> { + let comment = Comment::from_apub( + &self.object, + context, + self.common.actor.clone(), + request_counter, + false, + ) + .await?; + let recipients = + get_notif_recipients(&self.common.actor, &comment, context, request_counter).await?; + send_websocket_message( + comment.id, + recipients, + UserOperationCrud::CreateComment, + context, + ) + .await + } + + fn common(&self) -> &ActivityCommonFields { + &self.common + } +} diff --git a/crates/apub_receive/src/activities/comment/mod.rs b/crates/apub_receive/src/activities/comment/mod.rs new file mode 100644 index 00000000..e5cdb4dd --- /dev/null +++ b/crates/apub_receive/src/activities/comment/mod.rs @@ -0,0 +1,65 @@ +use lemmy_api_common::{blocking, comment::CommentResponse, send_local_notifs}; +use lemmy_apub::fetcher::person::get_or_fetch_and_upsert_person; +use lemmy_db_queries::Crud; +use lemmy_db_schema::{ + source::{comment::Comment, post::Post}, + CommentId, + LocalUserId, +}; +use lemmy_db_views::comment_view::CommentView; +use lemmy_utils::{utils::scrape_text_for_mentions, LemmyError}; +use lemmy_websocket::{messages::SendComment, LemmyContext}; +use url::Url; + +pub mod create; +pub mod update; + +async fn get_notif_recipients( + actor: &Url, + comment: &Comment, + context: &LemmyContext, + request_counter: &mut i32, +) -> Result, LemmyError> { + let post_id = comment.post_id; + let post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??; + let actor = get_or_fetch_and_upsert_person(actor, context, request_counter).await?; + + // Note: + // Although mentions could be gotten from the post tags (they are included there), or the ccs, + // Its much easier to scrape them from the comment body, since the API has to do that + // anyway. + // TODO: for compatibility with other projects, it would be much better to read this from cc or tags + let mentions = scrape_text_for_mentions(&comment.content); + send_local_notifs(mentions, comment.clone(), actor, post, context.pool(), true).await +} + +// TODO: in many call sites we are setting an empty vec for recipient_ids, we should get the actual +// recipient actors from somewhere +pub(crate) async fn send_websocket_message< + OP: ToString + Send + lemmy_websocket::OperationType + 'static, +>( + comment_id: CommentId, + recipient_ids: Vec, + op: OP, + context: &LemmyContext, +) -> Result<(), LemmyError> { + // Refetch the view + let comment_view = blocking(context.pool(), move |conn| { + CommentView::read(conn, comment_id, None) + }) + .await??; + + let res = CommentResponse { + comment_view, + recipient_ids, + form_id: None, + }; + + context.chat_server().do_send(SendComment { + op, + comment: res, + websocket_id: None, + }); + + Ok(()) +} diff --git a/crates/apub_receive/src/activities/comment/remove.rs b/crates/apub_receive/src/activities/comment/remove.rs new file mode 100644 index 00000000..53e69b65 --- /dev/null +++ b/crates/apub_receive/src/activities/comment/remove.rs @@ -0,0 +1,56 @@ +use crate::activities::{comment::send_websocket_message, verify_mod_action}; +use activitystreams::activity::kind::RemoveType; +use lemmy_api_common::blocking; +use lemmy_apub::{check_is_apub_id_valid, fetcher::objects::get_or_fetch_and_insert_comment}; +use lemmy_apub_lib::{verify_domains_match, ActivityCommonFields, ActivityHandlerNew, PublicUrl}; +use lemmy_db_queries::source::comment::Comment_; +use lemmy_db_schema::source::comment::Comment; +use lemmy_utils::LemmyError; +use lemmy_websocket::{LemmyContext, UserOperationCrud}; +use url::Url; + +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] +#[serde(rename_all = "camelCase")] +pub struct RemoveComment { + to: PublicUrl, + pub(in crate::activities::comment) object: Url, + cc: [Url; 1], + #[serde(rename = "type")] + kind: RemoveType, + #[serde(flatten)] + common: ActivityCommonFields, +} + +#[async_trait::async_trait(?Send)] +impl ActivityHandlerNew for RemoveComment { + async fn verify(&self, context: &LemmyContext, _: &mut i32) -> Result<(), LemmyError> { + verify_domains_match(&self.common.actor, self.common.id_unchecked())?; + check_is_apub_id_valid(&self.common.actor, false)?; + verify_mod_action(&self.common.actor, self.cc[0].clone(), context).await + } + + async fn receive( + &self, + context: &LemmyContext, + request_counter: &mut i32, + ) -> Result<(), LemmyError> { + let comment = get_or_fetch_and_insert_comment(&self.object, context, request_counter).await?; + + let removed_comment = blocking(context.pool(), move |conn| { + Comment::update_removed(conn, comment.id, true) + }) + .await??; + + send_websocket_message( + removed_comment.id, + vec![], + UserOperationCrud::EditComment, + context, + ) + .await + } + + fn common(&self) -> &ActivityCommonFields { + &self.common + } +} diff --git a/crates/apub_receive/src/activities/comment/undo_remove.rs b/crates/apub_receive/src/activities/comment/undo_remove.rs new file mode 100644 index 00000000..444f3ce4 --- /dev/null +++ b/crates/apub_receive/src/activities/comment/undo_remove.rs @@ -0,0 +1,65 @@ +use crate::activities::{ + comment::{remove::RemoveComment, send_websocket_message}, + verify_mod_action, +}; +use activitystreams::activity::kind::UndoType; +use lemmy_api_common::blocking; +use lemmy_apub::{check_is_apub_id_valid, fetcher::objects::get_or_fetch_and_insert_comment}; +use lemmy_apub_lib::{verify_domains_match, ActivityCommonFields, ActivityHandlerNew, PublicUrl}; +use lemmy_db_queries::source::comment::Comment_; +use lemmy_db_schema::source::comment::Comment; +use lemmy_utils::LemmyError; +use lemmy_websocket::{LemmyContext, UserOperationCrud}; +use url::Url; + +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] +#[serde(rename_all = "camelCase")] +pub struct UndoRemoveComment { + to: PublicUrl, + object: RemoveComment, + cc: [Url; 1], + #[serde(rename = "type")] + kind: UndoType, + #[serde(flatten)] + common: ActivityCommonFields, +} + +#[async_trait::async_trait(?Send)] +impl ActivityHandlerNew for UndoRemoveComment { + async fn verify( + &self, + context: &LemmyContext, + request_counter: &mut i32, + ) -> Result<(), LemmyError> { + verify_domains_match(&self.common.actor, self.common.id_unchecked())?; + check_is_apub_id_valid(&self.common.actor, false)?; + verify_mod_action(&self.common.actor, self.cc[0].clone(), context).await?; + self.object.verify(context, request_counter).await + } + + async fn receive( + &self, + context: &LemmyContext, + request_counter: &mut i32, + ) -> Result<(), LemmyError> { + let comment = + get_or_fetch_and_insert_comment(&self.object.object, context, request_counter).await?; + + let removed_comment = blocking(context.pool(), move |conn| { + Comment::update_removed(conn, comment.id, false) + }) + .await??; + + send_websocket_message( + removed_comment.id, + vec![], + UserOperationCrud::EditComment, + context, + ) + .await + } + + fn common(&self) -> &ActivityCommonFields { + &self.common + } +} diff --git a/crates/apub_receive/src/activities/comment/update.rs b/crates/apub_receive/src/activities/comment/update.rs new file mode 100644 index 00000000..c07837e3 --- /dev/null +++ b/crates/apub_receive/src/activities/comment/update.rs @@ -0,0 +1,67 @@ +use crate::activities::{ + comment::{get_notif_recipients, send_websocket_message}, + verify_activity, + verify_person_in_community, +}; +use activitystreams::{activity::kind::UpdateType, base::BaseExt}; +use lemmy_apub::{objects::FromApub, NoteExt}; +use lemmy_apub_lib::{verify_domains_match_opt, ActivityCommonFields, ActivityHandler, PublicUrl}; +use lemmy_db_schema::source::comment::Comment; +use lemmy_utils::LemmyError; +use lemmy_websocket::{LemmyContext, UserOperationCrud}; +use url::Url; + +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] +#[serde(rename_all = "camelCase")] +pub struct UpdateComment { + to: PublicUrl, + object: NoteExt, + cc: Vec, + #[serde(rename = "type")] + kind: UpdateType, + #[serde(flatten)] + common: ActivityCommonFields, +} + +#[async_trait::async_trait(?Send)] +impl ActivityHandler for UpdateComment { + async fn verify( + &self, + context: &LemmyContext, + request_counter: &mut i32, + ) -> Result<(), LemmyError> { + verify_activity(self.common())?; + verify_person_in_community(&self.common.actor, &self.cc, context, request_counter).await?; + verify_domains_match_opt(&self.common.actor, self.object.id_unchecked())?; + Ok(()) + } + + async fn receive( + &self, + context: &LemmyContext, + request_counter: &mut i32, + ) -> Result<(), LemmyError> { + let comment = Comment::from_apub( + &self.object, + context, + self.common.actor.clone(), + request_counter, + false, + ) + .await?; + + let recipients = + get_notif_recipients(&self.common.actor, &comment, context, request_counter).await?; + send_websocket_message( + comment.id, + recipients, + UserOperationCrud::EditComment, + context, + ) + .await + } + + fn common(&self) -> &ActivityCommonFields { + &self.common + } +} diff --git a/crates/apub_receive/src/activities/community/add_mod.rs b/crates/apub_receive/src/activities/community/add_mod.rs new file mode 100644 index 00000000..2cf64f71 --- /dev/null +++ b/crates/apub_receive/src/activities/community/add_mod.rs @@ -0,0 +1,86 @@ +use crate::activities::{ + verify_activity, + verify_add_remove_moderator_target, + verify_mod_action, + verify_person_in_community, +}; +use activitystreams::{activity::kind::AddType, base::AnyBase}; +use lemmy_api_common::blocking; +use lemmy_apub::{ + fetcher::{community::get_or_fetch_and_upsert_community, person::get_or_fetch_and_upsert_person}, + CommunityType, +}; +use lemmy_apub_lib::{ActivityCommonFields, ActivityHandler, PublicUrl}; +use lemmy_db_queries::{source::community::CommunityModerator_, Joinable}; +use lemmy_db_schema::source::community::{CommunityModerator, CommunityModeratorForm}; +use lemmy_utils::LemmyError; +use lemmy_websocket::LemmyContext; +use url::Url; + +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] +#[serde(rename_all = "camelCase")] +pub struct AddMod { + to: PublicUrl, + object: Url, + target: Url, + cc: [Url; 1], + #[serde(rename = "type")] + kind: AddType, + #[serde(flatten)] + common: ActivityCommonFields, +} + +#[async_trait::async_trait(?Send)] +impl ActivityHandler for AddMod { + async fn verify( + &self, + context: &LemmyContext, + request_counter: &mut i32, + ) -> Result<(), LemmyError> { + verify_activity(self.common())?; + verify_person_in_community(&self.common.actor, &self.cc, context, request_counter).await?; + verify_mod_action(&self.common.actor, self.cc[0].clone(), context).await?; + verify_add_remove_moderator_target(&self.target, self.cc[0].clone())?; + Ok(()) + } + + async fn receive( + &self, + context: &LemmyContext, + request_counter: &mut i32, + ) -> Result<(), LemmyError> { + let community = + get_or_fetch_and_upsert_community(&self.cc[0], context, request_counter).await?; + let new_mod = get_or_fetch_and_upsert_person(&self.object, context, request_counter).await?; + + // If we had to refetch the community while parsing the activity, then the new mod has already + // been added. Skip it here as it would result in a duplicate key error. + let new_mod_id = new_mod.id; + let moderated_communities = blocking(context.pool(), move |conn| { + CommunityModerator::get_person_moderated_communities(conn, new_mod_id) + }) + .await??; + if !moderated_communities.contains(&community.id) { + let form = CommunityModeratorForm { + community_id: community.id, + person_id: new_mod.id, + }; + blocking(context.pool(), move |conn| { + CommunityModerator::join(conn, &form) + }) + .await??; + } + if community.local { + let anybase = AnyBase::from_arbitrary_json(serde_json::to_string(self)?)?; + community + .send_announce(anybase, Some(self.object.clone()), context) + .await?; + } + // TODO: send websocket notification about added mod + Ok(()) + } + + fn common(&self) -> &ActivityCommonFields { + &self.common + } +} diff --git a/crates/apub_receive/src/activities/community/announce.rs b/crates/apub_receive/src/activities/community/announce.rs new file mode 100644 index 00000000..09f51a7e --- /dev/null +++ b/crates/apub_receive/src/activities/community/announce.rs @@ -0,0 +1,104 @@ +use crate::{ + activities::{ + comment::{create::CreateComment, update::UpdateComment}, + community::{ + add_mod::AddMod, + block_user::BlockUserFromCommunity, + undo_block_user::UndoBlockUserFromCommunity, + }, + deletion::{ + delete::DeletePostCommentOrCommunity, + undo_delete::UndoDeletePostCommentOrCommunity, + }, + post::{create::CreatePost, update::UpdatePost}, + removal::{ + remove::RemovePostCommentCommunityOrMod, + undo_remove::UndoRemovePostCommentOrCommunity, + }, + verify_activity, + verify_community, + voting::{ + dislike::DislikePostOrComment, + like::LikePostOrComment, + undo_dislike::UndoDislikePostOrComment, + undo_like::UndoLikePostOrComment, + }, + }, + http::is_activity_already_known, +}; +use activitystreams::activity::kind::AnnounceType; +use lemmy_apub::insert_activity; +use lemmy_apub_lib::{ActivityCommonFields, ActivityHandler, PublicUrl}; +use lemmy_utils::LemmyError; +use lemmy_websocket::LemmyContext; +use serde::{Deserialize, Serialize}; +use url::Url; + +#[derive(Clone, Debug, Deserialize, Serialize, ActivityHandler)] +#[serde(untagged)] +pub enum AnnouncableActivities { + CreateComment(CreateComment), + UpdateComment(UpdateComment), + CreatePost(CreatePost), + UpdatePost(UpdatePost), + LikePostOrComment(LikePostOrComment), + DislikePostOrComment(DislikePostOrComment), + UndoLikePostOrComment(UndoLikePostOrComment), + UndoDislikePostOrComment(UndoDislikePostOrComment), + DeletePostCommentOrCommunity(DeletePostCommentOrCommunity), + UndoDeletePostCommentOrCommunity(UndoDeletePostCommentOrCommunity), + RemovePostCommentCommunityOrMod(RemovePostCommentCommunityOrMod), + UndoRemovePostCommentOrCommunity(UndoRemovePostCommentOrCommunity), + BlockUserFromCommunity(BlockUserFromCommunity), + UndoBlockUserFromCommunity(UndoBlockUserFromCommunity), + AddMod(AddMod), +} + +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] +#[serde(rename_all = "camelCase")] +pub struct AnnounceActivity { + to: PublicUrl, + object: AnnouncableActivities, + cc: Vec, + #[serde(rename = "type")] + kind: AnnounceType, + #[serde(flatten)] + common: ActivityCommonFields, +} + +#[async_trait::async_trait(?Send)] +impl ActivityHandler for AnnounceActivity { + async fn verify( + &self, + context: &LemmyContext, + request_counter: &mut i32, + ) -> Result<(), LemmyError> { + verify_activity(self.common())?; + verify_community(&self.common.actor, context, request_counter).await?; + self.object.verify(context, request_counter).await?; + Ok(()) + } + + async fn receive( + &self, + context: &LemmyContext, + request_counter: &mut i32, + ) -> Result<(), LemmyError> { + if is_activity_already_known(context.pool(), self.object.common().id_unchecked()).await? { + return Ok(()); + } + insert_activity( + self.object.common().id_unchecked(), + self.object.clone(), + false, + true, + context.pool(), + ) + .await?; + self.object.receive(context, request_counter).await + } + + fn common(&self) -> &ActivityCommonFields { + &self.common + } +} diff --git a/crates/apub_receive/src/activities/community/block_user.rs b/crates/apub_receive/src/activities/community/block_user.rs new file mode 100644 index 00000000..03a19e9c --- /dev/null +++ b/crates/apub_receive/src/activities/community/block_user.rs @@ -0,0 +1,83 @@ +use crate::activities::{verify_activity, verify_mod_action, verify_person_in_community}; +use activitystreams::activity::kind::BlockType; +use lemmy_api_common::blocking; +use lemmy_apub::fetcher::{ + community::get_or_fetch_and_upsert_community, + person::get_or_fetch_and_upsert_person, +}; +use lemmy_apub_lib::{ActivityCommonFields, ActivityHandler, PublicUrl}; +use lemmy_db_queries::{Bannable, Followable}; +use lemmy_db_schema::source::community::{ + CommunityFollower, + CommunityFollowerForm, + CommunityPersonBan, + CommunityPersonBanForm, +}; +use lemmy_utils::LemmyError; +use lemmy_websocket::LemmyContext; +use url::Url; + +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] +#[serde(rename_all = "camelCase")] +pub struct BlockUserFromCommunity { + to: PublicUrl, + pub(in crate::activities::community) object: Url, + cc: [Url; 1], + #[serde(rename = "type")] + kind: BlockType, + #[serde(flatten)] + common: ActivityCommonFields, +} + +#[async_trait::async_trait(?Send)] +impl ActivityHandler for BlockUserFromCommunity { + async fn verify( + &self, + context: &LemmyContext, + request_counter: &mut i32, + ) -> Result<(), LemmyError> { + verify_activity(self.common())?; + verify_person_in_community(&self.common.actor, &self.cc, context, request_counter).await?; + verify_mod_action(&self.common.actor, self.cc[0].clone(), context).await?; + Ok(()) + } + + async fn receive( + &self, + context: &LemmyContext, + request_counter: &mut i32, + ) -> Result<(), LemmyError> { + let community = + get_or_fetch_and_upsert_community(&self.cc[0], context, request_counter).await?; + let blocked_user = + get_or_fetch_and_upsert_person(&self.object, context, request_counter).await?; + + let community_user_ban_form = CommunityPersonBanForm { + community_id: community.id, + person_id: blocked_user.id, + }; + + blocking(context.pool(), move |conn: &'_ _| { + CommunityPersonBan::ban(conn, &community_user_ban_form) + }) + .await??; + + // Also unsubscribe them from the community, if they are subscribed + let community_follower_form = CommunityFollowerForm { + community_id: community.id, + person_id: blocked_user.id, + pending: false, + }; + blocking(context.pool(), move |conn: &'_ _| { + CommunityFollower::unfollow(conn, &community_follower_form) + }) + .await? + .ok(); + + Ok(()) + } + + fn common(&self) -> &ActivityCommonFields { + &self.common + } +} diff --git a/crates/apub_receive/src/activities/community/mod.rs b/crates/apub_receive/src/activities/community/mod.rs new file mode 100644 index 00000000..81152d92 --- /dev/null +++ b/crates/apub_receive/src/activities/community/mod.rs @@ -0,0 +1,35 @@ +use lemmy_api_common::{blocking, community::CommunityResponse}; +use lemmy_db_schema::CommunityId; +use lemmy_db_views_actor::community_view::CommunityView; +use lemmy_utils::LemmyError; +use lemmy_websocket::{messages::SendCommunityRoomMessage, LemmyContext}; + +pub mod add_mod; +pub mod announce; +pub mod block_user; +pub mod undo_block_user; +pub mod update; + +pub(crate) async fn send_websocket_message< + OP: ToString + Send + lemmy_websocket::OperationType + 'static, +>( + community_id: CommunityId, + op: OP, + context: &LemmyContext, +) -> Result<(), LemmyError> { + let community_view = blocking(context.pool(), move |conn| { + CommunityView::read(conn, community_id, None) + }) + .await??; + + let res = CommunityResponse { community_view }; + + context.chat_server().do_send(SendCommunityRoomMessage { + op, + response: res, + community_id, + websocket_id: None, + }); + + Ok(()) +} diff --git a/crates/apub_receive/src/activities/community/undo_block_user.rs b/crates/apub_receive/src/activities/community/undo_block_user.rs new file mode 100644 index 00000000..88518385 --- /dev/null +++ b/crates/apub_receive/src/activities/community/undo_block_user.rs @@ -0,0 +1,72 @@ +use crate::activities::{ + community::block_user::BlockUserFromCommunity, + verify_activity, + verify_mod_action, + verify_person_in_community, +}; +use activitystreams::activity::kind::UndoType; +use lemmy_api_common::blocking; +use lemmy_apub::fetcher::{ + community::get_or_fetch_and_upsert_community, + person::get_or_fetch_and_upsert_person, +}; +use lemmy_apub_lib::{ActivityCommonFields, ActivityHandler, PublicUrl}; +use lemmy_db_queries::Bannable; +use lemmy_db_schema::source::community::{CommunityPersonBan, CommunityPersonBanForm}; +use lemmy_utils::LemmyError; +use lemmy_websocket::LemmyContext; +use url::Url; + +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] +#[serde(rename_all = "camelCase")] +pub struct UndoBlockUserFromCommunity { + to: PublicUrl, + object: BlockUserFromCommunity, + cc: [Url; 1], + #[serde(rename = "type")] + kind: UndoType, + #[serde(flatten)] + common: ActivityCommonFields, +} + +#[async_trait::async_trait(?Send)] +impl ActivityHandler for UndoBlockUserFromCommunity { + async fn verify( + &self, + context: &LemmyContext, + request_counter: &mut i32, + ) -> Result<(), LemmyError> { + verify_activity(self.common())?; + verify_person_in_community(&self.common.actor, &self.cc, context, request_counter).await?; + verify_mod_action(&self.common.actor, self.cc[0].clone(), context).await?; + self.object.verify(context, request_counter).await?; + Ok(()) + } + + async fn receive( + &self, + context: &LemmyContext, + request_counter: &mut i32, + ) -> Result<(), LemmyError> { + let community = + get_or_fetch_and_upsert_community(&self.cc[0], context, request_counter).await?; + let blocked_user = + get_or_fetch_and_upsert_person(&self.object.object, context, request_counter).await?; + + let community_user_ban_form = CommunityPersonBanForm { + community_id: community.id, + person_id: blocked_user.id, + }; + + blocking(context.pool(), move |conn: &'_ _| { + CommunityPersonBan::unban(conn, &community_user_ban_form) + }) + .await??; + + Ok(()) + } + + fn common(&self) -> &ActivityCommonFields { + &self.common + } +} diff --git a/crates/apub_receive/src/activities/community/update.rs b/crates/apub_receive/src/activities/community/update.rs new file mode 100644 index 00000000..a333c1bb --- /dev/null +++ b/crates/apub_receive/src/activities/community/update.rs @@ -0,0 +1,89 @@ +use crate::activities::{ + community::send_websocket_message, + verify_activity, + verify_mod_action, + verify_person_in_community, +}; +use activitystreams::activity::kind::UpdateType; +use lemmy_api_common::blocking; +use lemmy_apub::{objects::FromApubToForm, GroupExt}; +use lemmy_apub_lib::{ActivityCommonFields, ActivityHandler, PublicUrl}; +use lemmy_db_queries::{ApubObject, Crud}; +use lemmy_db_schema::source::community::{Community, CommunityForm}; +use lemmy_utils::LemmyError; +use lemmy_websocket::{LemmyContext, UserOperationCrud}; +use url::Url; + +/// This activity is received from a remote community mod, and updates the description or other +/// fields of a local community. +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] +#[serde(rename_all = "camelCase")] +pub struct UpdateCommunity { + to: PublicUrl, + object: GroupExt, + cc: [Url; 1], + #[serde(rename = "type")] + kind: UpdateType, + #[serde(flatten)] + common: ActivityCommonFields, +} + +#[async_trait::async_trait(?Send)] +impl ActivityHandler for UpdateCommunity { + async fn verify( + &self, + context: &LemmyContext, + request_counter: &mut i32, + ) -> Result<(), LemmyError> { + verify_activity(self.common())?; + verify_person_in_community(&self.common.actor, &self.cc, context, request_counter).await?; + verify_mod_action(&self.common.actor, self.cc[0].clone(), context).await?; + Ok(()) + } + + async fn receive( + &self, + context: &LemmyContext, + request_counter: &mut i32, + ) -> Result<(), LemmyError> { + let cc = self.cc[0].clone().into(); + let community = blocking(context.pool(), move |conn| { + Community::read_from_apub_id(conn, &cc) + }) + .await??; + + let updated_community = CommunityForm::from_apub( + &self.object, + context, + community.actor_id.clone().into(), + request_counter, + false, + ) + .await?; + let cf = CommunityForm { + name: updated_community.name, + title: updated_community.title, + description: updated_community.description, + nsfw: updated_community.nsfw, + // TODO: icon and banner would be hosted on the other instance, ideally we would copy it to ours + icon: updated_community.icon, + banner: updated_community.banner, + ..CommunityForm::default() + }; + let updated_community = blocking(context.pool(), move |conn| { + Community::update(conn, community.id, &cf) + }) + .await??; + + send_websocket_message( + updated_community.id, + UserOperationCrud::EditCommunity, + context, + ) + .await + } + + fn common(&self) -> &ActivityCommonFields { + &self.common + } +} diff --git a/crates/apub_receive/src/activities/deletion/delete.rs b/crates/apub_receive/src/activities/deletion/delete.rs new file mode 100644 index 00000000..0276516d --- /dev/null +++ b/crates/apub_receive/src/activities/deletion/delete.rs @@ -0,0 +1,158 @@ +use crate::activities::{ + comment::send_websocket_message as send_comment_message, + community::send_websocket_message as send_community_message, + post::send_websocket_message as send_post_message, + verify_activity, + verify_mod_action, + verify_person_in_community, +}; +use activitystreams::activity::kind::DeleteType; +use lemmy_api_common::blocking; +use lemmy_apub::{ + fetcher::{ + community::get_or_fetch_and_upsert_community, + objects::get_or_fetch_and_insert_post_or_comment, + person::get_or_fetch_and_upsert_person, + }, + ActorType, + CommunityType, + PostOrComment, +}; +use lemmy_apub_lib::{verify_urls_match, ActivityCommonFields, ActivityHandler, PublicUrl}; +use lemmy_db_queries::{ + source::{comment::Comment_, community::Community_, post::Post_}, + Crud, +}; +use lemmy_db_schema::source::{comment::Comment, community::Community, person::Person, post::Post}; +use lemmy_utils::LemmyError; +use lemmy_websocket::{LemmyContext, UserOperationCrud}; +use url::Url; + +/// This is very confusing, because there are four distinct cases to handle: +/// - user deletes their post +/// - user deletes their comment +/// - remote community mod deletes local community +/// - remote community deletes itself (triggered by a mod) +/// +/// TODO: we should probably change how community deletions work to simplify this. Probably by +/// wrapping it in an announce just like other activities, instead of having the community send it. +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] +#[serde(rename_all = "camelCase")] +pub struct DeletePostCommentOrCommunity { + to: PublicUrl, + pub(in crate::activities::deletion) object: Url, + cc: [Url; 1], + #[serde(rename = "type")] + kind: DeleteType, + #[serde(flatten)] + common: ActivityCommonFields, +} + +#[async_trait::async_trait(?Send)] +impl ActivityHandler for DeletePostCommentOrCommunity { + async fn verify( + &self, + context: &LemmyContext, + request_counter: &mut i32, + ) -> Result<(), LemmyError> { + verify_activity(self.common())?; + let object_community = + get_or_fetch_and_upsert_community(&self.object, context, request_counter).await; + // deleting a community (set counter 0 to only fetch from local db) + if object_community.is_ok() { + verify_mod_action(&self.common.actor, self.object.clone(), context).await?; + } + // deleting a post or comment + else { + verify_person_in_community(&self.common().actor, &self.cc, context, request_counter).await?; + let object_creator = + get_post_or_comment_actor_id(&self.object, context, request_counter).await?; + verify_urls_match(&self.common.actor, &object_creator)?; + } + Ok(()) + } + + async fn receive( + &self, + context: &LemmyContext, + request_counter: &mut i32, + ) -> Result<(), LemmyError> { + let object_community = + get_or_fetch_and_upsert_community(&self.object, context, request_counter).await; + // deleting a community + if let Ok(community) = object_community { + if community.local { + // repeat these checks just to be sure + verify_person_in_community(&self.common().actor, &self.cc, context, request_counter) + .await?; + verify_mod_action(&self.common.actor, self.object.clone(), context).await?; + let mod_ = + get_or_fetch_and_upsert_person(&self.common.actor, context, request_counter).await?; + community.send_delete(mod_, context).await?; + } + let deleted_community = blocking(context.pool(), move |conn| { + Community::update_deleted(conn, community.id, true) + }) + .await??; + + send_community_message( + deleted_community.id, + UserOperationCrud::DeleteCommunity, + context, + ) + .await + } + // deleting a post or comment + else { + match get_or_fetch_and_insert_post_or_comment(&self.object, context, request_counter).await? { + PostOrComment::Post(post) => { + let deleted_post = blocking(context.pool(), move |conn| { + Post::update_deleted(conn, post.id, true) + }) + .await??; + send_post_message(deleted_post.id, UserOperationCrud::EditPost, context).await + } + PostOrComment::Comment(comment) => { + let deleted_comment = blocking(context.pool(), move |conn| { + Comment::update_deleted(conn, comment.id, true) + }) + .await??; + send_comment_message( + deleted_comment.id, + vec![], + UserOperationCrud::EditComment, + context, + ) + .await + } + } + } + } + + fn common(&self) -> &ActivityCommonFields { + &self.common + } +} + +async fn get_post_or_comment_actor_id( + object: &Url, + context: &LemmyContext, + request_counter: &mut i32, +) -> Result { + let actor_id = + match get_or_fetch_and_insert_post_or_comment(object, context, request_counter).await? { + PostOrComment::Post(post) => { + let creator_id = post.creator_id; + blocking(context.pool(), move |conn| Person::read(conn, creator_id)) + .await?? + .actor_id() + } + PostOrComment::Comment(comment) => { + let creator_id = comment.creator_id; + blocking(context.pool(), move |conn| Person::read(conn, creator_id)) + .await?? + .actor_id() + } + }; + Ok(actor_id) +} diff --git a/crates/apub_receive/src/activities/deletion/mod.rs b/crates/apub_receive/src/activities/deletion/mod.rs new file mode 100644 index 00000000..b440edd6 --- /dev/null +++ b/crates/apub_receive/src/activities/deletion/mod.rs @@ -0,0 +1,2 @@ +pub mod delete; +pub mod undo_delete; diff --git a/crates/apub_receive/src/activities/deletion/undo_delete.rs b/crates/apub_receive/src/activities/deletion/undo_delete.rs new file mode 100644 index 00000000..9875a2a9 --- /dev/null +++ b/crates/apub_receive/src/activities/deletion/undo_delete.rs @@ -0,0 +1,125 @@ +use crate::activities::{ + comment::send_websocket_message as send_comment_message, + community::send_websocket_message as send_community_message, + deletion::delete::DeletePostCommentOrCommunity, + post::send_websocket_message as send_post_message, + verify_activity, + verify_mod_action, + verify_person_in_community, +}; +use activitystreams::activity::kind::UndoType; +use lemmy_api_common::blocking; +use lemmy_apub::{ + fetcher::{ + community::get_or_fetch_and_upsert_community, + objects::get_or_fetch_and_insert_post_or_comment, + person::get_or_fetch_and_upsert_person, + }, + CommunityType, + PostOrComment, +}; +use lemmy_apub_lib::{verify_urls_match, ActivityCommonFields, ActivityHandler, PublicUrl}; +use lemmy_db_queries::source::{comment::Comment_, community::Community_, post::Post_}; +use lemmy_db_schema::source::{comment::Comment, community::Community, post::Post}; +use lemmy_utils::LemmyError; +use lemmy_websocket::{LemmyContext, UserOperationCrud}; +use url::Url; + +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] +#[serde(rename_all = "camelCase")] +pub struct UndoDeletePostCommentOrCommunity { + to: PublicUrl, + object: DeletePostCommentOrCommunity, + cc: [Url; 1], + #[serde(rename = "type")] + kind: UndoType, + #[serde(flatten)] + common: ActivityCommonFields, +} + +#[async_trait::async_trait(?Send)] +impl ActivityHandler for UndoDeletePostCommentOrCommunity { + async fn verify( + &self, + context: &LemmyContext, + request_counter: &mut i32, + ) -> Result<(), LemmyError> { + verify_activity(self.common())?; + self.object.verify(context, request_counter).await?; + let object_community = + get_or_fetch_and_upsert_community(&self.object.object, context, request_counter).await; + // restoring a community + if object_community.is_ok() { + verify_mod_action(&self.common.actor, self.object.object.clone(), context).await?; + } + // restoring a post or comment + else { + verify_person_in_community(&self.common().actor, &self.cc, context, request_counter).await?; + verify_urls_match(&self.common.actor, &self.object.common().actor)?; + } + Ok(()) + } + + async fn receive( + &self, + context: &LemmyContext, + request_counter: &mut i32, + ) -> Result<(), LemmyError> { + let object_community = + get_or_fetch_and_upsert_community(&self.object.object, context, request_counter).await; + // restoring a community + if let Ok(community) = object_community { + if community.local { + // repeat these checks just to be sure + verify_person_in_community(&self.common().actor, &self.cc, context, request_counter) + .await?; + verify_mod_action(&self.common.actor, self.object.object.clone(), context).await?; + let mod_ = + get_or_fetch_and_upsert_person(&self.common.actor, context, request_counter).await?; + community.send_undo_delete(mod_, context).await?; + } + let deleted_community = blocking(context.pool(), move |conn| { + Community::update_deleted(conn, community.id, false) + }) + .await??; + + send_community_message( + deleted_community.id, + UserOperationCrud::EditCommunity, + context, + ) + .await + } + // restoring a post or comment + else { + match get_or_fetch_and_insert_post_or_comment(&self.object.object, context, request_counter) + .await? + { + PostOrComment::Post(post) => { + let deleted_post = blocking(context.pool(), move |conn| { + Post::update_deleted(conn, post.id, false) + }) + .await??; + send_post_message(deleted_post.id, UserOperationCrud::EditPost, context).await + } + PostOrComment::Comment(comment) => { + let deleted_comment = blocking(context.pool(), move |conn| { + Comment::update_deleted(conn, comment.id, false) + }) + .await??; + send_comment_message( + deleted_comment.id, + vec![], + UserOperationCrud::EditComment, + context, + ) + .await + } + } + } + } + + fn common(&self) -> &ActivityCommonFields { + &self.common + } +} diff --git a/crates/apub_receive/src/activities/following/accept.rs b/crates/apub_receive/src/activities/following/accept.rs new file mode 100644 index 00000000..d5cf474d --- /dev/null +++ b/crates/apub_receive/src/activities/following/accept.rs @@ -0,0 +1,62 @@ +use crate::activities::{following::follow::FollowCommunity, verify_activity, verify_community}; +use activitystreams::activity::kind::AcceptType; +use lemmy_api_common::blocking; +use lemmy_apub::fetcher::{ + community::get_or_fetch_and_upsert_community, + person::get_or_fetch_and_upsert_person, +}; +use lemmy_apub_lib::{verify_urls_match, ActivityCommonFields, ActivityHandler}; +use lemmy_db_queries::Followable; +use lemmy_db_schema::source::community::CommunityFollower; +use lemmy_utils::LemmyError; +use lemmy_websocket::LemmyContext; +use url::Url; + +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] +#[serde(rename_all = "camelCase")] +pub struct AcceptFollowCommunity { + to: Url, + object: FollowCommunity, + #[serde(rename = "type")] + kind: AcceptType, + #[serde(flatten)] + common: ActivityCommonFields, +} + +/// Handle accepted follows +#[async_trait::async_trait(?Send)] +impl ActivityHandler for AcceptFollowCommunity { + async fn verify( + &self, + context: &LemmyContext, + request_counter: &mut i32, + ) -> Result<(), LemmyError> { + verify_activity(self.common())?; + verify_urls_match(&self.to, &self.object.common.actor)?; + verify_urls_match(&self.common.actor, &self.object.to)?; + verify_community(&self.common.actor, context, request_counter).await?; + self.object.verify(context, request_counter).await?; + Ok(()) + } + + async fn receive( + &self, + context: &LemmyContext, + request_counter: &mut i32, + ) -> Result<(), LemmyError> { + let actor = + get_or_fetch_and_upsert_community(&self.common.actor, context, request_counter).await?; + let to = get_or_fetch_and_upsert_person(&self.to, context, request_counter).await?; + // This will throw an error if no follow was requested + blocking(context.pool(), move |conn| { + CommunityFollower::follow_accepted(conn, actor.id, to.id) + }) + .await??; + + Ok(()) + } + + fn common(&self) -> &ActivityCommonFields { + &self.common + } +} diff --git a/crates/apub_receive/src/activities/following/follow.rs b/crates/apub_receive/src/activities/following/follow.rs new file mode 100644 index 00000000..fd0649f1 --- /dev/null +++ b/crates/apub_receive/src/activities/following/follow.rs @@ -0,0 +1,73 @@ +use crate::activities::{verify_activity, verify_person}; +use activitystreams::{ + activity::{kind::FollowType, Follow}, + base::{AnyBase, ExtendsExt}, +}; +use anyhow::Context; +use lemmy_api_common::blocking; +use lemmy_apub::{ + fetcher::{community::get_or_fetch_and_upsert_community, person::get_or_fetch_and_upsert_person}, + CommunityType, +}; +use lemmy_apub_lib::{verify_urls_match, ActivityCommonFields, ActivityHandler}; +use lemmy_db_queries::Followable; +use lemmy_db_schema::source::community::{CommunityFollower, CommunityFollowerForm}; +use lemmy_utils::{location_info, LemmyError}; +use lemmy_websocket::LemmyContext; +use url::Url; + +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] +#[serde(rename_all = "camelCase")] +pub struct FollowCommunity { + pub(in crate::activities::following) to: Url, + pub(in crate::activities::following) object: Url, + #[serde(rename = "type")] + kind: FollowType, + #[serde(flatten)] + pub(in crate::activities::following) common: ActivityCommonFields, +} + +#[async_trait::async_trait(?Send)] +impl ActivityHandler for FollowCommunity { + async fn verify( + &self, + context: &LemmyContext, + request_counter: &mut i32, + ) -> Result<(), LemmyError> { + verify_activity(self.common())?; + verify_urls_match(&self.to, &self.object)?; + verify_person(&self.common.actor, context, request_counter).await?; + Ok(()) + } + + async fn receive( + &self, + context: &LemmyContext, + request_counter: &mut i32, + ) -> Result<(), LemmyError> { + let actor = + get_or_fetch_and_upsert_person(&self.common.actor, context, request_counter).await?; + let community = + get_or_fetch_and_upsert_community(&self.object, context, request_counter).await?; + let community_follower_form = CommunityFollowerForm { + community_id: community.id, + person_id: actor.id, + pending: false, + }; + + // This will fail if they're already a follower, but ignore the error. + blocking(context.pool(), move |conn| { + CommunityFollower::follow(conn, &community_follower_form).ok() + }) + .await?; + + // TODO: avoid the conversion and pass our own follow struct directly + let anybase = AnyBase::from_arbitrary_json(serde_json::to_string(self)?)?; + let anybase = Follow::from_any_base(anybase)?.context(location_info!())?; + community.send_accept_follow(anybase, context).await + } + + fn common(&self) -> &ActivityCommonFields { + &self.common + } +} diff --git a/crates/apub_receive/src/activities/following/mod.rs b/crates/apub_receive/src/activities/following/mod.rs new file mode 100644 index 00000000..050c3691 --- /dev/null +++ b/crates/apub_receive/src/activities/following/mod.rs @@ -0,0 +1,3 @@ +pub mod accept; +pub mod follow; +pub mod undo; diff --git a/crates/apub_receive/src/activities/following/undo.rs b/crates/apub_receive/src/activities/following/undo.rs new file mode 100644 index 00000000..1f3d12e6 --- /dev/null +++ b/crates/apub_receive/src/activities/following/undo.rs @@ -0,0 +1,67 @@ +use crate::activities::{following::follow::FollowCommunity, verify_activity, verify_person}; +use activitystreams::activity::kind::UndoType; +use lemmy_api_common::blocking; +use lemmy_apub::fetcher::{ + community::get_or_fetch_and_upsert_community, + person::get_or_fetch_and_upsert_person, +}; +use lemmy_apub_lib::{verify_urls_match, ActivityCommonFields, ActivityHandler}; +use lemmy_db_queries::Followable; +use lemmy_db_schema::source::community::{CommunityFollower, CommunityFollowerForm}; +use lemmy_utils::LemmyError; +use lemmy_websocket::LemmyContext; +use url::Url; + +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] +#[serde(rename_all = "camelCase")] +pub struct UndoFollowCommunity { + to: Url, + object: FollowCommunity, + #[serde(rename = "type")] + kind: UndoType, + #[serde(flatten)] + common: ActivityCommonFields, +} + +#[async_trait::async_trait(?Send)] +impl ActivityHandler for UndoFollowCommunity { + async fn verify( + &self, + context: &LemmyContext, + request_counter: &mut i32, + ) -> Result<(), LemmyError> { + verify_activity(self.common())?; + verify_urls_match(&self.to, &self.object.object)?; + verify_urls_match(&self.common.actor, &self.object.common.actor)?; + verify_person(&self.common.actor, context, request_counter).await?; + self.object.verify(context, request_counter).await?; + Ok(()) + } + + async fn receive( + &self, + context: &LemmyContext, + request_counter: &mut i32, + ) -> Result<(), LemmyError> { + let actor = + get_or_fetch_and_upsert_person(&self.common.actor, context, request_counter).await?; + let community = get_or_fetch_and_upsert_community(&self.to, context, request_counter).await?; + + let community_follower_form = CommunityFollowerForm { + community_id: community.id, + person_id: actor.id, + pending: false, + }; + + // This will fail if they aren't a follower, but ignore the error. + blocking(context.pool(), move |conn| { + CommunityFollower::unfollow(conn, &community_follower_form).ok() + }) + .await?; + Ok(()) + } + + fn common(&self) -> &ActivityCommonFields { + &self.common + } +} diff --git a/crates/apub_receive/src/activities/mod.rs b/crates/apub_receive/src/activities/mod.rs index cbdeafaf..098d209f 100644 --- a/crates/apub_receive/src/activities/mod.rs +++ b/crates/apub_receive/src/activities/mod.rs @@ -1 +1,121 @@ -pub(crate) mod receive; +use anyhow::anyhow; +use lemmy_api_common::blocking; +use lemmy_apub::{ + check_community_or_site_ban, + check_is_apub_id_valid, + fetcher::{community::get_or_fetch_and_upsert_community, person::get_or_fetch_and_upsert_person}, + generate_moderators_url, +}; +use lemmy_apub_lib::{verify_domains_match, ActivityCommonFields}; +use lemmy_db_queries::ApubObject; +use lemmy_db_schema::{ + source::{community::Community, person::Person}, + DbUrl, +}; +use lemmy_db_views_actor::community_view::CommunityView; +use lemmy_utils::LemmyError; +use lemmy_websocket::LemmyContext; +use url::Url; + +pub mod comment; +pub mod community; +pub mod deletion; +pub mod following; +pub mod post; +pub mod private_message; +pub mod removal; +pub mod voting; + +/// Checks that the specified Url actually identifies a Person (by fetching it), and that the person +/// doesn't have a site ban. +async fn verify_person( + person_id: &Url, + context: &LemmyContext, + request_counter: &mut i32, +) -> Result<(), LemmyError> { + let person = get_or_fetch_and_upsert_person(person_id, context, request_counter).await?; + if person.banned { + return Err(anyhow!("Person {} is banned", person_id).into()); + } + Ok(()) +} + +/// Fetches the person and community to verify their type, then checks if person is banned from site +/// or community. +async fn verify_person_in_community( + person_id: &Url, + cc: &[Url], + context: &LemmyContext, + request_counter: &mut i32, +) -> Result { + let person = get_or_fetch_and_upsert_person(person_id, context, request_counter).await?; + let mut cc_iter = cc.iter(); + let community: Community = loop { + if let Some(cid) = cc_iter.next() { + if let Ok(c) = get_or_fetch_and_upsert_community(cid, context, request_counter).await { + break c; + } + } else { + return Err(anyhow!("No community found in cc").into()); + } + }; + check_community_or_site_ban(&person, community.id, context.pool()).await?; + Ok(community) +} + +/// Simply check that the url actually refers to a valid group. +async fn verify_community( + community_id: &Url, + context: &LemmyContext, + request_counter: &mut i32, +) -> Result<(), LemmyError> { + get_or_fetch_and_upsert_community(community_id, context, request_counter).await?; + Ok(()) +} + +fn verify_activity(common: &ActivityCommonFields) -> Result<(), LemmyError> { + check_is_apub_id_valid(&common.actor, false)?; + verify_domains_match(common.id_unchecked(), &common.actor)?; + Ok(()) +} + +async fn verify_mod_action( + actor_id: &Url, + activity_cc: Url, + context: &LemmyContext, +) -> Result<(), LemmyError> { + let community = blocking(context.pool(), move |conn| { + Community::read_from_apub_id(conn, &activity_cc.into()) + }) + .await??; + + if community.local { + let actor_id: DbUrl = actor_id.clone().into(); + let actor = blocking(context.pool(), move |conn| { + Person::read_from_apub_id(conn, &actor_id) + }) + .await??; + + // Note: this will also return true for admins in addition to mods, but as we dont know about + // remote admins, it doesnt make any difference. + let community_id = community.id; + let actor_id = actor.id; + let is_mod_or_admin = blocking(context.pool(), move |conn| { + CommunityView::is_mod_or_admin(conn, actor_id, community_id) + }) + .await?; + if !is_mod_or_admin { + return Err(anyhow!("Not a mod").into()); + } + } + Ok(()) +} + +/// For Add/Remove community moderator activities, check that the target field actually contains +/// /c/community/moderators. Any different values are unsupported. +fn verify_add_remove_moderator_target(target: &Url, community: Url) -> Result<(), LemmyError> { + if target != &generate_moderators_url(&community.into())?.into_inner() { + return Err(anyhow!("Unkown target url").into()); + } + Ok(()) +} diff --git a/crates/apub_receive/src/activities/post/create.rs b/crates/apub_receive/src/activities/post/create.rs new file mode 100644 index 00000000..2d31fa0d --- /dev/null +++ b/crates/apub_receive/src/activities/post/create.rs @@ -0,0 +1,66 @@ +use crate::activities::{ + post::send_websocket_message, + verify_activity, + verify_person_in_community, +}; +use activitystreams::{activity::kind::CreateType, base::BaseExt}; +use lemmy_apub::{ + fetcher::person::get_or_fetch_and_upsert_person, + objects::FromApub, + ActorType, + PageExt, +}; +use lemmy_apub_lib::{verify_domains_match_opt, ActivityCommonFields, ActivityHandler, PublicUrl}; +use lemmy_db_schema::source::post::Post; +use lemmy_utils::LemmyError; +use lemmy_websocket::{LemmyContext, UserOperationCrud}; +use url::Url; + +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CreatePost { + to: PublicUrl, + object: PageExt, + cc: Vec, + #[serde(rename = "type")] + kind: CreateType, + #[serde(flatten)] + common: ActivityCommonFields, +} + +#[async_trait::async_trait(?Send)] +impl ActivityHandler for CreatePost { + async fn verify( + &self, + context: &LemmyContext, + request_counter: &mut i32, + ) -> Result<(), LemmyError> { + verify_activity(self.common())?; + verify_person_in_community(&self.common.actor, &self.cc, context, request_counter).await?; + verify_domains_match_opt(&self.common.actor, self.object.id_unchecked())?; + Ok(()) + } + + async fn receive( + &self, + context: &LemmyContext, + request_counter: &mut i32, + ) -> Result<(), LemmyError> { + let actor = + get_or_fetch_and_upsert_person(&self.common.actor, context, request_counter).await?; + let post = Post::from_apub( + &self.object, + context, + actor.actor_id(), + request_counter, + false, + ) + .await?; + + send_websocket_message(post.id, UserOperationCrud::CreatePost, context).await + } + + fn common(&self) -> &ActivityCommonFields { + &self.common + } +} diff --git a/crates/apub_receive/src/activities/post/mod.rs b/crates/apub_receive/src/activities/post/mod.rs new file mode 100644 index 00000000..a4662040 --- /dev/null +++ b/crates/apub_receive/src/activities/post/mod.rs @@ -0,0 +1,31 @@ +use lemmy_api_common::{blocking, post::PostResponse}; +use lemmy_db_schema::PostId; +use lemmy_db_views::post_view::PostView; +use lemmy_utils::LemmyError; +use lemmy_websocket::{messages::SendPost, LemmyContext}; + +pub mod create; +pub mod update; + +pub(crate) async fn send_websocket_message< + OP: ToString + Send + lemmy_websocket::OperationType + 'static, +>( + post_id: PostId, + op: OP, + context: &LemmyContext, +) -> Result<(), LemmyError> { + let post_view = blocking(context.pool(), move |conn| { + PostView::read(conn, post_id, None) + }) + .await??; + + let res = PostResponse { post_view }; + + context.chat_server().do_send(SendPost { + op, + post: res, + websocket_id: None, + }); + + Ok(()) +} diff --git a/crates/apub_receive/src/activities/post/update.rs b/crates/apub_receive/src/activities/post/update.rs new file mode 100644 index 00000000..1f592492 --- /dev/null +++ b/crates/apub_receive/src/activities/post/update.rs @@ -0,0 +1,96 @@ +use crate::activities::{ + post::send_websocket_message, + verify_activity, + verify_mod_action, + verify_person_in_community, +}; +use activitystreams::{activity::kind::UpdateType, base::BaseExt}; +use anyhow::Context; +use lemmy_api_common::blocking; +use lemmy_apub::{ + objects::{FromApub, FromApubToForm}, + ActorType, + PageExt, +}; +use lemmy_apub_lib::{verify_domains_match_opt, ActivityCommonFields, ActivityHandler, PublicUrl}; +use lemmy_db_queries::ApubObject; +use lemmy_db_schema::{ + source::post::{Post, PostForm}, + DbUrl, +}; +use lemmy_utils::{location_info, LemmyError}; +use lemmy_websocket::{LemmyContext, UserOperationCrud}; +use url::Url; + +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] +#[serde(rename_all = "camelCase")] +pub struct UpdatePost { + to: PublicUrl, + object: PageExt, + cc: Vec, + #[serde(rename = "type")] + kind: UpdateType, + #[serde(flatten)] + common: ActivityCommonFields, +} + +#[async_trait::async_trait(?Send)] +impl ActivityHandler for UpdatePost { + async fn verify( + &self, + context: &LemmyContext, + request_counter: &mut i32, + ) -> Result<(), LemmyError> { + verify_activity(self.common())?; + let community = + verify_person_in_community(&self.common.actor, &self.cc, context, request_counter).await?; + + let temp_post = PostForm::from_apub( + &self.object, + context, + self.common.actor.clone(), + request_counter, + true, + ) + .await?; + let post_id: DbUrl = temp_post.ap_id.context(location_info!())?; + let old_post = blocking(context.pool(), move |conn| { + Post::read_from_apub_id(conn, &post_id) + }) + .await??; + let stickied = temp_post.stickied.context(location_info!())?; + let locked = temp_post.locked.context(location_info!())?; + // community mod changed locked/sticky status + if (stickied != old_post.stickied) || (locked != old_post.locked) { + verify_mod_action(&self.common.actor, community.actor_id(), context).await?; + } + // user edited their own post + else { + verify_domains_match_opt(&self.common.actor, self.object.id_unchecked())?; + } + + Ok(()) + } + + async fn receive( + &self, + context: &LemmyContext, + request_counter: &mut i32, + ) -> Result<(), LemmyError> { + let post = Post::from_apub( + &self.object, + context, + self.common.actor.clone(), + request_counter, + // TODO: we already check here if the mod action is valid, can remove that check param + true, + ) + .await?; + + send_websocket_message(post.id, UserOperationCrud::EditPost, context).await + } + + fn common(&self) -> &ActivityCommonFields { + &self.common + } +} diff --git a/crates/apub_receive/src/activities/private_message/create.rs b/crates/apub_receive/src/activities/private_message/create.rs new file mode 100644 index 00000000..2d766565 --- /dev/null +++ b/crates/apub_receive/src/activities/private_message/create.rs @@ -0,0 +1,61 @@ +use crate::activities::{private_message::send_websocket_message, verify_activity, verify_person}; +use activitystreams::{activity::kind::CreateType, base::BaseExt}; +use lemmy_apub::{objects::FromApub, NoteExt}; +use lemmy_apub_lib::{verify_domains_match_opt, ActivityCommonFields, ActivityHandler}; +use lemmy_db_schema::source::private_message::PrivateMessage; +use lemmy_utils::LemmyError; +use lemmy_websocket::{LemmyContext, UserOperationCrud}; +use url::Url; + +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CreatePrivateMessage { + to: Url, + object: NoteExt, + #[serde(rename = "type")] + kind: CreateType, + #[serde(flatten)] + common: ActivityCommonFields, +} + +#[async_trait::async_trait(?Send)] +impl ActivityHandler for CreatePrivateMessage { + async fn verify( + &self, + context: &LemmyContext, + request_counter: &mut i32, + ) -> Result<(), LemmyError> { + verify_activity(self.common())?; + verify_person(&self.common.actor, context, request_counter).await?; + verify_domains_match_opt(&self.common.actor, self.object.id_unchecked())?; + Ok(()) + } + + async fn receive( + &self, + context: &LemmyContext, + request_counter: &mut i32, + ) -> Result<(), LemmyError> { + let private_message = PrivateMessage::from_apub( + &self.object, + context, + self.common.actor.clone(), + request_counter, + false, + ) + .await?; + + send_websocket_message( + private_message.id, + UserOperationCrud::CreatePrivateMessage, + context, + ) + .await?; + + Ok(()) + } + + fn common(&self) -> &ActivityCommonFields { + &self.common + } +} diff --git a/crates/apub_receive/src/activities/private_message/delete.rs b/crates/apub_receive/src/activities/private_message/delete.rs new file mode 100644 index 00000000..b7f1a3cd --- /dev/null +++ b/crates/apub_receive/src/activities/private_message/delete.rs @@ -0,0 +1,63 @@ +use crate::activities::{private_message::send_websocket_message, verify_activity, verify_person}; +use activitystreams::activity::kind::DeleteType; +use lemmy_api_common::blocking; +use lemmy_apub_lib::{verify_domains_match, ActivityCommonFields, ActivityHandler}; +use lemmy_db_queries::{source::private_message::PrivateMessage_, ApubObject}; +use lemmy_db_schema::source::private_message::PrivateMessage; +use lemmy_utils::LemmyError; +use lemmy_websocket::{LemmyContext, UserOperationCrud}; +use url::Url; + +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] +#[serde(rename_all = "camelCase")] +pub struct DeletePrivateMessage { + to: Url, + pub(in crate::activities::private_message) object: Url, + #[serde(rename = "type")] + kind: DeleteType, + #[serde(flatten)] + pub(in crate::activities::private_message) common: ActivityCommonFields, +} + +#[async_trait::async_trait(?Send)] +impl ActivityHandler for DeletePrivateMessage { + async fn verify( + &self, + context: &LemmyContext, + request_counter: &mut i32, + ) -> Result<(), LemmyError> { + verify_activity(self.common())?; + verify_person(&self.common.actor, context, request_counter).await?; + verify_domains_match(&self.common.actor, &self.object)?; + Ok(()) + } + + async fn receive( + &self, + context: &LemmyContext, + _request_counter: &mut i32, + ) -> Result<(), LemmyError> { + let ap_id = self.object.clone(); + let private_message = blocking(context.pool(), move |conn| { + PrivateMessage::read_from_apub_id(conn, &ap_id.into()) + }) + .await??; + let deleted_private_message = blocking(context.pool(), move |conn| { + PrivateMessage::update_deleted(conn, private_message.id, true) + }) + .await??; + + send_websocket_message( + deleted_private_message.id, + UserOperationCrud::DeletePrivateMessage, + context, + ) + .await?; + + Ok(()) + } + + fn common(&self) -> &ActivityCommonFields { + &self.common + } +} diff --git a/crates/apub_receive/src/activities/private_message/mod.rs b/crates/apub_receive/src/activities/private_message/mod.rs new file mode 100644 index 00000000..beb28b29 --- /dev/null +++ b/crates/apub_receive/src/activities/private_message/mod.rs @@ -0,0 +1,42 @@ +use lemmy_api_common::{blocking, person::PrivateMessageResponse}; +use lemmy_db_schema::PrivateMessageId; +use lemmy_db_views::{local_user_view::LocalUserView, private_message_view::PrivateMessageView}; +use lemmy_utils::LemmyError; +use lemmy_websocket::{messages::SendUserRoomMessage, LemmyContext, UserOperationCrud}; + +pub mod create; +pub mod delete; +pub mod undo_delete; +pub mod update; + +async fn send_websocket_message( + private_message_id: PrivateMessageId, + op: UserOperationCrud, + context: &LemmyContext, +) -> Result<(), LemmyError> { + let message = blocking(context.pool(), move |conn| { + PrivateMessageView::read(conn, private_message_id) + }) + .await??; + let res = PrivateMessageResponse { + private_message_view: message, + }; + + // Send notifications to the local recipient, if one exists + let recipient_id = res.private_message_view.recipient.id; + let local_recipient_id = blocking(context.pool(), move |conn| { + LocalUserView::read_person(conn, recipient_id) + }) + .await?? + .local_user + .id; + + context.chat_server().do_send(SendUserRoomMessage { + op, + response: res, + local_recipient_id, + websocket_id: None, + }); + + Ok(()) +} diff --git a/crates/apub_receive/src/activities/private_message/undo_delete.rs b/crates/apub_receive/src/activities/private_message/undo_delete.rs new file mode 100644 index 00000000..dc297b5a --- /dev/null +++ b/crates/apub_receive/src/activities/private_message/undo_delete.rs @@ -0,0 +1,75 @@ +use crate::activities::{ + private_message::{delete::DeletePrivateMessage, send_websocket_message}, + verify_activity, + verify_person, +}; +use activitystreams::activity::kind::UndoType; +use lemmy_api_common::blocking; +use lemmy_apub_lib::{ + verify_domains_match, + verify_urls_match, + ActivityCommonFields, + ActivityHandler, +}; +use lemmy_db_queries::{source::private_message::PrivateMessage_, ApubObject}; +use lemmy_db_schema::source::private_message::PrivateMessage; +use lemmy_utils::LemmyError; +use lemmy_websocket::{LemmyContext, UserOperationCrud}; +use url::Url; + +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] +#[serde(rename_all = "camelCase")] +pub struct UndoDeletePrivateMessage { + to: Url, + object: DeletePrivateMessage, + #[serde(rename = "type")] + kind: UndoType, + #[serde(flatten)] + common: ActivityCommonFields, +} + +#[async_trait::async_trait(?Send)] +impl ActivityHandler for UndoDeletePrivateMessage { + async fn verify( + &self, + context: &LemmyContext, + request_counter: &mut i32, + ) -> Result<(), LemmyError> { + verify_activity(self.common())?; + verify_person(&self.common.actor, context, request_counter).await?; + verify_urls_match(&self.common.actor, &self.object.common.actor)?; + verify_domains_match(&self.common.actor, &self.object.object)?; + self.object.verify(context, request_counter).await?; + Ok(()) + } + + async fn receive( + &self, + context: &LemmyContext, + _request_counter: &mut i32, + ) -> Result<(), LemmyError> { + let ap_id = self.object.object.clone(); + let private_message = blocking(context.pool(), move |conn| { + PrivateMessage::read_from_apub_id(conn, &ap_id.into()) + }) + .await??; + + let deleted_private_message = blocking(context.pool(), move |conn| { + PrivateMessage::update_deleted(conn, private_message.id, false) + }) + .await??; + + send_websocket_message( + deleted_private_message.id, + UserOperationCrud::EditPrivateMessage, + context, + ) + .await?; + + Ok(()) + } + + fn common(&self) -> &ActivityCommonFields { + &self.common + } +} diff --git a/crates/apub_receive/src/activities/private_message/update.rs b/crates/apub_receive/src/activities/private_message/update.rs new file mode 100644 index 00000000..56a79f8e --- /dev/null +++ b/crates/apub_receive/src/activities/private_message/update.rs @@ -0,0 +1,61 @@ +use crate::activities::{private_message::send_websocket_message, verify_activity, verify_person}; +use activitystreams::{activity::kind::UpdateType, base::BaseExt}; +use lemmy_apub::{objects::FromApub, NoteExt}; +use lemmy_apub_lib::{verify_domains_match_opt, ActivityCommonFields, ActivityHandler}; +use lemmy_db_schema::source::private_message::PrivateMessage; +use lemmy_utils::LemmyError; +use lemmy_websocket::{LemmyContext, UserOperationCrud}; +use url::Url; + +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] +#[serde(rename_all = "camelCase")] +pub struct UpdatePrivateMessage { + to: Url, + object: NoteExt, + #[serde(rename = "type")] + kind: UpdateType, + #[serde(flatten)] + common: ActivityCommonFields, +} + +#[async_trait::async_trait(?Send)] +impl ActivityHandler for UpdatePrivateMessage { + async fn verify( + &self, + context: &LemmyContext, + request_counter: &mut i32, + ) -> Result<(), LemmyError> { + verify_activity(self.common())?; + verify_person(&self.common.actor, context, request_counter).await?; + verify_domains_match_opt(&self.common.actor, self.object.id_unchecked())?; + Ok(()) + } + + async fn receive( + &self, + context: &LemmyContext, + request_counter: &mut i32, + ) -> Result<(), LemmyError> { + let private_message = PrivateMessage::from_apub( + &self.object, + context, + self.common.actor.clone(), + request_counter, + false, + ) + .await?; + + send_websocket_message( + private_message.id, + UserOperationCrud::EditPrivateMessage, + context, + ) + .await?; + + Ok(()) + } + + fn common(&self) -> &ActivityCommonFields { + &self.common + } +} diff --git a/crates/apub_receive/src/activities/receive/comment.rs b/crates/apub_receive/src/activities/receive/comment.rs deleted file mode 100644 index 18508fa2..00000000 --- a/crates/apub_receive/src/activities/receive/comment.rs +++ /dev/null @@ -1,262 +0,0 @@ -use crate::activities::receive::get_actor_as_person; -use activitystreams::{ - activity::{ActorAndObjectRefExt, Create, Dislike, Like, Update}, - base::ExtendsExt, -}; -use anyhow::Context; -use lemmy_api_common::{blocking, comment::CommentResponse, send_local_notifs}; -use lemmy_apub::{objects::FromApub, ActorType, NoteExt}; -use lemmy_db_queries::{source::comment::Comment_, Crud, Likeable}; -use lemmy_db_schema::source::{ - comment::{Comment, CommentLike, CommentLikeForm}, - post::Post, -}; -use lemmy_db_views::comment_view::CommentView; -use lemmy_utils::{location_info, utils::scrape_text_for_mentions, LemmyError}; -use lemmy_websocket::{messages::SendComment, LemmyContext, UserOperation, UserOperationCrud}; - -pub(crate) async fn receive_create_comment( - create: Create, - context: &LemmyContext, - request_counter: &mut i32, -) -> Result<(), LemmyError> { - let person = get_actor_as_person(&create, context, request_counter).await?; - let note = NoteExt::from_any_base(create.object().to_owned().one().context(location_info!())?)? - .context(location_info!())?; - - let comment = - Comment::from_apub(¬e, context, person.actor_id(), request_counter, false).await?; - - let post_id = comment.post_id; - let post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??; - - // Note: - // Although mentions could be gotten from the post tags (they are included there), or the ccs, - // Its much easier to scrape them from the comment body, since the API has to do that - // anyway. - let mentions = scrape_text_for_mentions(&comment.content); - let recipient_ids = send_local_notifs( - mentions, - comment.clone(), - person, - post, - context.pool(), - true, - ) - .await?; - - // Refetch the view - let comment_view = blocking(context.pool(), move |conn| { - CommentView::read(conn, comment.id, None) - }) - .await??; - - let res = CommentResponse { - comment_view, - recipient_ids, - form_id: None, - }; - - context.chat_server().do_send(SendComment { - op: UserOperationCrud::CreateComment, - comment: res, - websocket_id: None, - }); - - Ok(()) -} - -pub(crate) async fn receive_update_comment( - update: Update, - context: &LemmyContext, - request_counter: &mut i32, -) -> Result<(), LemmyError> { - let note = NoteExt::from_any_base(update.object().to_owned().one().context(location_info!())?)? - .context(location_info!())?; - let person = get_actor_as_person(&update, context, request_counter).await?; - - let comment = - Comment::from_apub(¬e, context, person.actor_id(), request_counter, false).await?; - - let comment_id = comment.id; - let post_id = comment.post_id; - let post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??; - - let mentions = scrape_text_for_mentions(&comment.content); - let recipient_ids = - send_local_notifs(mentions, comment, person, post, context.pool(), false).await?; - - // Refetch the view - let comment_view = blocking(context.pool(), move |conn| { - CommentView::read(conn, comment_id, None) - }) - .await??; - - let res = CommentResponse { - comment_view, - recipient_ids, - form_id: None, - }; - - context.chat_server().do_send(SendComment { - op: UserOperationCrud::EditComment, - comment: res, - websocket_id: None, - }); - - Ok(()) -} - -pub(crate) async fn receive_like_comment( - like: Like, - comment: Comment, - context: &LemmyContext, - request_counter: &mut i32, -) -> Result<(), LemmyError> { - let person = get_actor_as_person(&like, context, request_counter).await?; - - let comment_id = comment.id; - let like_form = CommentLikeForm { - comment_id, - post_id: comment.post_id, - person_id: person.id, - score: 1, - }; - let person_id = person.id; - blocking(context.pool(), move |conn| { - CommentLike::remove(conn, person_id, comment_id)?; - CommentLike::like(conn, &like_form) - }) - .await??; - - // Refetch the view - let comment_view = blocking(context.pool(), move |conn| { - CommentView::read(conn, comment_id, None) - }) - .await??; - - // TODO get those recipient actor ids from somewhere - let recipient_ids = vec![]; - let res = CommentResponse { - comment_view, - recipient_ids, - form_id: None, - }; - - context.chat_server().do_send(SendComment { - op: UserOperation::CreateCommentLike, - comment: res, - websocket_id: None, - }); - - Ok(()) -} - -pub(crate) async fn receive_dislike_comment( - dislike: Dislike, - comment: Comment, - context: &LemmyContext, - request_counter: &mut i32, -) -> Result<(), LemmyError> { - let person = get_actor_as_person(&dislike, context, request_counter).await?; - - let comment_id = comment.id; - let like_form = CommentLikeForm { - comment_id, - post_id: comment.post_id, - person_id: person.id, - score: -1, - }; - let person_id = person.id; - blocking(context.pool(), move |conn| { - CommentLike::remove(conn, person_id, comment_id)?; - CommentLike::like(conn, &like_form) - }) - .await??; - - // Refetch the view - let comment_view = blocking(context.pool(), move |conn| { - CommentView::read(conn, comment_id, None) - }) - .await??; - - // TODO get those recipient actor ids from somewhere - let recipient_ids = vec![]; - let res = CommentResponse { - comment_view, - recipient_ids, - form_id: None, - }; - - context.chat_server().do_send(SendComment { - op: UserOperation::CreateCommentLike, - comment: res, - websocket_id: None, - }); - - Ok(()) -} - -pub(crate) async fn receive_delete_comment( - context: &LemmyContext, - comment: Comment, -) -> Result<(), LemmyError> { - let deleted_comment = blocking(context.pool(), move |conn| { - Comment::update_deleted(conn, comment.id, true) - }) - .await??; - - // Refetch the view - let comment_id = deleted_comment.id; - let comment_view = blocking(context.pool(), move |conn| { - CommentView::read(conn, comment_id, None) - }) - .await??; - - // TODO get those recipient actor ids from somewhere - let recipient_ids = vec![]; - let res = CommentResponse { - comment_view, - recipient_ids, - form_id: None, - }; - context.chat_server().do_send(SendComment { - op: UserOperationCrud::EditComment, - comment: res, - websocket_id: None, - }); - - Ok(()) -} - -pub(crate) async fn receive_remove_comment( - context: &LemmyContext, - comment: Comment, -) -> Result<(), LemmyError> { - let removed_comment = blocking(context.pool(), move |conn| { - Comment::update_removed(conn, comment.id, true) - }) - .await??; - - // Refetch the view - let comment_id = removed_comment.id; - let comment_view = blocking(context.pool(), move |conn| { - CommentView::read(conn, comment_id, None) - }) - .await??; - - // TODO get those recipient actor ids from somewhere - let recipient_ids = vec![]; - let res = CommentResponse { - comment_view, - recipient_ids, - form_id: None, - }; - context.chat_server().do_send(SendComment { - op: UserOperationCrud::EditComment, - comment: res, - websocket_id: None, - }); - - Ok(()) -} diff --git a/crates/apub_receive/src/activities/receive/comment_undo.rs b/crates/apub_receive/src/activities/receive/comment_undo.rs deleted file mode 100644 index 7214c8f0..00000000 --- a/crates/apub_receive/src/activities/receive/comment_undo.rs +++ /dev/null @@ -1,150 +0,0 @@ -use crate::activities::receive::get_actor_as_person; -use activitystreams::activity::{Dislike, Like}; -use lemmy_api_common::{blocking, comment::CommentResponse}; -use lemmy_db_queries::{source::comment::Comment_, Likeable}; -use lemmy_db_schema::source::comment::{Comment, CommentLike}; -use lemmy_db_views::comment_view::CommentView; -use lemmy_utils::LemmyError; -use lemmy_websocket::{messages::SendComment, LemmyContext, UserOperation, UserOperationCrud}; - -pub(crate) async fn receive_undo_like_comment( - like: &Like, - comment: Comment, - context: &LemmyContext, - request_counter: &mut i32, -) -> Result<(), LemmyError> { - let person = get_actor_as_person(like, context, request_counter).await?; - - let comment_id = comment.id; - let person_id = person.id; - blocking(context.pool(), move |conn| { - CommentLike::remove(conn, person_id, comment_id) - }) - .await??; - - // Refetch the view - let comment_view = blocking(context.pool(), move |conn| { - CommentView::read(conn, comment_id, None) - }) - .await??; - - // TODO get those recipient actor ids from somewhere - let recipient_ids = vec![]; - let res = CommentResponse { - comment_view, - recipient_ids, - form_id: None, - }; - - context.chat_server().do_send(SendComment { - op: UserOperation::CreateCommentLike, - comment: res, - websocket_id: None, - }); - - Ok(()) -} - -pub(crate) async fn receive_undo_dislike_comment( - dislike: &Dislike, - comment: Comment, - context: &LemmyContext, - request_counter: &mut i32, -) -> Result<(), LemmyError> { - let person = get_actor_as_person(dislike, context, request_counter).await?; - - let comment_id = comment.id; - let person_id = person.id; - blocking(context.pool(), move |conn| { - CommentLike::remove(conn, person_id, comment_id) - }) - .await??; - - // Refetch the view - let comment_view = blocking(context.pool(), move |conn| { - CommentView::read(conn, comment_id, None) - }) - .await??; - - // TODO get those recipient actor ids from somewhere - let recipient_ids = vec![]; - let res = CommentResponse { - comment_view, - recipient_ids, - form_id: None, - }; - - context.chat_server().do_send(SendComment { - op: UserOperation::CreateCommentLike, - comment: res, - websocket_id: None, - }); - - Ok(()) -} - -pub(crate) async fn receive_undo_delete_comment( - context: &LemmyContext, - comment: Comment, -) -> Result<(), LemmyError> { - let deleted_comment = blocking(context.pool(), move |conn| { - Comment::update_deleted(conn, comment.id, false) - }) - .await??; - - // Refetch the view - let comment_id = deleted_comment.id; - let comment_view = blocking(context.pool(), move |conn| { - CommentView::read(conn, comment_id, None) - }) - .await??; - - // TODO get those recipient actor ids from somewhere - let recipient_ids = vec![]; - let res = CommentResponse { - comment_view, - recipient_ids, - form_id: None, - }; - - context.chat_server().do_send(SendComment { - op: UserOperationCrud::EditComment, - comment: res, - websocket_id: None, - }); - - Ok(()) -} - -pub(crate) async fn receive_undo_remove_comment( - context: &LemmyContext, - comment: Comment, -) -> Result<(), LemmyError> { - let removed_comment = blocking(context.pool(), move |conn| { - Comment::update_removed(conn, comment.id, false) - }) - .await??; - - // Refetch the view - let comment_id = removed_comment.id; - let comment_view = blocking(context.pool(), move |conn| { - CommentView::read(conn, comment_id, None) - }) - .await??; - - // TODO get those recipient actor ids from somewhere - let recipient_ids = vec![]; - let res = CommentResponse { - comment_view, - recipient_ids, - form_id: None, - }; - - context.chat_server().do_send(SendComment { - op: UserOperationCrud::EditComment, - comment: res, - websocket_id: None, - }); - - Ok(()) -} diff --git a/crates/apub_receive/src/activities/receive/community.rs b/crates/apub_receive/src/activities/receive/community.rs deleted file mode 100644 index 18ebbed3..00000000 --- a/crates/apub_receive/src/activities/receive/community.rs +++ /dev/null @@ -1,232 +0,0 @@ -use crate::{ - activities::receive::get_actor_as_person, - inbox::receive_for_community::verify_actor_is_community_mod, -}; -use activitystreams::{ - activity::{ActorAndObjectRefExt, Delete, Undo, Update}, - base::ExtendsExt, -}; -use anyhow::{anyhow, Context}; -use lemmy_api_common::{blocking, community::CommunityResponse}; -use lemmy_apub::{ - get_community_from_to_or_cc, - objects::FromApubToForm, - ActorType, - CommunityType, - GroupExt, -}; -use lemmy_db_queries::{source::community::Community_, Crud}; -use lemmy_db_schema::source::{ - community::{Community, CommunityForm}, - person::Person, -}; -use lemmy_db_views_actor::{ - community_moderator_view::CommunityModeratorView, - community_view::CommunityView, -}; -use lemmy_utils::{location_info, LemmyError}; -use lemmy_websocket::{messages::SendCommunityRoomMessage, LemmyContext, UserOperationCrud}; - -/// This activity is received from a remote community mod, and updates the description or other -/// fields of a local community. -pub(crate) async fn receive_remote_mod_update_community( - update: Update, - context: &LemmyContext, - request_counter: &mut i32, -) -> Result<(), LemmyError> { - let community = get_community_from_to_or_cc(&update, context, request_counter).await?; - verify_actor_is_community_mod(&update, &community, context).await?; - let group = GroupExt::from_any_base(update.object().to_owned().one().context(location_info!())?)? - .context(location_info!())?; - let updated_community = CommunityForm::from_apub( - &group, - context, - community.actor_id(), - request_counter, - false, - ) - .await?; - let cf = CommunityForm { - name: updated_community.name, - title: updated_community.title, - description: updated_community.description, - nsfw: updated_community.nsfw, - // TODO: icon and banner would be hosted on the other instance, ideally we would copy it to ours - icon: updated_community.icon, - banner: updated_community.banner, - ..CommunityForm::default() - }; - blocking(context.pool(), move |conn| { - Community::update(conn, community.id, &cf) - }) - .await??; - - Ok(()) -} - -pub(crate) async fn receive_remote_mod_delete_community( - delete: Delete, - community: Community, - context: &LemmyContext, - request_counter: &mut i32, -) -> Result<(), LemmyError> { - verify_actor_is_community_mod(&delete, &community, context).await?; - let actor = get_actor_as_person(&delete, context, request_counter).await?; - verify_is_remote_community_creator(&actor, &community, context).await?; - let community_id = community.id; - blocking(context.pool(), move |conn| { - Community::update_deleted(conn, community_id, true) - }) - .await??; - community.send_delete(actor, context).await -} - -pub(crate) async fn receive_delete_community( - context: &LemmyContext, - community: Community, -) -> Result<(), LemmyError> { - let deleted_community = blocking(context.pool(), move |conn| { - Community::update_deleted(conn, community.id, true) - }) - .await??; - - let community_id = deleted_community.id; - let res = CommunityResponse { - community_view: blocking(context.pool(), move |conn| { - CommunityView::read(conn, community_id, None) - }) - .await??, - }; - - let community_id = res.community_view.community.id; - context.chat_server().do_send(SendCommunityRoomMessage { - op: UserOperationCrud::EditCommunity, - response: res, - community_id, - websocket_id: None, - }); - - Ok(()) -} - -pub(crate) async fn receive_remove_community( - context: &LemmyContext, - community: Community, -) -> Result<(), LemmyError> { - let removed_community = blocking(context.pool(), move |conn| { - Community::update_removed(conn, community.id, true) - }) - .await??; - - let community_id = removed_community.id; - let res = CommunityResponse { - community_view: blocking(context.pool(), move |conn| { - CommunityView::read(conn, community_id, None) - }) - .await??, - }; - - let community_id = res.community_view.community.id; - context.chat_server().do_send(SendCommunityRoomMessage { - op: UserOperationCrud::EditCommunity, - response: res, - community_id, - websocket_id: None, - }); - - Ok(()) -} - -pub(crate) async fn receive_remote_mod_undo_delete_community( - undo: Undo, - community: Community, - context: &LemmyContext, - request_counter: &mut i32, -) -> Result<(), LemmyError> { - verify_actor_is_community_mod(&undo, &community, context).await?; - let actor = get_actor_as_person(&undo, context, request_counter).await?; - verify_is_remote_community_creator(&actor, &community, context).await?; - let community_id = community.id; - blocking(context.pool(), move |conn| { - Community::update_deleted(conn, community_id, false) - }) - .await??; - community.send_undo_delete(actor, context).await -} - -pub(crate) async fn receive_undo_delete_community( - context: &LemmyContext, - community: Community, -) -> Result<(), LemmyError> { - let deleted_community = blocking(context.pool(), move |conn| { - Community::update_deleted(conn, community.id, false) - }) - .await??; - - let community_id = deleted_community.id; - let res = CommunityResponse { - community_view: blocking(context.pool(), move |conn| { - CommunityView::read(conn, community_id, None) - }) - .await??, - }; - - let community_id = res.community_view.community.id; - context.chat_server().do_send(SendCommunityRoomMessage { - op: UserOperationCrud::EditCommunity, - response: res, - community_id, - websocket_id: None, - }); - - Ok(()) -} - -pub(crate) async fn receive_undo_remove_community( - context: &LemmyContext, - community: Community, -) -> Result<(), LemmyError> { - let removed_community = blocking(context.pool(), move |conn| { - Community::update_removed(conn, community.id, false) - }) - .await??; - - let community_id = removed_community.id; - let res = CommunityResponse { - community_view: blocking(context.pool(), move |conn| { - CommunityView::read(conn, community_id, None) - }) - .await??, - }; - - let community_id = res.community_view.community.id; - - context.chat_server().do_send(SendCommunityRoomMessage { - op: UserOperationCrud::EditCommunity, - response: res, - community_id, - websocket_id: None, - }); - - Ok(()) -} - -/// Checks if the remote user is creator of the local community. This can only happen if a community -/// is created by a local user, and then transferred to a remote user. -async fn verify_is_remote_community_creator( - user: &Person, - community: &Community, - context: &LemmyContext, -) -> Result<(), LemmyError> { - let community_id = community.id; - let community_mods = blocking(context.pool(), move |conn| { - CommunityModeratorView::for_community(conn, community_id) - }) - .await??; - - if user.id != community_mods[0].moderator.id { - Err(anyhow!("Actor is not community creator").into()) - } else { - Ok(()) - } -} diff --git a/crates/apub_receive/src/activities/receive/mod.rs b/crates/apub_receive/src/activities/receive/mod.rs deleted file mode 100644 index f421a45e..00000000 --- a/crates/apub_receive/src/activities/receive/mod.rs +++ /dev/null @@ -1,81 +0,0 @@ -use activitystreams::{ - activity::{ActorAndObjectRef, ActorAndObjectRefExt}, - base::{AsBase, BaseExt}, - error::DomainError, -}; -use anyhow::{anyhow, Context}; -use lemmy_apub::fetcher::person::get_or_fetch_and_upsert_person; -use lemmy_db_schema::source::person::Person; -use lemmy_utils::{location_info, LemmyError}; -use lemmy_websocket::LemmyContext; -use log::debug; -use std::fmt::Debug; -use url::Url; - -pub(crate) mod comment; -pub(crate) mod comment_undo; -pub(crate) mod community; -pub(crate) mod post; -pub(crate) mod post_undo; -pub(crate) mod private_message; - -/// Return HTTP 501 for unsupported activities in inbox. -pub(crate) fn receive_unhandled_activity(activity: A) -> Result<(), LemmyError> -where - A: Debug, -{ - debug!("received unhandled activity type: {:?}", activity); - Err(anyhow!("Activity not supported").into()) -} - -/// Reads the actor field of an activity and returns the corresponding `Person`. -pub(crate) async fn get_actor_as_person( - activity: &T, - context: &LemmyContext, - request_counter: &mut i32, -) -> Result -where - T: AsBase + ActorAndObjectRef, -{ - let actor = activity.actor()?; - let person_uri = actor.as_single_xsd_any_uri().context(location_info!())?; - get_or_fetch_and_upsert_person(person_uri, context, request_counter).await -} - -/// Ensure that the ID of an incoming activity comes from the same domain as the actor. Optionally -/// also checks the ID of the inner object. -/// -/// The reason that this starts with the actor ID is that it was already confirmed as correct by the -/// HTTP signature. -pub(crate) fn verify_activity_domains_valid( - activity: &T, - actor_id: &Url, - object_domain_must_match: bool, -) -> Result<(), LemmyError> -where - T: AsBase + ActorAndObjectRef, -{ - let expected_domain = actor_id.domain().context(location_info!())?; - - activity.id(expected_domain)?; - - let object_id = match activity.object().to_owned().single_xsd_any_uri() { - // object is just an ID - Some(id) => id, - // object is something like an activity, a comment or a post - None => activity - .object() - .to_owned() - .one() - .context(location_info!())? - .id() - .context(location_info!())? - .to_owned(), - }; - - if object_domain_must_match && object_id.domain() != Some(expected_domain) { - return Err(DomainError.into()); - } - - Ok(()) -} diff --git a/crates/apub_receive/src/activities/receive/post.rs b/crates/apub_receive/src/activities/receive/post.rs deleted file mode 100644 index 07f6a665..00000000 --- a/crates/apub_receive/src/activities/receive/post.rs +++ /dev/null @@ -1,242 +0,0 @@ -use crate::{ - activities::receive::get_actor_as_person, - inbox::receive_for_community::verify_mod_activity, -}; -use activitystreams::{ - activity::{Announce, Create, Dislike, Like, Update}, - prelude::*, -}; -use anyhow::Context; -use lemmy_api_common::{blocking, post::PostResponse}; -use lemmy_apub::{objects::FromApub, ActorType, PageExt}; -use lemmy_db_queries::{source::post::Post_, ApubObject, Crud, Likeable}; -use lemmy_db_schema::{ - source::{ - community::Community, - post::{Post, PostLike, PostLikeForm}, - }, - DbUrl, -}; -use lemmy_db_views::post_view::PostView; -use lemmy_utils::{location_info, LemmyError}; -use lemmy_websocket::{messages::SendPost, LemmyContext, UserOperation, UserOperationCrud}; - -pub(crate) async fn receive_create_post( - create: Create, - context: &LemmyContext, - request_counter: &mut i32, -) -> Result<(), LemmyError> { - let person = get_actor_as_person(&create, context, request_counter).await?; - let page = PageExt::from_any_base(create.object().to_owned().one().context(location_info!())?)? - .context(location_info!())?; - - let post = Post::from_apub(&page, context, person.actor_id(), request_counter, false).await?; - - // Refetch the view - let post_id = post.id; - let post_view = blocking(context.pool(), move |conn| { - PostView::read(conn, post_id, None) - }) - .await??; - - let res = PostResponse { post_view }; - - context.chat_server().do_send(SendPost { - op: UserOperationCrud::CreatePost, - post: res, - websocket_id: None, - }); - - Ok(()) -} - -pub(crate) async fn receive_update_post( - update: Update, - announce: Option, - context: &LemmyContext, - request_counter: &mut i32, -) -> Result<(), LemmyError> { - let person = get_actor_as_person(&update, context, request_counter).await?; - let page = PageExt::from_any_base(update.object().to_owned().one().context(location_info!())?)? - .context(location_info!())?; - - let post_id: DbUrl = page - .id_unchecked() - .context(location_info!())? - .to_owned() - .into(); - let old_post = blocking(context.pool(), move |conn| { - Post::read_from_apub_id(conn, &post_id) - }) - .await??; - - // If sticked or locked state was changed, make sure the actor is a mod - let stickied = page.ext_one.stickied.context(location_info!())?; - let locked = !page.ext_one.comments_enabled.context(location_info!())?; - let mut mod_action_allowed = false; - if (stickied != old_post.stickied) || (locked != old_post.locked) { - let community = blocking(context.pool(), move |conn| { - Community::read(conn, old_post.community_id) - }) - .await??; - // Only check mod status if the community is local, otherwise we trust that it was sent correctly. - if community.local { - verify_mod_activity(&update, announce, &community, context).await?; - } - mod_action_allowed = true; - } - - let post = Post::from_apub( - &page, - context, - person.actor_id(), - request_counter, - mod_action_allowed, - ) - .await?; - - let post_id = post.id; - // Refetch the view - let post_view = blocking(context.pool(), move |conn| { - PostView::read(conn, post_id, None) - }) - .await??; - - let res = PostResponse { post_view }; - - context.chat_server().do_send(SendPost { - op: UserOperationCrud::EditPost, - post: res, - websocket_id: None, - }); - - Ok(()) -} - -pub(crate) async fn receive_like_post( - like: Like, - post: Post, - context: &LemmyContext, - request_counter: &mut i32, -) -> Result<(), LemmyError> { - let person = get_actor_as_person(&like, context, request_counter).await?; - - let post_id = post.id; - let like_form = PostLikeForm { - post_id, - person_id: person.id, - score: 1, - }; - let person_id = person.id; - blocking(context.pool(), move |conn| { - PostLike::remove(conn, person_id, post_id)?; - PostLike::like(conn, &like_form) - }) - .await??; - - // Refetch the view - let post_view = blocking(context.pool(), move |conn| { - PostView::read(conn, post_id, None) - }) - .await??; - - let res = PostResponse { post_view }; - - context.chat_server().do_send(SendPost { - op: UserOperation::CreatePostLike, - post: res, - websocket_id: None, - }); - - Ok(()) -} - -pub(crate) async fn receive_dislike_post( - dislike: Dislike, - post: Post, - context: &LemmyContext, - request_counter: &mut i32, -) -> Result<(), LemmyError> { - let person = get_actor_as_person(&dislike, context, request_counter).await?; - - let post_id = post.id; - let like_form = PostLikeForm { - post_id, - person_id: person.id, - score: -1, - }; - let person_id = person.id; - blocking(context.pool(), move |conn| { - PostLike::remove(conn, person_id, post_id)?; - PostLike::like(conn, &like_form) - }) - .await??; - - // Refetch the view - let post_view = blocking(context.pool(), move |conn| { - PostView::read(conn, post_id, None) - }) - .await??; - - let res = PostResponse { post_view }; - - context.chat_server().do_send(SendPost { - op: UserOperation::CreatePostLike, - post: res, - websocket_id: None, - }); - - Ok(()) -} - -pub(crate) async fn receive_delete_post( - context: &LemmyContext, - post: Post, -) -> Result<(), LemmyError> { - let deleted_post = blocking(context.pool(), move |conn| { - Post::update_deleted(conn, post.id, true) - }) - .await??; - - // Refetch the view - let post_id = deleted_post.id; - let post_view = blocking(context.pool(), move |conn| { - PostView::read(conn, post_id, None) - }) - .await??; - - let res = PostResponse { post_view }; - context.chat_server().do_send(SendPost { - op: UserOperationCrud::EditPost, - post: res, - websocket_id: None, - }); - - Ok(()) -} - -pub(crate) async fn receive_remove_post( - context: &LemmyContext, - post: Post, -) -> Result<(), LemmyError> { - let removed_post = blocking(context.pool(), move |conn| { - Post::update_removed(conn, post.id, true) - }) - .await??; - - // Refetch the view - let post_id = removed_post.id; - let post_view = blocking(context.pool(), move |conn| { - PostView::read(conn, post_id, None) - }) - .await??; - - let res = PostResponse { post_view }; - context.chat_server().do_send(SendPost { - op: UserOperationCrud::EditPost, - post: res, - websocket_id: None, - }); - - Ok(()) -} diff --git a/crates/apub_receive/src/activities/receive/post_undo.rs b/crates/apub_receive/src/activities/receive/post_undo.rs deleted file mode 100644 index 2ed6ecac..00000000 --- a/crates/apub_receive/src/activities/receive/post_undo.rs +++ /dev/null @@ -1,125 +0,0 @@ -use crate::activities::receive::get_actor_as_person; -use activitystreams::activity::{Dislike, Like}; -use lemmy_api_common::{blocking, post::PostResponse}; -use lemmy_db_queries::{source::post::Post_, Likeable}; -use lemmy_db_schema::source::post::{Post, PostLike}; -use lemmy_db_views::post_view::PostView; -use lemmy_utils::LemmyError; -use lemmy_websocket::{messages::SendPost, LemmyContext, UserOperation, UserOperationCrud}; - -pub(crate) async fn receive_undo_like_post( - like: &Like, - post: Post, - context: &LemmyContext, - request_counter: &mut i32, -) -> Result<(), LemmyError> { - let person = get_actor_as_person(like, context, request_counter).await?; - - let post_id = post.id; - let person_id = person.id; - blocking(context.pool(), move |conn| { - PostLike::remove(conn, person_id, post_id) - }) - .await??; - - // Refetch the view - let post_view = blocking(context.pool(), move |conn| { - PostView::read(conn, post_id, None) - }) - .await??; - - let res = PostResponse { post_view }; - - context.chat_server().do_send(SendPost { - op: UserOperation::CreatePostLike, - post: res, - websocket_id: None, - }); - - Ok(()) -} - -pub(crate) async fn receive_undo_dislike_post( - dislike: &Dislike, - post: Post, - context: &LemmyContext, - request_counter: &mut i32, -) -> Result<(), LemmyError> { - let person = get_actor_as_person(dislike, context, request_counter).await?; - - let post_id = post.id; - let person_id = person.id; - blocking(context.pool(), move |conn| { - PostLike::remove(conn, person_id, post_id) - }) - .await??; - - // Refetch the view - let post_view = blocking(context.pool(), move |conn| { - PostView::read(conn, post_id, None) - }) - .await??; - - let res = PostResponse { post_view }; - - context.chat_server().do_send(SendPost { - op: UserOperation::CreatePostLike, - post: res, - websocket_id: None, - }); - - Ok(()) -} - -pub(crate) async fn receive_undo_delete_post( - context: &LemmyContext, - post: Post, -) -> Result<(), LemmyError> { - let deleted_post = blocking(context.pool(), move |conn| { - Post::update_deleted(conn, post.id, false) - }) - .await??; - - // Refetch the view - let post_id = deleted_post.id; - let post_view = blocking(context.pool(), move |conn| { - PostView::read(conn, post_id, None) - }) - .await??; - - let res = PostResponse { post_view }; - context.chat_server().do_send(SendPost { - op: UserOperationCrud::EditPost, - post: res, - websocket_id: None, - }); - - Ok(()) -} - -pub(crate) async fn receive_undo_remove_post( - context: &LemmyContext, - post: Post, -) -> Result<(), LemmyError> { - let removed_post = blocking(context.pool(), move |conn| { - Post::update_removed(conn, post.id, false) - }) - .await??; - - // Refetch the view - let post_id = removed_post.id; - let post_view = blocking(context.pool(), move |conn| { - PostView::read(conn, post_id, None) - }) - .await??; - - let res = PostResponse { post_view }; - - context.chat_server().do_send(SendPost { - op: UserOperationCrud::EditPost, - post: res, - websocket_id: None, - }); - - Ok(()) -} diff --git a/crates/apub_receive/src/activities/receive/private_message.rs b/crates/apub_receive/src/activities/receive/private_message.rs deleted file mode 100644 index 3d82c5c2..00000000 --- a/crates/apub_receive/src/activities/receive/private_message.rs +++ /dev/null @@ -1,228 +0,0 @@ -use crate::activities::receive::verify_activity_domains_valid; -use activitystreams::{ - activity::{ActorAndObjectRefExt, Create, Delete, Undo, Update}, - base::{AsBase, ExtendsExt}, - object::AsObject, - public, -}; -use anyhow::{anyhow, Context}; -use lemmy_api_common::{blocking, person::PrivateMessageResponse}; -use lemmy_apub::{ - check_is_apub_id_valid, - fetcher::person::get_or_fetch_and_upsert_person, - get_activity_to_and_cc, - objects::FromApub, - NoteExt, -}; -use lemmy_db_queries::source::private_message::PrivateMessage_; -use lemmy_db_schema::source::private_message::PrivateMessage; -use lemmy_db_views::{local_user_view::LocalUserView, private_message_view::PrivateMessageView}; -use lemmy_utils::{location_info, LemmyError}; -use lemmy_websocket::{messages::SendUserRoomMessage, LemmyContext, UserOperationCrud}; -use url::Url; - -pub(crate) async fn receive_create_private_message( - context: &LemmyContext, - create: Create, - expected_domain: Url, - request_counter: &mut i32, -) -> Result<(), LemmyError> { - check_private_message_activity_valid(&create, context, request_counter).await?; - - let note = NoteExt::from_any_base( - create - .object() - .as_one() - .context(location_info!())? - .to_owned(), - )? - .context(location_info!())?; - - let private_message = - PrivateMessage::from_apub(¬e, context, expected_domain, request_counter, false).await?; - - let message = blocking(context.pool(), move |conn| { - PrivateMessageView::read(conn, private_message.id) - }) - .await??; - - let res = PrivateMessageResponse { - private_message_view: message, - }; - - // Send notifications to the local recipient, if one exists - let recipient_id = res.private_message_view.recipient.id; - let local_recipient_id = blocking(context.pool(), move |conn| { - LocalUserView::read_person(conn, recipient_id) - }) - .await?? - .local_user - .id; - - context.chat_server().do_send(SendUserRoomMessage { - op: UserOperationCrud::CreatePrivateMessage, - response: res, - local_recipient_id, - websocket_id: None, - }); - - Ok(()) -} - -pub(crate) async fn receive_update_private_message( - context: &LemmyContext, - update: Update, - expected_domain: Url, - request_counter: &mut i32, -) -> Result<(), LemmyError> { - check_private_message_activity_valid(&update, context, request_counter).await?; - - let object = update - .object() - .as_one() - .context(location_info!())? - .to_owned(); - let note = NoteExt::from_any_base(object)?.context(location_info!())?; - - let private_message = - PrivateMessage::from_apub(¬e, context, expected_domain, request_counter, false).await?; - - let private_message_id = private_message.id; - let message = blocking(context.pool(), move |conn| { - PrivateMessageView::read(conn, private_message_id) - }) - .await??; - - let res = PrivateMessageResponse { - private_message_view: message, - }; - - let recipient_id = res.private_message_view.recipient.id; - let local_recipient_id = blocking(context.pool(), move |conn| { - LocalUserView::read_person(conn, recipient_id) - }) - .await?? - .local_user - .id; - - context.chat_server().do_send(SendUserRoomMessage { - op: UserOperationCrud::EditPrivateMessage, - response: res, - local_recipient_id, - websocket_id: None, - }); - - Ok(()) -} - -pub(crate) async fn receive_delete_private_message( - context: &LemmyContext, - delete: Delete, - private_message: PrivateMessage, - request_counter: &mut i32, -) -> Result<(), LemmyError> { - check_private_message_activity_valid(&delete, context, request_counter).await?; - - let deleted_private_message = blocking(context.pool(), move |conn| { - PrivateMessage::update_deleted(conn, private_message.id, true) - }) - .await??; - - let message = blocking(context.pool(), move |conn| { - PrivateMessageView::read(conn, deleted_private_message.id) - }) - .await??; - - let res = PrivateMessageResponse { - private_message_view: message, - }; - - let recipient_id = res.private_message_view.recipient.id; - let local_recipient_id = blocking(context.pool(), move |conn| { - LocalUserView::read_person(conn, recipient_id) - }) - .await?? - .local_user - .id; - - context.chat_server().do_send(SendUserRoomMessage { - op: UserOperationCrud::EditPrivateMessage, - response: res, - local_recipient_id, - websocket_id: None, - }); - - Ok(()) -} - -pub(crate) async fn receive_undo_delete_private_message( - context: &LemmyContext, - undo: Undo, - expected_domain: &Url, - private_message: PrivateMessage, - request_counter: &mut i32, -) -> Result<(), LemmyError> { - check_private_message_activity_valid(&undo, context, request_counter).await?; - let object = undo.object().to_owned().one().context(location_info!())?; - let delete = Delete::from_any_base(object)?.context(location_info!())?; - verify_activity_domains_valid(&delete, expected_domain, true)?; - check_private_message_activity_valid(&delete, context, request_counter).await?; - - let deleted_private_message = blocking(context.pool(), move |conn| { - PrivateMessage::update_deleted(conn, private_message.id, false) - }) - .await??; - - let message = blocking(context.pool(), move |conn| { - PrivateMessageView::read(conn, deleted_private_message.id) - }) - .await??; - - let res = PrivateMessageResponse { - private_message_view: message, - }; - - let recipient_id = res.private_message_view.recipient.id; - let local_recipient_id = blocking(context.pool(), move |conn| { - LocalUserView::read_person(conn, recipient_id) - }) - .await?? - .local_user - .id; - - context.chat_server().do_send(SendUserRoomMessage { - op: UserOperationCrud::EditPrivateMessage, - response: res, - local_recipient_id, - websocket_id: None, - }); - - Ok(()) -} - -async fn check_private_message_activity_valid( - activity: &T, - context: &LemmyContext, - request_counter: &mut i32, -) -> Result<(), LemmyError> -where - T: AsBase + AsObject + ActorAndObjectRefExt, -{ - let to_and_cc = get_activity_to_and_cc(activity); - if to_and_cc.len() != 1 { - return Err(anyhow!("Private message can only be addressed to one person").into()); - } - if to_and_cc.contains(&public()) { - return Err(anyhow!("Private message cant be public").into()); - } - let person_id = activity - .actor()? - .to_owned() - .single_xsd_any_uri() - .context(location_info!())?; - check_is_apub_id_valid(&person_id, false)?; - // check that the sender is a person, not a community - get_or_fetch_and_upsert_person(&person_id, context, request_counter).await?; - - Ok(()) -} diff --git a/crates/apub_receive/src/activities/removal/mod.rs b/crates/apub_receive/src/activities/removal/mod.rs new file mode 100644 index 00000000..01c031dd --- /dev/null +++ b/crates/apub_receive/src/activities/removal/mod.rs @@ -0,0 +1,2 @@ +pub mod remove; +pub mod undo_remove; diff --git a/crates/apub_receive/src/activities/removal/remove.rs b/crates/apub_receive/src/activities/removal/remove.rs new file mode 100644 index 00000000..bb1c307b --- /dev/null +++ b/crates/apub_receive/src/activities/removal/remove.rs @@ -0,0 +1,155 @@ +use crate::activities::{ + comment::send_websocket_message as send_comment_message, + community::send_websocket_message as send_community_message, + post::send_websocket_message as send_post_message, + verify_activity, + verify_add_remove_moderator_target, + verify_mod_action, + verify_person_in_community, +}; +use activitystreams::{activity::kind::RemoveType, base::AnyBase}; +use anyhow::anyhow; +use lemmy_api_common::blocking; +use lemmy_apub::{ + fetcher::{ + community::get_or_fetch_and_upsert_community, + objects::get_or_fetch_and_insert_post_or_comment, + person::get_or_fetch_and_upsert_person, + }, + CommunityType, + PostOrComment, +}; +use lemmy_apub_lib::{ActivityCommonFields, ActivityHandler, PublicUrl}; +use lemmy_db_queries::{ + source::{comment::Comment_, community::Community_, post::Post_}, + Joinable, +}; +use lemmy_db_schema::source::{ + comment::Comment, + community::{Community, CommunityModerator, CommunityModeratorForm}, + post::Post, +}; +use lemmy_utils::LemmyError; +use lemmy_websocket::{LemmyContext, UserOperationCrud}; +use url::Url; + +// TODO: we can probably deduplicate a bunch of code between this and DeletePostCommentOrCommunity +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] +#[serde(rename_all = "camelCase")] +pub struct RemovePostCommentCommunityOrMod { + to: PublicUrl, + pub(in crate::activities::removal) object: Url, + cc: [Url; 1], + #[serde(rename = "type")] + kind: RemoveType, + // if target is set, this is means remove mod from community + target: Option, + #[serde(flatten)] + common: ActivityCommonFields, +} + +#[async_trait::async_trait(?Send)] +impl ActivityHandler for RemovePostCommentCommunityOrMod { + async fn verify( + &self, + context: &LemmyContext, + request_counter: &mut i32, + ) -> Result<(), LemmyError> { + verify_activity(self.common())?; + let object_community = + get_or_fetch_and_upsert_community(&self.object, context, request_counter).await; + // removing a community + if object_community.is_ok() { + verify_mod_action(&self.common.actor, self.object.clone(), context).await?; + } + // removing community mod + else if let Some(target) = &self.target { + verify_person_in_community(&self.common.actor, &self.cc, context, request_counter).await?; + verify_mod_action(&self.common.actor, self.cc[0].clone(), context).await?; + verify_add_remove_moderator_target(target, self.cc[0].clone())?; + } + // removing a post or comment + else { + verify_person_in_community(&self.common.actor, &self.cc, context, request_counter).await?; + verify_mod_action(&self.common.actor, self.cc[0].clone(), context).await?; + } + Ok(()) + } + + async fn receive( + &self, + context: &LemmyContext, + request_counter: &mut i32, + ) -> Result<(), LemmyError> { + let object_community = + get_or_fetch_and_upsert_community(&self.object, context, request_counter).await; + // removing a community + if let Ok(community) = object_community { + if community.local { + return Err(anyhow!("Only local admin can remove community").into()); + } + let deleted_community = blocking(context.pool(), move |conn| { + Community::update_removed(conn, community.id, true) + }) + .await??; + + send_community_message( + deleted_community.id, + UserOperationCrud::RemoveCommunity, + context, + ) + .await + } + // removing community mod + else if self.target.is_some() { + let community = + get_or_fetch_and_upsert_community(&self.cc[0], context, request_counter).await?; + let remove_mod = + get_or_fetch_and_upsert_person(&self.object, context, request_counter).await?; + + let form = CommunityModeratorForm { + community_id: community.id, + person_id: remove_mod.id, + }; + blocking(context.pool(), move |conn| { + CommunityModerator::leave(conn, &form) + }) + .await??; + let anybase = AnyBase::from_arbitrary_json(serde_json::to_string(self)?)?; + community + .send_announce(anybase, Some(self.object.clone()), context) + .await?; + // TODO: send websocket notification about removed mod + Ok(()) + } + // removing a post or comment + else { + match get_or_fetch_and_insert_post_or_comment(&self.object, context, request_counter).await? { + PostOrComment::Post(post) => { + let removed_post = blocking(context.pool(), move |conn| { + Post::update_removed(conn, post.id, true) + }) + .await??; + send_post_message(removed_post.id, UserOperationCrud::EditPost, context).await + } + PostOrComment::Comment(comment) => { + let removed_comment = blocking(context.pool(), move |conn| { + Comment::update_removed(conn, comment.id, true) + }) + .await??; + send_comment_message( + removed_comment.id, + vec![], + UserOperationCrud::EditComment, + context, + ) + .await + } + } + } + } + + fn common(&self) -> &ActivityCommonFields { + &self.common + } +} diff --git a/crates/apub_receive/src/activities/removal/undo_remove.rs b/crates/apub_receive/src/activities/removal/undo_remove.rs new file mode 100644 index 00000000..e19c4194 --- /dev/null +++ b/crates/apub_receive/src/activities/removal/undo_remove.rs @@ -0,0 +1,120 @@ +use crate::activities::{ + comment::send_websocket_message as send_comment_message, + community::send_websocket_message as send_community_message, + post::send_websocket_message as send_post_message, + removal::remove::RemovePostCommentCommunityOrMod, + verify_activity, + verify_mod_action, + verify_person_in_community, +}; +use activitystreams::activity::kind::UndoType; +use anyhow::anyhow; +use lemmy_api_common::blocking; +use lemmy_apub::{ + fetcher::{ + community::get_or_fetch_and_upsert_community, + objects::get_or_fetch_and_insert_post_or_comment, + }, + PostOrComment, +}; +use lemmy_apub_lib::{ActivityCommonFields, ActivityHandler, PublicUrl}; +use lemmy_db_queries::source::{comment::Comment_, community::Community_, post::Post_}; +use lemmy_db_schema::source::{comment::Comment, community::Community, post::Post}; +use lemmy_utils::LemmyError; +use lemmy_websocket::{LemmyContext, UserOperationCrud}; +use url::Url; + +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] +#[serde(rename_all = "camelCase")] +pub struct UndoRemovePostCommentOrCommunity { + to: PublicUrl, + object: RemovePostCommentCommunityOrMod, + cc: [Url; 1], + #[serde(rename = "type")] + kind: UndoType, + #[serde(flatten)] + common: ActivityCommonFields, +} + +#[async_trait::async_trait(?Send)] +impl ActivityHandler for UndoRemovePostCommentOrCommunity { + async fn verify( + &self, + context: &LemmyContext, + request_counter: &mut i32, + ) -> Result<(), LemmyError> { + verify_activity(self.common())?; + let object_community = + get_or_fetch_and_upsert_community(&self.object.object, context, request_counter).await; + // removing a community + if object_community.is_ok() { + verify_mod_action(&self.common.actor, self.object.object.clone(), context).await?; + } + // removing a post or comment + else { + verify_person_in_community(&self.common.actor, &self.cc, context, request_counter).await?; + verify_mod_action(&self.common.actor, self.cc[0].clone(), context).await?; + } + self.object.verify(context, request_counter).await?; + // dont check that actor and object.actor are identical, so that one mod can + // undo the action of another + Ok(()) + } + + async fn receive( + &self, + context: &LemmyContext, + request_counter: &mut i32, + ) -> Result<(), LemmyError> { + let object_community = + get_or_fetch_and_upsert_community(&self.object.object, context, request_counter).await; + // restoring a community + if let Ok(community) = object_community { + if community.local { + return Err(anyhow!("Only local admin can undo remove community").into()); + } + let deleted_community = blocking(context.pool(), move |conn| { + Community::update_removed(conn, community.id, false) + }) + .await??; + + send_community_message( + deleted_community.id, + UserOperationCrud::EditCommunity, + context, + ) + .await + } + // restoring a post or comment + else { + match get_or_fetch_and_insert_post_or_comment(&self.object.object, context, request_counter) + .await? + { + PostOrComment::Post(post) => { + let removed_post = blocking(context.pool(), move |conn| { + Post::update_removed(conn, post.id, false) + }) + .await??; + send_post_message(removed_post.id, UserOperationCrud::EditPost, context).await + } + PostOrComment::Comment(comment) => { + let removed_comment = blocking(context.pool(), move |conn| { + Comment::update_removed(conn, comment.id, false) + }) + .await??; + send_comment_message( + removed_comment.id, + vec![], + UserOperationCrud::EditComment, + context, + ) + .await + } + } + } + } + + fn common(&self) -> &ActivityCommonFields { + &self.common + } +} diff --git a/crates/apub_receive/src/activities/voting/dislike.rs b/crates/apub_receive/src/activities/voting/dislike.rs new file mode 100644 index 00000000..18d72f39 --- /dev/null +++ b/crates/apub_receive/src/activities/voting/dislike.rs @@ -0,0 +1,54 @@ +use crate::activities::{ + verify_activity, + verify_person_in_community, + voting::receive_like_or_dislike, +}; +use activitystreams::activity::kind::DislikeType; +use lemmy_apub_lib::{ActivityCommonFields, ActivityHandler, PublicUrl}; +use lemmy_utils::LemmyError; +use lemmy_websocket::LemmyContext; +use url::Url; + +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] +#[serde(rename_all = "camelCase")] +pub struct DislikePostOrComment { + to: PublicUrl, + pub(in crate::activities) object: Url, + cc: [Url; 1], + #[serde(rename = "type")] + kind: DislikeType, + #[serde(flatten)] + common: ActivityCommonFields, +} + +#[async_trait::async_trait(?Send)] +impl ActivityHandler for DislikePostOrComment { + async fn verify( + &self, + context: &LemmyContext, + request_counter: &mut i32, + ) -> Result<(), LemmyError> { + verify_activity(self.common())?; + verify_person_in_community(&self.common.actor, &self.cc, context, request_counter).await?; + Ok(()) + } + + async fn receive( + &self, + context: &LemmyContext, + request_counter: &mut i32, + ) -> Result<(), LemmyError> { + receive_like_or_dislike( + -1, + &self.common.actor, + &self.object, + context, + request_counter, + ) + .await + } + + fn common(&self) -> &ActivityCommonFields { + &self.common + } +} diff --git a/crates/apub_receive/src/activities/voting/like.rs b/crates/apub_receive/src/activities/voting/like.rs new file mode 100644 index 00000000..ca899d3d --- /dev/null +++ b/crates/apub_receive/src/activities/voting/like.rs @@ -0,0 +1,54 @@ +use crate::activities::{ + verify_activity, + verify_person_in_community, + voting::receive_like_or_dislike, +}; +use activitystreams::activity::kind::LikeType; +use lemmy_apub_lib::{ActivityCommonFields, ActivityHandler, PublicUrl}; +use lemmy_utils::LemmyError; +use lemmy_websocket::LemmyContext; +use url::Url; + +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] +#[serde(rename_all = "camelCase")] +pub struct LikePostOrComment { + to: PublicUrl, + pub(in crate::activities::voting) object: Url, + cc: [Url; 1], + #[serde(rename = "type")] + kind: LikeType, + #[serde(flatten)] + common: ActivityCommonFields, +} + +#[async_trait::async_trait(?Send)] +impl ActivityHandler for LikePostOrComment { + async fn verify( + &self, + context: &LemmyContext, + request_counter: &mut i32, + ) -> Result<(), LemmyError> { + verify_activity(self.common())?; + verify_person_in_community(&self.common.actor, &self.cc, context, request_counter).await?; + Ok(()) + } + + async fn receive( + &self, + context: &LemmyContext, + request_counter: &mut i32, + ) -> Result<(), LemmyError> { + receive_like_or_dislike( + 1, + &self.common.actor, + &self.object, + context, + request_counter, + ) + .await + } + + fn common(&self) -> &ActivityCommonFields { + &self.common + } +} diff --git a/crates/apub_receive/src/activities/voting/mod.rs b/crates/apub_receive/src/activities/voting/mod.rs new file mode 100644 index 00000000..123523e2 --- /dev/null +++ b/crates/apub_receive/src/activities/voting/mod.rs @@ -0,0 +1,157 @@ +use crate::activities::{ + comment::send_websocket_message as send_comment_message, + post::send_websocket_message as send_post_message, +}; +use lemmy_api_common::blocking; +use lemmy_apub::{ + fetcher::{ + objects::get_or_fetch_and_insert_post_or_comment, + person::get_or_fetch_and_upsert_person, + }, + PostOrComment, +}; +use lemmy_db_queries::Likeable; +use lemmy_db_schema::source::{ + comment::{Comment, CommentLike, CommentLikeForm}, + post::{Post, PostLike, PostLikeForm}, +}; +use lemmy_utils::LemmyError; +use lemmy_websocket::{LemmyContext, UserOperation}; +use std::ops::Deref; +use url::Url; + +pub mod dislike; +pub mod like; +pub mod undo_dislike; +pub mod undo_like; + +pub(in crate::activities::voting) async fn receive_like_or_dislike( + score: i16, + actor: &Url, + object: &Url, + context: &LemmyContext, + request_counter: &mut i32, +) -> Result<(), LemmyError> { + match get_or_fetch_and_insert_post_or_comment(object, context, request_counter).await? { + PostOrComment::Post(p) => { + like_or_dislike_post(score, actor, p.deref(), context, request_counter).await + } + PostOrComment::Comment(c) => { + like_or_dislike_comment(score, actor, c.deref(), context, request_counter).await + } + } +} + +async fn like_or_dislike_comment( + score: i16, + actor: &Url, + comment: &Comment, + context: &LemmyContext, + request_counter: &mut i32, +) -> Result<(), LemmyError> { + let actor = get_or_fetch_and_upsert_person(actor, context, request_counter).await?; + + let comment_id = comment.id; + let like_form = CommentLikeForm { + comment_id, + post_id: comment.post_id, + person_id: actor.id, + score, + }; + let person_id = actor.id; + blocking(context.pool(), move |conn| { + CommentLike::remove(conn, person_id, comment_id)?; + CommentLike::like(conn, &like_form) + }) + .await??; + + send_comment_message( + comment_id, + vec![], + UserOperation::CreateCommentLike, + context, + ) + .await +} + +async fn like_or_dislike_post( + score: i16, + actor: &Url, + post: &Post, + context: &LemmyContext, + request_counter: &mut i32, +) -> Result<(), LemmyError> { + let actor = get_or_fetch_and_upsert_person(actor, context, request_counter).await?; + + let post_id = post.id; + let like_form = PostLikeForm { + post_id: post.id, + person_id: actor.id, + score, + }; + let person_id = actor.id; + blocking(context.pool(), move |conn| { + PostLike::remove(conn, person_id, post_id)?; + PostLike::like(conn, &like_form) + }) + .await??; + + send_post_message(post.id, UserOperation::CreatePostLike, context).await +} + +pub(in crate::activities::voting) async fn receive_undo_like_or_dislike( + actor: &Url, + object: &Url, + context: &LemmyContext, + request_counter: &mut i32, +) -> Result<(), LemmyError> { + match get_or_fetch_and_insert_post_or_comment(object, context, request_counter).await? { + PostOrComment::Post(p) => { + undo_like_or_dislike_post(actor, p.deref(), context, request_counter).await + } + PostOrComment::Comment(c) => { + undo_like_or_dislike_comment(actor, c.deref(), context, request_counter).await + } + } +} + +async fn undo_like_or_dislike_comment( + actor: &Url, + comment: &Comment, + context: &LemmyContext, + request_counter: &mut i32, +) -> Result<(), LemmyError> { + let actor = get_or_fetch_and_upsert_person(actor, context, request_counter).await?; + + let comment_id = comment.id; + let person_id = actor.id; + blocking(context.pool(), move |conn| { + CommentLike::remove(conn, person_id, comment_id) + }) + .await??; + + send_comment_message( + comment.id, + vec![], + UserOperation::CreateCommentLike, + context, + ) + .await +} + +async fn undo_like_or_dislike_post( + actor: &Url, + post: &Post, + context: &LemmyContext, + request_counter: &mut i32, +) -> Result<(), LemmyError> { + let actor = get_or_fetch_and_upsert_person(actor, context, request_counter).await?; + + let post_id = post.id; + let person_id = actor.id; + blocking(context.pool(), move |conn| { + PostLike::remove(conn, person_id, post_id) + }) + .await??; + send_post_message(post.id, UserOperation::CreatePostLike, context).await +} diff --git a/crates/apub_receive/src/activities/voting/undo_dislike.rs b/crates/apub_receive/src/activities/voting/undo_dislike.rs new file mode 100644 index 00000000..11871e79 --- /dev/null +++ b/crates/apub_receive/src/activities/voting/undo_dislike.rs @@ -0,0 +1,55 @@ +use crate::activities::{ + verify_activity, + verify_person_in_community, + voting::{dislike::DislikePostOrComment, receive_undo_like_or_dislike}, +}; +use activitystreams::activity::kind::UndoType; +use lemmy_apub_lib::{verify_urls_match, ActivityCommonFields, ActivityHandler, PublicUrl}; +use lemmy_utils::LemmyError; +use lemmy_websocket::LemmyContext; +use url::Url; + +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] +#[serde(rename_all = "camelCase")] +pub struct UndoDislikePostOrComment { + to: PublicUrl, + object: DislikePostOrComment, + cc: [Url; 1], + #[serde(rename = "type")] + kind: UndoType, + #[serde(flatten)] + common: ActivityCommonFields, +} + +#[async_trait::async_trait(?Send)] +impl ActivityHandler for UndoDislikePostOrComment { + async fn verify( + &self, + context: &LemmyContext, + request_counter: &mut i32, + ) -> Result<(), LemmyError> { + verify_activity(self.common())?; + verify_person_in_community(&self.common.actor, &self.cc, context, request_counter).await?; + verify_urls_match(&self.common.actor, &self.object.common().actor)?; + self.object.verify(context, request_counter).await?; + Ok(()) + } + + async fn receive( + &self, + context: &LemmyContext, + request_counter: &mut i32, + ) -> Result<(), LemmyError> { + receive_undo_like_or_dislike( + &self.common.actor, + &self.object.object, + context, + request_counter, + ) + .await + } + + fn common(&self) -> &ActivityCommonFields { + &self.common + } +} diff --git a/crates/apub_receive/src/activities/voting/undo_like.rs b/crates/apub_receive/src/activities/voting/undo_like.rs new file mode 100644 index 00000000..07c3c470 --- /dev/null +++ b/crates/apub_receive/src/activities/voting/undo_like.rs @@ -0,0 +1,55 @@ +use crate::activities::{ + verify_activity, + verify_person_in_community, + voting::{like::LikePostOrComment, receive_undo_like_or_dislike}, +}; +use activitystreams::activity::kind::UndoType; +use lemmy_apub_lib::{verify_urls_match, ActivityCommonFields, ActivityHandler, PublicUrl}; +use lemmy_utils::LemmyError; +use lemmy_websocket::LemmyContext; +use url::Url; + +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] +#[serde(rename_all = "camelCase")] +pub struct UndoLikePostOrComment { + to: PublicUrl, + object: LikePostOrComment, + cc: [Url; 1], + #[serde(rename = "type")] + kind: UndoType, + #[serde(flatten)] + common: ActivityCommonFields, +} + +#[async_trait::async_trait(?Send)] +impl ActivityHandler for UndoLikePostOrComment { + async fn verify( + &self, + context: &LemmyContext, + request_counter: &mut i32, + ) -> Result<(), LemmyError> { + verify_activity(self.common())?; + verify_person_in_community(&self.common.actor, &self.cc, context, request_counter).await?; + verify_urls_match(&self.common.actor, &self.object.common().actor)?; + self.object.verify(context, request_counter).await?; + Ok(()) + } + + async fn receive( + &self, + context: &LemmyContext, + request_counter: &mut i32, + ) -> Result<(), LemmyError> { + receive_undo_like_or_dislike( + &self.common.actor, + &self.object.object, + context, + request_counter, + ) + .await + } + + fn common(&self) -> &ActivityCommonFields { + &self.common + } +} diff --git a/crates/apub_receive/src/http/community.rs b/crates/apub_receive/src/http/community.rs index 30daa3b1..106eeff2 100644 --- a/crates/apub_receive/src/http/community.rs +++ b/crates/apub_receive/src/http/community.rs @@ -1,10 +1,16 @@ -use crate::http::{create_apub_response, create_apub_tombstone_response}; +use crate::http::{ + create_apub_response, + create_apub_tombstone_response, + inbox_enums::GroupInboxActivities, + payload_to_string, + receive_activity, +}; use activitystreams::{ base::{AnyBase, BaseExt}, collection::{CollectionExt, OrderedCollection, UnorderedCollection}, url::Url, }; -use actix_web::{body::Body, web, HttpResponse}; +use actix_web::{body::Body, web, web::Payload, HttpRequest, HttpResponse}; use lemmy_api_common::blocking; use lemmy_apub::{ extensions::context::lemmy_context, @@ -46,6 +52,17 @@ pub(crate) async fn get_apub_community_http( } } +/// Handler for all incoming receive to community inboxes. +pub async fn community_inbox( + request: HttpRequest, + payload: Payload, + _path: web::Path, + context: web::Data, +) -> Result { + let unparsed = payload_to_string(payload).await?; + receive_activity::(request, &unparsed, context).await +} + /// Returns an empty followers collection, only populating the size (for privacy). pub(crate) async fn get_apub_community_followers( info: web::Path, diff --git a/crates/apub_receive/src/http/inbox_enums.rs b/crates/apub_receive/src/http/inbox_enums.rs new file mode 100644 index 00000000..0f31f534 --- /dev/null +++ b/crates/apub_receive/src/http/inbox_enums.rs @@ -0,0 +1,100 @@ +use crate::activities::{ + comment::{create::CreateComment, update::UpdateComment}, + community::{ + add_mod::AddMod, + announce::AnnounceActivity, + block_user::BlockUserFromCommunity, + undo_block_user::UndoBlockUserFromCommunity, + update::UpdateCommunity, + }, + deletion::{delete::DeletePostCommentOrCommunity, undo_delete::UndoDeletePostCommentOrCommunity}, + following::{accept::AcceptFollowCommunity, follow::FollowCommunity, undo::UndoFollowCommunity}, + post::{create::CreatePost, update::UpdatePost}, + private_message::{ + create::CreatePrivateMessage, + delete::DeletePrivateMessage, + undo_delete::UndoDeletePrivateMessage, + update::UpdatePrivateMessage, + }, + removal::{ + remove::RemovePostCommentCommunityOrMod, + undo_remove::UndoRemovePostCommentOrCommunity, + }, + voting::{ + dislike::DislikePostOrComment, + like::LikePostOrComment, + undo_dislike::UndoDislikePostOrComment, + undo_like::UndoLikePostOrComment, + }, +}; +use lemmy_apub_lib::{ActivityCommonFields, ActivityHandler}; +use lemmy_utils::LemmyError; +use lemmy_websocket::LemmyContext; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, Deserialize, Serialize, ActivityHandler)] +#[serde(untagged)] +pub enum PersonInboxActivities { + AcceptFollowCommunity(AcceptFollowCommunity), + CreatePrivateMessage(CreatePrivateMessage), + UpdatePrivateMessage(UpdatePrivateMessage), + DeletePrivateMessage(DeletePrivateMessage), + UndoDeletePrivateMessage(UndoDeletePrivateMessage), + AnnounceActivity(Box), +} + +#[derive(Clone, Debug, Deserialize, Serialize, ActivityHandler)] +#[serde(untagged)] +pub enum GroupInboxActivities { + FollowCommunity(FollowCommunity), + UndoFollowCommunity(UndoFollowCommunity), + CreateComment(CreateComment), + UpdateComment(UpdateComment), + CreatePost(CreatePost), + UpdatePost(UpdatePost), + LikePostOrComment(LikePostOrComment), + DislikePostOrComment(DislikePostOrComment), + UndoLikePostOrComment(UndoLikePostOrComment), + UndoDislikePostOrComment(UndoDislikePostOrComment), + DeletePostCommentOrCommunity(DeletePostCommentOrCommunity), + UndoDeletePostCommentOrCommunity(UndoDeletePostCommentOrCommunity), + RemovePostCommentOrCommunity(RemovePostCommentCommunityOrMod), + UndoRemovePostCommentOrCommunity(UndoRemovePostCommentOrCommunity), + UpdateCommunity(Box), + BlockUserFromCommunity(BlockUserFromCommunity), + UndoBlockUserFromCommunity(UndoBlockUserFromCommunity), + AddMod(AddMod), +} + +#[derive(Clone, Debug, Deserialize, Serialize, ActivityHandler)] +#[serde(untagged)] +pub enum SharedInboxActivities { + // received by group + FollowCommunity(FollowCommunity), + UndoFollowCommunity(UndoFollowCommunity), + CreateComment(CreateComment), + UpdateComment(UpdateComment), + CreatePost(CreatePost), + UpdatePost(UpdatePost), + LikePostOrComment(LikePostOrComment), + DislikePostOrComment(DislikePostOrComment), + UndoDislikePostOrComment(UndoDislikePostOrComment), + UndoLikePostOrComment(UndoLikePostOrComment), + DeletePostCommentOrCommunity(DeletePostCommentOrCommunity), + UndoDeletePostCommentOrCommunity(UndoDeletePostCommentOrCommunity), + RemovePostCommentOrCommunity(RemovePostCommentCommunityOrMod), + UndoRemovePostCommentOrCommunity(UndoRemovePostCommentOrCommunity), + UpdateCommunity(Box), + BlockUserFromCommunity(BlockUserFromCommunity), + UndoBlockUserFromCommunity(UndoBlockUserFromCommunity), + AddMod(AddMod), + // received by person + AcceptFollowCommunity(AcceptFollowCommunity), + // Note, pm activities need to be at the end, otherwise comments will end up here. We can probably + // avoid this problem by replacing createpm.object with our own struct, instead of NoteExt. + CreatePrivateMessage(CreatePrivateMessage), + UpdatePrivateMessage(UpdatePrivateMessage), + DeletePrivateMessage(DeletePrivateMessage), + UndoDeletePrivateMessage(UndoDeletePrivateMessage), + AnnounceActivity(Box), +} diff --git a/crates/apub_receive/src/http/mod.rs b/crates/apub_receive/src/http/mod.rs index 4f332849..44302d8d 100644 --- a/crates/apub_receive/src/http/mod.rs +++ b/crates/apub_receive/src/http/mod.rs @@ -1,18 +1,104 @@ -use actix_web::{body::Body, web, HttpResponse}; +use crate::http::inbox_enums::SharedInboxActivities; +use actix_web::{ + body::Body, + web, + web::{Bytes, BytesMut, Payload}, + HttpRequest, + HttpResponse, +}; +use anyhow::{anyhow, Context}; +use futures::StreamExt; use http::StatusCode; use lemmy_api_common::blocking; -use lemmy_apub::APUB_JSON_CONTENT_TYPE; -use lemmy_db_queries::source::activity::Activity_; +use lemmy_apub::{ + check_is_apub_id_valid, + extensions::signatures::verify_signature, + fetcher::get_or_fetch_and_upsert_actor, + insert_activity, + APUB_JSON_CONTENT_TYPE, +}; +use lemmy_apub_lib::ActivityHandler; +use lemmy_db_queries::{source::activity::Activity_, DbPool}; use lemmy_db_schema::source::activity::Activity; -use lemmy_utils::{settings::structs::Settings, LemmyError}; +use lemmy_utils::{location_info, settings::structs::Settings, LemmyError}; use lemmy_websocket::LemmyContext; use serde::{Deserialize, Serialize}; +use std::{fmt::Debug, io::Read}; use url::Url; -pub mod comment; -pub mod community; -pub mod person; -pub mod post; +mod comment; +mod community; +mod inbox_enums; +mod person; +mod post; +pub mod routes; + +pub async fn shared_inbox( + request: HttpRequest, + payload: Payload, + context: web::Data, +) -> Result { + let unparsed = payload_to_string(payload).await?; + receive_activity::(request, &unparsed, context).await +} + +async fn payload_to_string(mut payload: Payload) -> Result { + let mut bytes = BytesMut::new(); + while let Some(item) = payload.next().await { + bytes.extend_from_slice(&item?); + } + let mut unparsed = String::new(); + Bytes::from(bytes).as_ref().read_to_string(&mut unparsed)?; + Ok(unparsed) +} + +// TODO: move most of this code to library +async fn receive_activity<'a, T>( + request: HttpRequest, + activity: &'a str, + context: web::Data, +) -> Result +where + T: ActivityHandler + Clone + Deserialize<'a> + Serialize + std::fmt::Debug + Send + 'static, +{ + let activity = serde_json::from_str::(activity)?; + let activity_data = activity.common(); + + let request_counter = &mut 0; + let actor = + get_or_fetch_and_upsert_actor(&activity_data.actor, &context, request_counter).await?; + verify_signature(&request, &actor.public_key().context(location_info!())?)?; + + // Do nothing if we received the same activity before + if is_activity_already_known(context.pool(), activity_data.id_unchecked()).await? { + return Ok(HttpResponse::Ok().finish()); + } + check_is_apub_id_valid(&activity_data.actor, false)?; + println!( + "Verifying activity {}", + activity_data.id_unchecked().to_string() + ); + activity.verify(&context, request_counter).await?; + assert_activity_not_local(&activity)?; + + // Log the activity, so we avoid receiving and parsing it twice. Note that this could still happen + // if we receive the same activity twice in very quick succession. + insert_activity( + activity_data.id_unchecked(), + activity.clone(), + false, + true, + context.pool(), + ) + .await?; + + println!( + "Receiving activity {}", + activity_data.id_unchecked().to_string() + ); + activity.receive(&context, request_counter).await?; + Ok(HttpResponse::Ok().finish()) +} /// Convert the data to json and turn it into an HTTP Response with the correct ActivityPub /// headers. @@ -36,14 +122,14 @@ where } #[derive(Deserialize)] -pub struct CommunityQuery { +pub struct ActivityQuery { type_: String, id: String, } -/// Return the ActivityPub json representation of a local community over HTTP. +/// Return the ActivityPub json representation of a local activity over HTTP. pub(crate) async fn get_activity( - info: web::Path, + info: web::Path, context: web::Data, ) -> Result, LemmyError> { let settings = Settings::get(); @@ -66,3 +152,37 @@ pub(crate) async fn get_activity( Ok(create_apub_response(&activity.data)) } } + +pub(crate) async fn is_activity_already_known( + pool: &DbPool, + activity_id: &Url, +) -> Result { + let activity_id = activity_id.to_owned().into(); + let existing = blocking(pool, move |conn| { + Activity::read_from_apub_id(conn, &activity_id) + }) + .await?; + match existing { + Ok(_) => Ok(true), + Err(_) => Ok(false), + } +} + +fn assert_activity_not_local(activity: &T) -> Result<(), LemmyError> { + let activity_domain = activity + .common() + .id_unchecked() + .domain() + .context(location_info!())?; + + if activity_domain == Settings::get().hostname() { + return Err( + anyhow!( + "Error: received activity which was sent by local instance: {:?}", + activity + ) + .into(), + ); + } + Ok(()) +} diff --git a/crates/apub_receive/src/http/person.rs b/crates/apub_receive/src/http/person.rs index 0d0b8f76..59cc8697 100644 --- a/crates/apub_receive/src/http/person.rs +++ b/crates/apub_receive/src/http/person.rs @@ -1,9 +1,15 @@ -use crate::http::{create_apub_response, create_apub_tombstone_response}; +use crate::http::{ + create_apub_response, + create_apub_tombstone_response, + inbox_enums::PersonInboxActivities, + payload_to_string, + receive_activity, +}; use activitystreams::{ base::BaseExt, collection::{CollectionExt, OrderedCollection}, }; -use actix_web::{body::Body, web, HttpResponse}; +use actix_web::{body::Body, web, web::Payload, HttpRequest, HttpResponse}; use lemmy_api_common::blocking; use lemmy_apub::{extensions::context::lemmy_context, objects::ToApub, ActorType}; use lemmy_db_queries::source::person::Person_; @@ -39,6 +45,16 @@ pub(crate) async fn get_apub_person_http( } } +pub async fn person_inbox( + request: HttpRequest, + payload: Payload, + _path: web::Path, + context: web::Data, +) -> Result { + let unparsed = payload_to_string(payload).await?; + receive_activity::(request, &unparsed, context).await +} + pub(crate) async fn get_apub_person_outbox( info: web::Path, context: web::Data, diff --git a/crates/apub_receive/src/routes.rs b/crates/apub_receive/src/http/routes.rs similarity index 83% rename from crates/apub_receive/src/routes.rs rename to crates/apub_receive/src/http/routes.rs index d112afbe..929df38b 100644 --- a/crates/apub_receive/src/routes.rs +++ b/crates/apub_receive/src/http/routes.rs @@ -1,22 +1,17 @@ -use crate::{ - http::{ - comment::get_apub_comment, - community::{ - get_apub_community_followers, - get_apub_community_http, - get_apub_community_inbox, - get_apub_community_moderators, - get_apub_community_outbox, - }, - get_activity, - person::{get_apub_person_http, get_apub_person_inbox, get_apub_person_outbox}, - post::get_apub_post, - }, - inbox::{ - community_inbox::community_inbox, - person_inbox::person_inbox, - shared_inbox::shared_inbox, +use crate::http::{ + comment::get_apub_comment, + community::{ + community_inbox, + get_apub_community_followers, + get_apub_community_http, + get_apub_community_inbox, + get_apub_community_moderators, + get_apub_community_outbox, }, + get_activity, + person::{get_apub_person_http, get_apub_person_inbox, get_apub_person_outbox, person_inbox}, + post::get_apub_post, + shared_inbox, }; use actix_web::*; use http_signature_normalization_actix::digest::middleware::VerifyDigest; diff --git a/crates/apub_receive/src/inbox/community_inbox.rs b/crates/apub_receive/src/inbox/community_inbox.rs deleted file mode 100644 index d851b7c0..00000000 --- a/crates/apub_receive/src/inbox/community_inbox.rs +++ /dev/null @@ -1,346 +0,0 @@ -use crate::{ - activities::receive::verify_activity_domains_valid, - inbox::{ - assert_activity_not_local, - get_activity_id, - inbox_verify_http_signature, - is_activity_already_known, - receive_for_community::{ - receive_add_for_community, - receive_block_user_for_community, - receive_create_for_community, - receive_delete_for_community, - receive_dislike_for_community, - receive_like_for_community, - receive_remove_for_community, - receive_undo_for_community, - receive_update_for_community, - }, - verify_is_addressed_to_public, - }, -}; -use activitystreams::{ - activity::{kind::FollowType, ActorAndObject, Follow, Undo}, - base::AnyBase, - prelude::*, -}; -use actix_web::{web, HttpRequest, HttpResponse}; -use anyhow::{anyhow, Context}; -use lemmy_api_common::blocking; -use lemmy_apub::{ - check_community_or_site_ban, - get_activity_to_and_cc, - insert_activity, - ActorType, - CommunityType, -}; -use lemmy_db_queries::{source::community::Community_, ApubObject, Followable}; -use lemmy_db_schema::source::{ - community::{Community, CommunityFollower, CommunityFollowerForm}, - person::Person, -}; -use lemmy_utils::{location_info, LemmyError}; -use lemmy_websocket::LemmyContext; -use log::info; -use serde::{Deserialize, Serialize}; -use std::fmt::Debug; -use url::Url; - -/// Allowed activities for community inbox. -#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, Deserialize, Serialize)] -#[serde(rename_all = "PascalCase")] -pub enum CommunityValidTypes { - Follow, // follow request from a person - Undo, // unfollow from a person - Create, // create post or comment - Update, // update post or comment - Like, // upvote post or comment - Dislike, // downvote post or comment - Delete, // post or comment deleted by creator - Remove, // post or comment removed by mod or admin, or mod removed from community - Add, // mod added to community - Block, // user blocked by community -} - -pub type CommunityAcceptedActivities = ActorAndObject; - -/// Handler for all incoming receive to community inboxes. -pub async fn community_inbox( - request: HttpRequest, - input: web::Json, - path: web::Path, - context: web::Data, -) -> Result { - let activity = input.into_inner(); - // First of all check the http signature - let request_counter = &mut 0; - let actor = inbox_verify_http_signature(&activity, &context, request, request_counter).await?; - - // Do nothing if we received the same activity before - let activity_id = get_activity_id(&activity, &actor.actor_id())?; - if is_activity_already_known(context.pool(), &activity_id).await? { - return Ok(HttpResponse::Ok().finish()); - } - - // Check if the activity is actually meant for us - let path = path.into_inner(); - let community = blocking(context.pool(), move |conn| { - Community::read_from_name(conn, &path) - }) - .await??; - let to_and_cc = get_activity_to_and_cc(&activity); - if !to_and_cc.contains(&community.actor_id()) { - return Err(anyhow!("Activity delivered to wrong community").into()); - } - - assert_activity_not_local(&activity)?; - insert_activity(&activity_id, activity.clone(), false, true, context.pool()).await?; - - community_receive_message( - activity.clone(), - community.clone(), - actor.as_ref(), - &context, - request_counter, - ) - .await -} - -/// Receives Follow, Undo/Follow, post actions, comment actions (including votes) -pub(crate) async fn community_receive_message( - activity: CommunityAcceptedActivities, - to_community: Community, - actor: &dyn ActorType, - context: &LemmyContext, - request_counter: &mut i32, -) -> Result { - // Only persons can send activities to the community, so we can get the actor as person - // unconditionally. - let actor_id = actor.actor_id(); - let person = blocking(context.pool(), move |conn| { - Person::read_from_apub_id(conn, &actor_id.into()) - }) - .await??; - check_community_or_site_ban(&person, to_community.id, context.pool()).await?; - - info!( - "Community {} received activity {} from {}", - to_community.name, - &activity - .id_unchecked() - .context(location_info!())? - .to_string(), - &person.actor_id().to_string() - ); - - let any_base = activity.clone().into_any_base()?; - let actor_url = actor.actor_id(); - let activity_kind = activity.kind().context(location_info!())?; - let do_announce = match activity_kind { - CommunityValidTypes::Follow => { - Box::pin(handle_follow( - any_base.clone(), - person, - &to_community, - context, - )) - .await?; - false - } - CommunityValidTypes::Undo => { - Box::pin(handle_undo( - context, - activity.clone(), - actor_url, - &to_community, - request_counter, - )) - .await? - } - CommunityValidTypes::Create => { - Box::pin(receive_create_for_community( - context, - any_base.clone(), - &actor_url, - request_counter, - )) - .await?; - true - } - CommunityValidTypes::Update => { - Box::pin(receive_update_for_community( - context, - any_base.clone(), - None, - &actor_url, - request_counter, - )) - .await?; - true - } - CommunityValidTypes::Like => { - Box::pin(receive_like_for_community( - context, - any_base.clone(), - &actor_url, - request_counter, - )) - .await?; - true - } - CommunityValidTypes::Dislike => { - Box::pin(receive_dislike_for_community( - context, - any_base.clone(), - &actor_url, - request_counter, - )) - .await?; - true - } - CommunityValidTypes::Delete => { - Box::pin(receive_delete_for_community( - context, - any_base.clone(), - None, - &actor_url, - request_counter, - )) - .await?; - true - } - CommunityValidTypes::Add => { - Box::pin(receive_add_for_community( - context, - any_base.clone(), - None, - request_counter, - )) - .await?; - true - } - CommunityValidTypes::Remove => { - Box::pin(receive_remove_for_community( - context, - any_base.clone(), - None, - request_counter, - )) - .await?; - true - } - CommunityValidTypes::Block => { - Box::pin(receive_block_user_for_community( - context, - any_base.clone(), - None, - request_counter, - )) - .await?; - true - } - }; - - if do_announce { - // Check again that the activity is public, just to be sure - verify_is_addressed_to_public(&activity)?; - let mut object_actor = activity.object().clone().single_xsd_any_uri(); - // If activity is something like Undo/Block, we need to access activity.object.object - if object_actor.is_none() { - object_actor = activity - .object() - .as_one() - .map(|a| ActorAndObject::from_any_base(a.to_owned()).ok()) - .flatten() - .flatten() - .map(|a: ActorAndObject| a.object().to_owned().single_xsd_any_uri()) - .flatten(); - } - to_community - .send_announce(activity.into_any_base()?, object_actor, context) - .await?; - } - - Ok(HttpResponse::Ok().finish()) -} - -/// Handle a follow request from a remote person, adding the person as follower and returning an -/// Accept activity. -async fn handle_follow( - activity: AnyBase, - person: Person, - community: &Community, - context: &LemmyContext, -) -> Result { - let follow = Follow::from_any_base(activity)?.context(location_info!())?; - verify_activity_domains_valid(&follow, &person.actor_id(), false)?; - - let community_follower_form = CommunityFollowerForm { - community_id: community.id, - person_id: person.id, - pending: false, - }; - - // This will fail if they're already a follower, but ignore the error. - blocking(context.pool(), move |conn| { - CommunityFollower::follow(conn, &community_follower_form).ok() - }) - .await?; - - community.send_accept_follow(follow, context).await?; - - Ok(HttpResponse::Ok().finish()) -} - -async fn handle_undo( - context: &LemmyContext, - activity: CommunityAcceptedActivities, - actor_url: Url, - to_community: &Community, - request_counter: &mut i32, -) -> Result { - let inner_kind = activity - .object() - .is_single_kind(&FollowType::Follow.to_string()); - let any_base = activity.into_any_base()?; - if inner_kind { - handle_undo_follow(any_base, actor_url, to_community, context).await?; - Ok(false) - } else { - receive_undo_for_community(context, any_base, None, &actor_url, request_counter).await?; - Ok(true) - } -} - -/// Handle `Undo/Follow` from a person, removing the person from followers list. -async fn handle_undo_follow( - activity: AnyBase, - person_url: Url, - community: &Community, - context: &LemmyContext, -) -> Result<(), LemmyError> { - let undo = Undo::from_any_base(activity)?.context(location_info!())?; - verify_activity_domains_valid(&undo, &person_url, true)?; - - let object = undo.object().to_owned().one().context(location_info!())?; - let follow = Follow::from_any_base(object)?.context(location_info!())?; - verify_activity_domains_valid(&follow, &person_url, false)?; - - let person = blocking(context.pool(), move |conn| { - Person::read_from_apub_id(conn, &person_url.into()) - }) - .await??; - let community_follower_form = CommunityFollowerForm { - community_id: community.id, - person_id: person.id, - pending: false, - }; - - // This will fail if they aren't a follower, but ignore the error. - blocking(context.pool(), move |conn| { - CommunityFollower::unfollow(conn, &community_follower_form).ok() - }) - .await?; - - Ok(()) -} diff --git a/crates/apub_receive/src/inbox/mod.rs b/crates/apub_receive/src/inbox/mod.rs deleted file mode 100644 index 6b6f7b3f..00000000 --- a/crates/apub_receive/src/inbox/mod.rs +++ /dev/null @@ -1,153 +0,0 @@ -use activitystreams::{ - activity::ActorAndObjectRefExt, - base::{AsBase, BaseExt, Extends}, - object::AsObject, - public, -}; -use actix_web::HttpRequest; -use anyhow::{anyhow, Context}; -use lemmy_api_common::blocking; -use lemmy_apub::{ - check_is_apub_id_valid, - extensions::signatures::verify_signature, - fetcher::get_or_fetch_and_upsert_actor, - get_activity_to_and_cc, - ActorType, -}; -use lemmy_db_queries::{ - source::{activity::Activity_, community::Community_}, - ApubObject, - DbPool, -}; -use lemmy_db_schema::source::{activity::Activity, community::Community, person::Person}; -use lemmy_utils::{location_info, settings::structs::Settings, LemmyError}; -use lemmy_websocket::LemmyContext; -use serde::Serialize; -use std::fmt::Debug; -use url::Url; - -pub mod community_inbox; -pub mod person_inbox; -pub(crate) mod receive_for_community; -pub mod shared_inbox; - -pub(crate) fn get_activity_id(activity: &T, creator_uri: &Url) -> Result -where - T: BaseExt + Extends + Debug, - Kind: Serialize, - >::Error: From + Send + Sync + 'static, -{ - let creator_domain = creator_uri.host_str().context(location_info!())?; - let activity_id = activity.id(creator_domain)?; - Ok(activity_id.context(location_info!())?.to_owned()) -} - -pub(crate) async fn is_activity_already_known( - pool: &DbPool, - activity_id: &Url, -) -> Result { - let activity_id = activity_id.to_owned().into(); - let existing = blocking(pool, move |conn| { - Activity::read_from_apub_id(conn, &activity_id) - }) - .await?; - match existing { - Ok(_) => Ok(true), - Err(_) => Ok(false), - } -} - -pub(crate) fn verify_is_addressed_to_public(activity: &T) -> Result<(), LemmyError> -where - T: AsBase + AsObject + ActorAndObjectRefExt, -{ - let to_and_cc = get_activity_to_and_cc(activity); - if to_and_cc.contains(&public()) { - Ok(()) - } else { - Err(anyhow!("Activity is not addressed to public").into()) - } -} - -pub(crate) async fn inbox_verify_http_signature( - activity: &T, - context: &LemmyContext, - request: HttpRequest, - request_counter: &mut i32, -) -> Result, LemmyError> -where - T: AsObject + ActorAndObjectRefExt + Extends + AsBase, - Kind: Serialize, - >::Error: From + Send + Sync + 'static, -{ - let actor_id = activity - .actor()? - .to_owned() - .single_xsd_any_uri() - .context(location_info!())?; - check_is_apub_id_valid(&actor_id, false)?; - let actor = get_or_fetch_and_upsert_actor(&actor_id, context, request_counter).await?; - verify_signature(&request, actor.as_ref())?; - Ok(actor) -} - -/// Returns true if `to_and_cc` contains at least one local user. -pub(crate) async fn is_addressed_to_local_person( - to_and_cc: &[Url], - pool: &DbPool, -) -> Result { - for url in to_and_cc { - let url = url.to_owned(); - let person = blocking(pool, move |conn| { - Person::read_from_apub_id(conn, &url.into()) - }) - .await?; - if let Ok(u) = person { - if u.local { - return Ok(true); - } - } - } - Ok(false) -} - -/// If `to_and_cc` contains the followers collection of a remote community, returns this community -/// (like `https://example.com/c/main/followers`) -pub(crate) async fn is_addressed_to_community_followers( - to_and_cc: &[Url], - pool: &DbPool, -) -> Result, LemmyError> { - for url in to_and_cc { - let url = url.to_owned().into(); - let community = blocking(pool, move |conn| { - // ignore errors here, because the current url might not actually be a followers url - Community::read_from_followers_url(conn, &url).ok() - }) - .await?; - if let Some(c) = community { - if !c.local { - return Ok(Some(c)); - } - } - } - Ok(None) -} - -pub(in crate::inbox) fn assert_activity_not_local(activity: &T) -> Result<(), LemmyError> -where - T: BaseExt + Debug, -{ - let id = activity.id_unchecked().context(location_info!())?; - let activity_domain = id.domain().context(location_info!())?; - - if activity_domain == Settings::get().hostname() { - return Err( - anyhow!( - "Error: received activity which was sent by local instance: {:?}", - activity - ) - .into(), - ); - } - Ok(()) -} diff --git a/crates/apub_receive/src/inbox/person_inbox.rs b/crates/apub_receive/src/inbox/person_inbox.rs deleted file mode 100644 index 080e143c..00000000 --- a/crates/apub_receive/src/inbox/person_inbox.rs +++ /dev/null @@ -1,515 +0,0 @@ -use crate::{ - activities::receive::{ - comment::{receive_create_comment, receive_update_comment}, - community::{ - receive_delete_community, - receive_remove_community, - receive_undo_delete_community, - receive_undo_remove_community, - }, - private_message::{ - receive_create_private_message, - receive_delete_private_message, - receive_undo_delete_private_message, - receive_update_private_message, - }, - receive_unhandled_activity, - verify_activity_domains_valid, - }, - inbox::{ - assert_activity_not_local, - get_activity_id, - inbox_verify_http_signature, - is_activity_already_known, - is_addressed_to_community_followers, - is_addressed_to_local_person, - receive_for_community::{ - receive_add_for_community, - receive_block_user_for_community, - receive_create_for_community, - receive_delete_for_community, - receive_dislike_for_community, - receive_like_for_community, - receive_remove_for_community, - receive_undo_for_community, - receive_update_for_community, - }, - verify_is_addressed_to_public, - }, -}; -use activitystreams::{ - activity::{Accept, ActorAndObject, Announce, Create, Delete, Follow, Remove, Undo, Update}, - base::AnyBase, - prelude::*, -}; -use actix_web::{web, HttpRequest, HttpResponse}; -use anyhow::{anyhow, Context}; -use diesel::NotFound; -use lemmy_api_common::blocking; -use lemmy_apub::{ - check_is_apub_id_valid, - fetcher::community::get_or_fetch_and_upsert_community, - get_activity_to_and_cc, - insert_activity, - ActorType, -}; -use lemmy_db_queries::{source::person::Person_, ApubObject, Followable}; -use lemmy_db_schema::source::{ - community::{Community, CommunityFollower}, - person::Person, - private_message::PrivateMessage, -}; -use lemmy_utils::{location_info, LemmyError}; -use lemmy_websocket::LemmyContext; -use log::info; -use serde::{Deserialize, Serialize}; -use std::fmt::Debug; -use strum_macros::EnumString; -use url::Url; - -/// Allowed activities for person inbox. -#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, Deserialize, Serialize)] -#[serde(rename_all = "PascalCase")] -pub enum PersonValidTypes { - Accept, // community accepted our follow request - Create, // create private message - Update, // edit private message - Delete, // private message or community deleted by creator - Undo, // private message or community restored - Remove, // community removed by admin - Announce, // post, comment or vote in community -} - -pub type PersonAcceptedActivities = ActorAndObject; - -/// Handler for all incoming activities to person inboxes. -pub async fn person_inbox( - request: HttpRequest, - input: web::Json, - path: web::Path, - context: web::Data, -) -> Result { - let activity = input.into_inner(); - // First of all check the http signature - let request_counter = &mut 0; - let actor = inbox_verify_http_signature(&activity, &context, request, request_counter).await?; - - // Do nothing if we received the same activity before - let activity_id = get_activity_id(&activity, &actor.actor_id())?; - if is_activity_already_known(context.pool(), &activity_id).await? { - return Ok(HttpResponse::Ok().finish()); - } - - // Check if the activity is actually meant for us - let username = path.into_inner(); - let person = blocking(context.pool(), move |conn| { - Person::find_by_name(conn, &username) - }) - .await??; - let to_and_cc = get_activity_to_and_cc(&activity); - // TODO: we should also accept activities that are sent to community followers - if !to_and_cc.contains(&person.actor_id()) { - return Err(anyhow!("Activity delivered to wrong person").into()); - } - - assert_activity_not_local(&activity)?; - insert_activity(&activity_id, activity.clone(), false, true, context.pool()).await?; - - person_receive_message( - activity.clone(), - Some(person.clone()), - actor.as_ref(), - &context, - request_counter, - ) - .await -} - -/// Receives Accept/Follow, Announce, private messages and community (undo) remove, (undo) delete -pub(crate) async fn person_receive_message( - activity: PersonAcceptedActivities, - to_person: Option, - actor: &dyn ActorType, - context: &LemmyContext, - request_counter: &mut i32, -) -> Result { - is_for_person_inbox(context, &activity).await?; - - info!( - "User received activity {:?} from {}", - &activity - .id_unchecked() - .context(location_info!())? - .to_string(), - &actor.actor_id().to_string() - ); - - let any_base = activity.clone().into_any_base()?; - let kind = activity.kind().context(location_info!())?; - let actor_url = actor.actor_id(); - match kind { - PersonValidTypes::Accept => { - receive_accept( - context, - any_base, - actor, - to_person.expect("person provided"), - request_counter, - ) - .await?; - } - PersonValidTypes::Announce => { - Box::pin(receive_announce(context, any_base, actor, request_counter)).await? - } - PersonValidTypes::Create => { - Box::pin(receive_create( - context, - any_base, - actor_url, - request_counter, - )) - .await? - } - PersonValidTypes::Update => { - Box::pin(receive_update( - context, - any_base, - actor_url, - request_counter, - )) - .await? - } - PersonValidTypes::Delete => { - Box::pin(receive_delete( - context, - any_base, - &actor_url, - request_counter, - )) - .await? - } - PersonValidTypes::Undo => { - Box::pin(receive_undo(context, any_base, &actor_url, request_counter)).await? - } - PersonValidTypes::Remove => Box::pin(receive_remove(context, any_base, &actor_url)).await?, - }; - - // TODO: would be logical to move websocket notification code here - - Ok(HttpResponse::Ok().finish()) -} - -/// Returns true if the activity is addressed directly to one or more local persons, or if it is -/// addressed to the followers collection of a remote community, and at least one local person follows -/// it. -async fn is_for_person_inbox( - context: &LemmyContext, - activity: &PersonAcceptedActivities, -) -> Result<(), LemmyError> { - let to_and_cc = get_activity_to_and_cc(activity); - // Check if it is addressed directly to any local person - if is_addressed_to_local_person(&to_and_cc, context.pool()).await? { - return Ok(()); - } - - // Check if it is addressed to any followers collection of a remote community, and that the - // community has local followers. - let community = is_addressed_to_community_followers(&to_and_cc, context.pool()).await?; - if let Some(c) = community { - let community_id = c.id; - let has_local_followers = blocking(context.pool(), move |conn| { - CommunityFollower::has_local_followers(conn, community_id) - }) - .await??; - if c.local { - return Err( - anyhow!("Remote activity cant be addressed to followers of local community").into(), - ); - } - if has_local_followers { - return Ok(()); - } - } - - Err(anyhow!("Not addressed for any local person").into()) -} - -/// Handle accepted follows. -async fn receive_accept( - context: &LemmyContext, - activity: AnyBase, - actor: &dyn ActorType, - person: Person, - request_counter: &mut i32, -) -> Result<(), LemmyError> { - let accept = Accept::from_any_base(activity)?.context(location_info!())?; - verify_activity_domains_valid(&accept, &actor.actor_id(), false)?; - - let object = accept.object().to_owned().one().context(location_info!())?; - let follow = Follow::from_any_base(object)?.context(location_info!())?; - verify_activity_domains_valid(&follow, &person.actor_id(), false)?; - - let community_uri = accept - .actor()? - .to_owned() - .single_xsd_any_uri() - .context(location_info!())?; - - let community = - get_or_fetch_and_upsert_community(&community_uri, context, request_counter).await?; - - let community_id = community.id; - let person_id = person.id; - // This will throw an error if no follow was requested - blocking(context.pool(), move |conn| { - CommunityFollower::follow_accepted(conn, community_id, person_id) - }) - .await??; - - Ok(()) -} - -#[derive(EnumString)] -enum AnnouncableActivities { - Create, - Update, - Like, - Dislike, - Delete, - Remove, - Undo, - Add, - Block, -} - -/// Takes an announce and passes the inner activity to the appropriate handler. -pub async fn receive_announce( - context: &LemmyContext, - activity: AnyBase, - actor: &dyn ActorType, - request_counter: &mut i32, -) -> Result<(), LemmyError> { - let announce = Announce::from_any_base(activity)?.context(location_info!())?; - verify_activity_domains_valid(&announce, &actor.actor_id(), false)?; - verify_is_addressed_to_public(&announce)?; - - let kind = announce - .object() - .as_single_kind_str() - .and_then(|s| s.parse().ok()); - let inner_activity = announce - .object() - .to_owned() - .one() - .context(location_info!())?; - - let inner_id = inner_activity.id().context(location_info!())?.to_owned(); - check_is_apub_id_valid(&inner_id, false)?; - if is_activity_already_known(context.pool(), &inner_id).await? { - return Ok(()); - } - - use AnnouncableActivities::*; - match kind { - Some(Create) => { - receive_create_for_community(context, inner_activity, &inner_id, request_counter).await - } - Some(Update) => { - receive_update_for_community( - context, - inner_activity, - Some(announce), - &inner_id, - request_counter, - ) - .await - } - Some(Like) => { - receive_like_for_community(context, inner_activity, &inner_id, request_counter).await - } - Some(Dislike) => { - receive_dislike_for_community(context, inner_activity, &inner_id, request_counter).await - } - Some(Delete) => { - receive_delete_for_community( - context, - inner_activity, - Some(announce), - &inner_id, - request_counter, - ) - .await - } - Some(Remove) => { - receive_remove_for_community(context, inner_activity, Some(announce), request_counter).await - } - Some(Undo) => { - receive_undo_for_community( - context, - inner_activity, - Some(announce), - &inner_id, - request_counter, - ) - .await - } - Some(Add) => { - receive_add_for_community(context, inner_activity, Some(announce), request_counter).await - } - Some(Block) => { - receive_block_user_for_community(context, inner_activity, Some(announce), request_counter) - .await - } - _ => receive_unhandled_activity(inner_activity), - } -} - -/// Receive either a new private message, or a new comment mention. We distinguish them by checking -/// whether the activity is public. -async fn receive_create( - context: &LemmyContext, - activity: AnyBase, - expected_domain: Url, - request_counter: &mut i32, -) -> Result<(), LemmyError> { - let create = Create::from_any_base(activity)?.context(location_info!())?; - verify_activity_domains_valid(&create, &expected_domain, true)?; - if verify_is_addressed_to_public(&create).is_ok() { - receive_create_comment(create, context, request_counter).await - } else { - receive_create_private_message(context, create, expected_domain, request_counter).await - } -} - -/// Receive either an updated private message, or an updated comment mention. We distinguish -/// them by checking whether the activity is public. -async fn receive_update( - context: &LemmyContext, - activity: AnyBase, - expected_domain: Url, - request_counter: &mut i32, -) -> Result<(), LemmyError> { - let update = Update::from_any_base(activity)?.context(location_info!())?; - verify_activity_domains_valid(&update, &expected_domain, true)?; - if verify_is_addressed_to_public(&update).is_ok() { - receive_update_comment(update, context, request_counter).await - } else { - receive_update_private_message(context, update, expected_domain, request_counter).await - } -} - -async fn receive_delete( - context: &LemmyContext, - any_base: AnyBase, - expected_domain: &Url, - request_counter: &mut i32, -) -> Result<(), LemmyError> { - use CommunityOrPrivateMessage::*; - - let delete = Delete::from_any_base(any_base.clone())?.context(location_info!())?; - verify_activity_domains_valid(&delete, expected_domain, true)?; - let object_uri = delete - .object() - .to_owned() - .single_xsd_any_uri() - .context(location_info!())?; - - match find_community_or_private_message_by_id(context, object_uri).await? { - Community(c) => receive_delete_community(context, c).await, - PrivateMessage(p) => receive_delete_private_message(context, delete, p, request_counter).await, - } -} - -async fn receive_remove( - context: &LemmyContext, - any_base: AnyBase, - expected_domain: &Url, -) -> Result<(), LemmyError> { - let remove = Remove::from_any_base(any_base.clone())?.context(location_info!())?; - verify_activity_domains_valid(&remove, expected_domain, true)?; - let object_uri = remove - .object() - .to_owned() - .single_xsd_any_uri() - .context(location_info!())?; - let community = blocking(context.pool(), move |conn| { - Community::read_from_apub_id(conn, &object_uri.into()) - }) - .await??; - receive_remove_community(context, community).await -} - -async fn receive_undo( - context: &LemmyContext, - any_base: AnyBase, - expected_domain: &Url, - request_counter: &mut i32, -) -> Result<(), LemmyError> { - let undo = Undo::from_any_base(any_base)?.context(location_info!())?; - verify_activity_domains_valid(&undo, expected_domain, true)?; - - let inner_activity = undo.object().to_owned().one().context(location_info!())?; - let kind = inner_activity.kind_str(); - match kind { - Some("Delete") => { - let delete = Delete::from_any_base(inner_activity)?.context(location_info!())?; - verify_activity_domains_valid(&delete, expected_domain, true)?; - let object_uri = delete - .object() - .to_owned() - .single_xsd_any_uri() - .context(location_info!())?; - use CommunityOrPrivateMessage::*; - match find_community_or_private_message_by_id(context, object_uri).await? { - Community(c) => receive_undo_delete_community(context, c).await, - PrivateMessage(p) => { - receive_undo_delete_private_message(context, undo, expected_domain, p, request_counter) - .await - } - } - } - Some("Remove") => { - let remove = Remove::from_any_base(inner_activity)?.context(location_info!())?; - let object_uri = remove - .object() - .to_owned() - .single_xsd_any_uri() - .context(location_info!())?; - let community = blocking(context.pool(), move |conn| { - Community::read_from_apub_id(conn, &object_uri.into()) - }) - .await??; - receive_undo_remove_community(context, community).await - } - _ => receive_unhandled_activity(undo), - } -} -enum CommunityOrPrivateMessage { - Community(Community), - PrivateMessage(PrivateMessage), -} - -async fn find_community_or_private_message_by_id( - context: &LemmyContext, - apub_id: Url, -) -> Result { - let ap_id = apub_id.to_owned(); - let community = blocking(context.pool(), move |conn| { - Community::read_from_apub_id(conn, &ap_id.into()) - }) - .await?; - if let Ok(c) = community { - return Ok(CommunityOrPrivateMessage::Community(c)); - } - - let ap_id = apub_id.to_owned(); - let private_message = blocking(context.pool(), move |conn| { - PrivateMessage::read_from_apub_id(conn, &ap_id.into()) - }) - .await?; - if let Ok(p) = private_message { - return Ok(CommunityOrPrivateMessage::PrivateMessage(p)); - } - - Err(NotFound.into()) -} diff --git a/crates/apub_receive/src/inbox/receive_for_community.rs b/crates/apub_receive/src/inbox/receive_for_community.rs deleted file mode 100644 index 4bb8d325..00000000 --- a/crates/apub_receive/src/inbox/receive_for_community.rs +++ /dev/null @@ -1,802 +0,0 @@ -use crate::{ - activities::receive::{ - comment::{ - receive_create_comment, - receive_delete_comment, - receive_dislike_comment, - receive_like_comment, - receive_remove_comment, - receive_update_comment, - }, - comment_undo::{ - receive_undo_delete_comment, - receive_undo_dislike_comment, - receive_undo_like_comment, - receive_undo_remove_comment, - }, - community::{ - receive_remote_mod_delete_community, - receive_remote_mod_undo_delete_community, - receive_remote_mod_update_community, - }, - post::{ - receive_create_post, - receive_delete_post, - receive_dislike_post, - receive_like_post, - receive_remove_post, - receive_update_post, - }, - post_undo::{ - receive_undo_delete_post, - receive_undo_dislike_post, - receive_undo_like_post, - receive_undo_remove_post, - }, - receive_unhandled_activity, - verify_activity_domains_valid, - }, - inbox::verify_is_addressed_to_public, -}; -use activitystreams::{ - activity::{ - ActorAndObjectRef, - Add, - Announce, - Block, - Create, - Delete, - Dislike, - Like, - OptTargetRef, - Remove, - Undo, - Update, - }, - base::AnyBase, - object::AsObject, - prelude::*, -}; -use anyhow::{anyhow, Context}; -use diesel::result::Error::NotFound; -use lemmy_api_common::blocking; -use lemmy_apub::{ - fetcher::{ - objects::{get_or_fetch_and_insert_comment, get_or_fetch_and_insert_post}, - person::get_or_fetch_and_upsert_person, - }, - find_object_by_id, - find_post_or_comment_by_id, - generate_moderators_url, - ActorType, - CommunityType, - Object, - PostOrComment, -}; -use lemmy_db_queries::{ - source::community::CommunityModerator_, - ApubObject, - Bannable, - Crud, - Followable, - Joinable, -}; -use lemmy_db_schema::{ - source::{ - community::{ - Community, - CommunityFollower, - CommunityFollowerForm, - CommunityModerator, - CommunityModeratorForm, - CommunityPersonBan, - CommunityPersonBanForm, - }, - person::Person, - site::Site, - }, - DbUrl, -}; -use lemmy_db_views_actor::community_view::CommunityView; -use lemmy_utils::{location_info, LemmyError}; -use lemmy_websocket::LemmyContext; -use strum_macros::EnumString; -use url::Url; - -#[derive(EnumString)] -enum PageOrNote { - Page, - Note, -} - -#[derive(EnumString)] -enum ObjectTypes { - Page, - Note, - Group, - Person, -} - -/// This file is for post/comment activities received by the community, and for post/comment -/// activities announced by the community and received by the person. - -/// A post or comment being created -pub(in crate::inbox) async fn receive_create_for_community( - context: &LemmyContext, - activity: AnyBase, - expected_domain: &Url, - request_counter: &mut i32, -) -> Result<(), LemmyError> { - let create = Create::from_any_base(activity)?.context(location_info!())?; - verify_activity_domains_valid(&create, expected_domain, true)?; - verify_is_addressed_to_public(&create)?; - - let kind = create - .object() - .as_single_kind_str() - .and_then(|s| s.parse().ok()); - match kind { - Some(ObjectTypes::Page) => receive_create_post(create, context, request_counter).await, - Some(ObjectTypes::Note) => receive_create_comment(create, context, request_counter).await, - _ => receive_unhandled_activity(create), - } -} - -/// A post or comment being edited -pub(in crate::inbox) async fn receive_update_for_community( - context: &LemmyContext, - activity: AnyBase, - announce: Option, - expected_domain: &Url, - request_counter: &mut i32, -) -> Result<(), LemmyError> { - let update = Update::from_any_base(activity.to_owned())?.context(location_info!())?; - verify_activity_domains_valid(&update, expected_domain, false)?; - verify_is_addressed_to_public(&update)?; - verify_modification_actor_instance(&update, &announce, context, request_counter).await?; - - let kind = update - .object() - .as_single_kind_str() - .and_then(|s| s.parse().ok()); - match kind { - Some(ObjectTypes::Page) => { - receive_update_post(update, announce, context, request_counter).await - } - Some(ObjectTypes::Note) => receive_update_comment(update, context, request_counter).await, - Some(ObjectTypes::Group) => { - receive_remote_mod_update_community(update, context, request_counter).await - } - _ => receive_unhandled_activity(update), - } -} - -/// A post or comment being upvoted -pub(in crate::inbox) async fn receive_like_for_community( - context: &LemmyContext, - activity: AnyBase, - expected_domain: &Url, - request_counter: &mut i32, -) -> Result<(), LemmyError> { - let like = Like::from_any_base(activity)?.context(location_info!())?; - verify_activity_domains_valid(&like, expected_domain, false)?; - verify_is_addressed_to_public(&like)?; - - let object_id = like - .object() - .as_single_xsd_any_uri() - .context(location_info!())?; - match fetch_post_or_comment_by_id(object_id, context, request_counter).await? { - PostOrComment::Post(post) => receive_like_post(like, *post, context, request_counter).await, - PostOrComment::Comment(comment) => { - receive_like_comment(like, *comment, context, request_counter).await - } - } -} - -/// A post or comment being downvoted -pub(in crate::inbox) async fn receive_dislike_for_community( - context: &LemmyContext, - activity: AnyBase, - expected_domain: &Url, - request_counter: &mut i32, -) -> Result<(), LemmyError> { - let enable_downvotes = blocking(context.pool(), move |conn| { - Site::read(conn, 1).map(|s| s.enable_downvotes) - }) - .await??; - if !enable_downvotes { - return Ok(()); - } - - let dislike = Dislike::from_any_base(activity)?.context(location_info!())?; - verify_activity_domains_valid(&dislike, expected_domain, false)?; - verify_is_addressed_to_public(&dislike)?; - - let object_id = dislike - .object() - .as_single_xsd_any_uri() - .context(location_info!())?; - match fetch_post_or_comment_by_id(object_id, context, request_counter).await? { - PostOrComment::Post(post) => { - receive_dislike_post(dislike, *post, context, request_counter).await - } - PostOrComment::Comment(comment) => { - receive_dislike_comment(dislike, *comment, context, request_counter).await - } - } -} - -/// A post or comment being deleted by its creator -pub(in crate::inbox) async fn receive_delete_for_community( - context: &LemmyContext, - activity: AnyBase, - announce: Option, - expected_domain: &Url, - request_counter: &mut i32, -) -> Result<(), LemmyError> { - let delete = Delete::from_any_base(activity)?.context(location_info!())?; - // TODO: skip this check if action is done by remote mod - verify_is_addressed_to_public(&delete)?; - verify_modification_actor_instance(&delete, &announce, context, request_counter).await?; - - let object = delete - .object() - .to_owned() - .single_xsd_any_uri() - .context(location_info!())?; - - match find_object_by_id(context, object).await { - Ok(Object::Post(p)) => { - verify_activity_domains_valid(&delete, expected_domain, true)?; - receive_delete_post(context, *p).await - } - Ok(Object::Comment(c)) => { - verify_activity_domains_valid(&delete, expected_domain, true)?; - receive_delete_comment(context, *c).await - } - Ok(Object::Community(c)) => { - receive_remote_mod_delete_community(delete, *c, context, request_counter).await - } - // if we dont have the object or dont support its deletion, no need to do anything - _ => Ok(()), - } -} - -/// A post or comment being removed by a mod/admin -pub(in crate::inbox) async fn receive_remove_for_community( - context: &LemmyContext, - remove_any_base: AnyBase, - announce: Option, - request_counter: &mut i32, -) -> Result<(), LemmyError> { - let remove = Remove::from_any_base(remove_any_base.to_owned())?.context(location_info!())?; - let community = extract_community_from_cc(&remove, context).await?; - - verify_mod_activity(&remove, announce, &community, context).await?; - verify_is_addressed_to_public(&remove)?; - - if remove.target().is_some() { - let remove_mod = remove - .object() - .as_single_xsd_any_uri() - .context(location_info!())?; - let remove_mod = get_or_fetch_and_upsert_person(remove_mod, context, request_counter).await?; - let form = CommunityModeratorForm { - community_id: community.id, - person_id: remove_mod.id, - }; - blocking(context.pool(), move |conn| { - CommunityModerator::leave(conn, &form) - }) - .await??; - community - .send_announce( - remove_any_base, - remove.object().clone().single_xsd_any_uri(), - context, - ) - .await?; - // TODO: send websocket notification about removed mod - Ok(()) - } - // Remove a post or comment - else { - let object = remove - .object() - .to_owned() - .single_xsd_any_uri() - .context(location_info!())?; - - match find_post_or_comment_by_id(context, object).await { - Ok(PostOrComment::Post(p)) => receive_remove_post(context, *p).await, - Ok(PostOrComment::Comment(c)) => receive_remove_comment(context, *c).await, - // if we dont have the object, no need to do anything - Err(_) => Ok(()), - } - } -} - -#[derive(EnumString)] -enum UndoableActivities { - Delete, - Remove, - Like, - Dislike, - Block, -} - -/// A post/comment action being reverted (either a delete, remove, upvote or downvote) -pub(in crate::inbox) async fn receive_undo_for_community( - context: &LemmyContext, - activity: AnyBase, - announce: Option, - expected_domain: &Url, - request_counter: &mut i32, -) -> Result<(), LemmyError> { - let undo = Undo::from_any_base(activity)?.context(location_info!())?; - verify_activity_domains_valid(&undo, &expected_domain.to_owned(), true)?; - verify_is_addressed_to_public(&undo)?; - - use UndoableActivities::*; - match undo - .object() - .as_single_kind_str() - .and_then(|s| s.parse().ok()) - { - Some(Delete) => { - receive_undo_delete_for_community(context, undo, expected_domain, request_counter).await - } - Some(Remove) => { - receive_undo_remove_for_community(context, undo, announce, expected_domain).await - } - Some(Like) => { - receive_undo_like_for_community(context, undo, expected_domain, request_counter).await - } - Some(Dislike) => { - receive_undo_dislike_for_community(context, undo, expected_domain, request_counter).await - } - Some(Block) => { - receive_undo_block_user_for_community( - context, - undo, - announce, - expected_domain, - request_counter, - ) - .await - } - _ => receive_unhandled_activity(undo), - } -} - -/// A post, comment or community deletion being reverted -pub(in crate::inbox) async fn receive_undo_delete_for_community( - context: &LemmyContext, - undo: Undo, - expected_domain: &Url, - request_counter: &mut i32, -) -> Result<(), LemmyError> { - let delete = Delete::from_any_base(undo.object().to_owned().one().context(location_info!())?)? - .context(location_info!())?; - verify_is_addressed_to_public(&delete)?; - - let object = delete - .object() - .to_owned() - .single_xsd_any_uri() - .context(location_info!())?; - match find_object_by_id(context, object).await { - Ok(Object::Post(p)) => { - verify_activity_domains_valid(&delete, expected_domain, true)?; - receive_undo_delete_post(context, *p).await - } - Ok(Object::Comment(c)) => { - verify_activity_domains_valid(&delete, expected_domain, true)?; - receive_undo_delete_comment(context, *c).await - } - Ok(Object::Community(c)) => { - verify_actor_is_community_mod(&undo, &c, context).await?; - receive_remote_mod_undo_delete_community(undo, *c, context, request_counter).await - } - // if we dont have the object or dont support its deletion, no need to do anything - _ => Ok(()), - } -} - -/// A post or comment removal being reverted -pub(in crate::inbox) async fn receive_undo_remove_for_community( - context: &LemmyContext, - undo: Undo, - announce: Option, - expected_domain: &Url, -) -> Result<(), LemmyError> { - let remove = Remove::from_any_base(undo.object().to_owned().one().context(location_info!())?)? - .context(location_info!())?; - verify_activity_domains_valid(&remove, expected_domain, false)?; - verify_is_addressed_to_public(&remove)?; - verify_undo_remove_actor_instance(&undo, &remove, &announce, context).await?; - - let object = remove - .object() - .to_owned() - .single_xsd_any_uri() - .context(location_info!())?; - match find_post_or_comment_by_id(context, object).await { - Ok(PostOrComment::Post(p)) => receive_undo_remove_post(context, *p).await, - Ok(PostOrComment::Comment(c)) => receive_undo_remove_comment(context, *c).await, - // if we dont have the object, no need to do anything - Err(_) => Ok(()), - } -} - -/// A post or comment upvote being reverted -pub(in crate::inbox) async fn receive_undo_like_for_community( - context: &LemmyContext, - undo: Undo, - expected_domain: &Url, - request_counter: &mut i32, -) -> Result<(), LemmyError> { - let like = Like::from_any_base(undo.object().to_owned().one().context(location_info!())?)? - .context(location_info!())?; - verify_activity_domains_valid(&like, expected_domain, false)?; - verify_is_addressed_to_public(&like)?; - - let object_id = like - .object() - .as_single_xsd_any_uri() - .context(location_info!())?; - match fetch_post_or_comment_by_id(object_id, context, request_counter).await? { - PostOrComment::Post(post) => { - receive_undo_like_post(&like, *post, context, request_counter).await - } - PostOrComment::Comment(comment) => { - receive_undo_like_comment(&like, *comment, context, request_counter).await - } - } -} - -/// Add a new mod to the community (can only be done by an existing mod). -pub(in crate::inbox) async fn receive_add_for_community( - context: &LemmyContext, - add_any_base: AnyBase, - announce: Option, - request_counter: &mut i32, -) -> Result<(), LemmyError> { - let add = Add::from_any_base(add_any_base.to_owned())?.context(location_info!())?; - let community = extract_community_from_cc(&add, context).await?; - - verify_mod_activity(&add, announce, &community, context).await?; - verify_is_addressed_to_public(&add)?; - verify_add_remove_moderator_target(&add, &community)?; - - let new_mod = add - .object() - .as_single_xsd_any_uri() - .context(location_info!())?; - let new_mod = get_or_fetch_and_upsert_person(new_mod, context, request_counter).await?; - - // If we had to refetch the community while parsing the activity, then the new mod has already - // been added. Skip it here as it would result in a duplicate key error. - let new_mod_id = new_mod.id; - let moderated_communities = blocking(context.pool(), move |conn| { - CommunityModerator::get_person_moderated_communities(conn, new_mod_id) - }) - .await??; - if !moderated_communities.contains(&community.id) { - let form = CommunityModeratorForm { - community_id: community.id, - person_id: new_mod.id, - }; - blocking(context.pool(), move |conn| { - CommunityModerator::join(conn, &form) - }) - .await??; - } - if community.local { - community - .send_announce( - add_any_base, - add.object().clone().single_xsd_any_uri(), - context, - ) - .await?; - } - // TODO: send websocket notification about added mod - Ok(()) -} - -/// A post or comment downvote being reverted -pub(in crate::inbox) async fn receive_undo_dislike_for_community( - context: &LemmyContext, - undo: Undo, - expected_domain: &Url, - request_counter: &mut i32, -) -> Result<(), LemmyError> { - let dislike = Dislike::from_any_base(undo.object().to_owned().one().context(location_info!())?)? - .context(location_info!())?; - verify_activity_domains_valid(&dislike, expected_domain, false)?; - verify_is_addressed_to_public(&dislike)?; - - let object_id = dislike - .object() - .as_single_xsd_any_uri() - .context(location_info!())?; - match fetch_post_or_comment_by_id(object_id, context, request_counter).await? { - PostOrComment::Post(post) => { - receive_undo_dislike_post(&dislike, *post, context, request_counter).await - } - PostOrComment::Comment(comment) => { - receive_undo_dislike_comment(&dislike, *comment, context, request_counter).await - } - } -} - -pub(crate) async fn receive_block_user_for_community( - context: &LemmyContext, - block_any_base: AnyBase, - announce: Option, - request_counter: &mut i32, -) -> Result<(), LemmyError> { - let block = Block::from_any_base(block_any_base.to_owned())?.context(location_info!())?; - let community = extract_community_from_cc(&block, context).await?; - - verify_mod_activity(&block, announce, &community, context).await?; - verify_is_addressed_to_public(&block)?; - - let blocked_user = block - .object() - .as_single_xsd_any_uri() - .context(location_info!())?; - let blocked_user = get_or_fetch_and_upsert_person(blocked_user, context, request_counter).await?; - - let community_user_ban_form = CommunityPersonBanForm { - community_id: community.id, - person_id: blocked_user.id, - }; - - blocking(context.pool(), move |conn: &'_ _| { - CommunityPersonBan::ban(conn, &community_user_ban_form) - }) - .await??; - - // Also unsubscribe them from the community, if they are subscribed - let community_follower_form = CommunityFollowerForm { - community_id: community.id, - person_id: blocked_user.id, - pending: false, - }; - blocking(context.pool(), move |conn: &'_ _| { - CommunityFollower::unfollow(conn, &community_follower_form) - }) - .await? - .ok(); - - Ok(()) -} - -pub(crate) async fn receive_undo_block_user_for_community( - context: &LemmyContext, - undo: Undo, - announce: Option, - expected_domain: &Url, - request_counter: &mut i32, -) -> Result<(), LemmyError> { - let object = undo.object().clone().one().context(location_info!())?; - let block = Block::from_any_base(object)?.context(location_info!())?; - let community = extract_community_from_cc(&block, context).await?; - - verify_activity_domains_valid(&block, expected_domain, false)?; - verify_is_addressed_to_public(&block)?; - verify_undo_remove_actor_instance(&undo, &block, &announce, context).await?; - - let blocked_user = block - .object() - .as_single_xsd_any_uri() - .context(location_info!())?; - let blocked_user = get_or_fetch_and_upsert_person(blocked_user, context, request_counter).await?; - - let community_user_ban_form = CommunityPersonBanForm { - community_id: community.id, - person_id: blocked_user.id, - }; - - blocking(context.pool(), move |conn: &'_ _| { - CommunityPersonBan::unban(conn, &community_user_ban_form) - }) - .await??; - - Ok(()) -} - -async fn fetch_post_or_comment_by_id( - apub_id: &Url, - context: &LemmyContext, - request_counter: &mut i32, -) -> Result { - if let Ok(post) = get_or_fetch_and_insert_post(apub_id, context, request_counter).await { - return Ok(PostOrComment::Post(Box::new(post))); - } - - if let Ok(comment) = get_or_fetch_and_insert_comment(apub_id, context, request_counter).await { - return Ok(PostOrComment::Comment(Box::new(comment))); - } - - Err(NotFound.into()) -} - -/// Searches the activity's cc field for a Community ID, and returns the community. -async fn extract_community_from_cc( - activity: &T, - context: &LemmyContext, -) -> Result -where - T: AsObject, -{ - let cc = activity - .cc() - .map(|c| c.as_many()) - .flatten() - .context(location_info!())?; - let community_id = cc - .first() - .map(|c| c.as_xsd_any_uri()) - .flatten() - .context(location_info!())?; - let community_id: DbUrl = community_id.to_owned().into(); - let community = blocking(context.pool(), move |conn| { - Community::read_from_apub_id(conn, &community_id) - }) - .await??; - Ok(community) -} - -/// Checks that a moderation activity was sent by a user who is listed as mod for the community. -/// This is only used in the case of remote mods, as local mod actions don't go through the -/// community inbox. -/// -/// This method should only be used for activities received by the community, not for activities -/// used by community followers. -pub(crate) async fn verify_actor_is_community_mod( - activity: &T, - community: &Community, - context: &LemmyContext, -) -> Result<(), LemmyError> -where - T: ActorAndObjectRef + BaseExt, -{ - let actor = activity - .actor()? - .as_single_xsd_any_uri() - .context(location_info!())? - .to_owned(); - let actor = blocking(context.pool(), move |conn| { - Person::read_from_apub_id(conn, &actor.into()) - }) - .await??; - - // Note: this will also return true for admins in addition to mods, but as we dont know about - // remote admins, it doesnt make any difference. - let community_id = community.id; - let actor_id = actor.id; - let is_mod_or_admin = blocking(context.pool(), move |conn| { - CommunityView::is_mod_or_admin(conn, actor_id, community_id) - }) - .await?; - if !is_mod_or_admin { - return Err(anyhow!("Not a mod").into()); - } - - Ok(()) -} - -/// This method behaves differently, depending if it is called via community inbox (activity -/// received by community from a remote user), or via user inbox (activity received by user from -/// community). We distinguish the cases by checking if the activity is wrapper in an announce -/// (only true when sent from user to community). -/// -/// In the first case, we check that the actor is listed as community mod. In the second case, we -/// only check that the announce comes from the same domain as the activity. We trust the -/// community's instance to have validated the inner activity correctly. We can't do this validation -/// here, because we don't know who the instance admins are. Plus this allows for compatibility with -/// software that uses different rules for mod actions. -pub(crate) async fn verify_mod_activity( - mod_action: &T, - announce: Option, - community: &Community, - context: &LemmyContext, -) -> Result<(), LemmyError> -where - T: ActorAndObjectRef + BaseExt, -{ - match announce { - None => verify_actor_is_community_mod(mod_action, community, context).await?, - Some(a) => verify_activity_domains_valid(&a, &community.actor_id.to_owned().into(), false)?, - } - - Ok(()) -} - -/// For Add/Remove community moderator activities, check that the target field actually contains -/// /c/community/moderators. Any different values are unsupported. -fn verify_add_remove_moderator_target( - activity: &T, - community: &Community, -) -> Result<(), LemmyError> -where - T: ActorAndObjectRef + BaseExt + OptTargetRef, -{ - let target = activity - .target() - .map(|t| t.as_single_xsd_any_uri()) - .flatten() - .context(location_info!())?; - if target != &generate_moderators_url(&community.actor_id)?.into_inner() { - return Err(anyhow!("Unkown target url").into()); - } - Ok(()) -} - -/// For activities like Update, Delete or Remove, check that the actor is from the same instance -/// as the original object itself (or is a remote mod). -/// -/// Note: This is only needed for mod actions. Normal user actions (edit post, undo vote etc) are -/// already verified with `expected_domain`, so this serves as an additional check. -async fn verify_modification_actor_instance( - activity: &T, - announce: &Option, - context: &LemmyContext, - request_counter: &mut i32, -) -> Result<(), LemmyError> -where - T: ActorAndObjectRef + BaseExt + AsObject, -{ - let actor_id = activity - .actor()? - .to_owned() - .single_xsd_any_uri() - .context(location_info!())?; - let object_id = activity - .object() - .as_one() - .map(|o| o.id()) - .flatten() - .context(location_info!())?; - let original_id = match fetch_post_or_comment_by_id(object_id, context, request_counter).await { - Ok(PostOrComment::Post(p)) => p.ap_id.into_inner(), - Ok(PostOrComment::Comment(c)) => c.ap_id.into_inner(), - Err(_) => { - // We can also receive Update activity from remote mod for local activity - let object_id = object_id.to_owned().into(); - blocking(context.pool(), move |conn| { - Community::read_from_apub_id(conn, &object_id) - }) - .await?? - .actor_id() - } - }; - if actor_id.domain() != original_id.domain() { - let community = extract_community_from_cc(activity, context).await?; - verify_mod_activity(activity, announce.to_owned(), &community, context).await?; - } - - Ok(()) -} - -pub(crate) async fn verify_undo_remove_actor_instance( - undo: &Undo, - inner: &T, - announce: &Option, - context: &LemmyContext, -) -> Result<(), LemmyError> -where - T: ActorAndObjectRef + BaseExt + AsObject, -{ - if announce.is_none() { - let community = extract_community_from_cc(undo, context).await?; - verify_mod_activity(undo, announce.to_owned(), &community, context).await?; - verify_mod_activity(inner, announce.to_owned(), &community, context).await?; - } - - Ok(()) -} diff --git a/crates/apub_receive/src/inbox/shared_inbox.rs b/crates/apub_receive/src/inbox/shared_inbox.rs deleted file mode 100644 index 17691d4f..00000000 --- a/crates/apub_receive/src/inbox/shared_inbox.rs +++ /dev/null @@ -1,151 +0,0 @@ -use crate::inbox::{ - assert_activity_not_local, - community_inbox::{community_receive_message, CommunityAcceptedActivities}, - get_activity_id, - inbox_verify_http_signature, - is_activity_already_known, - is_addressed_to_community_followers, - is_addressed_to_local_person, - person_inbox::{person_receive_message, PersonAcceptedActivities}, -}; -use activitystreams::{activity::ActorAndObject, prelude::*}; -use actix_web::{web, HttpRequest, HttpResponse}; -use anyhow::Context; -use lemmy_api_common::blocking; -use lemmy_apub::{get_activity_to_and_cc, insert_activity}; -use lemmy_db_queries::{ApubObject, DbPool}; -use lemmy_db_schema::source::community::Community; -use lemmy_utils::{location_info, LemmyError}; -use lemmy_websocket::LemmyContext; -use serde::{Deserialize, Serialize}; -use std::fmt::Debug; -use url::Url; - -/// Allowed activity types for shared inbox. -#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, Deserialize, Serialize)] -#[serde(rename_all = "PascalCase")] -pub enum ValidTypes { - Create, - Update, - Like, - Dislike, - Delete, - Undo, - Remove, - Announce, - Add, - Block, -} - -// TODO: this isnt entirely correct, cause some of these receive are not ActorAndObject, -// but it still works due to the anybase conversion -pub type AcceptedActivities = ActorAndObject; - -/// Handler for all incoming requests to shared inbox. -pub async fn shared_inbox( - request: HttpRequest, - input: web::Json, - context: web::Data, -) -> Result { - let activity = input.into_inner(); - // First of all check the http signature - let request_counter = &mut 0; - let actor = inbox_verify_http_signature(&activity, &context, request, request_counter).await?; - - // Do nothing if we received the same activity before - let actor_id = actor.actor_id(); - let activity_id = get_activity_id(&activity, &actor_id)?; - if is_activity_already_known(context.pool(), &activity_id).await? { - return Ok(HttpResponse::Ok().finish()); - } - - assert_activity_not_local(&activity)?; - // Log the activity, so we avoid receiving and parsing it twice. Note that this could still happen - // if we receive the same activity twice in very quick succession. - insert_activity(&activity_id, activity.clone(), false, true, context.pool()).await?; - - let activity_any_base = activity.clone().into_any_base()?; - let mut res: Option = None; - let to_and_cc = get_activity_to_and_cc(&activity); - // Handle community first, so in case the sender is banned by the community, it will error out. - // If we handled the person receive first, the activity would be inserted to the database before the - // community could check for bans. - // Note that an activity can be addressed to a community and to a person (or multiple persons) at the - // same time. In this case we still only handle it once, to avoid duplicate websocket - // notifications. - let community = extract_local_community_from_destinations(&to_and_cc, context.pool()).await?; - if let Some(community) = community { - let community_activity = CommunityAcceptedActivities::from_any_base(activity_any_base.clone())? - .context(location_info!())?; - res = Some( - Box::pin(community_receive_message( - community_activity, - community, - actor.as_ref(), - &context, - request_counter, - )) - .await?, - ); - } else if is_addressed_to_local_person(&to_and_cc, context.pool()).await? { - let person_activity = PersonAcceptedActivities::from_any_base(activity_any_base.clone())? - .context(location_info!())?; - // `to_person` is only used for follow activities (which we dont receive here), so no need to pass - // it in - Box::pin(person_receive_message( - person_activity, - None, - actor.as_ref(), - &context, - request_counter, - )) - .await?; - } else if is_addressed_to_community_followers(&to_and_cc, context.pool()) - .await? - .is_some() - { - let person_activity = PersonAcceptedActivities::from_any_base(activity_any_base.clone())? - .context(location_info!())?; - res = Some( - Box::pin(person_receive_message( - person_activity, - None, - actor.as_ref(), - &context, - request_counter, - )) - .await?, - ); - } - - // If none of those, throw an error - if let Some(r) = res { - Ok(r) - } else { - Ok(HttpResponse::NotImplemented().finish()) - } -} - -/// If `to_and_cc` contains the ID of a local community, return that community, otherwise return -/// None. -/// -/// This doesnt handle the case where an activity is addressed to multiple communities (because -/// Lemmy doesnt generate such activities). -async fn extract_local_community_from_destinations( - to_and_cc: &[Url], - pool: &DbPool, -) -> Result, LemmyError> { - for url in to_and_cc { - let url = url.to_owned(); - let community = blocking(pool, move |conn| { - Community::read_from_apub_id(conn, &url.into()) - }) - .await?; - if let Ok(c) = community { - if c.local { - return Ok(Some(c)); - } - } - } - Ok(None) -} diff --git a/crates/apub_receive/src/lib.rs b/crates/apub_receive/src/lib.rs index 69c32b35..dadc76b6 100644 --- a/crates/apub_receive/src/lib.rs +++ b/crates/apub_receive/src/lib.rs @@ -1,4 +1,2 @@ mod activities; -mod http; -mod inbox; -pub mod routes; +pub mod http; diff --git a/docker/federation/start-local-instances.bash b/docker/federation/start-local-instances.bash index 74f629e9..ad261c6a 100755 --- a/docker/federation/start-local-instances.bash +++ b/docker/federation/start-local-instances.bash @@ -8,5 +8,5 @@ for Item in alpha beta gamma delta epsilon ; do sudo chown -R 991:991 volumes/pictrs_$Item done -sudo docker-compose pull --ignore-pull-failures || true +#sudo docker-compose pull --ignore-pull-failures || true sudo docker-compose up diff --git a/scripts/compilation_benchmark.sh b/scripts/compilation_benchmark.sh index 6d454795..af355734 100755 --- a/scripts/compilation_benchmark.sh +++ b/scripts/compilation_benchmark.sh @@ -8,8 +8,8 @@ for ((i=0; i < times; i++)) ; do echo "cargo clean" # to benchmark incremental compilation time, do a full build with the same compiler version first, # and use the following clean command: - #cargo clean -p lemmy_utils - cargo clean + cargo clean -p lemmy_utils + #cargo clean echo "cargo build" start=$(date +%s.%N) RUSTC_WRAPPER='' cargo build -q @@ -20,4 +20,4 @@ done average=$(bc <<< "scale=0; $duration / $times") -echo "Average compilation time over $times runs is $average seconds" \ No newline at end of file +echo "Average compilation time over $times runs is $average seconds" diff --git a/src/main.rs b/src/main.rs index efccece1..fd5394fc 100644 --- a/src/main.rs +++ b/src/main.rs @@ -91,7 +91,7 @@ async fn main() -> Result<(), LemmyError> { .app_data(Data::new(context)) // The routes .configure(|cfg| api_routes::config(cfg, &rate_limiter)) - .configure(lemmy_apub_receive::routes::config) + .configure(lemmy_apub_receive::http::routes::config) .configure(feeds::config) .configure(|cfg| images::config(cfg, &rate_limiter)) .configure(nodeinfo::config)