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
This commit is contained in:
parent
f4c7c2bf28
commit
c7de1fcf24
71 changed files with 3191 additions and 3352 deletions
61
Cargo.lock
generated
61
Cargo.lock
generated
|
@ -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"
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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<String> {
|
||||
self.public_key.to_owned()
|
||||
}
|
||||
|
|
|
@ -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<String> {
|
||||
self.public_key.to_owned()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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<PostOrComment, LemmyError> {
|
||||
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))
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
|
@ -52,6 +52,7 @@ pub type GroupExt =
|
|||
/// Activitystreams type for person
|
||||
type PersonExt =
|
||||
Ext2<actor::ApActor<ApObject<actor::Actor<UserTypes>>>, PersonExtension, PublicKeyExtension>;
|
||||
pub type SiteExt = actor::ApActor<ApObject<actor::Service>>;
|
||||
/// Activitystreams type for post
|
||||
pub type PageExt = Ext1<ApObject<Page>, PageExtension>;
|
||||
pub type NoteExt = ApObject<Note>;
|
||||
|
@ -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<String>;
|
||||
|
|
|
@ -123,19 +123,9 @@ 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait(?Send)]
|
||||
impl FromApubToForm<NoteExt> for CommentForm {
|
||||
|
@ -174,6 +164,9 @@ impl FromApubToForm<NoteExt> 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
|
||||
|
|
14
crates/apub_lib/Cargo.toml
Normal file
14
crates/apub_lib/Cargo.toml
Normal file
|
@ -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"] }
|
72
crates/apub_lib/src/lib.rs
Normal file
72
crates/apub_lib/src/lib.rs
Normal file
|
@ -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<AnyBase>,
|
||||
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(())
|
||||
}
|
15
crates/apub_lib_derive/Cargo.toml
Normal file
15
crates/apub_lib_derive/Cargo.toml
Normal file
|
@ -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"
|
149
crates/apub_lib_derive/src/lib.rs
Normal file
149
crates/apub_lib_derive/src/lib.rs
Normal file
|
@ -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!(),
|
||||
}
|
||||
}
|
|
@ -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" }
|
||||
|
|
68
crates/apub_receive/src/activities/comment/create.rs
Normal file
68
crates/apub_receive/src/activities/comment/create.rs
Normal file
|
@ -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<Url>,
|
||||
#[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
|
||||
}
|
||||
}
|
65
crates/apub_receive/src/activities/comment/mod.rs
Normal file
65
crates/apub_receive/src/activities/comment/mod.rs
Normal file
|
@ -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<Vec<LocalUserId>, 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<LocalUserId>,
|
||||
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(())
|
||||
}
|
56
crates/apub_receive/src/activities/comment/remove.rs
Normal file
56
crates/apub_receive/src/activities/comment/remove.rs
Normal file
|
@ -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
|
||||
}
|
||||
}
|
65
crates/apub_receive/src/activities/comment/undo_remove.rs
Normal file
65
crates/apub_receive/src/activities/comment/undo_remove.rs
Normal file
|
@ -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
|
||||
}
|
||||
}
|
67
crates/apub_receive/src/activities/comment/update.rs
Normal file
67
crates/apub_receive/src/activities/comment/update.rs
Normal file
|
@ -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<Url>,
|
||||
#[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
|
||||
}
|
||||
}
|
86
crates/apub_receive/src/activities/community/add_mod.rs
Normal file
86
crates/apub_receive/src/activities/community/add_mod.rs
Normal file
|
@ -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
|
||||
}
|
||||
}
|
104
crates/apub_receive/src/activities/community/announce.rs
Normal file
104
crates/apub_receive/src/activities/community/announce.rs
Normal file
|
@ -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<Url>,
|
||||
#[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
|
||||
}
|
||||
}
|
83
crates/apub_receive/src/activities/community/block_user.rs
Normal file
83
crates/apub_receive/src/activities/community/block_user.rs
Normal file
|
@ -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
|
||||
}
|
||||
}
|
35
crates/apub_receive/src/activities/community/mod.rs
Normal file
35
crates/apub_receive/src/activities/community/mod.rs
Normal file
|
@ -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(())
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
89
crates/apub_receive/src/activities/community/update.rs
Normal file
89
crates/apub_receive/src/activities/community/update.rs
Normal file
|
@ -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
|
||||
}
|
||||
}
|
158
crates/apub_receive/src/activities/deletion/delete.rs
Normal file
158
crates/apub_receive/src/activities/deletion/delete.rs
Normal file
|
@ -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<Url, LemmyError> {
|
||||
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)
|
||||
}
|
2
crates/apub_receive/src/activities/deletion/mod.rs
Normal file
2
crates/apub_receive/src/activities/deletion/mod.rs
Normal file
|
@ -0,0 +1,2 @@
|
|||
pub mod delete;
|
||||
pub mod undo_delete;
|
125
crates/apub_receive/src/activities/deletion/undo_delete.rs
Normal file
125
crates/apub_receive/src/activities/deletion/undo_delete.rs
Normal file
|
@ -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
|
||||
}
|
||||
}
|
62
crates/apub_receive/src/activities/following/accept.rs
Normal file
62
crates/apub_receive/src/activities/following/accept.rs
Normal file
|
@ -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
|
||||
}
|
||||
}
|
73
crates/apub_receive/src/activities/following/follow.rs
Normal file
73
crates/apub_receive/src/activities/following/follow.rs
Normal file
|
@ -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
|
||||
}
|
||||
}
|
3
crates/apub_receive/src/activities/following/mod.rs
Normal file
3
crates/apub_receive/src/activities/following/mod.rs
Normal file
|
@ -0,0 +1,3 @@
|
|||
pub mod accept;
|
||||
pub mod follow;
|
||||
pub mod undo;
|
67
crates/apub_receive/src/activities/following/undo.rs
Normal file
67
crates/apub_receive/src/activities/following/undo.rs
Normal file
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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<Community, LemmyError> {
|
||||
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(())
|
||||
}
|
||||
|
|
66
crates/apub_receive/src/activities/post/create.rs
Normal file
66
crates/apub_receive/src/activities/post/create.rs
Normal file
|
@ -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<Url>,
|
||||
#[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
|
||||
}
|
||||
}
|
31
crates/apub_receive/src/activities/post/mod.rs
Normal file
31
crates/apub_receive/src/activities/post/mod.rs
Normal file
|
@ -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(())
|
||||
}
|
96
crates/apub_receive/src/activities/post/update.rs
Normal file
96
crates/apub_receive/src/activities/post/update.rs
Normal file
|
@ -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<Url>,
|
||||
#[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
|
||||
}
|
||||
}
|
61
crates/apub_receive/src/activities/private_message/create.rs
Normal file
61
crates/apub_receive/src/activities/private_message/create.rs
Normal file
|
@ -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
|
||||
}
|
||||
}
|
63
crates/apub_receive/src/activities/private_message/delete.rs
Normal file
63
crates/apub_receive/src/activities/private_message/delete.rs
Normal file
|
@ -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
|
||||
}
|
||||
}
|
42
crates/apub_receive/src/activities/private_message/mod.rs
Normal file
42
crates/apub_receive/src/activities/private_message/mod.rs
Normal file
|
@ -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(())
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
61
crates/apub_receive/src/activities/private_message/update.rs
Normal file
61
crates/apub_receive/src/activities/private_message/update.rs
Normal file
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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(())
|
||||
}
|
|
@ -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(())
|
||||
}
|
|
@ -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(())
|
||||
}
|
||||
}
|
|
@ -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<A>(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<T, A>(
|
||||
activity: &T,
|
||||
context: &LemmyContext,
|
||||
request_counter: &mut i32,
|
||||
) -> Result<Person, LemmyError>
|
||||
where
|
||||
T: AsBase<A> + 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<T, Kind>(
|
||||
activity: &T,
|
||||
actor_id: &Url,
|
||||
object_domain_must_match: bool,
|
||||
) -> Result<(), LemmyError>
|
||||
where
|
||||
T: AsBase<Kind> + 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(())
|
||||
}
|
|
@ -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<Announce>,
|
||||
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(())
|
||||
}
|
|
@ -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(())
|
||||
}
|
|
@ -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<T, Kind>(
|
||||
activity: &T,
|
||||
context: &LemmyContext,
|
||||
request_counter: &mut i32,
|
||||
) -> Result<(), LemmyError>
|
||||
where
|
||||
T: AsBase<Kind> + AsObject<Kind> + 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(())
|
||||
}
|
2
crates/apub_receive/src/activities/removal/mod.rs
Normal file
2
crates/apub_receive/src/activities/removal/mod.rs
Normal file
|
@ -0,0 +1,2 @@
|
|||
pub mod remove;
|
||||
pub mod undo_remove;
|
155
crates/apub_receive/src/activities/removal/remove.rs
Normal file
155
crates/apub_receive/src/activities/removal/remove.rs
Normal file
|
@ -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<Url>,
|
||||
#[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
|
||||
}
|
||||
}
|
120
crates/apub_receive/src/activities/removal/undo_remove.rs
Normal file
120
crates/apub_receive/src/activities/removal/undo_remove.rs
Normal file
|
@ -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
|
||||
}
|
||||
}
|
54
crates/apub_receive/src/activities/voting/dislike.rs
Normal file
54
crates/apub_receive/src/activities/voting/dislike.rs
Normal file
|
@ -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
|
||||
}
|
||||
}
|
54
crates/apub_receive/src/activities/voting/like.rs
Normal file
54
crates/apub_receive/src/activities/voting/like.rs
Normal file
|
@ -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
|
||||
}
|
||||
}
|
157
crates/apub_receive/src/activities/voting/mod.rs
Normal file
157
crates/apub_receive/src/activities/voting/mod.rs
Normal file
|
@ -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
|
||||
}
|
55
crates/apub_receive/src/activities/voting/undo_dislike.rs
Normal file
55
crates/apub_receive/src/activities/voting/undo_dislike.rs
Normal file
|
@ -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
|
||||
}
|
||||
}
|
55
crates/apub_receive/src/activities/voting/undo_like.rs
Normal file
55
crates/apub_receive/src/activities/voting/undo_like.rs
Normal file
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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<String>,
|
||||
context: web::Data<LemmyContext>,
|
||||
) -> Result<HttpResponse, LemmyError> {
|
||||
let unparsed = payload_to_string(payload).await?;
|
||||
receive_activity::<GroupInboxActivities>(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<CommunityQuery>,
|
||||
|
|
100
crates/apub_receive/src/http/inbox_enums.rs
Normal file
100
crates/apub_receive/src/http/inbox_enums.rs
Normal file
|
@ -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<AnnounceActivity>),
|
||||
}
|
||||
|
||||
#[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<UpdateCommunity>),
|
||||
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<UpdateCommunity>),
|
||||
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<AnnounceActivity>),
|
||||
}
|
|
@ -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<LemmyContext>,
|
||||
) -> Result<HttpResponse, LemmyError> {
|
||||
let unparsed = payload_to_string(payload).await?;
|
||||
receive_activity::<SharedInboxActivities>(request, &unparsed, context).await
|
||||
}
|
||||
|
||||
async fn payload_to_string(mut payload: Payload) -> Result<String, LemmyError> {
|
||||
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<LemmyContext>,
|
||||
) -> Result<HttpResponse, LemmyError>
|
||||
where
|
||||
T: ActivityHandler + Clone + Deserialize<'a> + Serialize + std::fmt::Debug + Send + 'static,
|
||||
{
|
||||
let activity = serde_json::from_str::<T>(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<CommunityQuery>,
|
||||
info: web::Path<ActivityQuery>,
|
||||
context: web::Data<LemmyContext>,
|
||||
) -> Result<HttpResponse<Body>, 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<bool, LemmyError> {
|
||||
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<T: Debug + ActivityHandler>(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(())
|
||||
}
|
||||
|
|
|
@ -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<String>,
|
||||
context: web::Data<LemmyContext>,
|
||||
) -> Result<HttpResponse, LemmyError> {
|
||||
let unparsed = payload_to_string(payload).await?;
|
||||
receive_activity::<PersonInboxActivities>(request, &unparsed, context).await
|
||||
}
|
||||
|
||||
pub(crate) async fn get_apub_person_outbox(
|
||||
info: web::Path<PersonQuery>,
|
||||
context: web::Data<LemmyContext>,
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
use crate::{
|
||||
http::{
|
||||
use crate::http::{
|
||||
comment::get_apub_comment,
|
||||
community::{
|
||||
community_inbox,
|
||||
get_apub_community_followers,
|
||||
get_apub_community_http,
|
||||
get_apub_community_inbox,
|
||||
|
@ -9,14 +9,9 @@ use crate::{
|
|||
get_apub_community_outbox,
|
||||
},
|
||||
get_activity,
|
||||
person::{get_apub_person_http, get_apub_person_inbox, get_apub_person_outbox},
|
||||
person::{get_apub_person_http, get_apub_person_inbox, get_apub_person_outbox, person_inbox},
|
||||
post::get_apub_post,
|
||||
},
|
||||
inbox::{
|
||||
community_inbox::community_inbox,
|
||||
person_inbox::person_inbox,
|
||||
shared_inbox::shared_inbox,
|
||||
},
|
||||
shared_inbox,
|
||||
};
|
||||
use actix_web::*;
|
||||
use http_signature_normalization_actix::digest::middleware::VerifyDigest;
|
|
@ -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<CommunityValidTypes>;
|
||||
|
||||
/// Handler for all incoming receive to community inboxes.
|
||||
pub async fn community_inbox(
|
||||
request: HttpRequest,
|
||||
input: web::Json<CommunityAcceptedActivities>,
|
||||
path: web::Path<String>,
|
||||
context: web::Data<LemmyContext>,
|
||||
) -> Result<HttpResponse, LemmyError> {
|
||||
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<HttpResponse, LemmyError> {
|
||||
// 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<CommunityValidTypes>| 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<HttpResponse, LemmyError> {
|
||||
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<bool, LemmyError> {
|
||||
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(())
|
||||
}
|
|
@ -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<T, Kind>(activity: &T, creator_uri: &Url) -> Result<Url, LemmyError>
|
||||
where
|
||||
T: BaseExt<Kind> + Extends<Kind> + Debug,
|
||||
Kind: Serialize,
|
||||
<T as Extends<Kind>>::Error: From<serde_json::Error> + 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<bool, LemmyError> {
|
||||
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<T, Kind>(activity: &T) -> Result<(), LemmyError>
|
||||
where
|
||||
T: AsBase<Kind> + AsObject<Kind> + 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<T, Kind>(
|
||||
activity: &T,
|
||||
context: &LemmyContext,
|
||||
request: HttpRequest,
|
||||
request_counter: &mut i32,
|
||||
) -> Result<Box<dyn ActorType>, LemmyError>
|
||||
where
|
||||
T: AsObject<Kind> + ActorAndObjectRefExt + Extends<Kind> + AsBase<Kind>,
|
||||
Kind: Serialize,
|
||||
<T as Extends<Kind>>::Error: From<serde_json::Error> + 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<bool, LemmyError> {
|
||||
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<Option<Community>, 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<T, Kind>(activity: &T) -> Result<(), LemmyError>
|
||||
where
|
||||
T: BaseExt<Kind> + 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(())
|
||||
}
|
|
@ -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<PersonValidTypes>;
|
||||
|
||||
/// Handler for all incoming activities to person inboxes.
|
||||
pub async fn person_inbox(
|
||||
request: HttpRequest,
|
||||
input: web::Json<PersonAcceptedActivities>,
|
||||
path: web::Path<String>,
|
||||
context: web::Data<LemmyContext>,
|
||||
) -> Result<HttpResponse, LemmyError> {
|
||||
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<Person>,
|
||||
actor: &dyn ActorType,
|
||||
context: &LemmyContext,
|
||||
request_counter: &mut i32,
|
||||
) -> Result<HttpResponse, LemmyError> {
|
||||
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<CommunityOrPrivateMessage, LemmyError> {
|
||||
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())
|
||||
}
|
|
@ -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<Announce>,
|
||||
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<Announce>,
|
||||
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<Announce>,
|
||||
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<Announce>,
|
||||
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<Announce>,
|
||||
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<Announce>,
|
||||
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<Announce>,
|
||||
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<Announce>,
|
||||
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<PostOrComment, LemmyError> {
|
||||
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<T, Kind>(
|
||||
activity: &T,
|
||||
context: &LemmyContext,
|
||||
) -> Result<Community, LemmyError>
|
||||
where
|
||||
T: AsObject<Kind>,
|
||||
{
|
||||
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<T, Kind>(
|
||||
activity: &T,
|
||||
community: &Community,
|
||||
context: &LemmyContext,
|
||||
) -> Result<(), LemmyError>
|
||||
where
|
||||
T: ActorAndObjectRef + BaseExt<Kind>,
|
||||
{
|
||||
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<T, Kind>(
|
||||
mod_action: &T,
|
||||
announce: Option<Announce>,
|
||||
community: &Community,
|
||||
context: &LemmyContext,
|
||||
) -> Result<(), LemmyError>
|
||||
where
|
||||
T: ActorAndObjectRef + BaseExt<Kind>,
|
||||
{
|
||||
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<T, Kind>(
|
||||
activity: &T,
|
||||
community: &Community,
|
||||
) -> Result<(), LemmyError>
|
||||
where
|
||||
T: ActorAndObjectRef + BaseExt<Kind> + 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<T, Kind>(
|
||||
activity: &T,
|
||||
announce: &Option<Announce>,
|
||||
context: &LemmyContext,
|
||||
request_counter: &mut i32,
|
||||
) -> Result<(), LemmyError>
|
||||
where
|
||||
T: ActorAndObjectRef + BaseExt<Kind> + AsObject<Kind>,
|
||||
{
|
||||
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<T, Kind>(
|
||||
undo: &Undo,
|
||||
inner: &T,
|
||||
announce: &Option<Announce>,
|
||||
context: &LemmyContext,
|
||||
) -> Result<(), LemmyError>
|
||||
where
|
||||
T: ActorAndObjectRef + BaseExt<Kind> + AsObject<Kind>,
|
||||
{
|
||||
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(())
|
||||
}
|
|
@ -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<ValidTypes>;
|
||||
|
||||
/// Handler for all incoming requests to shared inbox.
|
||||
pub async fn shared_inbox(
|
||||
request: HttpRequest,
|
||||
input: web::Json<AcceptedActivities>,
|
||||
context: web::Data<LemmyContext>,
|
||||
) -> Result<HttpResponse, LemmyError> {
|
||||
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<HttpResponse> = 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<Option<Community>, 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)
|
||||
}
|
|
@ -1,4 +1,2 @@
|
|||
mod activities;
|
||||
mod http;
|
||||
mod inbox;
|
||||
pub mod routes;
|
||||
pub mod http;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in a new issue