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:
Nutomic 2021-07-17 16:08:46 +00:00 committed by GitHub
parent f4c7c2bf28
commit c7de1fcf24
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
71 changed files with 3191 additions and 3352 deletions

61
Cargo.lock generated
View file

@ -988,6 +988,12 @@ version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "212d0f5754cb6769937f4501cc0e67f4f4483c8d2c3e1e922ee9edbe4ab4c7c0" checksum = "212d0f5754cb6769937f4501cc0e67f4f4483c8d2c3e1e922ee9edbe4ab4c7c0"
[[package]]
name = "dissimilar"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc4b29f4b9bb94bf267d57269fd0706d343a160937108e9619fe380645428abb"
[[package]] [[package]]
name = "either" name = "either"
version = "1.6.1" version = "1.6.1"
@ -1261,6 +1267,12 @@ version = "0.24.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0e4075386626662786ddb0ec9081e7c7eeb1ba31951f447ca780ef9f5d568189" checksum = "0e4075386626662786ddb0ec9081e7c7eeb1ba31951f447ca780ef9f5d568189"
[[package]]
name = "glob"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574"
[[package]] [[package]]
name = "h2" name = "h2"
version = "0.3.3" version = "0.3.3"
@ -1736,6 +1748,30 @@ dependencies = [
"uuid", "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]] [[package]]
name = "lemmy_apub_receive" name = "lemmy_apub_receive"
version = "0.1.0" version = "0.1.0"
@ -1760,6 +1796,7 @@ dependencies = [
"itertools", "itertools",
"lemmy_api_common", "lemmy_api_common",
"lemmy_apub", "lemmy_apub",
"lemmy_apub_lib",
"lemmy_db_queries", "lemmy_db_queries",
"lemmy_db_schema", "lemmy_db_schema",
"lemmy_db_views", "lemmy_db_views",
@ -3361,6 +3398,15 @@ dependencies = [
"tokio", "tokio",
] ]
[[package]]
name = "toml"
version = "0.5.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a31142970826733df8241ef35dc040ef98c679ab14d7c3e54d827099b3acecaa"
dependencies = [
"serde",
]
[[package]] [[package]]
name = "tower-service" name = "tower-service"
version = "0.3.1" version = "0.3.1"
@ -3393,6 +3439,21 @@ version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" 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]] [[package]]
name = "twoway" name = "twoway"
version = "0.2.2" version = "0.2.2"

View file

@ -14,6 +14,8 @@ members = [
"crates/api", "crates/api",
"crates/api_crud", "crates/api_crud",
"crates/api_common", "crates/api_common",
"crates/apub_lib",
"crates/apub_lib_derive",
"crates/apub", "crates/apub",
"crates/apub_receive", "crates/apub_receive",
"crates/utils", "crates/utils",

View file

@ -3,6 +3,7 @@ set -e
export LEMMY_TEST_SEND_SYNC=1 export LEMMY_TEST_SEND_SYNC=1
export RUST_BACKTRACE=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 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" psql "${LEMMY_DATABASE_URL}/lemmy" -c "DROP DATABASE IF EXISTS $INSTANCE"

View file

@ -49,7 +49,6 @@ use lemmy_utils::{location_info, settings::structs::Settings, LemmyError};
use lemmy_websocket::LemmyContext; use lemmy_websocket::LemmyContext;
use url::Url; use url::Url;
#[async_trait::async_trait(?Send)]
impl ActorType for Community { impl ActorType for Community {
fn is_local(&self) -> bool { fn is_local(&self) -> bool {
self.local self.local
@ -57,6 +56,9 @@ impl ActorType for Community {
fn actor_id(&self) -> Url { fn actor_id(&self) -> Url {
self.actor_id.to_owned().into_inner() self.actor_id.to_owned().into_inner()
} }
fn name(&self) -> String {
self.name.clone()
}
fn public_key(&self) -> Option<String> { fn public_key(&self) -> Option<String> {
self.public_key.to_owned() self.public_key.to_owned()
} }

View file

@ -24,7 +24,6 @@ use lemmy_utils::LemmyError;
use lemmy_websocket::LemmyContext; use lemmy_websocket::LemmyContext;
use url::Url; use url::Url;
#[async_trait::async_trait(?Send)]
impl ActorType for Person { impl ActorType for Person {
fn is_local(&self) -> bool { fn is_local(&self) -> bool {
self.local self.local
@ -32,6 +31,9 @@ impl ActorType for Person {
fn actor_id(&self) -> Url { fn actor_id(&self) -> Url {
self.actor_id.to_owned().into_inner() self.actor_id.to_owned().into_inner()
} }
fn name(&self) -> String {
self.name.clone()
}
fn public_key(&self) -> Option<String> { fn public_key(&self) -> Option<String> {
self.public_key.to_owned() self.public_key.to_owned()

View file

@ -49,7 +49,7 @@ where
if check_is_apub_id_valid(&inbox, false).is_ok() { if check_is_apub_id_valid(&inbox, false).is_ok() {
debug!( debug!(
"Sending activity {:?} to {}", "Sending activity {:?} to {}",
&activity.id_unchecked(), &activity.id_unchecked().map(ToString::to_string),
&inbox &inbox
); );
send_activity_internal(context, activity, creator, vec![inbox], true, true).await?; send_activity_internal(context, activity, creator, vec![inbox], true, true).await?;
@ -88,7 +88,7 @@ where
.collect(); .collect();
debug!( debug!(
"Sending activity {:?} to followers of {}", "Sending activity {:?} to followers of {}",
&activity.id_unchecked().map(|i| i.to_string()), &activity.id_unchecked().map(ToString::to_string),
&community.actor_id &community.actor_id
); );
@ -127,7 +127,7 @@ where
check_is_apub_id_valid(&inbox, false)?; check_is_apub_id_valid(&inbox, false)?;
debug!( debug!(
"Sending activity {:?} to community {}", "Sending activity {:?} to community {}",
&activity.id_unchecked(), &activity.id_unchecked().map(ToString::to_string),
&community.actor_id &community.actor_id
); );
// dont send to object_actor here, as that is responsibility of the community itself // dont send to object_actor here, as that is responsibility of the community itself

View file

@ -1,12 +1,11 @@
use crate::ActorType;
use activitystreams::unparsed::UnparsedMutExt; use activitystreams::unparsed::UnparsedMutExt;
use activitystreams_ext::UnparsedExtension; use activitystreams_ext::UnparsedExtension;
use actix_web::HttpRequest; use actix_web::HttpRequest;
use anyhow::{anyhow, Context}; use anyhow::anyhow;
use http::{header::HeaderName, HeaderMap, HeaderValue}; use http::{header::HeaderName, HeaderMap, HeaderValue};
use http_signature_normalization_actix::Config as ConfigActix; use http_signature_normalization_actix::Config as ConfigActix;
use http_signature_normalization_reqwest::prelude::{Config, SignExt}; use http_signature_normalization_reqwest::prelude::{Config, SignExt};
use lemmy_utils::{location_info, LemmyError}; use lemmy_utils::LemmyError;
use log::debug; use log::debug;
use openssl::{ use openssl::{
hash::MessageDigest, hash::MessageDigest,
@ -65,8 +64,7 @@ pub(crate) async fn sign_and_send(
} }
/// Verifies the HTTP signature on an incoming inbox request. /// Verifies the HTTP signature on an incoming inbox request.
pub fn verify_signature(request: &HttpRequest, actor: &dyn ActorType) -> Result<(), LemmyError> { pub fn verify_signature(request: &HttpRequest, public_key: &str) -> Result<(), LemmyError> {
let public_key = actor.public_key().context(location_info!())?;
let verified = CONFIG2 let verified = CONFIG2
.begin_verify( .begin_verify(
request.method(), request.method(),

View file

@ -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 anyhow::anyhow;
use diesel::result::Error::NotFound; use diesel::result::Error::NotFound;
use lemmy_api_common::blocking; use lemmy_api_common::blocking;
@ -89,3 +95,19 @@ pub async fn get_or_fetch_and_insert_comment(
Err(e) => Err(e.into()), 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))
}
},
)
}

View file

@ -52,6 +52,7 @@ pub type GroupExt =
/// Activitystreams type for person /// Activitystreams type for person
type PersonExt = type PersonExt =
Ext2<actor::ApActor<ApObject<actor::Actor<UserTypes>>>, PersonExtension, PublicKeyExtension>; Ext2<actor::ApActor<ApObject<actor::Actor<UserTypes>>>, PersonExtension, PublicKeyExtension>;
pub type SiteExt = actor::ApActor<ApObject<actor::Service>>;
/// Activitystreams type for post /// Activitystreams type for post
pub type PageExt = Ext1<ApObject<Page>, PageExtension>; pub type PageExt = Ext1<ApObject<Page>, PageExtension>;
pub type NoteExt = ApObject<Note>; 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 /// Common methods provided by ActivityPub actors (community and person). Not all methods are
/// implemented by all actors. /// implemented by all actors.
#[async_trait::async_trait(?Send)]
pub trait ActorType { pub trait ActorType {
fn is_local(&self) -> bool; fn is_local(&self) -> bool;
fn actor_id(&self) -> Url; 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) // 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>; fn public_key(&self) -> Option<String>;

View file

@ -123,17 +123,7 @@ impl FromApub for Comment {
let post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??; 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) check_object_for_community_or_site_ban(note, post.community_id, context, request_counter)
.await?; .await?;
if post.locked { Ok(comment)
// 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)
}
} }
} }
@ -174,6 +164,9 @@ impl FromApubToForm<NoteExt> for CommentForm {
request_counter, request_counter,
)) ))
.await?; .await?;
if post.locked {
return Err(anyhow!("Post is locked").into());
}
// The 2nd item, if it exists, is the parent comment apub_id // The 2nd item, if it exists, is the parent comment apub_id
// For deeply nested comments, FromApub automatically gets called recursively // For deeply nested comments, FromApub automatically gets called recursively

View 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"] }

View 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(())
}

View 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"

View 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!(),
}
}

View file

@ -5,6 +5,7 @@ edition = "2018"
[dependencies] [dependencies]
lemmy_utils = { path = "../utils" } lemmy_utils = { path = "../utils" }
lemmy_apub_lib = { path = "../apub_lib" }
lemmy_apub = { path = "../apub" } lemmy_apub = { path = "../apub" }
lemmy_db_queries = { path = "../db_queries" } lemmy_db_queries = { path = "../db_queries" }
lemmy_db_schema = { path = "../db_schema" } lemmy_db_schema = { path = "../db_schema" }

View 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
}
}

View 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(())
}

View 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
}
}

View 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
}
}

View 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
}
}

View 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
}
}

View 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
}
}

View 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
}
}

View 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(())
}

View file

@ -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
}
}

View 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
}
}

View 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)
}

View file

@ -0,0 +1,2 @@
pub mod delete;
pub mod undo_delete;

View 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
}
}

View 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
}
}

View 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
}
}

View file

@ -0,0 +1,3 @@
pub mod accept;
pub mod follow;
pub mod undo;

View 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
}
}

View file

@ -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(())
}

View 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
}
}

View 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(())
}

View 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
}
}

View 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
}
}

View 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
}
}

View 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(())
}

View file

@ -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
}
}

View 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
}
}

View file

@ -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(&note, 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(&note, 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(())
}

View file

@ -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(())
}

View file

@ -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(())
}
}

View file

@ -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(())
}

View file

@ -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(())
}

View file

@ -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(())
}

View file

@ -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(&note, 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(&note, 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(())
}

View file

@ -0,0 +1,2 @@
pub mod remove;
pub mod undo_remove;

View 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
}
}

View 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
}
}

View 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
}
}

View 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
}
}

View 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
}

View 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
}
}

View 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
}
}

View file

@ -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::{ use activitystreams::{
base::{AnyBase, BaseExt}, base::{AnyBase, BaseExt},
collection::{CollectionExt, OrderedCollection, UnorderedCollection}, collection::{CollectionExt, OrderedCollection, UnorderedCollection},
url::Url, 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_api_common::blocking;
use lemmy_apub::{ use lemmy_apub::{
extensions::context::lemmy_context, 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). /// Returns an empty followers collection, only populating the size (for privacy).
pub(crate) async fn get_apub_community_followers( pub(crate) async fn get_apub_community_followers(
info: web::Path<CommunityQuery>, info: web::Path<CommunityQuery>,

View 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>),
}

View file

@ -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 http::StatusCode;
use lemmy_api_common::blocking; use lemmy_api_common::blocking;
use lemmy_apub::APUB_JSON_CONTENT_TYPE; use lemmy_apub::{
use lemmy_db_queries::source::activity::Activity_; 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_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 lemmy_websocket::LemmyContext;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::{fmt::Debug, io::Read};
use url::Url; use url::Url;
pub mod comment; mod comment;
pub mod community; mod community;
pub mod person; mod inbox_enums;
pub mod post; 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 /// Convert the data to json and turn it into an HTTP Response with the correct ActivityPub
/// headers. /// headers.
@ -36,14 +122,14 @@ where
} }
#[derive(Deserialize)] #[derive(Deserialize)]
pub struct CommunityQuery { pub struct ActivityQuery {
type_: String, type_: String,
id: 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( pub(crate) async fn get_activity(
info: web::Path<CommunityQuery>, info: web::Path<ActivityQuery>,
context: web::Data<LemmyContext>, context: web::Data<LemmyContext>,
) -> Result<HttpResponse<Body>, LemmyError> { ) -> Result<HttpResponse<Body>, LemmyError> {
let settings = Settings::get(); let settings = Settings::get();
@ -66,3 +152,37 @@ pub(crate) async fn get_activity(
Ok(create_apub_response(&activity.data)) 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(())
}

View file

@ -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::{ use activitystreams::{
base::BaseExt, base::BaseExt,
collection::{CollectionExt, OrderedCollection}, 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_api_common::blocking;
use lemmy_apub::{extensions::context::lemmy_context, objects::ToApub, ActorType}; use lemmy_apub::{extensions::context::lemmy_context, objects::ToApub, ActorType};
use lemmy_db_queries::source::person::Person_; 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( pub(crate) async fn get_apub_person_outbox(
info: web::Path<PersonQuery>, info: web::Path<PersonQuery>,
context: web::Data<LemmyContext>, context: web::Data<LemmyContext>,

View file

@ -1,22 +1,17 @@
use crate::{ use crate::http::{
http::{ comment::get_apub_comment,
comment::get_apub_comment, community::{
community::{ community_inbox,
get_apub_community_followers, get_apub_community_followers,
get_apub_community_http, get_apub_community_http,
get_apub_community_inbox, get_apub_community_inbox,
get_apub_community_moderators, get_apub_community_moderators,
get_apub_community_outbox, get_apub_community_outbox,
},
get_activity,
person::{get_apub_person_http, get_apub_person_inbox, get_apub_person_outbox},
post::get_apub_post,
},
inbox::{
community_inbox::community_inbox,
person_inbox::person_inbox,
shared_inbox::shared_inbox,
}, },
get_activity,
person::{get_apub_person_http, get_apub_person_inbox, get_apub_person_outbox, person_inbox},
post::get_apub_post,
shared_inbox,
}; };
use actix_web::*; use actix_web::*;
use http_signature_normalization_actix::digest::middleware::VerifyDigest; use http_signature_normalization_actix::digest::middleware::VerifyDigest;

View file

@ -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(())
}

View file

@ -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(())
}

View file

@ -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())
}

View file

@ -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(())
}

View file

@ -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)
}

View file

@ -1,4 +1,2 @@
mod activities; mod activities;
mod http; pub mod http;
mod inbox;
pub mod routes;

View file

@ -8,5 +8,5 @@ for Item in alpha beta gamma delta epsilon ; do
sudo chown -R 991:991 volumes/pictrs_$Item sudo chown -R 991:991 volumes/pictrs_$Item
done done
sudo docker-compose pull --ignore-pull-failures || true #sudo docker-compose pull --ignore-pull-failures || true
sudo docker-compose up sudo docker-compose up

View file

@ -8,8 +8,8 @@ for ((i=0; i < times; i++)) ; do
echo "cargo clean" echo "cargo clean"
# to benchmark incremental compilation time, do a full build with the same compiler version first, # to benchmark incremental compilation time, do a full build with the same compiler version first,
# and use the following clean command: # and use the following clean command:
#cargo clean -p lemmy_utils cargo clean -p lemmy_utils
cargo clean #cargo clean
echo "cargo build" echo "cargo build"
start=$(date +%s.%N) start=$(date +%s.%N)
RUSTC_WRAPPER='' cargo build -q RUSTC_WRAPPER='' cargo build -q

View file

@ -91,7 +91,7 @@ async fn main() -> Result<(), LemmyError> {
.app_data(Data::new(context)) .app_data(Data::new(context))
// The routes // The routes
.configure(|cfg| api_routes::config(cfg, &rate_limiter)) .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(feeds::config)
.configure(|cfg| images::config(cfg, &rate_limiter)) .configure(|cfg| images::config(cfg, &rate_limiter))
.configure(nodeinfo::config) .configure(nodeinfo::config)