Apub inbox rewrite (#1652)
* start to implement apub inbox routing lib * got something that almost works * it compiles! * implemented some more * move library code to separate crate (most of it) * convert private message handlers * convert all comment receivers (except undo comment) * convert post receiver * add verify trait * convert community receivers * add cc field for all activities which i forgot before * convert inbox functions, add missing checks * convert undo like/dislike receivers * convert undo_delete and undo_remove receivers * move block/unblock activities * convert remaining activity receivers * reimplement http signature verification and other checks * also use actor type for routing, VerifyActivity and SendActivity traits * cleanup and restructure apub_receive code * wip: try to fix activity routing * implement a (very bad) derive macro for activityhandler * working activity routing! * rework pm verify(), fix tests and confirm manually also remove inbox username check which was broken * rework following verify(), fix tests and test manually * fix post/comment create/update, rework voting * Rewrite remove/delete post/comment, fix tests, test manually * Rework and fix (un)block user, announce, update post * some code cleanup * rework delete/remove activity receivers (still quite messy) * rewrite, test and fix add/remove mod, update community handlers * add docs for ActivityHandler derive macro * dont try to compile macro comments
This commit is contained in:
parent
f4c7c2bf28
commit
c7de1fcf24
71 changed files with 3191 additions and 3352 deletions
61
Cargo.lock
generated
61
Cargo.lock
generated
|
@ -988,6 +988,12 @@ version = "1.0.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
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"
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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(),
|
||||||
|
|
|
@ -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))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
|
@ -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>;
|
||||||
|
|
|
@ -123,19 +123,9 @@ 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 {
|
|
||||||
// This is not very efficient because a comment gets inserted just to be deleted right
|
|
||||||
// afterwards, but it seems to be the easiest way to implement it.
|
|
||||||
blocking(context.pool(), move |conn| {
|
|
||||||
Comment::delete(conn, comment.id)
|
|
||||||
})
|
|
||||||
.await??;
|
|
||||||
Err(anyhow!("Post is locked").into())
|
|
||||||
} else {
|
|
||||||
Ok(comment)
|
Ok(comment)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait::async_trait(?Send)]
|
#[async_trait::async_trait(?Send)]
|
||||||
impl FromApubToForm<NoteExt> for CommentForm {
|
impl FromApubToForm<NoteExt> for CommentForm {
|
||||||
|
@ -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
|
||||||
|
|
14
crates/apub_lib/Cargo.toml
Normal file
14
crates/apub_lib/Cargo.toml
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
[package]
|
||||||
|
name = "lemmy_apub_lib"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2018"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
lemmy_utils = { path = "../utils" }
|
||||||
|
lemmy_websocket = { path = "../websocket" }
|
||||||
|
lemmy_apub_lib_derive = { path = "../apub_lib_derive" }
|
||||||
|
activitystreams = "0.7.0-alpha.11"
|
||||||
|
activitystreams-ext = "0.1.0-alpha.2"
|
||||||
|
serde = { version = "1.0.123", features = ["derive"] }
|
||||||
|
async-trait = "0.1.42"
|
||||||
|
url = { version = "2.2.1", features = ["serde"] }
|
72
crates/apub_lib/src/lib.rs
Normal file
72
crates/apub_lib/src/lib.rs
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
use activitystreams::{
|
||||||
|
base::AnyBase,
|
||||||
|
error::DomainError,
|
||||||
|
primitives::OneOrMany,
|
||||||
|
unparsed::Unparsed,
|
||||||
|
};
|
||||||
|
pub use lemmy_apub_lib_derive::*;
|
||||||
|
use lemmy_utils::LemmyError;
|
||||||
|
use lemmy_websocket::LemmyContext;
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
|
||||||
|
pub enum PublicUrl {
|
||||||
|
#[serde(rename = "https://www.w3.org/ns/activitystreams#Public")]
|
||||||
|
Public,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct ActivityCommonFields {
|
||||||
|
#[serde(rename = "@context")]
|
||||||
|
pub context: OneOrMany<AnyBase>,
|
||||||
|
id: Url,
|
||||||
|
pub actor: Url,
|
||||||
|
|
||||||
|
// unparsed fields
|
||||||
|
#[serde(flatten)]
|
||||||
|
pub unparsed: Unparsed,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ActivityCommonFields {
|
||||||
|
pub fn id_unchecked(&self) -> &Url {
|
||||||
|
&self.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait(?Send)]
|
||||||
|
pub trait ActivityHandler {
|
||||||
|
async fn verify(
|
||||||
|
&self,
|
||||||
|
context: &LemmyContext,
|
||||||
|
request_counter: &mut i32,
|
||||||
|
) -> Result<(), LemmyError>;
|
||||||
|
|
||||||
|
async fn receive(
|
||||||
|
&self,
|
||||||
|
context: &LemmyContext,
|
||||||
|
request_counter: &mut i32,
|
||||||
|
) -> Result<(), LemmyError>;
|
||||||
|
fn common(&self) -> &ActivityCommonFields;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn verify_domains_match(a: &Url, b: &Url) -> Result<(), LemmyError> {
|
||||||
|
if a.domain() != b.domain() {
|
||||||
|
return Err(DomainError.into());
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn verify_domains_match_opt(a: &Url, b: Option<&Url>) -> Result<(), LemmyError> {
|
||||||
|
if let Some(b2) = b {
|
||||||
|
return verify_domains_match(a, b2);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn verify_urls_match(a: &Url, b: &Url) -> Result<(), LemmyError> {
|
||||||
|
if a != b {
|
||||||
|
return Err(DomainError.into());
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
15
crates/apub_lib_derive/Cargo.toml
Normal file
15
crates/apub_lib_derive/Cargo.toml
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
[package]
|
||||||
|
name = "lemmy_apub_lib_derive"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2018"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
proc-macro = true
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
trybuild = { version = "1.0", features = ["diff"] }
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
proc-macro2 = "1.0"
|
||||||
|
syn = "1.0"
|
||||||
|
quote = "1.0"
|
149
crates/apub_lib_derive/src/lib.rs
Normal file
149
crates/apub_lib_derive/src/lib.rs
Normal file
|
@ -0,0 +1,149 @@
|
||||||
|
use proc_macro2::TokenStream;
|
||||||
|
use quote::quote;
|
||||||
|
use syn::{parse_macro_input, Data, DeriveInput};
|
||||||
|
|
||||||
|
/// Generates implementation ActivityHandler for an enum, which looks like the following (handling
|
||||||
|
/// all enum variants).
|
||||||
|
///
|
||||||
|
/// Based on this code:
|
||||||
|
/// ```ignore
|
||||||
|
/// #[derive(serde::Deserialize, serde::Serialize, ActivityHandler)]
|
||||||
|
/// #[serde(untagged)]
|
||||||
|
/// pub enum PersonInboxActivities {
|
||||||
|
/// CreateNote(CreateNote),
|
||||||
|
/// UpdateNote(UpdateNote),
|
||||||
|
/// ```
|
||||||
|
/// It will generate this:
|
||||||
|
/// ```ignore
|
||||||
|
/// impl ActivityHandler for PersonInboxActivities {
|
||||||
|
///
|
||||||
|
/// async fn verify(
|
||||||
|
/// &self,
|
||||||
|
/// context: &LemmyContext,
|
||||||
|
/// request_counter: &mut i32,
|
||||||
|
/// ) -> Result<(), LemmyError> {
|
||||||
|
/// match self {
|
||||||
|
/// PersonInboxActivities::CreateNote(a) => a.verify(context, request_counter).await,
|
||||||
|
/// PersonInboxActivities::UpdateNote(a) => a.verify(context, request_counter).await,
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// async fn receive(
|
||||||
|
/// &self,
|
||||||
|
/// context: &LemmyContext,
|
||||||
|
/// request_counter: &mut i32,
|
||||||
|
/// ) -> Result<(), LemmyError> {
|
||||||
|
/// match self {
|
||||||
|
/// PersonInboxActivities::CreateNote(a) => a.receive(context, request_counter).await,
|
||||||
|
/// PersonInboxActivities::UpdateNote(a) => a.receive(context, request_counter).await,
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
/// fn common(&self) -> &ActivityCommonFields {
|
||||||
|
/// match self {
|
||||||
|
/// PersonInboxActivities::CreateNote(a) => a.common(),
|
||||||
|
/// PersonInboxActivities::UpdateNote(a) => a.common(),
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// TODO: consider replacing this macro with https://crates.io/crates/typetag crate, though it
|
||||||
|
/// doesnt support untagged enums which we need for apub.
|
||||||
|
#[proc_macro_derive(ActivityHandler)]
|
||||||
|
pub fn derive_activity_handler(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
|
||||||
|
// Parse the input tokens into a syntax tree.
|
||||||
|
let input = parse_macro_input!(input as DeriveInput);
|
||||||
|
|
||||||
|
// Used in the quasi-quotation below as `#name`.
|
||||||
|
let name = input.ident;
|
||||||
|
|
||||||
|
let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
|
||||||
|
|
||||||
|
let input_enum = if let Data::Enum(d) = input.data {
|
||||||
|
d
|
||||||
|
} else {
|
||||||
|
unimplemented!()
|
||||||
|
};
|
||||||
|
|
||||||
|
let impl_verify = input_enum
|
||||||
|
.variants
|
||||||
|
.iter()
|
||||||
|
.map(|variant| variant_impl_verify(&name, variant));
|
||||||
|
let impl_receive = input_enum
|
||||||
|
.variants
|
||||||
|
.iter()
|
||||||
|
.map(|variant| variant_impl_receive(&name, variant));
|
||||||
|
let impl_common = input_enum
|
||||||
|
.variants
|
||||||
|
.iter()
|
||||||
|
.map(|variant| variant_impl_common(&name, variant));
|
||||||
|
|
||||||
|
// The generated impl.
|
||||||
|
let expanded = quote! {
|
||||||
|
#[async_trait::async_trait(?Send)]
|
||||||
|
impl #impl_generics lemmy_apub_lib::ActivityHandler for #name #ty_generics #where_clause {
|
||||||
|
async fn verify(
|
||||||
|
&self,
|
||||||
|
context: &LemmyContext,
|
||||||
|
request_counter: &mut i32,
|
||||||
|
) -> Result<(), LemmyError> {
|
||||||
|
match self {
|
||||||
|
#(#impl_verify)*
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async fn receive(
|
||||||
|
&self,
|
||||||
|
context: &LemmyContext,
|
||||||
|
request_counter: &mut i32,
|
||||||
|
) -> Result<(), LemmyError> {
|
||||||
|
match self {
|
||||||
|
#(#impl_receive)*
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn common(&self) -> &ActivityCommonFields {
|
||||||
|
match self {
|
||||||
|
#(#impl_common)*
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Hand the output tokens back to the compiler.
|
||||||
|
proc_macro::TokenStream::from(expanded)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn variant_impl_common(name: &syn::Ident, variant: &syn::Variant) -> TokenStream {
|
||||||
|
let id = &variant.ident;
|
||||||
|
match &variant.fields {
|
||||||
|
syn::Fields::Unnamed(_) => {
|
||||||
|
quote! {
|
||||||
|
#name::#id(a) => a.common(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => unimplemented!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn variant_impl_verify(name: &syn::Ident, variant: &syn::Variant) -> TokenStream {
|
||||||
|
let id = &variant.ident;
|
||||||
|
match &variant.fields {
|
||||||
|
syn::Fields::Unnamed(_) => {
|
||||||
|
quote! {
|
||||||
|
#name::#id(a) => a.verify(context, request_counter).await,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => unimplemented!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn variant_impl_receive(name: &syn::Ident, variant: &syn::Variant) -> TokenStream {
|
||||||
|
let id = &variant.ident;
|
||||||
|
match &variant.fields {
|
||||||
|
syn::Fields::Unnamed(_) => {
|
||||||
|
quote! {
|
||||||
|
#name::#id(a) => a.receive(context, request_counter).await,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => unimplemented!(),
|
||||||
|
}
|
||||||
|
}
|
|
@ -5,6 +5,7 @@ edition = "2018"
|
||||||
|
|
||||||
[dependencies]
|
[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" }
|
||||||
|
|
68
crates/apub_receive/src/activities/comment/create.rs
Normal file
68
crates/apub_receive/src/activities/comment/create.rs
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
use crate::activities::{
|
||||||
|
comment::{get_notif_recipients, send_websocket_message},
|
||||||
|
verify_activity,
|
||||||
|
verify_person_in_community,
|
||||||
|
};
|
||||||
|
use activitystreams::{activity::kind::CreateType, base::BaseExt};
|
||||||
|
use lemmy_apub::{objects::FromApub, NoteExt};
|
||||||
|
use lemmy_apub_lib::{verify_domains_match_opt, ActivityCommonFields, ActivityHandler, PublicUrl};
|
||||||
|
use lemmy_db_schema::source::comment::Comment;
|
||||||
|
use lemmy_utils::LemmyError;
|
||||||
|
use lemmy_websocket::{LemmyContext, UserOperationCrud};
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct CreateComment {
|
||||||
|
to: PublicUrl,
|
||||||
|
object: NoteExt,
|
||||||
|
cc: Vec<Url>,
|
||||||
|
#[serde(rename = "type")]
|
||||||
|
kind: CreateType,
|
||||||
|
#[serde(flatten)]
|
||||||
|
common: ActivityCommonFields,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait(?Send)]
|
||||||
|
impl ActivityHandler for CreateComment {
|
||||||
|
async fn verify(
|
||||||
|
&self,
|
||||||
|
context: &LemmyContext,
|
||||||
|
request_counter: &mut i32,
|
||||||
|
) -> Result<(), LemmyError> {
|
||||||
|
verify_activity(self.common())?;
|
||||||
|
verify_person_in_community(&self.common.actor, &self.cc, context, request_counter).await?;
|
||||||
|
verify_domains_match_opt(&self.common.actor, self.object.id_unchecked())?;
|
||||||
|
// TODO: should add a check that the correct community is in cc (probably needs changes to
|
||||||
|
// comment deserialization)
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn receive(
|
||||||
|
&self,
|
||||||
|
context: &LemmyContext,
|
||||||
|
request_counter: &mut i32,
|
||||||
|
) -> Result<(), LemmyError> {
|
||||||
|
let comment = Comment::from_apub(
|
||||||
|
&self.object,
|
||||||
|
context,
|
||||||
|
self.common.actor.clone(),
|
||||||
|
request_counter,
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
let recipients =
|
||||||
|
get_notif_recipients(&self.common.actor, &comment, context, request_counter).await?;
|
||||||
|
send_websocket_message(
|
||||||
|
comment.id,
|
||||||
|
recipients,
|
||||||
|
UserOperationCrud::CreateComment,
|
||||||
|
context,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
fn common(&self) -> &ActivityCommonFields {
|
||||||
|
&self.common
|
||||||
|
}
|
||||||
|
}
|
65
crates/apub_receive/src/activities/comment/mod.rs
Normal file
65
crates/apub_receive/src/activities/comment/mod.rs
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
use lemmy_api_common::{blocking, comment::CommentResponse, send_local_notifs};
|
||||||
|
use lemmy_apub::fetcher::person::get_or_fetch_and_upsert_person;
|
||||||
|
use lemmy_db_queries::Crud;
|
||||||
|
use lemmy_db_schema::{
|
||||||
|
source::{comment::Comment, post::Post},
|
||||||
|
CommentId,
|
||||||
|
LocalUserId,
|
||||||
|
};
|
||||||
|
use lemmy_db_views::comment_view::CommentView;
|
||||||
|
use lemmy_utils::{utils::scrape_text_for_mentions, LemmyError};
|
||||||
|
use lemmy_websocket::{messages::SendComment, LemmyContext};
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
pub mod create;
|
||||||
|
pub mod update;
|
||||||
|
|
||||||
|
async fn get_notif_recipients(
|
||||||
|
actor: &Url,
|
||||||
|
comment: &Comment,
|
||||||
|
context: &LemmyContext,
|
||||||
|
request_counter: &mut i32,
|
||||||
|
) -> Result<Vec<LocalUserId>, LemmyError> {
|
||||||
|
let post_id = comment.post_id;
|
||||||
|
let post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??;
|
||||||
|
let actor = get_or_fetch_and_upsert_person(actor, context, request_counter).await?;
|
||||||
|
|
||||||
|
// Note:
|
||||||
|
// Although mentions could be gotten from the post tags (they are included there), or the ccs,
|
||||||
|
// Its much easier to scrape them from the comment body, since the API has to do that
|
||||||
|
// anyway.
|
||||||
|
// TODO: for compatibility with other projects, it would be much better to read this from cc or tags
|
||||||
|
let mentions = scrape_text_for_mentions(&comment.content);
|
||||||
|
send_local_notifs(mentions, comment.clone(), actor, post, context.pool(), true).await
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: in many call sites we are setting an empty vec for recipient_ids, we should get the actual
|
||||||
|
// recipient actors from somewhere
|
||||||
|
pub(crate) async fn send_websocket_message<
|
||||||
|
OP: ToString + Send + lemmy_websocket::OperationType + 'static,
|
||||||
|
>(
|
||||||
|
comment_id: CommentId,
|
||||||
|
recipient_ids: Vec<LocalUserId>,
|
||||||
|
op: OP,
|
||||||
|
context: &LemmyContext,
|
||||||
|
) -> Result<(), LemmyError> {
|
||||||
|
// Refetch the view
|
||||||
|
let comment_view = blocking(context.pool(), move |conn| {
|
||||||
|
CommentView::read(conn, comment_id, None)
|
||||||
|
})
|
||||||
|
.await??;
|
||||||
|
|
||||||
|
let res = CommentResponse {
|
||||||
|
comment_view,
|
||||||
|
recipient_ids,
|
||||||
|
form_id: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
context.chat_server().do_send(SendComment {
|
||||||
|
op,
|
||||||
|
comment: res,
|
||||||
|
websocket_id: None,
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
56
crates/apub_receive/src/activities/comment/remove.rs
Normal file
56
crates/apub_receive/src/activities/comment/remove.rs
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
use crate::activities::{comment::send_websocket_message, verify_mod_action};
|
||||||
|
use activitystreams::activity::kind::RemoveType;
|
||||||
|
use lemmy_api_common::blocking;
|
||||||
|
use lemmy_apub::{check_is_apub_id_valid, fetcher::objects::get_or_fetch_and_insert_comment};
|
||||||
|
use lemmy_apub_lib::{verify_domains_match, ActivityCommonFields, ActivityHandlerNew, PublicUrl};
|
||||||
|
use lemmy_db_queries::source::comment::Comment_;
|
||||||
|
use lemmy_db_schema::source::comment::Comment;
|
||||||
|
use lemmy_utils::LemmyError;
|
||||||
|
use lemmy_websocket::{LemmyContext, UserOperationCrud};
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct RemoveComment {
|
||||||
|
to: PublicUrl,
|
||||||
|
pub(in crate::activities::comment) object: Url,
|
||||||
|
cc: [Url; 1],
|
||||||
|
#[serde(rename = "type")]
|
||||||
|
kind: RemoveType,
|
||||||
|
#[serde(flatten)]
|
||||||
|
common: ActivityCommonFields,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait(?Send)]
|
||||||
|
impl ActivityHandlerNew for RemoveComment {
|
||||||
|
async fn verify(&self, context: &LemmyContext, _: &mut i32) -> Result<(), LemmyError> {
|
||||||
|
verify_domains_match(&self.common.actor, self.common.id_unchecked())?;
|
||||||
|
check_is_apub_id_valid(&self.common.actor, false)?;
|
||||||
|
verify_mod_action(&self.common.actor, self.cc[0].clone(), context).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn receive(
|
||||||
|
&self,
|
||||||
|
context: &LemmyContext,
|
||||||
|
request_counter: &mut i32,
|
||||||
|
) -> Result<(), LemmyError> {
|
||||||
|
let comment = get_or_fetch_and_insert_comment(&self.object, context, request_counter).await?;
|
||||||
|
|
||||||
|
let removed_comment = blocking(context.pool(), move |conn| {
|
||||||
|
Comment::update_removed(conn, comment.id, true)
|
||||||
|
})
|
||||||
|
.await??;
|
||||||
|
|
||||||
|
send_websocket_message(
|
||||||
|
removed_comment.id,
|
||||||
|
vec![],
|
||||||
|
UserOperationCrud::EditComment,
|
||||||
|
context,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
fn common(&self) -> &ActivityCommonFields {
|
||||||
|
&self.common
|
||||||
|
}
|
||||||
|
}
|
65
crates/apub_receive/src/activities/comment/undo_remove.rs
Normal file
65
crates/apub_receive/src/activities/comment/undo_remove.rs
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
use crate::activities::{
|
||||||
|
comment::{remove::RemoveComment, send_websocket_message},
|
||||||
|
verify_mod_action,
|
||||||
|
};
|
||||||
|
use activitystreams::activity::kind::UndoType;
|
||||||
|
use lemmy_api_common::blocking;
|
||||||
|
use lemmy_apub::{check_is_apub_id_valid, fetcher::objects::get_or_fetch_and_insert_comment};
|
||||||
|
use lemmy_apub_lib::{verify_domains_match, ActivityCommonFields, ActivityHandlerNew, PublicUrl};
|
||||||
|
use lemmy_db_queries::source::comment::Comment_;
|
||||||
|
use lemmy_db_schema::source::comment::Comment;
|
||||||
|
use lemmy_utils::LemmyError;
|
||||||
|
use lemmy_websocket::{LemmyContext, UserOperationCrud};
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct UndoRemoveComment {
|
||||||
|
to: PublicUrl,
|
||||||
|
object: RemoveComment,
|
||||||
|
cc: [Url; 1],
|
||||||
|
#[serde(rename = "type")]
|
||||||
|
kind: UndoType,
|
||||||
|
#[serde(flatten)]
|
||||||
|
common: ActivityCommonFields,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait(?Send)]
|
||||||
|
impl ActivityHandlerNew for UndoRemoveComment {
|
||||||
|
async fn verify(
|
||||||
|
&self,
|
||||||
|
context: &LemmyContext,
|
||||||
|
request_counter: &mut i32,
|
||||||
|
) -> Result<(), LemmyError> {
|
||||||
|
verify_domains_match(&self.common.actor, self.common.id_unchecked())?;
|
||||||
|
check_is_apub_id_valid(&self.common.actor, false)?;
|
||||||
|
verify_mod_action(&self.common.actor, self.cc[0].clone(), context).await?;
|
||||||
|
self.object.verify(context, request_counter).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn receive(
|
||||||
|
&self,
|
||||||
|
context: &LemmyContext,
|
||||||
|
request_counter: &mut i32,
|
||||||
|
) -> Result<(), LemmyError> {
|
||||||
|
let comment =
|
||||||
|
get_or_fetch_and_insert_comment(&self.object.object, context, request_counter).await?;
|
||||||
|
|
||||||
|
let removed_comment = blocking(context.pool(), move |conn| {
|
||||||
|
Comment::update_removed(conn, comment.id, false)
|
||||||
|
})
|
||||||
|
.await??;
|
||||||
|
|
||||||
|
send_websocket_message(
|
||||||
|
removed_comment.id,
|
||||||
|
vec![],
|
||||||
|
UserOperationCrud::EditComment,
|
||||||
|
context,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
fn common(&self) -> &ActivityCommonFields {
|
||||||
|
&self.common
|
||||||
|
}
|
||||||
|
}
|
67
crates/apub_receive/src/activities/comment/update.rs
Normal file
67
crates/apub_receive/src/activities/comment/update.rs
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
use crate::activities::{
|
||||||
|
comment::{get_notif_recipients, send_websocket_message},
|
||||||
|
verify_activity,
|
||||||
|
verify_person_in_community,
|
||||||
|
};
|
||||||
|
use activitystreams::{activity::kind::UpdateType, base::BaseExt};
|
||||||
|
use lemmy_apub::{objects::FromApub, NoteExt};
|
||||||
|
use lemmy_apub_lib::{verify_domains_match_opt, ActivityCommonFields, ActivityHandler, PublicUrl};
|
||||||
|
use lemmy_db_schema::source::comment::Comment;
|
||||||
|
use lemmy_utils::LemmyError;
|
||||||
|
use lemmy_websocket::{LemmyContext, UserOperationCrud};
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct UpdateComment {
|
||||||
|
to: PublicUrl,
|
||||||
|
object: NoteExt,
|
||||||
|
cc: Vec<Url>,
|
||||||
|
#[serde(rename = "type")]
|
||||||
|
kind: UpdateType,
|
||||||
|
#[serde(flatten)]
|
||||||
|
common: ActivityCommonFields,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait(?Send)]
|
||||||
|
impl ActivityHandler for UpdateComment {
|
||||||
|
async fn verify(
|
||||||
|
&self,
|
||||||
|
context: &LemmyContext,
|
||||||
|
request_counter: &mut i32,
|
||||||
|
) -> Result<(), LemmyError> {
|
||||||
|
verify_activity(self.common())?;
|
||||||
|
verify_person_in_community(&self.common.actor, &self.cc, context, request_counter).await?;
|
||||||
|
verify_domains_match_opt(&self.common.actor, self.object.id_unchecked())?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn receive(
|
||||||
|
&self,
|
||||||
|
context: &LemmyContext,
|
||||||
|
request_counter: &mut i32,
|
||||||
|
) -> Result<(), LemmyError> {
|
||||||
|
let comment = Comment::from_apub(
|
||||||
|
&self.object,
|
||||||
|
context,
|
||||||
|
self.common.actor.clone(),
|
||||||
|
request_counter,
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let recipients =
|
||||||
|
get_notif_recipients(&self.common.actor, &comment, context, request_counter).await?;
|
||||||
|
send_websocket_message(
|
||||||
|
comment.id,
|
||||||
|
recipients,
|
||||||
|
UserOperationCrud::EditComment,
|
||||||
|
context,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
fn common(&self) -> &ActivityCommonFields {
|
||||||
|
&self.common
|
||||||
|
}
|
||||||
|
}
|
86
crates/apub_receive/src/activities/community/add_mod.rs
Normal file
86
crates/apub_receive/src/activities/community/add_mod.rs
Normal file
|
@ -0,0 +1,86 @@
|
||||||
|
use crate::activities::{
|
||||||
|
verify_activity,
|
||||||
|
verify_add_remove_moderator_target,
|
||||||
|
verify_mod_action,
|
||||||
|
verify_person_in_community,
|
||||||
|
};
|
||||||
|
use activitystreams::{activity::kind::AddType, base::AnyBase};
|
||||||
|
use lemmy_api_common::blocking;
|
||||||
|
use lemmy_apub::{
|
||||||
|
fetcher::{community::get_or_fetch_and_upsert_community, person::get_or_fetch_and_upsert_person},
|
||||||
|
CommunityType,
|
||||||
|
};
|
||||||
|
use lemmy_apub_lib::{ActivityCommonFields, ActivityHandler, PublicUrl};
|
||||||
|
use lemmy_db_queries::{source::community::CommunityModerator_, Joinable};
|
||||||
|
use lemmy_db_schema::source::community::{CommunityModerator, CommunityModeratorForm};
|
||||||
|
use lemmy_utils::LemmyError;
|
||||||
|
use lemmy_websocket::LemmyContext;
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct AddMod {
|
||||||
|
to: PublicUrl,
|
||||||
|
object: Url,
|
||||||
|
target: Url,
|
||||||
|
cc: [Url; 1],
|
||||||
|
#[serde(rename = "type")]
|
||||||
|
kind: AddType,
|
||||||
|
#[serde(flatten)]
|
||||||
|
common: ActivityCommonFields,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait(?Send)]
|
||||||
|
impl ActivityHandler for AddMod {
|
||||||
|
async fn verify(
|
||||||
|
&self,
|
||||||
|
context: &LemmyContext,
|
||||||
|
request_counter: &mut i32,
|
||||||
|
) -> Result<(), LemmyError> {
|
||||||
|
verify_activity(self.common())?;
|
||||||
|
verify_person_in_community(&self.common.actor, &self.cc, context, request_counter).await?;
|
||||||
|
verify_mod_action(&self.common.actor, self.cc[0].clone(), context).await?;
|
||||||
|
verify_add_remove_moderator_target(&self.target, self.cc[0].clone())?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn receive(
|
||||||
|
&self,
|
||||||
|
context: &LemmyContext,
|
||||||
|
request_counter: &mut i32,
|
||||||
|
) -> Result<(), LemmyError> {
|
||||||
|
let community =
|
||||||
|
get_or_fetch_and_upsert_community(&self.cc[0], context, request_counter).await?;
|
||||||
|
let new_mod = get_or_fetch_and_upsert_person(&self.object, context, request_counter).await?;
|
||||||
|
|
||||||
|
// If we had to refetch the community while parsing the activity, then the new mod has already
|
||||||
|
// been added. Skip it here as it would result in a duplicate key error.
|
||||||
|
let new_mod_id = new_mod.id;
|
||||||
|
let moderated_communities = blocking(context.pool(), move |conn| {
|
||||||
|
CommunityModerator::get_person_moderated_communities(conn, new_mod_id)
|
||||||
|
})
|
||||||
|
.await??;
|
||||||
|
if !moderated_communities.contains(&community.id) {
|
||||||
|
let form = CommunityModeratorForm {
|
||||||
|
community_id: community.id,
|
||||||
|
person_id: new_mod.id,
|
||||||
|
};
|
||||||
|
blocking(context.pool(), move |conn| {
|
||||||
|
CommunityModerator::join(conn, &form)
|
||||||
|
})
|
||||||
|
.await??;
|
||||||
|
}
|
||||||
|
if community.local {
|
||||||
|
let anybase = AnyBase::from_arbitrary_json(serde_json::to_string(self)?)?;
|
||||||
|
community
|
||||||
|
.send_announce(anybase, Some(self.object.clone()), context)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
// TODO: send websocket notification about added mod
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn common(&self) -> &ActivityCommonFields {
|
||||||
|
&self.common
|
||||||
|
}
|
||||||
|
}
|
104
crates/apub_receive/src/activities/community/announce.rs
Normal file
104
crates/apub_receive/src/activities/community/announce.rs
Normal file
|
@ -0,0 +1,104 @@
|
||||||
|
use crate::{
|
||||||
|
activities::{
|
||||||
|
comment::{create::CreateComment, update::UpdateComment},
|
||||||
|
community::{
|
||||||
|
add_mod::AddMod,
|
||||||
|
block_user::BlockUserFromCommunity,
|
||||||
|
undo_block_user::UndoBlockUserFromCommunity,
|
||||||
|
},
|
||||||
|
deletion::{
|
||||||
|
delete::DeletePostCommentOrCommunity,
|
||||||
|
undo_delete::UndoDeletePostCommentOrCommunity,
|
||||||
|
},
|
||||||
|
post::{create::CreatePost, update::UpdatePost},
|
||||||
|
removal::{
|
||||||
|
remove::RemovePostCommentCommunityOrMod,
|
||||||
|
undo_remove::UndoRemovePostCommentOrCommunity,
|
||||||
|
},
|
||||||
|
verify_activity,
|
||||||
|
verify_community,
|
||||||
|
voting::{
|
||||||
|
dislike::DislikePostOrComment,
|
||||||
|
like::LikePostOrComment,
|
||||||
|
undo_dislike::UndoDislikePostOrComment,
|
||||||
|
undo_like::UndoLikePostOrComment,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
http::is_activity_already_known,
|
||||||
|
};
|
||||||
|
use activitystreams::activity::kind::AnnounceType;
|
||||||
|
use lemmy_apub::insert_activity;
|
||||||
|
use lemmy_apub_lib::{ActivityCommonFields, ActivityHandler, PublicUrl};
|
||||||
|
use lemmy_utils::LemmyError;
|
||||||
|
use lemmy_websocket::LemmyContext;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize, Serialize, ActivityHandler)]
|
||||||
|
#[serde(untagged)]
|
||||||
|
pub enum AnnouncableActivities {
|
||||||
|
CreateComment(CreateComment),
|
||||||
|
UpdateComment(UpdateComment),
|
||||||
|
CreatePost(CreatePost),
|
||||||
|
UpdatePost(UpdatePost),
|
||||||
|
LikePostOrComment(LikePostOrComment),
|
||||||
|
DislikePostOrComment(DislikePostOrComment),
|
||||||
|
UndoLikePostOrComment(UndoLikePostOrComment),
|
||||||
|
UndoDislikePostOrComment(UndoDislikePostOrComment),
|
||||||
|
DeletePostCommentOrCommunity(DeletePostCommentOrCommunity),
|
||||||
|
UndoDeletePostCommentOrCommunity(UndoDeletePostCommentOrCommunity),
|
||||||
|
RemovePostCommentCommunityOrMod(RemovePostCommentCommunityOrMod),
|
||||||
|
UndoRemovePostCommentOrCommunity(UndoRemovePostCommentOrCommunity),
|
||||||
|
BlockUserFromCommunity(BlockUserFromCommunity),
|
||||||
|
UndoBlockUserFromCommunity(UndoBlockUserFromCommunity),
|
||||||
|
AddMod(AddMod),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct AnnounceActivity {
|
||||||
|
to: PublicUrl,
|
||||||
|
object: AnnouncableActivities,
|
||||||
|
cc: Vec<Url>,
|
||||||
|
#[serde(rename = "type")]
|
||||||
|
kind: AnnounceType,
|
||||||
|
#[serde(flatten)]
|
||||||
|
common: ActivityCommonFields,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait(?Send)]
|
||||||
|
impl ActivityHandler for AnnounceActivity {
|
||||||
|
async fn verify(
|
||||||
|
&self,
|
||||||
|
context: &LemmyContext,
|
||||||
|
request_counter: &mut i32,
|
||||||
|
) -> Result<(), LemmyError> {
|
||||||
|
verify_activity(self.common())?;
|
||||||
|
verify_community(&self.common.actor, context, request_counter).await?;
|
||||||
|
self.object.verify(context, request_counter).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn receive(
|
||||||
|
&self,
|
||||||
|
context: &LemmyContext,
|
||||||
|
request_counter: &mut i32,
|
||||||
|
) -> Result<(), LemmyError> {
|
||||||
|
if is_activity_already_known(context.pool(), self.object.common().id_unchecked()).await? {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
insert_activity(
|
||||||
|
self.object.common().id_unchecked(),
|
||||||
|
self.object.clone(),
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
context.pool(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
self.object.receive(context, request_counter).await
|
||||||
|
}
|
||||||
|
|
||||||
|
fn common(&self) -> &ActivityCommonFields {
|
||||||
|
&self.common
|
||||||
|
}
|
||||||
|
}
|
83
crates/apub_receive/src/activities/community/block_user.rs
Normal file
83
crates/apub_receive/src/activities/community/block_user.rs
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
use crate::activities::{verify_activity, verify_mod_action, verify_person_in_community};
|
||||||
|
use activitystreams::activity::kind::BlockType;
|
||||||
|
use lemmy_api_common::blocking;
|
||||||
|
use lemmy_apub::fetcher::{
|
||||||
|
community::get_or_fetch_and_upsert_community,
|
||||||
|
person::get_or_fetch_and_upsert_person,
|
||||||
|
};
|
||||||
|
use lemmy_apub_lib::{ActivityCommonFields, ActivityHandler, PublicUrl};
|
||||||
|
use lemmy_db_queries::{Bannable, Followable};
|
||||||
|
use lemmy_db_schema::source::community::{
|
||||||
|
CommunityFollower,
|
||||||
|
CommunityFollowerForm,
|
||||||
|
CommunityPersonBan,
|
||||||
|
CommunityPersonBanForm,
|
||||||
|
};
|
||||||
|
use lemmy_utils::LemmyError;
|
||||||
|
use lemmy_websocket::LemmyContext;
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct BlockUserFromCommunity {
|
||||||
|
to: PublicUrl,
|
||||||
|
pub(in crate::activities::community) object: Url,
|
||||||
|
cc: [Url; 1],
|
||||||
|
#[serde(rename = "type")]
|
||||||
|
kind: BlockType,
|
||||||
|
#[serde(flatten)]
|
||||||
|
common: ActivityCommonFields,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait(?Send)]
|
||||||
|
impl ActivityHandler for BlockUserFromCommunity {
|
||||||
|
async fn verify(
|
||||||
|
&self,
|
||||||
|
context: &LemmyContext,
|
||||||
|
request_counter: &mut i32,
|
||||||
|
) -> Result<(), LemmyError> {
|
||||||
|
verify_activity(self.common())?;
|
||||||
|
verify_person_in_community(&self.common.actor, &self.cc, context, request_counter).await?;
|
||||||
|
verify_mod_action(&self.common.actor, self.cc[0].clone(), context).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn receive(
|
||||||
|
&self,
|
||||||
|
context: &LemmyContext,
|
||||||
|
request_counter: &mut i32,
|
||||||
|
) -> Result<(), LemmyError> {
|
||||||
|
let community =
|
||||||
|
get_or_fetch_and_upsert_community(&self.cc[0], context, request_counter).await?;
|
||||||
|
let blocked_user =
|
||||||
|
get_or_fetch_and_upsert_person(&self.object, context, request_counter).await?;
|
||||||
|
|
||||||
|
let community_user_ban_form = CommunityPersonBanForm {
|
||||||
|
community_id: community.id,
|
||||||
|
person_id: blocked_user.id,
|
||||||
|
};
|
||||||
|
|
||||||
|
blocking(context.pool(), move |conn: &'_ _| {
|
||||||
|
CommunityPersonBan::ban(conn, &community_user_ban_form)
|
||||||
|
})
|
||||||
|
.await??;
|
||||||
|
|
||||||
|
// Also unsubscribe them from the community, if they are subscribed
|
||||||
|
let community_follower_form = CommunityFollowerForm {
|
||||||
|
community_id: community.id,
|
||||||
|
person_id: blocked_user.id,
|
||||||
|
pending: false,
|
||||||
|
};
|
||||||
|
blocking(context.pool(), move |conn: &'_ _| {
|
||||||
|
CommunityFollower::unfollow(conn, &community_follower_form)
|
||||||
|
})
|
||||||
|
.await?
|
||||||
|
.ok();
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn common(&self) -> &ActivityCommonFields {
|
||||||
|
&self.common
|
||||||
|
}
|
||||||
|
}
|
35
crates/apub_receive/src/activities/community/mod.rs
Normal file
35
crates/apub_receive/src/activities/community/mod.rs
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
use lemmy_api_common::{blocking, community::CommunityResponse};
|
||||||
|
use lemmy_db_schema::CommunityId;
|
||||||
|
use lemmy_db_views_actor::community_view::CommunityView;
|
||||||
|
use lemmy_utils::LemmyError;
|
||||||
|
use lemmy_websocket::{messages::SendCommunityRoomMessage, LemmyContext};
|
||||||
|
|
||||||
|
pub mod add_mod;
|
||||||
|
pub mod announce;
|
||||||
|
pub mod block_user;
|
||||||
|
pub mod undo_block_user;
|
||||||
|
pub mod update;
|
||||||
|
|
||||||
|
pub(crate) async fn send_websocket_message<
|
||||||
|
OP: ToString + Send + lemmy_websocket::OperationType + 'static,
|
||||||
|
>(
|
||||||
|
community_id: CommunityId,
|
||||||
|
op: OP,
|
||||||
|
context: &LemmyContext,
|
||||||
|
) -> Result<(), LemmyError> {
|
||||||
|
let community_view = blocking(context.pool(), move |conn| {
|
||||||
|
CommunityView::read(conn, community_id, None)
|
||||||
|
})
|
||||||
|
.await??;
|
||||||
|
|
||||||
|
let res = CommunityResponse { community_view };
|
||||||
|
|
||||||
|
context.chat_server().do_send(SendCommunityRoomMessage {
|
||||||
|
op,
|
||||||
|
response: res,
|
||||||
|
community_id,
|
||||||
|
websocket_id: None,
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
|
@ -0,0 +1,72 @@
|
||||||
|
use crate::activities::{
|
||||||
|
community::block_user::BlockUserFromCommunity,
|
||||||
|
verify_activity,
|
||||||
|
verify_mod_action,
|
||||||
|
verify_person_in_community,
|
||||||
|
};
|
||||||
|
use activitystreams::activity::kind::UndoType;
|
||||||
|
use lemmy_api_common::blocking;
|
||||||
|
use lemmy_apub::fetcher::{
|
||||||
|
community::get_or_fetch_and_upsert_community,
|
||||||
|
person::get_or_fetch_and_upsert_person,
|
||||||
|
};
|
||||||
|
use lemmy_apub_lib::{ActivityCommonFields, ActivityHandler, PublicUrl};
|
||||||
|
use lemmy_db_queries::Bannable;
|
||||||
|
use lemmy_db_schema::source::community::{CommunityPersonBan, CommunityPersonBanForm};
|
||||||
|
use lemmy_utils::LemmyError;
|
||||||
|
use lemmy_websocket::LemmyContext;
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct UndoBlockUserFromCommunity {
|
||||||
|
to: PublicUrl,
|
||||||
|
object: BlockUserFromCommunity,
|
||||||
|
cc: [Url; 1],
|
||||||
|
#[serde(rename = "type")]
|
||||||
|
kind: UndoType,
|
||||||
|
#[serde(flatten)]
|
||||||
|
common: ActivityCommonFields,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait(?Send)]
|
||||||
|
impl ActivityHandler for UndoBlockUserFromCommunity {
|
||||||
|
async fn verify(
|
||||||
|
&self,
|
||||||
|
context: &LemmyContext,
|
||||||
|
request_counter: &mut i32,
|
||||||
|
) -> Result<(), LemmyError> {
|
||||||
|
verify_activity(self.common())?;
|
||||||
|
verify_person_in_community(&self.common.actor, &self.cc, context, request_counter).await?;
|
||||||
|
verify_mod_action(&self.common.actor, self.cc[0].clone(), context).await?;
|
||||||
|
self.object.verify(context, request_counter).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn receive(
|
||||||
|
&self,
|
||||||
|
context: &LemmyContext,
|
||||||
|
request_counter: &mut i32,
|
||||||
|
) -> Result<(), LemmyError> {
|
||||||
|
let community =
|
||||||
|
get_or_fetch_and_upsert_community(&self.cc[0], context, request_counter).await?;
|
||||||
|
let blocked_user =
|
||||||
|
get_or_fetch_and_upsert_person(&self.object.object, context, request_counter).await?;
|
||||||
|
|
||||||
|
let community_user_ban_form = CommunityPersonBanForm {
|
||||||
|
community_id: community.id,
|
||||||
|
person_id: blocked_user.id,
|
||||||
|
};
|
||||||
|
|
||||||
|
blocking(context.pool(), move |conn: &'_ _| {
|
||||||
|
CommunityPersonBan::unban(conn, &community_user_ban_form)
|
||||||
|
})
|
||||||
|
.await??;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn common(&self) -> &ActivityCommonFields {
|
||||||
|
&self.common
|
||||||
|
}
|
||||||
|
}
|
89
crates/apub_receive/src/activities/community/update.rs
Normal file
89
crates/apub_receive/src/activities/community/update.rs
Normal file
|
@ -0,0 +1,89 @@
|
||||||
|
use crate::activities::{
|
||||||
|
community::send_websocket_message,
|
||||||
|
verify_activity,
|
||||||
|
verify_mod_action,
|
||||||
|
verify_person_in_community,
|
||||||
|
};
|
||||||
|
use activitystreams::activity::kind::UpdateType;
|
||||||
|
use lemmy_api_common::blocking;
|
||||||
|
use lemmy_apub::{objects::FromApubToForm, GroupExt};
|
||||||
|
use lemmy_apub_lib::{ActivityCommonFields, ActivityHandler, PublicUrl};
|
||||||
|
use lemmy_db_queries::{ApubObject, Crud};
|
||||||
|
use lemmy_db_schema::source::community::{Community, CommunityForm};
|
||||||
|
use lemmy_utils::LemmyError;
|
||||||
|
use lemmy_websocket::{LemmyContext, UserOperationCrud};
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
/// This activity is received from a remote community mod, and updates the description or other
|
||||||
|
/// fields of a local community.
|
||||||
|
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct UpdateCommunity {
|
||||||
|
to: PublicUrl,
|
||||||
|
object: GroupExt,
|
||||||
|
cc: [Url; 1],
|
||||||
|
#[serde(rename = "type")]
|
||||||
|
kind: UpdateType,
|
||||||
|
#[serde(flatten)]
|
||||||
|
common: ActivityCommonFields,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait(?Send)]
|
||||||
|
impl ActivityHandler for UpdateCommunity {
|
||||||
|
async fn verify(
|
||||||
|
&self,
|
||||||
|
context: &LemmyContext,
|
||||||
|
request_counter: &mut i32,
|
||||||
|
) -> Result<(), LemmyError> {
|
||||||
|
verify_activity(self.common())?;
|
||||||
|
verify_person_in_community(&self.common.actor, &self.cc, context, request_counter).await?;
|
||||||
|
verify_mod_action(&self.common.actor, self.cc[0].clone(), context).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn receive(
|
||||||
|
&self,
|
||||||
|
context: &LemmyContext,
|
||||||
|
request_counter: &mut i32,
|
||||||
|
) -> Result<(), LemmyError> {
|
||||||
|
let cc = self.cc[0].clone().into();
|
||||||
|
let community = blocking(context.pool(), move |conn| {
|
||||||
|
Community::read_from_apub_id(conn, &cc)
|
||||||
|
})
|
||||||
|
.await??;
|
||||||
|
|
||||||
|
let updated_community = CommunityForm::from_apub(
|
||||||
|
&self.object,
|
||||||
|
context,
|
||||||
|
community.actor_id.clone().into(),
|
||||||
|
request_counter,
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
let cf = CommunityForm {
|
||||||
|
name: updated_community.name,
|
||||||
|
title: updated_community.title,
|
||||||
|
description: updated_community.description,
|
||||||
|
nsfw: updated_community.nsfw,
|
||||||
|
// TODO: icon and banner would be hosted on the other instance, ideally we would copy it to ours
|
||||||
|
icon: updated_community.icon,
|
||||||
|
banner: updated_community.banner,
|
||||||
|
..CommunityForm::default()
|
||||||
|
};
|
||||||
|
let updated_community = blocking(context.pool(), move |conn| {
|
||||||
|
Community::update(conn, community.id, &cf)
|
||||||
|
})
|
||||||
|
.await??;
|
||||||
|
|
||||||
|
send_websocket_message(
|
||||||
|
updated_community.id,
|
||||||
|
UserOperationCrud::EditCommunity,
|
||||||
|
context,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
fn common(&self) -> &ActivityCommonFields {
|
||||||
|
&self.common
|
||||||
|
}
|
||||||
|
}
|
158
crates/apub_receive/src/activities/deletion/delete.rs
Normal file
158
crates/apub_receive/src/activities/deletion/delete.rs
Normal file
|
@ -0,0 +1,158 @@
|
||||||
|
use crate::activities::{
|
||||||
|
comment::send_websocket_message as send_comment_message,
|
||||||
|
community::send_websocket_message as send_community_message,
|
||||||
|
post::send_websocket_message as send_post_message,
|
||||||
|
verify_activity,
|
||||||
|
verify_mod_action,
|
||||||
|
verify_person_in_community,
|
||||||
|
};
|
||||||
|
use activitystreams::activity::kind::DeleteType;
|
||||||
|
use lemmy_api_common::blocking;
|
||||||
|
use lemmy_apub::{
|
||||||
|
fetcher::{
|
||||||
|
community::get_or_fetch_and_upsert_community,
|
||||||
|
objects::get_or_fetch_and_insert_post_or_comment,
|
||||||
|
person::get_or_fetch_and_upsert_person,
|
||||||
|
},
|
||||||
|
ActorType,
|
||||||
|
CommunityType,
|
||||||
|
PostOrComment,
|
||||||
|
};
|
||||||
|
use lemmy_apub_lib::{verify_urls_match, ActivityCommonFields, ActivityHandler, PublicUrl};
|
||||||
|
use lemmy_db_queries::{
|
||||||
|
source::{comment::Comment_, community::Community_, post::Post_},
|
||||||
|
Crud,
|
||||||
|
};
|
||||||
|
use lemmy_db_schema::source::{comment::Comment, community::Community, person::Person, post::Post};
|
||||||
|
use lemmy_utils::LemmyError;
|
||||||
|
use lemmy_websocket::{LemmyContext, UserOperationCrud};
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
/// This is very confusing, because there are four distinct cases to handle:
|
||||||
|
/// - user deletes their post
|
||||||
|
/// - user deletes their comment
|
||||||
|
/// - remote community mod deletes local community
|
||||||
|
/// - remote community deletes itself (triggered by a mod)
|
||||||
|
///
|
||||||
|
/// TODO: we should probably change how community deletions work to simplify this. Probably by
|
||||||
|
/// wrapping it in an announce just like other activities, instead of having the community send it.
|
||||||
|
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct DeletePostCommentOrCommunity {
|
||||||
|
to: PublicUrl,
|
||||||
|
pub(in crate::activities::deletion) object: Url,
|
||||||
|
cc: [Url; 1],
|
||||||
|
#[serde(rename = "type")]
|
||||||
|
kind: DeleteType,
|
||||||
|
#[serde(flatten)]
|
||||||
|
common: ActivityCommonFields,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait(?Send)]
|
||||||
|
impl ActivityHandler for DeletePostCommentOrCommunity {
|
||||||
|
async fn verify(
|
||||||
|
&self,
|
||||||
|
context: &LemmyContext,
|
||||||
|
request_counter: &mut i32,
|
||||||
|
) -> Result<(), LemmyError> {
|
||||||
|
verify_activity(self.common())?;
|
||||||
|
let object_community =
|
||||||
|
get_or_fetch_and_upsert_community(&self.object, context, request_counter).await;
|
||||||
|
// deleting a community (set counter 0 to only fetch from local db)
|
||||||
|
if object_community.is_ok() {
|
||||||
|
verify_mod_action(&self.common.actor, self.object.clone(), context).await?;
|
||||||
|
}
|
||||||
|
// deleting a post or comment
|
||||||
|
else {
|
||||||
|
verify_person_in_community(&self.common().actor, &self.cc, context, request_counter).await?;
|
||||||
|
let object_creator =
|
||||||
|
get_post_or_comment_actor_id(&self.object, context, request_counter).await?;
|
||||||
|
verify_urls_match(&self.common.actor, &object_creator)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn receive(
|
||||||
|
&self,
|
||||||
|
context: &LemmyContext,
|
||||||
|
request_counter: &mut i32,
|
||||||
|
) -> Result<(), LemmyError> {
|
||||||
|
let object_community =
|
||||||
|
get_or_fetch_and_upsert_community(&self.object, context, request_counter).await;
|
||||||
|
// deleting a community
|
||||||
|
if let Ok(community) = object_community {
|
||||||
|
if community.local {
|
||||||
|
// repeat these checks just to be sure
|
||||||
|
verify_person_in_community(&self.common().actor, &self.cc, context, request_counter)
|
||||||
|
.await?;
|
||||||
|
verify_mod_action(&self.common.actor, self.object.clone(), context).await?;
|
||||||
|
let mod_ =
|
||||||
|
get_or_fetch_and_upsert_person(&self.common.actor, context, request_counter).await?;
|
||||||
|
community.send_delete(mod_, context).await?;
|
||||||
|
}
|
||||||
|
let deleted_community = blocking(context.pool(), move |conn| {
|
||||||
|
Community::update_deleted(conn, community.id, true)
|
||||||
|
})
|
||||||
|
.await??;
|
||||||
|
|
||||||
|
send_community_message(
|
||||||
|
deleted_community.id,
|
||||||
|
UserOperationCrud::DeleteCommunity,
|
||||||
|
context,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
// deleting a post or comment
|
||||||
|
else {
|
||||||
|
match get_or_fetch_and_insert_post_or_comment(&self.object, context, request_counter).await? {
|
||||||
|
PostOrComment::Post(post) => {
|
||||||
|
let deleted_post = blocking(context.pool(), move |conn| {
|
||||||
|
Post::update_deleted(conn, post.id, true)
|
||||||
|
})
|
||||||
|
.await??;
|
||||||
|
send_post_message(deleted_post.id, UserOperationCrud::EditPost, context).await
|
||||||
|
}
|
||||||
|
PostOrComment::Comment(comment) => {
|
||||||
|
let deleted_comment = blocking(context.pool(), move |conn| {
|
||||||
|
Comment::update_deleted(conn, comment.id, true)
|
||||||
|
})
|
||||||
|
.await??;
|
||||||
|
send_comment_message(
|
||||||
|
deleted_comment.id,
|
||||||
|
vec![],
|
||||||
|
UserOperationCrud::EditComment,
|
||||||
|
context,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn common(&self) -> &ActivityCommonFields {
|
||||||
|
&self.common
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_post_or_comment_actor_id(
|
||||||
|
object: &Url,
|
||||||
|
context: &LemmyContext,
|
||||||
|
request_counter: &mut i32,
|
||||||
|
) -> Result<Url, LemmyError> {
|
||||||
|
let actor_id =
|
||||||
|
match get_or_fetch_and_insert_post_or_comment(object, context, request_counter).await? {
|
||||||
|
PostOrComment::Post(post) => {
|
||||||
|
let creator_id = post.creator_id;
|
||||||
|
blocking(context.pool(), move |conn| Person::read(conn, creator_id))
|
||||||
|
.await??
|
||||||
|
.actor_id()
|
||||||
|
}
|
||||||
|
PostOrComment::Comment(comment) => {
|
||||||
|
let creator_id = comment.creator_id;
|
||||||
|
blocking(context.pool(), move |conn| Person::read(conn, creator_id))
|
||||||
|
.await??
|
||||||
|
.actor_id()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Ok(actor_id)
|
||||||
|
}
|
2
crates/apub_receive/src/activities/deletion/mod.rs
Normal file
2
crates/apub_receive/src/activities/deletion/mod.rs
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
pub mod delete;
|
||||||
|
pub mod undo_delete;
|
125
crates/apub_receive/src/activities/deletion/undo_delete.rs
Normal file
125
crates/apub_receive/src/activities/deletion/undo_delete.rs
Normal file
|
@ -0,0 +1,125 @@
|
||||||
|
use crate::activities::{
|
||||||
|
comment::send_websocket_message as send_comment_message,
|
||||||
|
community::send_websocket_message as send_community_message,
|
||||||
|
deletion::delete::DeletePostCommentOrCommunity,
|
||||||
|
post::send_websocket_message as send_post_message,
|
||||||
|
verify_activity,
|
||||||
|
verify_mod_action,
|
||||||
|
verify_person_in_community,
|
||||||
|
};
|
||||||
|
use activitystreams::activity::kind::UndoType;
|
||||||
|
use lemmy_api_common::blocking;
|
||||||
|
use lemmy_apub::{
|
||||||
|
fetcher::{
|
||||||
|
community::get_or_fetch_and_upsert_community,
|
||||||
|
objects::get_or_fetch_and_insert_post_or_comment,
|
||||||
|
person::get_or_fetch_and_upsert_person,
|
||||||
|
},
|
||||||
|
CommunityType,
|
||||||
|
PostOrComment,
|
||||||
|
};
|
||||||
|
use lemmy_apub_lib::{verify_urls_match, ActivityCommonFields, ActivityHandler, PublicUrl};
|
||||||
|
use lemmy_db_queries::source::{comment::Comment_, community::Community_, post::Post_};
|
||||||
|
use lemmy_db_schema::source::{comment::Comment, community::Community, post::Post};
|
||||||
|
use lemmy_utils::LemmyError;
|
||||||
|
use lemmy_websocket::{LemmyContext, UserOperationCrud};
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct UndoDeletePostCommentOrCommunity {
|
||||||
|
to: PublicUrl,
|
||||||
|
object: DeletePostCommentOrCommunity,
|
||||||
|
cc: [Url; 1],
|
||||||
|
#[serde(rename = "type")]
|
||||||
|
kind: UndoType,
|
||||||
|
#[serde(flatten)]
|
||||||
|
common: ActivityCommonFields,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait(?Send)]
|
||||||
|
impl ActivityHandler for UndoDeletePostCommentOrCommunity {
|
||||||
|
async fn verify(
|
||||||
|
&self,
|
||||||
|
context: &LemmyContext,
|
||||||
|
request_counter: &mut i32,
|
||||||
|
) -> Result<(), LemmyError> {
|
||||||
|
verify_activity(self.common())?;
|
||||||
|
self.object.verify(context, request_counter).await?;
|
||||||
|
let object_community =
|
||||||
|
get_or_fetch_and_upsert_community(&self.object.object, context, request_counter).await;
|
||||||
|
// restoring a community
|
||||||
|
if object_community.is_ok() {
|
||||||
|
verify_mod_action(&self.common.actor, self.object.object.clone(), context).await?;
|
||||||
|
}
|
||||||
|
// restoring a post or comment
|
||||||
|
else {
|
||||||
|
verify_person_in_community(&self.common().actor, &self.cc, context, request_counter).await?;
|
||||||
|
verify_urls_match(&self.common.actor, &self.object.common().actor)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn receive(
|
||||||
|
&self,
|
||||||
|
context: &LemmyContext,
|
||||||
|
request_counter: &mut i32,
|
||||||
|
) -> Result<(), LemmyError> {
|
||||||
|
let object_community =
|
||||||
|
get_or_fetch_and_upsert_community(&self.object.object, context, request_counter).await;
|
||||||
|
// restoring a community
|
||||||
|
if let Ok(community) = object_community {
|
||||||
|
if community.local {
|
||||||
|
// repeat these checks just to be sure
|
||||||
|
verify_person_in_community(&self.common().actor, &self.cc, context, request_counter)
|
||||||
|
.await?;
|
||||||
|
verify_mod_action(&self.common.actor, self.object.object.clone(), context).await?;
|
||||||
|
let mod_ =
|
||||||
|
get_or_fetch_and_upsert_person(&self.common.actor, context, request_counter).await?;
|
||||||
|
community.send_undo_delete(mod_, context).await?;
|
||||||
|
}
|
||||||
|
let deleted_community = blocking(context.pool(), move |conn| {
|
||||||
|
Community::update_deleted(conn, community.id, false)
|
||||||
|
})
|
||||||
|
.await??;
|
||||||
|
|
||||||
|
send_community_message(
|
||||||
|
deleted_community.id,
|
||||||
|
UserOperationCrud::EditCommunity,
|
||||||
|
context,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
// restoring a post or comment
|
||||||
|
else {
|
||||||
|
match get_or_fetch_and_insert_post_or_comment(&self.object.object, context, request_counter)
|
||||||
|
.await?
|
||||||
|
{
|
||||||
|
PostOrComment::Post(post) => {
|
||||||
|
let deleted_post = blocking(context.pool(), move |conn| {
|
||||||
|
Post::update_deleted(conn, post.id, false)
|
||||||
|
})
|
||||||
|
.await??;
|
||||||
|
send_post_message(deleted_post.id, UserOperationCrud::EditPost, context).await
|
||||||
|
}
|
||||||
|
PostOrComment::Comment(comment) => {
|
||||||
|
let deleted_comment = blocking(context.pool(), move |conn| {
|
||||||
|
Comment::update_deleted(conn, comment.id, false)
|
||||||
|
})
|
||||||
|
.await??;
|
||||||
|
send_comment_message(
|
||||||
|
deleted_comment.id,
|
||||||
|
vec![],
|
||||||
|
UserOperationCrud::EditComment,
|
||||||
|
context,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn common(&self) -> &ActivityCommonFields {
|
||||||
|
&self.common
|
||||||
|
}
|
||||||
|
}
|
62
crates/apub_receive/src/activities/following/accept.rs
Normal file
62
crates/apub_receive/src/activities/following/accept.rs
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
use crate::activities::{following::follow::FollowCommunity, verify_activity, verify_community};
|
||||||
|
use activitystreams::activity::kind::AcceptType;
|
||||||
|
use lemmy_api_common::blocking;
|
||||||
|
use lemmy_apub::fetcher::{
|
||||||
|
community::get_or_fetch_and_upsert_community,
|
||||||
|
person::get_or_fetch_and_upsert_person,
|
||||||
|
};
|
||||||
|
use lemmy_apub_lib::{verify_urls_match, ActivityCommonFields, ActivityHandler};
|
||||||
|
use lemmy_db_queries::Followable;
|
||||||
|
use lemmy_db_schema::source::community::CommunityFollower;
|
||||||
|
use lemmy_utils::LemmyError;
|
||||||
|
use lemmy_websocket::LemmyContext;
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct AcceptFollowCommunity {
|
||||||
|
to: Url,
|
||||||
|
object: FollowCommunity,
|
||||||
|
#[serde(rename = "type")]
|
||||||
|
kind: AcceptType,
|
||||||
|
#[serde(flatten)]
|
||||||
|
common: ActivityCommonFields,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handle accepted follows
|
||||||
|
#[async_trait::async_trait(?Send)]
|
||||||
|
impl ActivityHandler for AcceptFollowCommunity {
|
||||||
|
async fn verify(
|
||||||
|
&self,
|
||||||
|
context: &LemmyContext,
|
||||||
|
request_counter: &mut i32,
|
||||||
|
) -> Result<(), LemmyError> {
|
||||||
|
verify_activity(self.common())?;
|
||||||
|
verify_urls_match(&self.to, &self.object.common.actor)?;
|
||||||
|
verify_urls_match(&self.common.actor, &self.object.to)?;
|
||||||
|
verify_community(&self.common.actor, context, request_counter).await?;
|
||||||
|
self.object.verify(context, request_counter).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn receive(
|
||||||
|
&self,
|
||||||
|
context: &LemmyContext,
|
||||||
|
request_counter: &mut i32,
|
||||||
|
) -> Result<(), LemmyError> {
|
||||||
|
let actor =
|
||||||
|
get_or_fetch_and_upsert_community(&self.common.actor, context, request_counter).await?;
|
||||||
|
let to = get_or_fetch_and_upsert_person(&self.to, context, request_counter).await?;
|
||||||
|
// This will throw an error if no follow was requested
|
||||||
|
blocking(context.pool(), move |conn| {
|
||||||
|
CommunityFollower::follow_accepted(conn, actor.id, to.id)
|
||||||
|
})
|
||||||
|
.await??;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn common(&self) -> &ActivityCommonFields {
|
||||||
|
&self.common
|
||||||
|
}
|
||||||
|
}
|
73
crates/apub_receive/src/activities/following/follow.rs
Normal file
73
crates/apub_receive/src/activities/following/follow.rs
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
use crate::activities::{verify_activity, verify_person};
|
||||||
|
use activitystreams::{
|
||||||
|
activity::{kind::FollowType, Follow},
|
||||||
|
base::{AnyBase, ExtendsExt},
|
||||||
|
};
|
||||||
|
use anyhow::Context;
|
||||||
|
use lemmy_api_common::blocking;
|
||||||
|
use lemmy_apub::{
|
||||||
|
fetcher::{community::get_or_fetch_and_upsert_community, person::get_or_fetch_and_upsert_person},
|
||||||
|
CommunityType,
|
||||||
|
};
|
||||||
|
use lemmy_apub_lib::{verify_urls_match, ActivityCommonFields, ActivityHandler};
|
||||||
|
use lemmy_db_queries::Followable;
|
||||||
|
use lemmy_db_schema::source::community::{CommunityFollower, CommunityFollowerForm};
|
||||||
|
use lemmy_utils::{location_info, LemmyError};
|
||||||
|
use lemmy_websocket::LemmyContext;
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct FollowCommunity {
|
||||||
|
pub(in crate::activities::following) to: Url,
|
||||||
|
pub(in crate::activities::following) object: Url,
|
||||||
|
#[serde(rename = "type")]
|
||||||
|
kind: FollowType,
|
||||||
|
#[serde(flatten)]
|
||||||
|
pub(in crate::activities::following) common: ActivityCommonFields,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait(?Send)]
|
||||||
|
impl ActivityHandler for FollowCommunity {
|
||||||
|
async fn verify(
|
||||||
|
&self,
|
||||||
|
context: &LemmyContext,
|
||||||
|
request_counter: &mut i32,
|
||||||
|
) -> Result<(), LemmyError> {
|
||||||
|
verify_activity(self.common())?;
|
||||||
|
verify_urls_match(&self.to, &self.object)?;
|
||||||
|
verify_person(&self.common.actor, context, request_counter).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn receive(
|
||||||
|
&self,
|
||||||
|
context: &LemmyContext,
|
||||||
|
request_counter: &mut i32,
|
||||||
|
) -> Result<(), LemmyError> {
|
||||||
|
let actor =
|
||||||
|
get_or_fetch_and_upsert_person(&self.common.actor, context, request_counter).await?;
|
||||||
|
let community =
|
||||||
|
get_or_fetch_and_upsert_community(&self.object, context, request_counter).await?;
|
||||||
|
let community_follower_form = CommunityFollowerForm {
|
||||||
|
community_id: community.id,
|
||||||
|
person_id: actor.id,
|
||||||
|
pending: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
// This will fail if they're already a follower, but ignore the error.
|
||||||
|
blocking(context.pool(), move |conn| {
|
||||||
|
CommunityFollower::follow(conn, &community_follower_form).ok()
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// TODO: avoid the conversion and pass our own follow struct directly
|
||||||
|
let anybase = AnyBase::from_arbitrary_json(serde_json::to_string(self)?)?;
|
||||||
|
let anybase = Follow::from_any_base(anybase)?.context(location_info!())?;
|
||||||
|
community.send_accept_follow(anybase, context).await
|
||||||
|
}
|
||||||
|
|
||||||
|
fn common(&self) -> &ActivityCommonFields {
|
||||||
|
&self.common
|
||||||
|
}
|
||||||
|
}
|
3
crates/apub_receive/src/activities/following/mod.rs
Normal file
3
crates/apub_receive/src/activities/following/mod.rs
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
pub mod accept;
|
||||||
|
pub mod follow;
|
||||||
|
pub mod undo;
|
67
crates/apub_receive/src/activities/following/undo.rs
Normal file
67
crates/apub_receive/src/activities/following/undo.rs
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
use crate::activities::{following::follow::FollowCommunity, verify_activity, verify_person};
|
||||||
|
use activitystreams::activity::kind::UndoType;
|
||||||
|
use lemmy_api_common::blocking;
|
||||||
|
use lemmy_apub::fetcher::{
|
||||||
|
community::get_or_fetch_and_upsert_community,
|
||||||
|
person::get_or_fetch_and_upsert_person,
|
||||||
|
};
|
||||||
|
use lemmy_apub_lib::{verify_urls_match, ActivityCommonFields, ActivityHandler};
|
||||||
|
use lemmy_db_queries::Followable;
|
||||||
|
use lemmy_db_schema::source::community::{CommunityFollower, CommunityFollowerForm};
|
||||||
|
use lemmy_utils::LemmyError;
|
||||||
|
use lemmy_websocket::LemmyContext;
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct UndoFollowCommunity {
|
||||||
|
to: Url,
|
||||||
|
object: FollowCommunity,
|
||||||
|
#[serde(rename = "type")]
|
||||||
|
kind: UndoType,
|
||||||
|
#[serde(flatten)]
|
||||||
|
common: ActivityCommonFields,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait(?Send)]
|
||||||
|
impl ActivityHandler for UndoFollowCommunity {
|
||||||
|
async fn verify(
|
||||||
|
&self,
|
||||||
|
context: &LemmyContext,
|
||||||
|
request_counter: &mut i32,
|
||||||
|
) -> Result<(), LemmyError> {
|
||||||
|
verify_activity(self.common())?;
|
||||||
|
verify_urls_match(&self.to, &self.object.object)?;
|
||||||
|
verify_urls_match(&self.common.actor, &self.object.common.actor)?;
|
||||||
|
verify_person(&self.common.actor, context, request_counter).await?;
|
||||||
|
self.object.verify(context, request_counter).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn receive(
|
||||||
|
&self,
|
||||||
|
context: &LemmyContext,
|
||||||
|
request_counter: &mut i32,
|
||||||
|
) -> Result<(), LemmyError> {
|
||||||
|
let actor =
|
||||||
|
get_or_fetch_and_upsert_person(&self.common.actor, context, request_counter).await?;
|
||||||
|
let community = get_or_fetch_and_upsert_community(&self.to, context, request_counter).await?;
|
||||||
|
|
||||||
|
let community_follower_form = CommunityFollowerForm {
|
||||||
|
community_id: community.id,
|
||||||
|
person_id: actor.id,
|
||||||
|
pending: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
// This will fail if they aren't a follower, but ignore the error.
|
||||||
|
blocking(context.pool(), move |conn| {
|
||||||
|
CommunityFollower::unfollow(conn, &community_follower_form).ok()
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn common(&self) -> &ActivityCommonFields {
|
||||||
|
&self.common
|
||||||
|
}
|
||||||
|
}
|
|
@ -1 +1,121 @@
|
||||||
pub(crate) mod receive;
|
use anyhow::anyhow;
|
||||||
|
use lemmy_api_common::blocking;
|
||||||
|
use lemmy_apub::{
|
||||||
|
check_community_or_site_ban,
|
||||||
|
check_is_apub_id_valid,
|
||||||
|
fetcher::{community::get_or_fetch_and_upsert_community, person::get_or_fetch_and_upsert_person},
|
||||||
|
generate_moderators_url,
|
||||||
|
};
|
||||||
|
use lemmy_apub_lib::{verify_domains_match, ActivityCommonFields};
|
||||||
|
use lemmy_db_queries::ApubObject;
|
||||||
|
use lemmy_db_schema::{
|
||||||
|
source::{community::Community, person::Person},
|
||||||
|
DbUrl,
|
||||||
|
};
|
||||||
|
use lemmy_db_views_actor::community_view::CommunityView;
|
||||||
|
use lemmy_utils::LemmyError;
|
||||||
|
use lemmy_websocket::LemmyContext;
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
pub mod comment;
|
||||||
|
pub mod community;
|
||||||
|
pub mod deletion;
|
||||||
|
pub mod following;
|
||||||
|
pub mod post;
|
||||||
|
pub mod private_message;
|
||||||
|
pub mod removal;
|
||||||
|
pub mod voting;
|
||||||
|
|
||||||
|
/// Checks that the specified Url actually identifies a Person (by fetching it), and that the person
|
||||||
|
/// doesn't have a site ban.
|
||||||
|
async fn verify_person(
|
||||||
|
person_id: &Url,
|
||||||
|
context: &LemmyContext,
|
||||||
|
request_counter: &mut i32,
|
||||||
|
) -> Result<(), LemmyError> {
|
||||||
|
let person = get_or_fetch_and_upsert_person(person_id, context, request_counter).await?;
|
||||||
|
if person.banned {
|
||||||
|
return Err(anyhow!("Person {} is banned", person_id).into());
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fetches the person and community to verify their type, then checks if person is banned from site
|
||||||
|
/// or community.
|
||||||
|
async fn verify_person_in_community(
|
||||||
|
person_id: &Url,
|
||||||
|
cc: &[Url],
|
||||||
|
context: &LemmyContext,
|
||||||
|
request_counter: &mut i32,
|
||||||
|
) -> Result<Community, LemmyError> {
|
||||||
|
let person = get_or_fetch_and_upsert_person(person_id, context, request_counter).await?;
|
||||||
|
let mut cc_iter = cc.iter();
|
||||||
|
let community: Community = loop {
|
||||||
|
if let Some(cid) = cc_iter.next() {
|
||||||
|
if let Ok(c) = get_or_fetch_and_upsert_community(cid, context, request_counter).await {
|
||||||
|
break c;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return Err(anyhow!("No community found in cc").into());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
check_community_or_site_ban(&person, community.id, context.pool()).await?;
|
||||||
|
Ok(community)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Simply check that the url actually refers to a valid group.
|
||||||
|
async fn verify_community(
|
||||||
|
community_id: &Url,
|
||||||
|
context: &LemmyContext,
|
||||||
|
request_counter: &mut i32,
|
||||||
|
) -> Result<(), LemmyError> {
|
||||||
|
get_or_fetch_and_upsert_community(community_id, context, request_counter).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn verify_activity(common: &ActivityCommonFields) -> Result<(), LemmyError> {
|
||||||
|
check_is_apub_id_valid(&common.actor, false)?;
|
||||||
|
verify_domains_match(common.id_unchecked(), &common.actor)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn verify_mod_action(
|
||||||
|
actor_id: &Url,
|
||||||
|
activity_cc: Url,
|
||||||
|
context: &LemmyContext,
|
||||||
|
) -> Result<(), LemmyError> {
|
||||||
|
let community = blocking(context.pool(), move |conn| {
|
||||||
|
Community::read_from_apub_id(conn, &activity_cc.into())
|
||||||
|
})
|
||||||
|
.await??;
|
||||||
|
|
||||||
|
if community.local {
|
||||||
|
let actor_id: DbUrl = actor_id.clone().into();
|
||||||
|
let actor = blocking(context.pool(), move |conn| {
|
||||||
|
Person::read_from_apub_id(conn, &actor_id)
|
||||||
|
})
|
||||||
|
.await??;
|
||||||
|
|
||||||
|
// Note: this will also return true for admins in addition to mods, but as we dont know about
|
||||||
|
// remote admins, it doesnt make any difference.
|
||||||
|
let community_id = community.id;
|
||||||
|
let actor_id = actor.id;
|
||||||
|
let is_mod_or_admin = blocking(context.pool(), move |conn| {
|
||||||
|
CommunityView::is_mod_or_admin(conn, actor_id, community_id)
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
if !is_mod_or_admin {
|
||||||
|
return Err(anyhow!("Not a mod").into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// For Add/Remove community moderator activities, check that the target field actually contains
|
||||||
|
/// /c/community/moderators. Any different values are unsupported.
|
||||||
|
fn verify_add_remove_moderator_target(target: &Url, community: Url) -> Result<(), LemmyError> {
|
||||||
|
if target != &generate_moderators_url(&community.into())?.into_inner() {
|
||||||
|
return Err(anyhow!("Unkown target url").into());
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
66
crates/apub_receive/src/activities/post/create.rs
Normal file
66
crates/apub_receive/src/activities/post/create.rs
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
use crate::activities::{
|
||||||
|
post::send_websocket_message,
|
||||||
|
verify_activity,
|
||||||
|
verify_person_in_community,
|
||||||
|
};
|
||||||
|
use activitystreams::{activity::kind::CreateType, base::BaseExt};
|
||||||
|
use lemmy_apub::{
|
||||||
|
fetcher::person::get_or_fetch_and_upsert_person,
|
||||||
|
objects::FromApub,
|
||||||
|
ActorType,
|
||||||
|
PageExt,
|
||||||
|
};
|
||||||
|
use lemmy_apub_lib::{verify_domains_match_opt, ActivityCommonFields, ActivityHandler, PublicUrl};
|
||||||
|
use lemmy_db_schema::source::post::Post;
|
||||||
|
use lemmy_utils::LemmyError;
|
||||||
|
use lemmy_websocket::{LemmyContext, UserOperationCrud};
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct CreatePost {
|
||||||
|
to: PublicUrl,
|
||||||
|
object: PageExt,
|
||||||
|
cc: Vec<Url>,
|
||||||
|
#[serde(rename = "type")]
|
||||||
|
kind: CreateType,
|
||||||
|
#[serde(flatten)]
|
||||||
|
common: ActivityCommonFields,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait(?Send)]
|
||||||
|
impl ActivityHandler for CreatePost {
|
||||||
|
async fn verify(
|
||||||
|
&self,
|
||||||
|
context: &LemmyContext,
|
||||||
|
request_counter: &mut i32,
|
||||||
|
) -> Result<(), LemmyError> {
|
||||||
|
verify_activity(self.common())?;
|
||||||
|
verify_person_in_community(&self.common.actor, &self.cc, context, request_counter).await?;
|
||||||
|
verify_domains_match_opt(&self.common.actor, self.object.id_unchecked())?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn receive(
|
||||||
|
&self,
|
||||||
|
context: &LemmyContext,
|
||||||
|
request_counter: &mut i32,
|
||||||
|
) -> Result<(), LemmyError> {
|
||||||
|
let actor =
|
||||||
|
get_or_fetch_and_upsert_person(&self.common.actor, context, request_counter).await?;
|
||||||
|
let post = Post::from_apub(
|
||||||
|
&self.object,
|
||||||
|
context,
|
||||||
|
actor.actor_id(),
|
||||||
|
request_counter,
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
send_websocket_message(post.id, UserOperationCrud::CreatePost, context).await
|
||||||
|
}
|
||||||
|
|
||||||
|
fn common(&self) -> &ActivityCommonFields {
|
||||||
|
&self.common
|
||||||
|
}
|
||||||
|
}
|
31
crates/apub_receive/src/activities/post/mod.rs
Normal file
31
crates/apub_receive/src/activities/post/mod.rs
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
use lemmy_api_common::{blocking, post::PostResponse};
|
||||||
|
use lemmy_db_schema::PostId;
|
||||||
|
use lemmy_db_views::post_view::PostView;
|
||||||
|
use lemmy_utils::LemmyError;
|
||||||
|
use lemmy_websocket::{messages::SendPost, LemmyContext};
|
||||||
|
|
||||||
|
pub mod create;
|
||||||
|
pub mod update;
|
||||||
|
|
||||||
|
pub(crate) async fn send_websocket_message<
|
||||||
|
OP: ToString + Send + lemmy_websocket::OperationType + 'static,
|
||||||
|
>(
|
||||||
|
post_id: PostId,
|
||||||
|
op: OP,
|
||||||
|
context: &LemmyContext,
|
||||||
|
) -> Result<(), LemmyError> {
|
||||||
|
let post_view = blocking(context.pool(), move |conn| {
|
||||||
|
PostView::read(conn, post_id, None)
|
||||||
|
})
|
||||||
|
.await??;
|
||||||
|
|
||||||
|
let res = PostResponse { post_view };
|
||||||
|
|
||||||
|
context.chat_server().do_send(SendPost {
|
||||||
|
op,
|
||||||
|
post: res,
|
||||||
|
websocket_id: None,
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
96
crates/apub_receive/src/activities/post/update.rs
Normal file
96
crates/apub_receive/src/activities/post/update.rs
Normal file
|
@ -0,0 +1,96 @@
|
||||||
|
use crate::activities::{
|
||||||
|
post::send_websocket_message,
|
||||||
|
verify_activity,
|
||||||
|
verify_mod_action,
|
||||||
|
verify_person_in_community,
|
||||||
|
};
|
||||||
|
use activitystreams::{activity::kind::UpdateType, base::BaseExt};
|
||||||
|
use anyhow::Context;
|
||||||
|
use lemmy_api_common::blocking;
|
||||||
|
use lemmy_apub::{
|
||||||
|
objects::{FromApub, FromApubToForm},
|
||||||
|
ActorType,
|
||||||
|
PageExt,
|
||||||
|
};
|
||||||
|
use lemmy_apub_lib::{verify_domains_match_opt, ActivityCommonFields, ActivityHandler, PublicUrl};
|
||||||
|
use lemmy_db_queries::ApubObject;
|
||||||
|
use lemmy_db_schema::{
|
||||||
|
source::post::{Post, PostForm},
|
||||||
|
DbUrl,
|
||||||
|
};
|
||||||
|
use lemmy_utils::{location_info, LemmyError};
|
||||||
|
use lemmy_websocket::{LemmyContext, UserOperationCrud};
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct UpdatePost {
|
||||||
|
to: PublicUrl,
|
||||||
|
object: PageExt,
|
||||||
|
cc: Vec<Url>,
|
||||||
|
#[serde(rename = "type")]
|
||||||
|
kind: UpdateType,
|
||||||
|
#[serde(flatten)]
|
||||||
|
common: ActivityCommonFields,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait(?Send)]
|
||||||
|
impl ActivityHandler for UpdatePost {
|
||||||
|
async fn verify(
|
||||||
|
&self,
|
||||||
|
context: &LemmyContext,
|
||||||
|
request_counter: &mut i32,
|
||||||
|
) -> Result<(), LemmyError> {
|
||||||
|
verify_activity(self.common())?;
|
||||||
|
let community =
|
||||||
|
verify_person_in_community(&self.common.actor, &self.cc, context, request_counter).await?;
|
||||||
|
|
||||||
|
let temp_post = PostForm::from_apub(
|
||||||
|
&self.object,
|
||||||
|
context,
|
||||||
|
self.common.actor.clone(),
|
||||||
|
request_counter,
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
let post_id: DbUrl = temp_post.ap_id.context(location_info!())?;
|
||||||
|
let old_post = blocking(context.pool(), move |conn| {
|
||||||
|
Post::read_from_apub_id(conn, &post_id)
|
||||||
|
})
|
||||||
|
.await??;
|
||||||
|
let stickied = temp_post.stickied.context(location_info!())?;
|
||||||
|
let locked = temp_post.locked.context(location_info!())?;
|
||||||
|
// community mod changed locked/sticky status
|
||||||
|
if (stickied != old_post.stickied) || (locked != old_post.locked) {
|
||||||
|
verify_mod_action(&self.common.actor, community.actor_id(), context).await?;
|
||||||
|
}
|
||||||
|
// user edited their own post
|
||||||
|
else {
|
||||||
|
verify_domains_match_opt(&self.common.actor, self.object.id_unchecked())?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn receive(
|
||||||
|
&self,
|
||||||
|
context: &LemmyContext,
|
||||||
|
request_counter: &mut i32,
|
||||||
|
) -> Result<(), LemmyError> {
|
||||||
|
let post = Post::from_apub(
|
||||||
|
&self.object,
|
||||||
|
context,
|
||||||
|
self.common.actor.clone(),
|
||||||
|
request_counter,
|
||||||
|
// TODO: we already check here if the mod action is valid, can remove that check param
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
send_websocket_message(post.id, UserOperationCrud::EditPost, context).await
|
||||||
|
}
|
||||||
|
|
||||||
|
fn common(&self) -> &ActivityCommonFields {
|
||||||
|
&self.common
|
||||||
|
}
|
||||||
|
}
|
61
crates/apub_receive/src/activities/private_message/create.rs
Normal file
61
crates/apub_receive/src/activities/private_message/create.rs
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
use crate::activities::{private_message::send_websocket_message, verify_activity, verify_person};
|
||||||
|
use activitystreams::{activity::kind::CreateType, base::BaseExt};
|
||||||
|
use lemmy_apub::{objects::FromApub, NoteExt};
|
||||||
|
use lemmy_apub_lib::{verify_domains_match_opt, ActivityCommonFields, ActivityHandler};
|
||||||
|
use lemmy_db_schema::source::private_message::PrivateMessage;
|
||||||
|
use lemmy_utils::LemmyError;
|
||||||
|
use lemmy_websocket::{LemmyContext, UserOperationCrud};
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct CreatePrivateMessage {
|
||||||
|
to: Url,
|
||||||
|
object: NoteExt,
|
||||||
|
#[serde(rename = "type")]
|
||||||
|
kind: CreateType,
|
||||||
|
#[serde(flatten)]
|
||||||
|
common: ActivityCommonFields,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait(?Send)]
|
||||||
|
impl ActivityHandler for CreatePrivateMessage {
|
||||||
|
async fn verify(
|
||||||
|
&self,
|
||||||
|
context: &LemmyContext,
|
||||||
|
request_counter: &mut i32,
|
||||||
|
) -> Result<(), LemmyError> {
|
||||||
|
verify_activity(self.common())?;
|
||||||
|
verify_person(&self.common.actor, context, request_counter).await?;
|
||||||
|
verify_domains_match_opt(&self.common.actor, self.object.id_unchecked())?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn receive(
|
||||||
|
&self,
|
||||||
|
context: &LemmyContext,
|
||||||
|
request_counter: &mut i32,
|
||||||
|
) -> Result<(), LemmyError> {
|
||||||
|
let private_message = PrivateMessage::from_apub(
|
||||||
|
&self.object,
|
||||||
|
context,
|
||||||
|
self.common.actor.clone(),
|
||||||
|
request_counter,
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
send_websocket_message(
|
||||||
|
private_message.id,
|
||||||
|
UserOperationCrud::CreatePrivateMessage,
|
||||||
|
context,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn common(&self) -> &ActivityCommonFields {
|
||||||
|
&self.common
|
||||||
|
}
|
||||||
|
}
|
63
crates/apub_receive/src/activities/private_message/delete.rs
Normal file
63
crates/apub_receive/src/activities/private_message/delete.rs
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
use crate::activities::{private_message::send_websocket_message, verify_activity, verify_person};
|
||||||
|
use activitystreams::activity::kind::DeleteType;
|
||||||
|
use lemmy_api_common::blocking;
|
||||||
|
use lemmy_apub_lib::{verify_domains_match, ActivityCommonFields, ActivityHandler};
|
||||||
|
use lemmy_db_queries::{source::private_message::PrivateMessage_, ApubObject};
|
||||||
|
use lemmy_db_schema::source::private_message::PrivateMessage;
|
||||||
|
use lemmy_utils::LemmyError;
|
||||||
|
use lemmy_websocket::{LemmyContext, UserOperationCrud};
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct DeletePrivateMessage {
|
||||||
|
to: Url,
|
||||||
|
pub(in crate::activities::private_message) object: Url,
|
||||||
|
#[serde(rename = "type")]
|
||||||
|
kind: DeleteType,
|
||||||
|
#[serde(flatten)]
|
||||||
|
pub(in crate::activities::private_message) common: ActivityCommonFields,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait(?Send)]
|
||||||
|
impl ActivityHandler for DeletePrivateMessage {
|
||||||
|
async fn verify(
|
||||||
|
&self,
|
||||||
|
context: &LemmyContext,
|
||||||
|
request_counter: &mut i32,
|
||||||
|
) -> Result<(), LemmyError> {
|
||||||
|
verify_activity(self.common())?;
|
||||||
|
verify_person(&self.common.actor, context, request_counter).await?;
|
||||||
|
verify_domains_match(&self.common.actor, &self.object)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn receive(
|
||||||
|
&self,
|
||||||
|
context: &LemmyContext,
|
||||||
|
_request_counter: &mut i32,
|
||||||
|
) -> Result<(), LemmyError> {
|
||||||
|
let ap_id = self.object.clone();
|
||||||
|
let private_message = blocking(context.pool(), move |conn| {
|
||||||
|
PrivateMessage::read_from_apub_id(conn, &ap_id.into())
|
||||||
|
})
|
||||||
|
.await??;
|
||||||
|
let deleted_private_message = blocking(context.pool(), move |conn| {
|
||||||
|
PrivateMessage::update_deleted(conn, private_message.id, true)
|
||||||
|
})
|
||||||
|
.await??;
|
||||||
|
|
||||||
|
send_websocket_message(
|
||||||
|
deleted_private_message.id,
|
||||||
|
UserOperationCrud::DeletePrivateMessage,
|
||||||
|
context,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn common(&self) -> &ActivityCommonFields {
|
||||||
|
&self.common
|
||||||
|
}
|
||||||
|
}
|
42
crates/apub_receive/src/activities/private_message/mod.rs
Normal file
42
crates/apub_receive/src/activities/private_message/mod.rs
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
use lemmy_api_common::{blocking, person::PrivateMessageResponse};
|
||||||
|
use lemmy_db_schema::PrivateMessageId;
|
||||||
|
use lemmy_db_views::{local_user_view::LocalUserView, private_message_view::PrivateMessageView};
|
||||||
|
use lemmy_utils::LemmyError;
|
||||||
|
use lemmy_websocket::{messages::SendUserRoomMessage, LemmyContext, UserOperationCrud};
|
||||||
|
|
||||||
|
pub mod create;
|
||||||
|
pub mod delete;
|
||||||
|
pub mod undo_delete;
|
||||||
|
pub mod update;
|
||||||
|
|
||||||
|
async fn send_websocket_message(
|
||||||
|
private_message_id: PrivateMessageId,
|
||||||
|
op: UserOperationCrud,
|
||||||
|
context: &LemmyContext,
|
||||||
|
) -> Result<(), LemmyError> {
|
||||||
|
let message = blocking(context.pool(), move |conn| {
|
||||||
|
PrivateMessageView::read(conn, private_message_id)
|
||||||
|
})
|
||||||
|
.await??;
|
||||||
|
let res = PrivateMessageResponse {
|
||||||
|
private_message_view: message,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Send notifications to the local recipient, if one exists
|
||||||
|
let recipient_id = res.private_message_view.recipient.id;
|
||||||
|
let local_recipient_id = blocking(context.pool(), move |conn| {
|
||||||
|
LocalUserView::read_person(conn, recipient_id)
|
||||||
|
})
|
||||||
|
.await??
|
||||||
|
.local_user
|
||||||
|
.id;
|
||||||
|
|
||||||
|
context.chat_server().do_send(SendUserRoomMessage {
|
||||||
|
op,
|
||||||
|
response: res,
|
||||||
|
local_recipient_id,
|
||||||
|
websocket_id: None,
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
|
@ -0,0 +1,75 @@
|
||||||
|
use crate::activities::{
|
||||||
|
private_message::{delete::DeletePrivateMessage, send_websocket_message},
|
||||||
|
verify_activity,
|
||||||
|
verify_person,
|
||||||
|
};
|
||||||
|
use activitystreams::activity::kind::UndoType;
|
||||||
|
use lemmy_api_common::blocking;
|
||||||
|
use lemmy_apub_lib::{
|
||||||
|
verify_domains_match,
|
||||||
|
verify_urls_match,
|
||||||
|
ActivityCommonFields,
|
||||||
|
ActivityHandler,
|
||||||
|
};
|
||||||
|
use lemmy_db_queries::{source::private_message::PrivateMessage_, ApubObject};
|
||||||
|
use lemmy_db_schema::source::private_message::PrivateMessage;
|
||||||
|
use lemmy_utils::LemmyError;
|
||||||
|
use lemmy_websocket::{LemmyContext, UserOperationCrud};
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct UndoDeletePrivateMessage {
|
||||||
|
to: Url,
|
||||||
|
object: DeletePrivateMessage,
|
||||||
|
#[serde(rename = "type")]
|
||||||
|
kind: UndoType,
|
||||||
|
#[serde(flatten)]
|
||||||
|
common: ActivityCommonFields,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait(?Send)]
|
||||||
|
impl ActivityHandler for UndoDeletePrivateMessage {
|
||||||
|
async fn verify(
|
||||||
|
&self,
|
||||||
|
context: &LemmyContext,
|
||||||
|
request_counter: &mut i32,
|
||||||
|
) -> Result<(), LemmyError> {
|
||||||
|
verify_activity(self.common())?;
|
||||||
|
verify_person(&self.common.actor, context, request_counter).await?;
|
||||||
|
verify_urls_match(&self.common.actor, &self.object.common.actor)?;
|
||||||
|
verify_domains_match(&self.common.actor, &self.object.object)?;
|
||||||
|
self.object.verify(context, request_counter).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn receive(
|
||||||
|
&self,
|
||||||
|
context: &LemmyContext,
|
||||||
|
_request_counter: &mut i32,
|
||||||
|
) -> Result<(), LemmyError> {
|
||||||
|
let ap_id = self.object.object.clone();
|
||||||
|
let private_message = blocking(context.pool(), move |conn| {
|
||||||
|
PrivateMessage::read_from_apub_id(conn, &ap_id.into())
|
||||||
|
})
|
||||||
|
.await??;
|
||||||
|
|
||||||
|
let deleted_private_message = blocking(context.pool(), move |conn| {
|
||||||
|
PrivateMessage::update_deleted(conn, private_message.id, false)
|
||||||
|
})
|
||||||
|
.await??;
|
||||||
|
|
||||||
|
send_websocket_message(
|
||||||
|
deleted_private_message.id,
|
||||||
|
UserOperationCrud::EditPrivateMessage,
|
||||||
|
context,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn common(&self) -> &ActivityCommonFields {
|
||||||
|
&self.common
|
||||||
|
}
|
||||||
|
}
|
61
crates/apub_receive/src/activities/private_message/update.rs
Normal file
61
crates/apub_receive/src/activities/private_message/update.rs
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
use crate::activities::{private_message::send_websocket_message, verify_activity, verify_person};
|
||||||
|
use activitystreams::{activity::kind::UpdateType, base::BaseExt};
|
||||||
|
use lemmy_apub::{objects::FromApub, NoteExt};
|
||||||
|
use lemmy_apub_lib::{verify_domains_match_opt, ActivityCommonFields, ActivityHandler};
|
||||||
|
use lemmy_db_schema::source::private_message::PrivateMessage;
|
||||||
|
use lemmy_utils::LemmyError;
|
||||||
|
use lemmy_websocket::{LemmyContext, UserOperationCrud};
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct UpdatePrivateMessage {
|
||||||
|
to: Url,
|
||||||
|
object: NoteExt,
|
||||||
|
#[serde(rename = "type")]
|
||||||
|
kind: UpdateType,
|
||||||
|
#[serde(flatten)]
|
||||||
|
common: ActivityCommonFields,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait(?Send)]
|
||||||
|
impl ActivityHandler for UpdatePrivateMessage {
|
||||||
|
async fn verify(
|
||||||
|
&self,
|
||||||
|
context: &LemmyContext,
|
||||||
|
request_counter: &mut i32,
|
||||||
|
) -> Result<(), LemmyError> {
|
||||||
|
verify_activity(self.common())?;
|
||||||
|
verify_person(&self.common.actor, context, request_counter).await?;
|
||||||
|
verify_domains_match_opt(&self.common.actor, self.object.id_unchecked())?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn receive(
|
||||||
|
&self,
|
||||||
|
context: &LemmyContext,
|
||||||
|
request_counter: &mut i32,
|
||||||
|
) -> Result<(), LemmyError> {
|
||||||
|
let private_message = PrivateMessage::from_apub(
|
||||||
|
&self.object,
|
||||||
|
context,
|
||||||
|
self.common.actor.clone(),
|
||||||
|
request_counter,
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
send_websocket_message(
|
||||||
|
private_message.id,
|
||||||
|
UserOperationCrud::EditPrivateMessage,
|
||||||
|
context,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn common(&self) -> &ActivityCommonFields {
|
||||||
|
&self.common
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,262 +0,0 @@
|
||||||
use crate::activities::receive::get_actor_as_person;
|
|
||||||
use activitystreams::{
|
|
||||||
activity::{ActorAndObjectRefExt, Create, Dislike, Like, Update},
|
|
||||||
base::ExtendsExt,
|
|
||||||
};
|
|
||||||
use anyhow::Context;
|
|
||||||
use lemmy_api_common::{blocking, comment::CommentResponse, send_local_notifs};
|
|
||||||
use lemmy_apub::{objects::FromApub, ActorType, NoteExt};
|
|
||||||
use lemmy_db_queries::{source::comment::Comment_, Crud, Likeable};
|
|
||||||
use lemmy_db_schema::source::{
|
|
||||||
comment::{Comment, CommentLike, CommentLikeForm},
|
|
||||||
post::Post,
|
|
||||||
};
|
|
||||||
use lemmy_db_views::comment_view::CommentView;
|
|
||||||
use lemmy_utils::{location_info, utils::scrape_text_for_mentions, LemmyError};
|
|
||||||
use lemmy_websocket::{messages::SendComment, LemmyContext, UserOperation, UserOperationCrud};
|
|
||||||
|
|
||||||
pub(crate) async fn receive_create_comment(
|
|
||||||
create: Create,
|
|
||||||
context: &LemmyContext,
|
|
||||||
request_counter: &mut i32,
|
|
||||||
) -> Result<(), LemmyError> {
|
|
||||||
let person = get_actor_as_person(&create, context, request_counter).await?;
|
|
||||||
let note = NoteExt::from_any_base(create.object().to_owned().one().context(location_info!())?)?
|
|
||||||
.context(location_info!())?;
|
|
||||||
|
|
||||||
let comment =
|
|
||||||
Comment::from_apub(¬e, context, person.actor_id(), request_counter, false).await?;
|
|
||||||
|
|
||||||
let post_id = comment.post_id;
|
|
||||||
let post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??;
|
|
||||||
|
|
||||||
// Note:
|
|
||||||
// Although mentions could be gotten from the post tags (they are included there), or the ccs,
|
|
||||||
// Its much easier to scrape them from the comment body, since the API has to do that
|
|
||||||
// anyway.
|
|
||||||
let mentions = scrape_text_for_mentions(&comment.content);
|
|
||||||
let recipient_ids = send_local_notifs(
|
|
||||||
mentions,
|
|
||||||
comment.clone(),
|
|
||||||
person,
|
|
||||||
post,
|
|
||||||
context.pool(),
|
|
||||||
true,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
// Refetch the view
|
|
||||||
let comment_view = blocking(context.pool(), move |conn| {
|
|
||||||
CommentView::read(conn, comment.id, None)
|
|
||||||
})
|
|
||||||
.await??;
|
|
||||||
|
|
||||||
let res = CommentResponse {
|
|
||||||
comment_view,
|
|
||||||
recipient_ids,
|
|
||||||
form_id: None,
|
|
||||||
};
|
|
||||||
|
|
||||||
context.chat_server().do_send(SendComment {
|
|
||||||
op: UserOperationCrud::CreateComment,
|
|
||||||
comment: res,
|
|
||||||
websocket_id: None,
|
|
||||||
});
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) async fn receive_update_comment(
|
|
||||||
update: Update,
|
|
||||||
context: &LemmyContext,
|
|
||||||
request_counter: &mut i32,
|
|
||||||
) -> Result<(), LemmyError> {
|
|
||||||
let note = NoteExt::from_any_base(update.object().to_owned().one().context(location_info!())?)?
|
|
||||||
.context(location_info!())?;
|
|
||||||
let person = get_actor_as_person(&update, context, request_counter).await?;
|
|
||||||
|
|
||||||
let comment =
|
|
||||||
Comment::from_apub(¬e, context, person.actor_id(), request_counter, false).await?;
|
|
||||||
|
|
||||||
let comment_id = comment.id;
|
|
||||||
let post_id = comment.post_id;
|
|
||||||
let post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??;
|
|
||||||
|
|
||||||
let mentions = scrape_text_for_mentions(&comment.content);
|
|
||||||
let recipient_ids =
|
|
||||||
send_local_notifs(mentions, comment, person, post, context.pool(), false).await?;
|
|
||||||
|
|
||||||
// Refetch the view
|
|
||||||
let comment_view = blocking(context.pool(), move |conn| {
|
|
||||||
CommentView::read(conn, comment_id, None)
|
|
||||||
})
|
|
||||||
.await??;
|
|
||||||
|
|
||||||
let res = CommentResponse {
|
|
||||||
comment_view,
|
|
||||||
recipient_ids,
|
|
||||||
form_id: None,
|
|
||||||
};
|
|
||||||
|
|
||||||
context.chat_server().do_send(SendComment {
|
|
||||||
op: UserOperationCrud::EditComment,
|
|
||||||
comment: res,
|
|
||||||
websocket_id: None,
|
|
||||||
});
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) async fn receive_like_comment(
|
|
||||||
like: Like,
|
|
||||||
comment: Comment,
|
|
||||||
context: &LemmyContext,
|
|
||||||
request_counter: &mut i32,
|
|
||||||
) -> Result<(), LemmyError> {
|
|
||||||
let person = get_actor_as_person(&like, context, request_counter).await?;
|
|
||||||
|
|
||||||
let comment_id = comment.id;
|
|
||||||
let like_form = CommentLikeForm {
|
|
||||||
comment_id,
|
|
||||||
post_id: comment.post_id,
|
|
||||||
person_id: person.id,
|
|
||||||
score: 1,
|
|
||||||
};
|
|
||||||
let person_id = person.id;
|
|
||||||
blocking(context.pool(), move |conn| {
|
|
||||||
CommentLike::remove(conn, person_id, comment_id)?;
|
|
||||||
CommentLike::like(conn, &like_form)
|
|
||||||
})
|
|
||||||
.await??;
|
|
||||||
|
|
||||||
// Refetch the view
|
|
||||||
let comment_view = blocking(context.pool(), move |conn| {
|
|
||||||
CommentView::read(conn, comment_id, None)
|
|
||||||
})
|
|
||||||
.await??;
|
|
||||||
|
|
||||||
// TODO get those recipient actor ids from somewhere
|
|
||||||
let recipient_ids = vec![];
|
|
||||||
let res = CommentResponse {
|
|
||||||
comment_view,
|
|
||||||
recipient_ids,
|
|
||||||
form_id: None,
|
|
||||||
};
|
|
||||||
|
|
||||||
context.chat_server().do_send(SendComment {
|
|
||||||
op: UserOperation::CreateCommentLike,
|
|
||||||
comment: res,
|
|
||||||
websocket_id: None,
|
|
||||||
});
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) async fn receive_dislike_comment(
|
|
||||||
dislike: Dislike,
|
|
||||||
comment: Comment,
|
|
||||||
context: &LemmyContext,
|
|
||||||
request_counter: &mut i32,
|
|
||||||
) -> Result<(), LemmyError> {
|
|
||||||
let person = get_actor_as_person(&dislike, context, request_counter).await?;
|
|
||||||
|
|
||||||
let comment_id = comment.id;
|
|
||||||
let like_form = CommentLikeForm {
|
|
||||||
comment_id,
|
|
||||||
post_id: comment.post_id,
|
|
||||||
person_id: person.id,
|
|
||||||
score: -1,
|
|
||||||
};
|
|
||||||
let person_id = person.id;
|
|
||||||
blocking(context.pool(), move |conn| {
|
|
||||||
CommentLike::remove(conn, person_id, comment_id)?;
|
|
||||||
CommentLike::like(conn, &like_form)
|
|
||||||
})
|
|
||||||
.await??;
|
|
||||||
|
|
||||||
// Refetch the view
|
|
||||||
let comment_view = blocking(context.pool(), move |conn| {
|
|
||||||
CommentView::read(conn, comment_id, None)
|
|
||||||
})
|
|
||||||
.await??;
|
|
||||||
|
|
||||||
// TODO get those recipient actor ids from somewhere
|
|
||||||
let recipient_ids = vec![];
|
|
||||||
let res = CommentResponse {
|
|
||||||
comment_view,
|
|
||||||
recipient_ids,
|
|
||||||
form_id: None,
|
|
||||||
};
|
|
||||||
|
|
||||||
context.chat_server().do_send(SendComment {
|
|
||||||
op: UserOperation::CreateCommentLike,
|
|
||||||
comment: res,
|
|
||||||
websocket_id: None,
|
|
||||||
});
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) async fn receive_delete_comment(
|
|
||||||
context: &LemmyContext,
|
|
||||||
comment: Comment,
|
|
||||||
) -> Result<(), LemmyError> {
|
|
||||||
let deleted_comment = blocking(context.pool(), move |conn| {
|
|
||||||
Comment::update_deleted(conn, comment.id, true)
|
|
||||||
})
|
|
||||||
.await??;
|
|
||||||
|
|
||||||
// Refetch the view
|
|
||||||
let comment_id = deleted_comment.id;
|
|
||||||
let comment_view = blocking(context.pool(), move |conn| {
|
|
||||||
CommentView::read(conn, comment_id, None)
|
|
||||||
})
|
|
||||||
.await??;
|
|
||||||
|
|
||||||
// TODO get those recipient actor ids from somewhere
|
|
||||||
let recipient_ids = vec![];
|
|
||||||
let res = CommentResponse {
|
|
||||||
comment_view,
|
|
||||||
recipient_ids,
|
|
||||||
form_id: None,
|
|
||||||
};
|
|
||||||
context.chat_server().do_send(SendComment {
|
|
||||||
op: UserOperationCrud::EditComment,
|
|
||||||
comment: res,
|
|
||||||
websocket_id: None,
|
|
||||||
});
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) async fn receive_remove_comment(
|
|
||||||
context: &LemmyContext,
|
|
||||||
comment: Comment,
|
|
||||||
) -> Result<(), LemmyError> {
|
|
||||||
let removed_comment = blocking(context.pool(), move |conn| {
|
|
||||||
Comment::update_removed(conn, comment.id, true)
|
|
||||||
})
|
|
||||||
.await??;
|
|
||||||
|
|
||||||
// Refetch the view
|
|
||||||
let comment_id = removed_comment.id;
|
|
||||||
let comment_view = blocking(context.pool(), move |conn| {
|
|
||||||
CommentView::read(conn, comment_id, None)
|
|
||||||
})
|
|
||||||
.await??;
|
|
||||||
|
|
||||||
// TODO get those recipient actor ids from somewhere
|
|
||||||
let recipient_ids = vec![];
|
|
||||||
let res = CommentResponse {
|
|
||||||
comment_view,
|
|
||||||
recipient_ids,
|
|
||||||
form_id: None,
|
|
||||||
};
|
|
||||||
context.chat_server().do_send(SendComment {
|
|
||||||
op: UserOperationCrud::EditComment,
|
|
||||||
comment: res,
|
|
||||||
websocket_id: None,
|
|
||||||
});
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
|
@ -1,150 +0,0 @@
|
||||||
use crate::activities::receive::get_actor_as_person;
|
|
||||||
use activitystreams::activity::{Dislike, Like};
|
|
||||||
use lemmy_api_common::{blocking, comment::CommentResponse};
|
|
||||||
use lemmy_db_queries::{source::comment::Comment_, Likeable};
|
|
||||||
use lemmy_db_schema::source::comment::{Comment, CommentLike};
|
|
||||||
use lemmy_db_views::comment_view::CommentView;
|
|
||||||
use lemmy_utils::LemmyError;
|
|
||||||
use lemmy_websocket::{messages::SendComment, LemmyContext, UserOperation, UserOperationCrud};
|
|
||||||
|
|
||||||
pub(crate) async fn receive_undo_like_comment(
|
|
||||||
like: &Like,
|
|
||||||
comment: Comment,
|
|
||||||
context: &LemmyContext,
|
|
||||||
request_counter: &mut i32,
|
|
||||||
) -> Result<(), LemmyError> {
|
|
||||||
let person = get_actor_as_person(like, context, request_counter).await?;
|
|
||||||
|
|
||||||
let comment_id = comment.id;
|
|
||||||
let person_id = person.id;
|
|
||||||
blocking(context.pool(), move |conn| {
|
|
||||||
CommentLike::remove(conn, person_id, comment_id)
|
|
||||||
})
|
|
||||||
.await??;
|
|
||||||
|
|
||||||
// Refetch the view
|
|
||||||
let comment_view = blocking(context.pool(), move |conn| {
|
|
||||||
CommentView::read(conn, comment_id, None)
|
|
||||||
})
|
|
||||||
.await??;
|
|
||||||
|
|
||||||
// TODO get those recipient actor ids from somewhere
|
|
||||||
let recipient_ids = vec![];
|
|
||||||
let res = CommentResponse {
|
|
||||||
comment_view,
|
|
||||||
recipient_ids,
|
|
||||||
form_id: None,
|
|
||||||
};
|
|
||||||
|
|
||||||
context.chat_server().do_send(SendComment {
|
|
||||||
op: UserOperation::CreateCommentLike,
|
|
||||||
comment: res,
|
|
||||||
websocket_id: None,
|
|
||||||
});
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) async fn receive_undo_dislike_comment(
|
|
||||||
dislike: &Dislike,
|
|
||||||
comment: Comment,
|
|
||||||
context: &LemmyContext,
|
|
||||||
request_counter: &mut i32,
|
|
||||||
) -> Result<(), LemmyError> {
|
|
||||||
let person = get_actor_as_person(dislike, context, request_counter).await?;
|
|
||||||
|
|
||||||
let comment_id = comment.id;
|
|
||||||
let person_id = person.id;
|
|
||||||
blocking(context.pool(), move |conn| {
|
|
||||||
CommentLike::remove(conn, person_id, comment_id)
|
|
||||||
})
|
|
||||||
.await??;
|
|
||||||
|
|
||||||
// Refetch the view
|
|
||||||
let comment_view = blocking(context.pool(), move |conn| {
|
|
||||||
CommentView::read(conn, comment_id, None)
|
|
||||||
})
|
|
||||||
.await??;
|
|
||||||
|
|
||||||
// TODO get those recipient actor ids from somewhere
|
|
||||||
let recipient_ids = vec![];
|
|
||||||
let res = CommentResponse {
|
|
||||||
comment_view,
|
|
||||||
recipient_ids,
|
|
||||||
form_id: None,
|
|
||||||
};
|
|
||||||
|
|
||||||
context.chat_server().do_send(SendComment {
|
|
||||||
op: UserOperation::CreateCommentLike,
|
|
||||||
comment: res,
|
|
||||||
websocket_id: None,
|
|
||||||
});
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) async fn receive_undo_delete_comment(
|
|
||||||
context: &LemmyContext,
|
|
||||||
comment: Comment,
|
|
||||||
) -> Result<(), LemmyError> {
|
|
||||||
let deleted_comment = blocking(context.pool(), move |conn| {
|
|
||||||
Comment::update_deleted(conn, comment.id, false)
|
|
||||||
})
|
|
||||||
.await??;
|
|
||||||
|
|
||||||
// Refetch the view
|
|
||||||
let comment_id = deleted_comment.id;
|
|
||||||
let comment_view = blocking(context.pool(), move |conn| {
|
|
||||||
CommentView::read(conn, comment_id, None)
|
|
||||||
})
|
|
||||||
.await??;
|
|
||||||
|
|
||||||
// TODO get those recipient actor ids from somewhere
|
|
||||||
let recipient_ids = vec![];
|
|
||||||
let res = CommentResponse {
|
|
||||||
comment_view,
|
|
||||||
recipient_ids,
|
|
||||||
form_id: None,
|
|
||||||
};
|
|
||||||
|
|
||||||
context.chat_server().do_send(SendComment {
|
|
||||||
op: UserOperationCrud::EditComment,
|
|
||||||
comment: res,
|
|
||||||
websocket_id: None,
|
|
||||||
});
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) async fn receive_undo_remove_comment(
|
|
||||||
context: &LemmyContext,
|
|
||||||
comment: Comment,
|
|
||||||
) -> Result<(), LemmyError> {
|
|
||||||
let removed_comment = blocking(context.pool(), move |conn| {
|
|
||||||
Comment::update_removed(conn, comment.id, false)
|
|
||||||
})
|
|
||||||
.await??;
|
|
||||||
|
|
||||||
// Refetch the view
|
|
||||||
let comment_id = removed_comment.id;
|
|
||||||
let comment_view = blocking(context.pool(), move |conn| {
|
|
||||||
CommentView::read(conn, comment_id, None)
|
|
||||||
})
|
|
||||||
.await??;
|
|
||||||
|
|
||||||
// TODO get those recipient actor ids from somewhere
|
|
||||||
let recipient_ids = vec![];
|
|
||||||
let res = CommentResponse {
|
|
||||||
comment_view,
|
|
||||||
recipient_ids,
|
|
||||||
form_id: None,
|
|
||||||
};
|
|
||||||
|
|
||||||
context.chat_server().do_send(SendComment {
|
|
||||||
op: UserOperationCrud::EditComment,
|
|
||||||
comment: res,
|
|
||||||
websocket_id: None,
|
|
||||||
});
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
|
@ -1,232 +0,0 @@
|
||||||
use crate::{
|
|
||||||
activities::receive::get_actor_as_person,
|
|
||||||
inbox::receive_for_community::verify_actor_is_community_mod,
|
|
||||||
};
|
|
||||||
use activitystreams::{
|
|
||||||
activity::{ActorAndObjectRefExt, Delete, Undo, Update},
|
|
||||||
base::ExtendsExt,
|
|
||||||
};
|
|
||||||
use anyhow::{anyhow, Context};
|
|
||||||
use lemmy_api_common::{blocking, community::CommunityResponse};
|
|
||||||
use lemmy_apub::{
|
|
||||||
get_community_from_to_or_cc,
|
|
||||||
objects::FromApubToForm,
|
|
||||||
ActorType,
|
|
||||||
CommunityType,
|
|
||||||
GroupExt,
|
|
||||||
};
|
|
||||||
use lemmy_db_queries::{source::community::Community_, Crud};
|
|
||||||
use lemmy_db_schema::source::{
|
|
||||||
community::{Community, CommunityForm},
|
|
||||||
person::Person,
|
|
||||||
};
|
|
||||||
use lemmy_db_views_actor::{
|
|
||||||
community_moderator_view::CommunityModeratorView,
|
|
||||||
community_view::CommunityView,
|
|
||||||
};
|
|
||||||
use lemmy_utils::{location_info, LemmyError};
|
|
||||||
use lemmy_websocket::{messages::SendCommunityRoomMessage, LemmyContext, UserOperationCrud};
|
|
||||||
|
|
||||||
/// This activity is received from a remote community mod, and updates the description or other
|
|
||||||
/// fields of a local community.
|
|
||||||
pub(crate) async fn receive_remote_mod_update_community(
|
|
||||||
update: Update,
|
|
||||||
context: &LemmyContext,
|
|
||||||
request_counter: &mut i32,
|
|
||||||
) -> Result<(), LemmyError> {
|
|
||||||
let community = get_community_from_to_or_cc(&update, context, request_counter).await?;
|
|
||||||
verify_actor_is_community_mod(&update, &community, context).await?;
|
|
||||||
let group = GroupExt::from_any_base(update.object().to_owned().one().context(location_info!())?)?
|
|
||||||
.context(location_info!())?;
|
|
||||||
let updated_community = CommunityForm::from_apub(
|
|
||||||
&group,
|
|
||||||
context,
|
|
||||||
community.actor_id(),
|
|
||||||
request_counter,
|
|
||||||
false,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
let cf = CommunityForm {
|
|
||||||
name: updated_community.name,
|
|
||||||
title: updated_community.title,
|
|
||||||
description: updated_community.description,
|
|
||||||
nsfw: updated_community.nsfw,
|
|
||||||
// TODO: icon and banner would be hosted on the other instance, ideally we would copy it to ours
|
|
||||||
icon: updated_community.icon,
|
|
||||||
banner: updated_community.banner,
|
|
||||||
..CommunityForm::default()
|
|
||||||
};
|
|
||||||
blocking(context.pool(), move |conn| {
|
|
||||||
Community::update(conn, community.id, &cf)
|
|
||||||
})
|
|
||||||
.await??;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) async fn receive_remote_mod_delete_community(
|
|
||||||
delete: Delete,
|
|
||||||
community: Community,
|
|
||||||
context: &LemmyContext,
|
|
||||||
request_counter: &mut i32,
|
|
||||||
) -> Result<(), LemmyError> {
|
|
||||||
verify_actor_is_community_mod(&delete, &community, context).await?;
|
|
||||||
let actor = get_actor_as_person(&delete, context, request_counter).await?;
|
|
||||||
verify_is_remote_community_creator(&actor, &community, context).await?;
|
|
||||||
let community_id = community.id;
|
|
||||||
blocking(context.pool(), move |conn| {
|
|
||||||
Community::update_deleted(conn, community_id, true)
|
|
||||||
})
|
|
||||||
.await??;
|
|
||||||
community.send_delete(actor, context).await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) async fn receive_delete_community(
|
|
||||||
context: &LemmyContext,
|
|
||||||
community: Community,
|
|
||||||
) -> Result<(), LemmyError> {
|
|
||||||
let deleted_community = blocking(context.pool(), move |conn| {
|
|
||||||
Community::update_deleted(conn, community.id, true)
|
|
||||||
})
|
|
||||||
.await??;
|
|
||||||
|
|
||||||
let community_id = deleted_community.id;
|
|
||||||
let res = CommunityResponse {
|
|
||||||
community_view: blocking(context.pool(), move |conn| {
|
|
||||||
CommunityView::read(conn, community_id, None)
|
|
||||||
})
|
|
||||||
.await??,
|
|
||||||
};
|
|
||||||
|
|
||||||
let community_id = res.community_view.community.id;
|
|
||||||
context.chat_server().do_send(SendCommunityRoomMessage {
|
|
||||||
op: UserOperationCrud::EditCommunity,
|
|
||||||
response: res,
|
|
||||||
community_id,
|
|
||||||
websocket_id: None,
|
|
||||||
});
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) async fn receive_remove_community(
|
|
||||||
context: &LemmyContext,
|
|
||||||
community: Community,
|
|
||||||
) -> Result<(), LemmyError> {
|
|
||||||
let removed_community = blocking(context.pool(), move |conn| {
|
|
||||||
Community::update_removed(conn, community.id, true)
|
|
||||||
})
|
|
||||||
.await??;
|
|
||||||
|
|
||||||
let community_id = removed_community.id;
|
|
||||||
let res = CommunityResponse {
|
|
||||||
community_view: blocking(context.pool(), move |conn| {
|
|
||||||
CommunityView::read(conn, community_id, None)
|
|
||||||
})
|
|
||||||
.await??,
|
|
||||||
};
|
|
||||||
|
|
||||||
let community_id = res.community_view.community.id;
|
|
||||||
context.chat_server().do_send(SendCommunityRoomMessage {
|
|
||||||
op: UserOperationCrud::EditCommunity,
|
|
||||||
response: res,
|
|
||||||
community_id,
|
|
||||||
websocket_id: None,
|
|
||||||
});
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) async fn receive_remote_mod_undo_delete_community(
|
|
||||||
undo: Undo,
|
|
||||||
community: Community,
|
|
||||||
context: &LemmyContext,
|
|
||||||
request_counter: &mut i32,
|
|
||||||
) -> Result<(), LemmyError> {
|
|
||||||
verify_actor_is_community_mod(&undo, &community, context).await?;
|
|
||||||
let actor = get_actor_as_person(&undo, context, request_counter).await?;
|
|
||||||
verify_is_remote_community_creator(&actor, &community, context).await?;
|
|
||||||
let community_id = community.id;
|
|
||||||
blocking(context.pool(), move |conn| {
|
|
||||||
Community::update_deleted(conn, community_id, false)
|
|
||||||
})
|
|
||||||
.await??;
|
|
||||||
community.send_undo_delete(actor, context).await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) async fn receive_undo_delete_community(
|
|
||||||
context: &LemmyContext,
|
|
||||||
community: Community,
|
|
||||||
) -> Result<(), LemmyError> {
|
|
||||||
let deleted_community = blocking(context.pool(), move |conn| {
|
|
||||||
Community::update_deleted(conn, community.id, false)
|
|
||||||
})
|
|
||||||
.await??;
|
|
||||||
|
|
||||||
let community_id = deleted_community.id;
|
|
||||||
let res = CommunityResponse {
|
|
||||||
community_view: blocking(context.pool(), move |conn| {
|
|
||||||
CommunityView::read(conn, community_id, None)
|
|
||||||
})
|
|
||||||
.await??,
|
|
||||||
};
|
|
||||||
|
|
||||||
let community_id = res.community_view.community.id;
|
|
||||||
context.chat_server().do_send(SendCommunityRoomMessage {
|
|
||||||
op: UserOperationCrud::EditCommunity,
|
|
||||||
response: res,
|
|
||||||
community_id,
|
|
||||||
websocket_id: None,
|
|
||||||
});
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) async fn receive_undo_remove_community(
|
|
||||||
context: &LemmyContext,
|
|
||||||
community: Community,
|
|
||||||
) -> Result<(), LemmyError> {
|
|
||||||
let removed_community = blocking(context.pool(), move |conn| {
|
|
||||||
Community::update_removed(conn, community.id, false)
|
|
||||||
})
|
|
||||||
.await??;
|
|
||||||
|
|
||||||
let community_id = removed_community.id;
|
|
||||||
let res = CommunityResponse {
|
|
||||||
community_view: blocking(context.pool(), move |conn| {
|
|
||||||
CommunityView::read(conn, community_id, None)
|
|
||||||
})
|
|
||||||
.await??,
|
|
||||||
};
|
|
||||||
|
|
||||||
let community_id = res.community_view.community.id;
|
|
||||||
|
|
||||||
context.chat_server().do_send(SendCommunityRoomMessage {
|
|
||||||
op: UserOperationCrud::EditCommunity,
|
|
||||||
response: res,
|
|
||||||
community_id,
|
|
||||||
websocket_id: None,
|
|
||||||
});
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Checks if the remote user is creator of the local community. This can only happen if a community
|
|
||||||
/// is created by a local user, and then transferred to a remote user.
|
|
||||||
async fn verify_is_remote_community_creator(
|
|
||||||
user: &Person,
|
|
||||||
community: &Community,
|
|
||||||
context: &LemmyContext,
|
|
||||||
) -> Result<(), LemmyError> {
|
|
||||||
let community_id = community.id;
|
|
||||||
let community_mods = blocking(context.pool(), move |conn| {
|
|
||||||
CommunityModeratorView::for_community(conn, community_id)
|
|
||||||
})
|
|
||||||
.await??;
|
|
||||||
|
|
||||||
if user.id != community_mods[0].moderator.id {
|
|
||||||
Err(anyhow!("Actor is not community creator").into())
|
|
||||||
} else {
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,81 +0,0 @@
|
||||||
use activitystreams::{
|
|
||||||
activity::{ActorAndObjectRef, ActorAndObjectRefExt},
|
|
||||||
base::{AsBase, BaseExt},
|
|
||||||
error::DomainError,
|
|
||||||
};
|
|
||||||
use anyhow::{anyhow, Context};
|
|
||||||
use lemmy_apub::fetcher::person::get_or_fetch_and_upsert_person;
|
|
||||||
use lemmy_db_schema::source::person::Person;
|
|
||||||
use lemmy_utils::{location_info, LemmyError};
|
|
||||||
use lemmy_websocket::LemmyContext;
|
|
||||||
use log::debug;
|
|
||||||
use std::fmt::Debug;
|
|
||||||
use url::Url;
|
|
||||||
|
|
||||||
pub(crate) mod comment;
|
|
||||||
pub(crate) mod comment_undo;
|
|
||||||
pub(crate) mod community;
|
|
||||||
pub(crate) mod post;
|
|
||||||
pub(crate) mod post_undo;
|
|
||||||
pub(crate) mod private_message;
|
|
||||||
|
|
||||||
/// Return HTTP 501 for unsupported activities in inbox.
|
|
||||||
pub(crate) fn receive_unhandled_activity<A>(activity: A) -> Result<(), LemmyError>
|
|
||||||
where
|
|
||||||
A: Debug,
|
|
||||||
{
|
|
||||||
debug!("received unhandled activity type: {:?}", activity);
|
|
||||||
Err(anyhow!("Activity not supported").into())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Reads the actor field of an activity and returns the corresponding `Person`.
|
|
||||||
pub(crate) async fn get_actor_as_person<T, A>(
|
|
||||||
activity: &T,
|
|
||||||
context: &LemmyContext,
|
|
||||||
request_counter: &mut i32,
|
|
||||||
) -> Result<Person, LemmyError>
|
|
||||||
where
|
|
||||||
T: AsBase<A> + ActorAndObjectRef,
|
|
||||||
{
|
|
||||||
let actor = activity.actor()?;
|
|
||||||
let person_uri = actor.as_single_xsd_any_uri().context(location_info!())?;
|
|
||||||
get_or_fetch_and_upsert_person(person_uri, context, request_counter).await
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Ensure that the ID of an incoming activity comes from the same domain as the actor. Optionally
|
|
||||||
/// also checks the ID of the inner object.
|
|
||||||
///
|
|
||||||
/// The reason that this starts with the actor ID is that it was already confirmed as correct by the
|
|
||||||
/// HTTP signature.
|
|
||||||
pub(crate) fn verify_activity_domains_valid<T, Kind>(
|
|
||||||
activity: &T,
|
|
||||||
actor_id: &Url,
|
|
||||||
object_domain_must_match: bool,
|
|
||||||
) -> Result<(), LemmyError>
|
|
||||||
where
|
|
||||||
T: AsBase<Kind> + ActorAndObjectRef,
|
|
||||||
{
|
|
||||||
let expected_domain = actor_id.domain().context(location_info!())?;
|
|
||||||
|
|
||||||
activity.id(expected_domain)?;
|
|
||||||
|
|
||||||
let object_id = match activity.object().to_owned().single_xsd_any_uri() {
|
|
||||||
// object is just an ID
|
|
||||||
Some(id) => id,
|
|
||||||
// object is something like an activity, a comment or a post
|
|
||||||
None => activity
|
|
||||||
.object()
|
|
||||||
.to_owned()
|
|
||||||
.one()
|
|
||||||
.context(location_info!())?
|
|
||||||
.id()
|
|
||||||
.context(location_info!())?
|
|
||||||
.to_owned(),
|
|
||||||
};
|
|
||||||
|
|
||||||
if object_domain_must_match && object_id.domain() != Some(expected_domain) {
|
|
||||||
return Err(DomainError.into());
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
|
@ -1,242 +0,0 @@
|
||||||
use crate::{
|
|
||||||
activities::receive::get_actor_as_person,
|
|
||||||
inbox::receive_for_community::verify_mod_activity,
|
|
||||||
};
|
|
||||||
use activitystreams::{
|
|
||||||
activity::{Announce, Create, Dislike, Like, Update},
|
|
||||||
prelude::*,
|
|
||||||
};
|
|
||||||
use anyhow::Context;
|
|
||||||
use lemmy_api_common::{blocking, post::PostResponse};
|
|
||||||
use lemmy_apub::{objects::FromApub, ActorType, PageExt};
|
|
||||||
use lemmy_db_queries::{source::post::Post_, ApubObject, Crud, Likeable};
|
|
||||||
use lemmy_db_schema::{
|
|
||||||
source::{
|
|
||||||
community::Community,
|
|
||||||
post::{Post, PostLike, PostLikeForm},
|
|
||||||
},
|
|
||||||
DbUrl,
|
|
||||||
};
|
|
||||||
use lemmy_db_views::post_view::PostView;
|
|
||||||
use lemmy_utils::{location_info, LemmyError};
|
|
||||||
use lemmy_websocket::{messages::SendPost, LemmyContext, UserOperation, UserOperationCrud};
|
|
||||||
|
|
||||||
pub(crate) async fn receive_create_post(
|
|
||||||
create: Create,
|
|
||||||
context: &LemmyContext,
|
|
||||||
request_counter: &mut i32,
|
|
||||||
) -> Result<(), LemmyError> {
|
|
||||||
let person = get_actor_as_person(&create, context, request_counter).await?;
|
|
||||||
let page = PageExt::from_any_base(create.object().to_owned().one().context(location_info!())?)?
|
|
||||||
.context(location_info!())?;
|
|
||||||
|
|
||||||
let post = Post::from_apub(&page, context, person.actor_id(), request_counter, false).await?;
|
|
||||||
|
|
||||||
// Refetch the view
|
|
||||||
let post_id = post.id;
|
|
||||||
let post_view = blocking(context.pool(), move |conn| {
|
|
||||||
PostView::read(conn, post_id, None)
|
|
||||||
})
|
|
||||||
.await??;
|
|
||||||
|
|
||||||
let res = PostResponse { post_view };
|
|
||||||
|
|
||||||
context.chat_server().do_send(SendPost {
|
|
||||||
op: UserOperationCrud::CreatePost,
|
|
||||||
post: res,
|
|
||||||
websocket_id: None,
|
|
||||||
});
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) async fn receive_update_post(
|
|
||||||
update: Update,
|
|
||||||
announce: Option<Announce>,
|
|
||||||
context: &LemmyContext,
|
|
||||||
request_counter: &mut i32,
|
|
||||||
) -> Result<(), LemmyError> {
|
|
||||||
let person = get_actor_as_person(&update, context, request_counter).await?;
|
|
||||||
let page = PageExt::from_any_base(update.object().to_owned().one().context(location_info!())?)?
|
|
||||||
.context(location_info!())?;
|
|
||||||
|
|
||||||
let post_id: DbUrl = page
|
|
||||||
.id_unchecked()
|
|
||||||
.context(location_info!())?
|
|
||||||
.to_owned()
|
|
||||||
.into();
|
|
||||||
let old_post = blocking(context.pool(), move |conn| {
|
|
||||||
Post::read_from_apub_id(conn, &post_id)
|
|
||||||
})
|
|
||||||
.await??;
|
|
||||||
|
|
||||||
// If sticked or locked state was changed, make sure the actor is a mod
|
|
||||||
let stickied = page.ext_one.stickied.context(location_info!())?;
|
|
||||||
let locked = !page.ext_one.comments_enabled.context(location_info!())?;
|
|
||||||
let mut mod_action_allowed = false;
|
|
||||||
if (stickied != old_post.stickied) || (locked != old_post.locked) {
|
|
||||||
let community = blocking(context.pool(), move |conn| {
|
|
||||||
Community::read(conn, old_post.community_id)
|
|
||||||
})
|
|
||||||
.await??;
|
|
||||||
// Only check mod status if the community is local, otherwise we trust that it was sent correctly.
|
|
||||||
if community.local {
|
|
||||||
verify_mod_activity(&update, announce, &community, context).await?;
|
|
||||||
}
|
|
||||||
mod_action_allowed = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
let post = Post::from_apub(
|
|
||||||
&page,
|
|
||||||
context,
|
|
||||||
person.actor_id(),
|
|
||||||
request_counter,
|
|
||||||
mod_action_allowed,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let post_id = post.id;
|
|
||||||
// Refetch the view
|
|
||||||
let post_view = blocking(context.pool(), move |conn| {
|
|
||||||
PostView::read(conn, post_id, None)
|
|
||||||
})
|
|
||||||
.await??;
|
|
||||||
|
|
||||||
let res = PostResponse { post_view };
|
|
||||||
|
|
||||||
context.chat_server().do_send(SendPost {
|
|
||||||
op: UserOperationCrud::EditPost,
|
|
||||||
post: res,
|
|
||||||
websocket_id: None,
|
|
||||||
});
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) async fn receive_like_post(
|
|
||||||
like: Like,
|
|
||||||
post: Post,
|
|
||||||
context: &LemmyContext,
|
|
||||||
request_counter: &mut i32,
|
|
||||||
) -> Result<(), LemmyError> {
|
|
||||||
let person = get_actor_as_person(&like, context, request_counter).await?;
|
|
||||||
|
|
||||||
let post_id = post.id;
|
|
||||||
let like_form = PostLikeForm {
|
|
||||||
post_id,
|
|
||||||
person_id: person.id,
|
|
||||||
score: 1,
|
|
||||||
};
|
|
||||||
let person_id = person.id;
|
|
||||||
blocking(context.pool(), move |conn| {
|
|
||||||
PostLike::remove(conn, person_id, post_id)?;
|
|
||||||
PostLike::like(conn, &like_form)
|
|
||||||
})
|
|
||||||
.await??;
|
|
||||||
|
|
||||||
// Refetch the view
|
|
||||||
let post_view = blocking(context.pool(), move |conn| {
|
|
||||||
PostView::read(conn, post_id, None)
|
|
||||||
})
|
|
||||||
.await??;
|
|
||||||
|
|
||||||
let res = PostResponse { post_view };
|
|
||||||
|
|
||||||
context.chat_server().do_send(SendPost {
|
|
||||||
op: UserOperation::CreatePostLike,
|
|
||||||
post: res,
|
|
||||||
websocket_id: None,
|
|
||||||
});
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) async fn receive_dislike_post(
|
|
||||||
dislike: Dislike,
|
|
||||||
post: Post,
|
|
||||||
context: &LemmyContext,
|
|
||||||
request_counter: &mut i32,
|
|
||||||
) -> Result<(), LemmyError> {
|
|
||||||
let person = get_actor_as_person(&dislike, context, request_counter).await?;
|
|
||||||
|
|
||||||
let post_id = post.id;
|
|
||||||
let like_form = PostLikeForm {
|
|
||||||
post_id,
|
|
||||||
person_id: person.id,
|
|
||||||
score: -1,
|
|
||||||
};
|
|
||||||
let person_id = person.id;
|
|
||||||
blocking(context.pool(), move |conn| {
|
|
||||||
PostLike::remove(conn, person_id, post_id)?;
|
|
||||||
PostLike::like(conn, &like_form)
|
|
||||||
})
|
|
||||||
.await??;
|
|
||||||
|
|
||||||
// Refetch the view
|
|
||||||
let post_view = blocking(context.pool(), move |conn| {
|
|
||||||
PostView::read(conn, post_id, None)
|
|
||||||
})
|
|
||||||
.await??;
|
|
||||||
|
|
||||||
let res = PostResponse { post_view };
|
|
||||||
|
|
||||||
context.chat_server().do_send(SendPost {
|
|
||||||
op: UserOperation::CreatePostLike,
|
|
||||||
post: res,
|
|
||||||
websocket_id: None,
|
|
||||||
});
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) async fn receive_delete_post(
|
|
||||||
context: &LemmyContext,
|
|
||||||
post: Post,
|
|
||||||
) -> Result<(), LemmyError> {
|
|
||||||
let deleted_post = blocking(context.pool(), move |conn| {
|
|
||||||
Post::update_deleted(conn, post.id, true)
|
|
||||||
})
|
|
||||||
.await??;
|
|
||||||
|
|
||||||
// Refetch the view
|
|
||||||
let post_id = deleted_post.id;
|
|
||||||
let post_view = blocking(context.pool(), move |conn| {
|
|
||||||
PostView::read(conn, post_id, None)
|
|
||||||
})
|
|
||||||
.await??;
|
|
||||||
|
|
||||||
let res = PostResponse { post_view };
|
|
||||||
context.chat_server().do_send(SendPost {
|
|
||||||
op: UserOperationCrud::EditPost,
|
|
||||||
post: res,
|
|
||||||
websocket_id: None,
|
|
||||||
});
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) async fn receive_remove_post(
|
|
||||||
context: &LemmyContext,
|
|
||||||
post: Post,
|
|
||||||
) -> Result<(), LemmyError> {
|
|
||||||
let removed_post = blocking(context.pool(), move |conn| {
|
|
||||||
Post::update_removed(conn, post.id, true)
|
|
||||||
})
|
|
||||||
.await??;
|
|
||||||
|
|
||||||
// Refetch the view
|
|
||||||
let post_id = removed_post.id;
|
|
||||||
let post_view = blocking(context.pool(), move |conn| {
|
|
||||||
PostView::read(conn, post_id, None)
|
|
||||||
})
|
|
||||||
.await??;
|
|
||||||
|
|
||||||
let res = PostResponse { post_view };
|
|
||||||
context.chat_server().do_send(SendPost {
|
|
||||||
op: UserOperationCrud::EditPost,
|
|
||||||
post: res,
|
|
||||||
websocket_id: None,
|
|
||||||
});
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
|
@ -1,125 +0,0 @@
|
||||||
use crate::activities::receive::get_actor_as_person;
|
|
||||||
use activitystreams::activity::{Dislike, Like};
|
|
||||||
use lemmy_api_common::{blocking, post::PostResponse};
|
|
||||||
use lemmy_db_queries::{source::post::Post_, Likeable};
|
|
||||||
use lemmy_db_schema::source::post::{Post, PostLike};
|
|
||||||
use lemmy_db_views::post_view::PostView;
|
|
||||||
use lemmy_utils::LemmyError;
|
|
||||||
use lemmy_websocket::{messages::SendPost, LemmyContext, UserOperation, UserOperationCrud};
|
|
||||||
|
|
||||||
pub(crate) async fn receive_undo_like_post(
|
|
||||||
like: &Like,
|
|
||||||
post: Post,
|
|
||||||
context: &LemmyContext,
|
|
||||||
request_counter: &mut i32,
|
|
||||||
) -> Result<(), LemmyError> {
|
|
||||||
let person = get_actor_as_person(like, context, request_counter).await?;
|
|
||||||
|
|
||||||
let post_id = post.id;
|
|
||||||
let person_id = person.id;
|
|
||||||
blocking(context.pool(), move |conn| {
|
|
||||||
PostLike::remove(conn, person_id, post_id)
|
|
||||||
})
|
|
||||||
.await??;
|
|
||||||
|
|
||||||
// Refetch the view
|
|
||||||
let post_view = blocking(context.pool(), move |conn| {
|
|
||||||
PostView::read(conn, post_id, None)
|
|
||||||
})
|
|
||||||
.await??;
|
|
||||||
|
|
||||||
let res = PostResponse { post_view };
|
|
||||||
|
|
||||||
context.chat_server().do_send(SendPost {
|
|
||||||
op: UserOperation::CreatePostLike,
|
|
||||||
post: res,
|
|
||||||
websocket_id: None,
|
|
||||||
});
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) async fn receive_undo_dislike_post(
|
|
||||||
dislike: &Dislike,
|
|
||||||
post: Post,
|
|
||||||
context: &LemmyContext,
|
|
||||||
request_counter: &mut i32,
|
|
||||||
) -> Result<(), LemmyError> {
|
|
||||||
let person = get_actor_as_person(dislike, context, request_counter).await?;
|
|
||||||
|
|
||||||
let post_id = post.id;
|
|
||||||
let person_id = person.id;
|
|
||||||
blocking(context.pool(), move |conn| {
|
|
||||||
PostLike::remove(conn, person_id, post_id)
|
|
||||||
})
|
|
||||||
.await??;
|
|
||||||
|
|
||||||
// Refetch the view
|
|
||||||
let post_view = blocking(context.pool(), move |conn| {
|
|
||||||
PostView::read(conn, post_id, None)
|
|
||||||
})
|
|
||||||
.await??;
|
|
||||||
|
|
||||||
let res = PostResponse { post_view };
|
|
||||||
|
|
||||||
context.chat_server().do_send(SendPost {
|
|
||||||
op: UserOperation::CreatePostLike,
|
|
||||||
post: res,
|
|
||||||
websocket_id: None,
|
|
||||||
});
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) async fn receive_undo_delete_post(
|
|
||||||
context: &LemmyContext,
|
|
||||||
post: Post,
|
|
||||||
) -> Result<(), LemmyError> {
|
|
||||||
let deleted_post = blocking(context.pool(), move |conn| {
|
|
||||||
Post::update_deleted(conn, post.id, false)
|
|
||||||
})
|
|
||||||
.await??;
|
|
||||||
|
|
||||||
// Refetch the view
|
|
||||||
let post_id = deleted_post.id;
|
|
||||||
let post_view = blocking(context.pool(), move |conn| {
|
|
||||||
PostView::read(conn, post_id, None)
|
|
||||||
})
|
|
||||||
.await??;
|
|
||||||
|
|
||||||
let res = PostResponse { post_view };
|
|
||||||
context.chat_server().do_send(SendPost {
|
|
||||||
op: UserOperationCrud::EditPost,
|
|
||||||
post: res,
|
|
||||||
websocket_id: None,
|
|
||||||
});
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) async fn receive_undo_remove_post(
|
|
||||||
context: &LemmyContext,
|
|
||||||
post: Post,
|
|
||||||
) -> Result<(), LemmyError> {
|
|
||||||
let removed_post = blocking(context.pool(), move |conn| {
|
|
||||||
Post::update_removed(conn, post.id, false)
|
|
||||||
})
|
|
||||||
.await??;
|
|
||||||
|
|
||||||
// Refetch the view
|
|
||||||
let post_id = removed_post.id;
|
|
||||||
let post_view = blocking(context.pool(), move |conn| {
|
|
||||||
PostView::read(conn, post_id, None)
|
|
||||||
})
|
|
||||||
.await??;
|
|
||||||
|
|
||||||
let res = PostResponse { post_view };
|
|
||||||
|
|
||||||
context.chat_server().do_send(SendPost {
|
|
||||||
op: UserOperationCrud::EditPost,
|
|
||||||
post: res,
|
|
||||||
websocket_id: None,
|
|
||||||
});
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
|
@ -1,228 +0,0 @@
|
||||||
use crate::activities::receive::verify_activity_domains_valid;
|
|
||||||
use activitystreams::{
|
|
||||||
activity::{ActorAndObjectRefExt, Create, Delete, Undo, Update},
|
|
||||||
base::{AsBase, ExtendsExt},
|
|
||||||
object::AsObject,
|
|
||||||
public,
|
|
||||||
};
|
|
||||||
use anyhow::{anyhow, Context};
|
|
||||||
use lemmy_api_common::{blocking, person::PrivateMessageResponse};
|
|
||||||
use lemmy_apub::{
|
|
||||||
check_is_apub_id_valid,
|
|
||||||
fetcher::person::get_or_fetch_and_upsert_person,
|
|
||||||
get_activity_to_and_cc,
|
|
||||||
objects::FromApub,
|
|
||||||
NoteExt,
|
|
||||||
};
|
|
||||||
use lemmy_db_queries::source::private_message::PrivateMessage_;
|
|
||||||
use lemmy_db_schema::source::private_message::PrivateMessage;
|
|
||||||
use lemmy_db_views::{local_user_view::LocalUserView, private_message_view::PrivateMessageView};
|
|
||||||
use lemmy_utils::{location_info, LemmyError};
|
|
||||||
use lemmy_websocket::{messages::SendUserRoomMessage, LemmyContext, UserOperationCrud};
|
|
||||||
use url::Url;
|
|
||||||
|
|
||||||
pub(crate) async fn receive_create_private_message(
|
|
||||||
context: &LemmyContext,
|
|
||||||
create: Create,
|
|
||||||
expected_domain: Url,
|
|
||||||
request_counter: &mut i32,
|
|
||||||
) -> Result<(), LemmyError> {
|
|
||||||
check_private_message_activity_valid(&create, context, request_counter).await?;
|
|
||||||
|
|
||||||
let note = NoteExt::from_any_base(
|
|
||||||
create
|
|
||||||
.object()
|
|
||||||
.as_one()
|
|
||||||
.context(location_info!())?
|
|
||||||
.to_owned(),
|
|
||||||
)?
|
|
||||||
.context(location_info!())?;
|
|
||||||
|
|
||||||
let private_message =
|
|
||||||
PrivateMessage::from_apub(¬e, context, expected_domain, request_counter, false).await?;
|
|
||||||
|
|
||||||
let message = blocking(context.pool(), move |conn| {
|
|
||||||
PrivateMessageView::read(conn, private_message.id)
|
|
||||||
})
|
|
||||||
.await??;
|
|
||||||
|
|
||||||
let res = PrivateMessageResponse {
|
|
||||||
private_message_view: message,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Send notifications to the local recipient, if one exists
|
|
||||||
let recipient_id = res.private_message_view.recipient.id;
|
|
||||||
let local_recipient_id = blocking(context.pool(), move |conn| {
|
|
||||||
LocalUserView::read_person(conn, recipient_id)
|
|
||||||
})
|
|
||||||
.await??
|
|
||||||
.local_user
|
|
||||||
.id;
|
|
||||||
|
|
||||||
context.chat_server().do_send(SendUserRoomMessage {
|
|
||||||
op: UserOperationCrud::CreatePrivateMessage,
|
|
||||||
response: res,
|
|
||||||
local_recipient_id,
|
|
||||||
websocket_id: None,
|
|
||||||
});
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) async fn receive_update_private_message(
|
|
||||||
context: &LemmyContext,
|
|
||||||
update: Update,
|
|
||||||
expected_domain: Url,
|
|
||||||
request_counter: &mut i32,
|
|
||||||
) -> Result<(), LemmyError> {
|
|
||||||
check_private_message_activity_valid(&update, context, request_counter).await?;
|
|
||||||
|
|
||||||
let object = update
|
|
||||||
.object()
|
|
||||||
.as_one()
|
|
||||||
.context(location_info!())?
|
|
||||||
.to_owned();
|
|
||||||
let note = NoteExt::from_any_base(object)?.context(location_info!())?;
|
|
||||||
|
|
||||||
let private_message =
|
|
||||||
PrivateMessage::from_apub(¬e, context, expected_domain, request_counter, false).await?;
|
|
||||||
|
|
||||||
let private_message_id = private_message.id;
|
|
||||||
let message = blocking(context.pool(), move |conn| {
|
|
||||||
PrivateMessageView::read(conn, private_message_id)
|
|
||||||
})
|
|
||||||
.await??;
|
|
||||||
|
|
||||||
let res = PrivateMessageResponse {
|
|
||||||
private_message_view: message,
|
|
||||||
};
|
|
||||||
|
|
||||||
let recipient_id = res.private_message_view.recipient.id;
|
|
||||||
let local_recipient_id = blocking(context.pool(), move |conn| {
|
|
||||||
LocalUserView::read_person(conn, recipient_id)
|
|
||||||
})
|
|
||||||
.await??
|
|
||||||
.local_user
|
|
||||||
.id;
|
|
||||||
|
|
||||||
context.chat_server().do_send(SendUserRoomMessage {
|
|
||||||
op: UserOperationCrud::EditPrivateMessage,
|
|
||||||
response: res,
|
|
||||||
local_recipient_id,
|
|
||||||
websocket_id: None,
|
|
||||||
});
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) async fn receive_delete_private_message(
|
|
||||||
context: &LemmyContext,
|
|
||||||
delete: Delete,
|
|
||||||
private_message: PrivateMessage,
|
|
||||||
request_counter: &mut i32,
|
|
||||||
) -> Result<(), LemmyError> {
|
|
||||||
check_private_message_activity_valid(&delete, context, request_counter).await?;
|
|
||||||
|
|
||||||
let deleted_private_message = blocking(context.pool(), move |conn| {
|
|
||||||
PrivateMessage::update_deleted(conn, private_message.id, true)
|
|
||||||
})
|
|
||||||
.await??;
|
|
||||||
|
|
||||||
let message = blocking(context.pool(), move |conn| {
|
|
||||||
PrivateMessageView::read(conn, deleted_private_message.id)
|
|
||||||
})
|
|
||||||
.await??;
|
|
||||||
|
|
||||||
let res = PrivateMessageResponse {
|
|
||||||
private_message_view: message,
|
|
||||||
};
|
|
||||||
|
|
||||||
let recipient_id = res.private_message_view.recipient.id;
|
|
||||||
let local_recipient_id = blocking(context.pool(), move |conn| {
|
|
||||||
LocalUserView::read_person(conn, recipient_id)
|
|
||||||
})
|
|
||||||
.await??
|
|
||||||
.local_user
|
|
||||||
.id;
|
|
||||||
|
|
||||||
context.chat_server().do_send(SendUserRoomMessage {
|
|
||||||
op: UserOperationCrud::EditPrivateMessage,
|
|
||||||
response: res,
|
|
||||||
local_recipient_id,
|
|
||||||
websocket_id: None,
|
|
||||||
});
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) async fn receive_undo_delete_private_message(
|
|
||||||
context: &LemmyContext,
|
|
||||||
undo: Undo,
|
|
||||||
expected_domain: &Url,
|
|
||||||
private_message: PrivateMessage,
|
|
||||||
request_counter: &mut i32,
|
|
||||||
) -> Result<(), LemmyError> {
|
|
||||||
check_private_message_activity_valid(&undo, context, request_counter).await?;
|
|
||||||
let object = undo.object().to_owned().one().context(location_info!())?;
|
|
||||||
let delete = Delete::from_any_base(object)?.context(location_info!())?;
|
|
||||||
verify_activity_domains_valid(&delete, expected_domain, true)?;
|
|
||||||
check_private_message_activity_valid(&delete, context, request_counter).await?;
|
|
||||||
|
|
||||||
let deleted_private_message = blocking(context.pool(), move |conn| {
|
|
||||||
PrivateMessage::update_deleted(conn, private_message.id, false)
|
|
||||||
})
|
|
||||||
.await??;
|
|
||||||
|
|
||||||
let message = blocking(context.pool(), move |conn| {
|
|
||||||
PrivateMessageView::read(conn, deleted_private_message.id)
|
|
||||||
})
|
|
||||||
.await??;
|
|
||||||
|
|
||||||
let res = PrivateMessageResponse {
|
|
||||||
private_message_view: message,
|
|
||||||
};
|
|
||||||
|
|
||||||
let recipient_id = res.private_message_view.recipient.id;
|
|
||||||
let local_recipient_id = blocking(context.pool(), move |conn| {
|
|
||||||
LocalUserView::read_person(conn, recipient_id)
|
|
||||||
})
|
|
||||||
.await??
|
|
||||||
.local_user
|
|
||||||
.id;
|
|
||||||
|
|
||||||
context.chat_server().do_send(SendUserRoomMessage {
|
|
||||||
op: UserOperationCrud::EditPrivateMessage,
|
|
||||||
response: res,
|
|
||||||
local_recipient_id,
|
|
||||||
websocket_id: None,
|
|
||||||
});
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn check_private_message_activity_valid<T, Kind>(
|
|
||||||
activity: &T,
|
|
||||||
context: &LemmyContext,
|
|
||||||
request_counter: &mut i32,
|
|
||||||
) -> Result<(), LemmyError>
|
|
||||||
where
|
|
||||||
T: AsBase<Kind> + AsObject<Kind> + ActorAndObjectRefExt,
|
|
||||||
{
|
|
||||||
let to_and_cc = get_activity_to_and_cc(activity);
|
|
||||||
if to_and_cc.len() != 1 {
|
|
||||||
return Err(anyhow!("Private message can only be addressed to one person").into());
|
|
||||||
}
|
|
||||||
if to_and_cc.contains(&public()) {
|
|
||||||
return Err(anyhow!("Private message cant be public").into());
|
|
||||||
}
|
|
||||||
let person_id = activity
|
|
||||||
.actor()?
|
|
||||||
.to_owned()
|
|
||||||
.single_xsd_any_uri()
|
|
||||||
.context(location_info!())?;
|
|
||||||
check_is_apub_id_valid(&person_id, false)?;
|
|
||||||
// check that the sender is a person, not a community
|
|
||||||
get_or_fetch_and_upsert_person(&person_id, context, request_counter).await?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
2
crates/apub_receive/src/activities/removal/mod.rs
Normal file
2
crates/apub_receive/src/activities/removal/mod.rs
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
pub mod remove;
|
||||||
|
pub mod undo_remove;
|
155
crates/apub_receive/src/activities/removal/remove.rs
Normal file
155
crates/apub_receive/src/activities/removal/remove.rs
Normal file
|
@ -0,0 +1,155 @@
|
||||||
|
use crate::activities::{
|
||||||
|
comment::send_websocket_message as send_comment_message,
|
||||||
|
community::send_websocket_message as send_community_message,
|
||||||
|
post::send_websocket_message as send_post_message,
|
||||||
|
verify_activity,
|
||||||
|
verify_add_remove_moderator_target,
|
||||||
|
verify_mod_action,
|
||||||
|
verify_person_in_community,
|
||||||
|
};
|
||||||
|
use activitystreams::{activity::kind::RemoveType, base::AnyBase};
|
||||||
|
use anyhow::anyhow;
|
||||||
|
use lemmy_api_common::blocking;
|
||||||
|
use lemmy_apub::{
|
||||||
|
fetcher::{
|
||||||
|
community::get_or_fetch_and_upsert_community,
|
||||||
|
objects::get_or_fetch_and_insert_post_or_comment,
|
||||||
|
person::get_or_fetch_and_upsert_person,
|
||||||
|
},
|
||||||
|
CommunityType,
|
||||||
|
PostOrComment,
|
||||||
|
};
|
||||||
|
use lemmy_apub_lib::{ActivityCommonFields, ActivityHandler, PublicUrl};
|
||||||
|
use lemmy_db_queries::{
|
||||||
|
source::{comment::Comment_, community::Community_, post::Post_},
|
||||||
|
Joinable,
|
||||||
|
};
|
||||||
|
use lemmy_db_schema::source::{
|
||||||
|
comment::Comment,
|
||||||
|
community::{Community, CommunityModerator, CommunityModeratorForm},
|
||||||
|
post::Post,
|
||||||
|
};
|
||||||
|
use lemmy_utils::LemmyError;
|
||||||
|
use lemmy_websocket::{LemmyContext, UserOperationCrud};
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
// TODO: we can probably deduplicate a bunch of code between this and DeletePostCommentOrCommunity
|
||||||
|
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct RemovePostCommentCommunityOrMod {
|
||||||
|
to: PublicUrl,
|
||||||
|
pub(in crate::activities::removal) object: Url,
|
||||||
|
cc: [Url; 1],
|
||||||
|
#[serde(rename = "type")]
|
||||||
|
kind: RemoveType,
|
||||||
|
// if target is set, this is means remove mod from community
|
||||||
|
target: Option<Url>,
|
||||||
|
#[serde(flatten)]
|
||||||
|
common: ActivityCommonFields,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait(?Send)]
|
||||||
|
impl ActivityHandler for RemovePostCommentCommunityOrMod {
|
||||||
|
async fn verify(
|
||||||
|
&self,
|
||||||
|
context: &LemmyContext,
|
||||||
|
request_counter: &mut i32,
|
||||||
|
) -> Result<(), LemmyError> {
|
||||||
|
verify_activity(self.common())?;
|
||||||
|
let object_community =
|
||||||
|
get_or_fetch_and_upsert_community(&self.object, context, request_counter).await;
|
||||||
|
// removing a community
|
||||||
|
if object_community.is_ok() {
|
||||||
|
verify_mod_action(&self.common.actor, self.object.clone(), context).await?;
|
||||||
|
}
|
||||||
|
// removing community mod
|
||||||
|
else if let Some(target) = &self.target {
|
||||||
|
verify_person_in_community(&self.common.actor, &self.cc, context, request_counter).await?;
|
||||||
|
verify_mod_action(&self.common.actor, self.cc[0].clone(), context).await?;
|
||||||
|
verify_add_remove_moderator_target(target, self.cc[0].clone())?;
|
||||||
|
}
|
||||||
|
// removing a post or comment
|
||||||
|
else {
|
||||||
|
verify_person_in_community(&self.common.actor, &self.cc, context, request_counter).await?;
|
||||||
|
verify_mod_action(&self.common.actor, self.cc[0].clone(), context).await?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn receive(
|
||||||
|
&self,
|
||||||
|
context: &LemmyContext,
|
||||||
|
request_counter: &mut i32,
|
||||||
|
) -> Result<(), LemmyError> {
|
||||||
|
let object_community =
|
||||||
|
get_or_fetch_and_upsert_community(&self.object, context, request_counter).await;
|
||||||
|
// removing a community
|
||||||
|
if let Ok(community) = object_community {
|
||||||
|
if community.local {
|
||||||
|
return Err(anyhow!("Only local admin can remove community").into());
|
||||||
|
}
|
||||||
|
let deleted_community = blocking(context.pool(), move |conn| {
|
||||||
|
Community::update_removed(conn, community.id, true)
|
||||||
|
})
|
||||||
|
.await??;
|
||||||
|
|
||||||
|
send_community_message(
|
||||||
|
deleted_community.id,
|
||||||
|
UserOperationCrud::RemoveCommunity,
|
||||||
|
context,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
// removing community mod
|
||||||
|
else if self.target.is_some() {
|
||||||
|
let community =
|
||||||
|
get_or_fetch_and_upsert_community(&self.cc[0], context, request_counter).await?;
|
||||||
|
let remove_mod =
|
||||||
|
get_or_fetch_and_upsert_person(&self.object, context, request_counter).await?;
|
||||||
|
|
||||||
|
let form = CommunityModeratorForm {
|
||||||
|
community_id: community.id,
|
||||||
|
person_id: remove_mod.id,
|
||||||
|
};
|
||||||
|
blocking(context.pool(), move |conn| {
|
||||||
|
CommunityModerator::leave(conn, &form)
|
||||||
|
})
|
||||||
|
.await??;
|
||||||
|
let anybase = AnyBase::from_arbitrary_json(serde_json::to_string(self)?)?;
|
||||||
|
community
|
||||||
|
.send_announce(anybase, Some(self.object.clone()), context)
|
||||||
|
.await?;
|
||||||
|
// TODO: send websocket notification about removed mod
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
// removing a post or comment
|
||||||
|
else {
|
||||||
|
match get_or_fetch_and_insert_post_or_comment(&self.object, context, request_counter).await? {
|
||||||
|
PostOrComment::Post(post) => {
|
||||||
|
let removed_post = blocking(context.pool(), move |conn| {
|
||||||
|
Post::update_removed(conn, post.id, true)
|
||||||
|
})
|
||||||
|
.await??;
|
||||||
|
send_post_message(removed_post.id, UserOperationCrud::EditPost, context).await
|
||||||
|
}
|
||||||
|
PostOrComment::Comment(comment) => {
|
||||||
|
let removed_comment = blocking(context.pool(), move |conn| {
|
||||||
|
Comment::update_removed(conn, comment.id, true)
|
||||||
|
})
|
||||||
|
.await??;
|
||||||
|
send_comment_message(
|
||||||
|
removed_comment.id,
|
||||||
|
vec![],
|
||||||
|
UserOperationCrud::EditComment,
|
||||||
|
context,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn common(&self) -> &ActivityCommonFields {
|
||||||
|
&self.common
|
||||||
|
}
|
||||||
|
}
|
120
crates/apub_receive/src/activities/removal/undo_remove.rs
Normal file
120
crates/apub_receive/src/activities/removal/undo_remove.rs
Normal file
|
@ -0,0 +1,120 @@
|
||||||
|
use crate::activities::{
|
||||||
|
comment::send_websocket_message as send_comment_message,
|
||||||
|
community::send_websocket_message as send_community_message,
|
||||||
|
post::send_websocket_message as send_post_message,
|
||||||
|
removal::remove::RemovePostCommentCommunityOrMod,
|
||||||
|
verify_activity,
|
||||||
|
verify_mod_action,
|
||||||
|
verify_person_in_community,
|
||||||
|
};
|
||||||
|
use activitystreams::activity::kind::UndoType;
|
||||||
|
use anyhow::anyhow;
|
||||||
|
use lemmy_api_common::blocking;
|
||||||
|
use lemmy_apub::{
|
||||||
|
fetcher::{
|
||||||
|
community::get_or_fetch_and_upsert_community,
|
||||||
|
objects::get_or_fetch_and_insert_post_or_comment,
|
||||||
|
},
|
||||||
|
PostOrComment,
|
||||||
|
};
|
||||||
|
use lemmy_apub_lib::{ActivityCommonFields, ActivityHandler, PublicUrl};
|
||||||
|
use lemmy_db_queries::source::{comment::Comment_, community::Community_, post::Post_};
|
||||||
|
use lemmy_db_schema::source::{comment::Comment, community::Community, post::Post};
|
||||||
|
use lemmy_utils::LemmyError;
|
||||||
|
use lemmy_websocket::{LemmyContext, UserOperationCrud};
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct UndoRemovePostCommentOrCommunity {
|
||||||
|
to: PublicUrl,
|
||||||
|
object: RemovePostCommentCommunityOrMod,
|
||||||
|
cc: [Url; 1],
|
||||||
|
#[serde(rename = "type")]
|
||||||
|
kind: UndoType,
|
||||||
|
#[serde(flatten)]
|
||||||
|
common: ActivityCommonFields,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait(?Send)]
|
||||||
|
impl ActivityHandler for UndoRemovePostCommentOrCommunity {
|
||||||
|
async fn verify(
|
||||||
|
&self,
|
||||||
|
context: &LemmyContext,
|
||||||
|
request_counter: &mut i32,
|
||||||
|
) -> Result<(), LemmyError> {
|
||||||
|
verify_activity(self.common())?;
|
||||||
|
let object_community =
|
||||||
|
get_or_fetch_and_upsert_community(&self.object.object, context, request_counter).await;
|
||||||
|
// removing a community
|
||||||
|
if object_community.is_ok() {
|
||||||
|
verify_mod_action(&self.common.actor, self.object.object.clone(), context).await?;
|
||||||
|
}
|
||||||
|
// removing a post or comment
|
||||||
|
else {
|
||||||
|
verify_person_in_community(&self.common.actor, &self.cc, context, request_counter).await?;
|
||||||
|
verify_mod_action(&self.common.actor, self.cc[0].clone(), context).await?;
|
||||||
|
}
|
||||||
|
self.object.verify(context, request_counter).await?;
|
||||||
|
// dont check that actor and object.actor are identical, so that one mod can
|
||||||
|
// undo the action of another
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn receive(
|
||||||
|
&self,
|
||||||
|
context: &LemmyContext,
|
||||||
|
request_counter: &mut i32,
|
||||||
|
) -> Result<(), LemmyError> {
|
||||||
|
let object_community =
|
||||||
|
get_or_fetch_and_upsert_community(&self.object.object, context, request_counter).await;
|
||||||
|
// restoring a community
|
||||||
|
if let Ok(community) = object_community {
|
||||||
|
if community.local {
|
||||||
|
return Err(anyhow!("Only local admin can undo remove community").into());
|
||||||
|
}
|
||||||
|
let deleted_community = blocking(context.pool(), move |conn| {
|
||||||
|
Community::update_removed(conn, community.id, false)
|
||||||
|
})
|
||||||
|
.await??;
|
||||||
|
|
||||||
|
send_community_message(
|
||||||
|
deleted_community.id,
|
||||||
|
UserOperationCrud::EditCommunity,
|
||||||
|
context,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
// restoring a post or comment
|
||||||
|
else {
|
||||||
|
match get_or_fetch_and_insert_post_or_comment(&self.object.object, context, request_counter)
|
||||||
|
.await?
|
||||||
|
{
|
||||||
|
PostOrComment::Post(post) => {
|
||||||
|
let removed_post = blocking(context.pool(), move |conn| {
|
||||||
|
Post::update_removed(conn, post.id, false)
|
||||||
|
})
|
||||||
|
.await??;
|
||||||
|
send_post_message(removed_post.id, UserOperationCrud::EditPost, context).await
|
||||||
|
}
|
||||||
|
PostOrComment::Comment(comment) => {
|
||||||
|
let removed_comment = blocking(context.pool(), move |conn| {
|
||||||
|
Comment::update_removed(conn, comment.id, false)
|
||||||
|
})
|
||||||
|
.await??;
|
||||||
|
send_comment_message(
|
||||||
|
removed_comment.id,
|
||||||
|
vec![],
|
||||||
|
UserOperationCrud::EditComment,
|
||||||
|
context,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn common(&self) -> &ActivityCommonFields {
|
||||||
|
&self.common
|
||||||
|
}
|
||||||
|
}
|
54
crates/apub_receive/src/activities/voting/dislike.rs
Normal file
54
crates/apub_receive/src/activities/voting/dislike.rs
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
use crate::activities::{
|
||||||
|
verify_activity,
|
||||||
|
verify_person_in_community,
|
||||||
|
voting::receive_like_or_dislike,
|
||||||
|
};
|
||||||
|
use activitystreams::activity::kind::DislikeType;
|
||||||
|
use lemmy_apub_lib::{ActivityCommonFields, ActivityHandler, PublicUrl};
|
||||||
|
use lemmy_utils::LemmyError;
|
||||||
|
use lemmy_websocket::LemmyContext;
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct DislikePostOrComment {
|
||||||
|
to: PublicUrl,
|
||||||
|
pub(in crate::activities) object: Url,
|
||||||
|
cc: [Url; 1],
|
||||||
|
#[serde(rename = "type")]
|
||||||
|
kind: DislikeType,
|
||||||
|
#[serde(flatten)]
|
||||||
|
common: ActivityCommonFields,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait(?Send)]
|
||||||
|
impl ActivityHandler for DislikePostOrComment {
|
||||||
|
async fn verify(
|
||||||
|
&self,
|
||||||
|
context: &LemmyContext,
|
||||||
|
request_counter: &mut i32,
|
||||||
|
) -> Result<(), LemmyError> {
|
||||||
|
verify_activity(self.common())?;
|
||||||
|
verify_person_in_community(&self.common.actor, &self.cc, context, request_counter).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn receive(
|
||||||
|
&self,
|
||||||
|
context: &LemmyContext,
|
||||||
|
request_counter: &mut i32,
|
||||||
|
) -> Result<(), LemmyError> {
|
||||||
|
receive_like_or_dislike(
|
||||||
|
-1,
|
||||||
|
&self.common.actor,
|
||||||
|
&self.object,
|
||||||
|
context,
|
||||||
|
request_counter,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
fn common(&self) -> &ActivityCommonFields {
|
||||||
|
&self.common
|
||||||
|
}
|
||||||
|
}
|
54
crates/apub_receive/src/activities/voting/like.rs
Normal file
54
crates/apub_receive/src/activities/voting/like.rs
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
use crate::activities::{
|
||||||
|
verify_activity,
|
||||||
|
verify_person_in_community,
|
||||||
|
voting::receive_like_or_dislike,
|
||||||
|
};
|
||||||
|
use activitystreams::activity::kind::LikeType;
|
||||||
|
use lemmy_apub_lib::{ActivityCommonFields, ActivityHandler, PublicUrl};
|
||||||
|
use lemmy_utils::LemmyError;
|
||||||
|
use lemmy_websocket::LemmyContext;
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct LikePostOrComment {
|
||||||
|
to: PublicUrl,
|
||||||
|
pub(in crate::activities::voting) object: Url,
|
||||||
|
cc: [Url; 1],
|
||||||
|
#[serde(rename = "type")]
|
||||||
|
kind: LikeType,
|
||||||
|
#[serde(flatten)]
|
||||||
|
common: ActivityCommonFields,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait(?Send)]
|
||||||
|
impl ActivityHandler for LikePostOrComment {
|
||||||
|
async fn verify(
|
||||||
|
&self,
|
||||||
|
context: &LemmyContext,
|
||||||
|
request_counter: &mut i32,
|
||||||
|
) -> Result<(), LemmyError> {
|
||||||
|
verify_activity(self.common())?;
|
||||||
|
verify_person_in_community(&self.common.actor, &self.cc, context, request_counter).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn receive(
|
||||||
|
&self,
|
||||||
|
context: &LemmyContext,
|
||||||
|
request_counter: &mut i32,
|
||||||
|
) -> Result<(), LemmyError> {
|
||||||
|
receive_like_or_dislike(
|
||||||
|
1,
|
||||||
|
&self.common.actor,
|
||||||
|
&self.object,
|
||||||
|
context,
|
||||||
|
request_counter,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
fn common(&self) -> &ActivityCommonFields {
|
||||||
|
&self.common
|
||||||
|
}
|
||||||
|
}
|
157
crates/apub_receive/src/activities/voting/mod.rs
Normal file
157
crates/apub_receive/src/activities/voting/mod.rs
Normal file
|
@ -0,0 +1,157 @@
|
||||||
|
use crate::activities::{
|
||||||
|
comment::send_websocket_message as send_comment_message,
|
||||||
|
post::send_websocket_message as send_post_message,
|
||||||
|
};
|
||||||
|
use lemmy_api_common::blocking;
|
||||||
|
use lemmy_apub::{
|
||||||
|
fetcher::{
|
||||||
|
objects::get_or_fetch_and_insert_post_or_comment,
|
||||||
|
person::get_or_fetch_and_upsert_person,
|
||||||
|
},
|
||||||
|
PostOrComment,
|
||||||
|
};
|
||||||
|
use lemmy_db_queries::Likeable;
|
||||||
|
use lemmy_db_schema::source::{
|
||||||
|
comment::{Comment, CommentLike, CommentLikeForm},
|
||||||
|
post::{Post, PostLike, PostLikeForm},
|
||||||
|
};
|
||||||
|
use lemmy_utils::LemmyError;
|
||||||
|
use lemmy_websocket::{LemmyContext, UserOperation};
|
||||||
|
use std::ops::Deref;
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
pub mod dislike;
|
||||||
|
pub mod like;
|
||||||
|
pub mod undo_dislike;
|
||||||
|
pub mod undo_like;
|
||||||
|
|
||||||
|
pub(in crate::activities::voting) async fn receive_like_or_dislike(
|
||||||
|
score: i16,
|
||||||
|
actor: &Url,
|
||||||
|
object: &Url,
|
||||||
|
context: &LemmyContext,
|
||||||
|
request_counter: &mut i32,
|
||||||
|
) -> Result<(), LemmyError> {
|
||||||
|
match get_or_fetch_and_insert_post_or_comment(object, context, request_counter).await? {
|
||||||
|
PostOrComment::Post(p) => {
|
||||||
|
like_or_dislike_post(score, actor, p.deref(), context, request_counter).await
|
||||||
|
}
|
||||||
|
PostOrComment::Comment(c) => {
|
||||||
|
like_or_dislike_comment(score, actor, c.deref(), context, request_counter).await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn like_or_dislike_comment(
|
||||||
|
score: i16,
|
||||||
|
actor: &Url,
|
||||||
|
comment: &Comment,
|
||||||
|
context: &LemmyContext,
|
||||||
|
request_counter: &mut i32,
|
||||||
|
) -> Result<(), LemmyError> {
|
||||||
|
let actor = get_or_fetch_and_upsert_person(actor, context, request_counter).await?;
|
||||||
|
|
||||||
|
let comment_id = comment.id;
|
||||||
|
let like_form = CommentLikeForm {
|
||||||
|
comment_id,
|
||||||
|
post_id: comment.post_id,
|
||||||
|
person_id: actor.id,
|
||||||
|
score,
|
||||||
|
};
|
||||||
|
let person_id = actor.id;
|
||||||
|
blocking(context.pool(), move |conn| {
|
||||||
|
CommentLike::remove(conn, person_id, comment_id)?;
|
||||||
|
CommentLike::like(conn, &like_form)
|
||||||
|
})
|
||||||
|
.await??;
|
||||||
|
|
||||||
|
send_comment_message(
|
||||||
|
comment_id,
|
||||||
|
vec![],
|
||||||
|
UserOperation::CreateCommentLike,
|
||||||
|
context,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn like_or_dislike_post(
|
||||||
|
score: i16,
|
||||||
|
actor: &Url,
|
||||||
|
post: &Post,
|
||||||
|
context: &LemmyContext,
|
||||||
|
request_counter: &mut i32,
|
||||||
|
) -> Result<(), LemmyError> {
|
||||||
|
let actor = get_or_fetch_and_upsert_person(actor, context, request_counter).await?;
|
||||||
|
|
||||||
|
let post_id = post.id;
|
||||||
|
let like_form = PostLikeForm {
|
||||||
|
post_id: post.id,
|
||||||
|
person_id: actor.id,
|
||||||
|
score,
|
||||||
|
};
|
||||||
|
let person_id = actor.id;
|
||||||
|
blocking(context.pool(), move |conn| {
|
||||||
|
PostLike::remove(conn, person_id, post_id)?;
|
||||||
|
PostLike::like(conn, &like_form)
|
||||||
|
})
|
||||||
|
.await??;
|
||||||
|
|
||||||
|
send_post_message(post.id, UserOperation::CreatePostLike, context).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(in crate::activities::voting) async fn receive_undo_like_or_dislike(
|
||||||
|
actor: &Url,
|
||||||
|
object: &Url,
|
||||||
|
context: &LemmyContext,
|
||||||
|
request_counter: &mut i32,
|
||||||
|
) -> Result<(), LemmyError> {
|
||||||
|
match get_or_fetch_and_insert_post_or_comment(object, context, request_counter).await? {
|
||||||
|
PostOrComment::Post(p) => {
|
||||||
|
undo_like_or_dislike_post(actor, p.deref(), context, request_counter).await
|
||||||
|
}
|
||||||
|
PostOrComment::Comment(c) => {
|
||||||
|
undo_like_or_dislike_comment(actor, c.deref(), context, request_counter).await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn undo_like_or_dislike_comment(
|
||||||
|
actor: &Url,
|
||||||
|
comment: &Comment,
|
||||||
|
context: &LemmyContext,
|
||||||
|
request_counter: &mut i32,
|
||||||
|
) -> Result<(), LemmyError> {
|
||||||
|
let actor = get_or_fetch_and_upsert_person(actor, context, request_counter).await?;
|
||||||
|
|
||||||
|
let comment_id = comment.id;
|
||||||
|
let person_id = actor.id;
|
||||||
|
blocking(context.pool(), move |conn| {
|
||||||
|
CommentLike::remove(conn, person_id, comment_id)
|
||||||
|
})
|
||||||
|
.await??;
|
||||||
|
|
||||||
|
send_comment_message(
|
||||||
|
comment.id,
|
||||||
|
vec![],
|
||||||
|
UserOperation::CreateCommentLike,
|
||||||
|
context,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn undo_like_or_dislike_post(
|
||||||
|
actor: &Url,
|
||||||
|
post: &Post,
|
||||||
|
context: &LemmyContext,
|
||||||
|
request_counter: &mut i32,
|
||||||
|
) -> Result<(), LemmyError> {
|
||||||
|
let actor = get_or_fetch_and_upsert_person(actor, context, request_counter).await?;
|
||||||
|
|
||||||
|
let post_id = post.id;
|
||||||
|
let person_id = actor.id;
|
||||||
|
blocking(context.pool(), move |conn| {
|
||||||
|
PostLike::remove(conn, person_id, post_id)
|
||||||
|
})
|
||||||
|
.await??;
|
||||||
|
send_post_message(post.id, UserOperation::CreatePostLike, context).await
|
||||||
|
}
|
55
crates/apub_receive/src/activities/voting/undo_dislike.rs
Normal file
55
crates/apub_receive/src/activities/voting/undo_dislike.rs
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
use crate::activities::{
|
||||||
|
verify_activity,
|
||||||
|
verify_person_in_community,
|
||||||
|
voting::{dislike::DislikePostOrComment, receive_undo_like_or_dislike},
|
||||||
|
};
|
||||||
|
use activitystreams::activity::kind::UndoType;
|
||||||
|
use lemmy_apub_lib::{verify_urls_match, ActivityCommonFields, ActivityHandler, PublicUrl};
|
||||||
|
use lemmy_utils::LemmyError;
|
||||||
|
use lemmy_websocket::LemmyContext;
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct UndoDislikePostOrComment {
|
||||||
|
to: PublicUrl,
|
||||||
|
object: DislikePostOrComment,
|
||||||
|
cc: [Url; 1],
|
||||||
|
#[serde(rename = "type")]
|
||||||
|
kind: UndoType,
|
||||||
|
#[serde(flatten)]
|
||||||
|
common: ActivityCommonFields,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait(?Send)]
|
||||||
|
impl ActivityHandler for UndoDislikePostOrComment {
|
||||||
|
async fn verify(
|
||||||
|
&self,
|
||||||
|
context: &LemmyContext,
|
||||||
|
request_counter: &mut i32,
|
||||||
|
) -> Result<(), LemmyError> {
|
||||||
|
verify_activity(self.common())?;
|
||||||
|
verify_person_in_community(&self.common.actor, &self.cc, context, request_counter).await?;
|
||||||
|
verify_urls_match(&self.common.actor, &self.object.common().actor)?;
|
||||||
|
self.object.verify(context, request_counter).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn receive(
|
||||||
|
&self,
|
||||||
|
context: &LemmyContext,
|
||||||
|
request_counter: &mut i32,
|
||||||
|
) -> Result<(), LemmyError> {
|
||||||
|
receive_undo_like_or_dislike(
|
||||||
|
&self.common.actor,
|
||||||
|
&self.object.object,
|
||||||
|
context,
|
||||||
|
request_counter,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
fn common(&self) -> &ActivityCommonFields {
|
||||||
|
&self.common
|
||||||
|
}
|
||||||
|
}
|
55
crates/apub_receive/src/activities/voting/undo_like.rs
Normal file
55
crates/apub_receive/src/activities/voting/undo_like.rs
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
use crate::activities::{
|
||||||
|
verify_activity,
|
||||||
|
verify_person_in_community,
|
||||||
|
voting::{like::LikePostOrComment, receive_undo_like_or_dislike},
|
||||||
|
};
|
||||||
|
use activitystreams::activity::kind::UndoType;
|
||||||
|
use lemmy_apub_lib::{verify_urls_match, ActivityCommonFields, ActivityHandler, PublicUrl};
|
||||||
|
use lemmy_utils::LemmyError;
|
||||||
|
use lemmy_websocket::LemmyContext;
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct UndoLikePostOrComment {
|
||||||
|
to: PublicUrl,
|
||||||
|
object: LikePostOrComment,
|
||||||
|
cc: [Url; 1],
|
||||||
|
#[serde(rename = "type")]
|
||||||
|
kind: UndoType,
|
||||||
|
#[serde(flatten)]
|
||||||
|
common: ActivityCommonFields,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait(?Send)]
|
||||||
|
impl ActivityHandler for UndoLikePostOrComment {
|
||||||
|
async fn verify(
|
||||||
|
&self,
|
||||||
|
context: &LemmyContext,
|
||||||
|
request_counter: &mut i32,
|
||||||
|
) -> Result<(), LemmyError> {
|
||||||
|
verify_activity(self.common())?;
|
||||||
|
verify_person_in_community(&self.common.actor, &self.cc, context, request_counter).await?;
|
||||||
|
verify_urls_match(&self.common.actor, &self.object.common().actor)?;
|
||||||
|
self.object.verify(context, request_counter).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn receive(
|
||||||
|
&self,
|
||||||
|
context: &LemmyContext,
|
||||||
|
request_counter: &mut i32,
|
||||||
|
) -> Result<(), LemmyError> {
|
||||||
|
receive_undo_like_or_dislike(
|
||||||
|
&self.common.actor,
|
||||||
|
&self.object.object,
|
||||||
|
context,
|
||||||
|
request_counter,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
fn common(&self) -> &ActivityCommonFields {
|
||||||
|
&self.common
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,10 +1,16 @@
|
||||||
use crate::http::{create_apub_response, create_apub_tombstone_response};
|
use crate::http::{
|
||||||
|
create_apub_response,
|
||||||
|
create_apub_tombstone_response,
|
||||||
|
inbox_enums::GroupInboxActivities,
|
||||||
|
payload_to_string,
|
||||||
|
receive_activity,
|
||||||
|
};
|
||||||
use activitystreams::{
|
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>,
|
||||||
|
|
100
crates/apub_receive/src/http/inbox_enums.rs
Normal file
100
crates/apub_receive/src/http/inbox_enums.rs
Normal file
|
@ -0,0 +1,100 @@
|
||||||
|
use crate::activities::{
|
||||||
|
comment::{create::CreateComment, update::UpdateComment},
|
||||||
|
community::{
|
||||||
|
add_mod::AddMod,
|
||||||
|
announce::AnnounceActivity,
|
||||||
|
block_user::BlockUserFromCommunity,
|
||||||
|
undo_block_user::UndoBlockUserFromCommunity,
|
||||||
|
update::UpdateCommunity,
|
||||||
|
},
|
||||||
|
deletion::{delete::DeletePostCommentOrCommunity, undo_delete::UndoDeletePostCommentOrCommunity},
|
||||||
|
following::{accept::AcceptFollowCommunity, follow::FollowCommunity, undo::UndoFollowCommunity},
|
||||||
|
post::{create::CreatePost, update::UpdatePost},
|
||||||
|
private_message::{
|
||||||
|
create::CreatePrivateMessage,
|
||||||
|
delete::DeletePrivateMessage,
|
||||||
|
undo_delete::UndoDeletePrivateMessage,
|
||||||
|
update::UpdatePrivateMessage,
|
||||||
|
},
|
||||||
|
removal::{
|
||||||
|
remove::RemovePostCommentCommunityOrMod,
|
||||||
|
undo_remove::UndoRemovePostCommentOrCommunity,
|
||||||
|
},
|
||||||
|
voting::{
|
||||||
|
dislike::DislikePostOrComment,
|
||||||
|
like::LikePostOrComment,
|
||||||
|
undo_dislike::UndoDislikePostOrComment,
|
||||||
|
undo_like::UndoLikePostOrComment,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
use lemmy_apub_lib::{ActivityCommonFields, ActivityHandler};
|
||||||
|
use lemmy_utils::LemmyError;
|
||||||
|
use lemmy_websocket::LemmyContext;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize, Serialize, ActivityHandler)]
|
||||||
|
#[serde(untagged)]
|
||||||
|
pub enum PersonInboxActivities {
|
||||||
|
AcceptFollowCommunity(AcceptFollowCommunity),
|
||||||
|
CreatePrivateMessage(CreatePrivateMessage),
|
||||||
|
UpdatePrivateMessage(UpdatePrivateMessage),
|
||||||
|
DeletePrivateMessage(DeletePrivateMessage),
|
||||||
|
UndoDeletePrivateMessage(UndoDeletePrivateMessage),
|
||||||
|
AnnounceActivity(Box<AnnounceActivity>),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize, Serialize, ActivityHandler)]
|
||||||
|
#[serde(untagged)]
|
||||||
|
pub enum GroupInboxActivities {
|
||||||
|
FollowCommunity(FollowCommunity),
|
||||||
|
UndoFollowCommunity(UndoFollowCommunity),
|
||||||
|
CreateComment(CreateComment),
|
||||||
|
UpdateComment(UpdateComment),
|
||||||
|
CreatePost(CreatePost),
|
||||||
|
UpdatePost(UpdatePost),
|
||||||
|
LikePostOrComment(LikePostOrComment),
|
||||||
|
DislikePostOrComment(DislikePostOrComment),
|
||||||
|
UndoLikePostOrComment(UndoLikePostOrComment),
|
||||||
|
UndoDislikePostOrComment(UndoDislikePostOrComment),
|
||||||
|
DeletePostCommentOrCommunity(DeletePostCommentOrCommunity),
|
||||||
|
UndoDeletePostCommentOrCommunity(UndoDeletePostCommentOrCommunity),
|
||||||
|
RemovePostCommentOrCommunity(RemovePostCommentCommunityOrMod),
|
||||||
|
UndoRemovePostCommentOrCommunity(UndoRemovePostCommentOrCommunity),
|
||||||
|
UpdateCommunity(Box<UpdateCommunity>),
|
||||||
|
BlockUserFromCommunity(BlockUserFromCommunity),
|
||||||
|
UndoBlockUserFromCommunity(UndoBlockUserFromCommunity),
|
||||||
|
AddMod(AddMod),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize, Serialize, ActivityHandler)]
|
||||||
|
#[serde(untagged)]
|
||||||
|
pub enum SharedInboxActivities {
|
||||||
|
// received by group
|
||||||
|
FollowCommunity(FollowCommunity),
|
||||||
|
UndoFollowCommunity(UndoFollowCommunity),
|
||||||
|
CreateComment(CreateComment),
|
||||||
|
UpdateComment(UpdateComment),
|
||||||
|
CreatePost(CreatePost),
|
||||||
|
UpdatePost(UpdatePost),
|
||||||
|
LikePostOrComment(LikePostOrComment),
|
||||||
|
DislikePostOrComment(DislikePostOrComment),
|
||||||
|
UndoDislikePostOrComment(UndoDislikePostOrComment),
|
||||||
|
UndoLikePostOrComment(UndoLikePostOrComment),
|
||||||
|
DeletePostCommentOrCommunity(DeletePostCommentOrCommunity),
|
||||||
|
UndoDeletePostCommentOrCommunity(UndoDeletePostCommentOrCommunity),
|
||||||
|
RemovePostCommentOrCommunity(RemovePostCommentCommunityOrMod),
|
||||||
|
UndoRemovePostCommentOrCommunity(UndoRemovePostCommentOrCommunity),
|
||||||
|
UpdateCommunity(Box<UpdateCommunity>),
|
||||||
|
BlockUserFromCommunity(BlockUserFromCommunity),
|
||||||
|
UndoBlockUserFromCommunity(UndoBlockUserFromCommunity),
|
||||||
|
AddMod(AddMod),
|
||||||
|
// received by person
|
||||||
|
AcceptFollowCommunity(AcceptFollowCommunity),
|
||||||
|
// Note, pm activities need to be at the end, otherwise comments will end up here. We can probably
|
||||||
|
// avoid this problem by replacing createpm.object with our own struct, instead of NoteExt.
|
||||||
|
CreatePrivateMessage(CreatePrivateMessage),
|
||||||
|
UpdatePrivateMessage(UpdatePrivateMessage),
|
||||||
|
DeletePrivateMessage(DeletePrivateMessage),
|
||||||
|
UndoDeletePrivateMessage(UndoDeletePrivateMessage),
|
||||||
|
AnnounceActivity(Box<AnnounceActivity>),
|
||||||
|
}
|
|
@ -1,18 +1,104 @@
|
||||||
use actix_web::{body::Body, web, HttpResponse};
|
use crate::http::inbox_enums::SharedInboxActivities;
|
||||||
|
use actix_web::{
|
||||||
|
body::Body,
|
||||||
|
web,
|
||||||
|
web::{Bytes, BytesMut, Payload},
|
||||||
|
HttpRequest,
|
||||||
|
HttpResponse,
|
||||||
|
};
|
||||||
|
use anyhow::{anyhow, Context};
|
||||||
|
use futures::StreamExt;
|
||||||
use http::StatusCode;
|
use 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(())
|
||||||
|
}
|
||||||
|
|
|
@ -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>,
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
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,
|
||||||
|
@ -9,14 +9,9 @@ use crate::{
|
||||||
get_apub_community_outbox,
|
get_apub_community_outbox,
|
||||||
},
|
},
|
||||||
get_activity,
|
get_activity,
|
||||||
person::{get_apub_person_http, get_apub_person_inbox, get_apub_person_outbox},
|
person::{get_apub_person_http, get_apub_person_inbox, get_apub_person_outbox, person_inbox},
|
||||||
post::get_apub_post,
|
post::get_apub_post,
|
||||||
},
|
shared_inbox,
|
||||||
inbox::{
|
|
||||||
community_inbox::community_inbox,
|
|
||||||
person_inbox::person_inbox,
|
|
||||||
shared_inbox::shared_inbox,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
use actix_web::*;
|
use actix_web::*;
|
||||||
use http_signature_normalization_actix::digest::middleware::VerifyDigest;
|
use http_signature_normalization_actix::digest::middleware::VerifyDigest;
|
|
@ -1,346 +0,0 @@
|
||||||
use crate::{
|
|
||||||
activities::receive::verify_activity_domains_valid,
|
|
||||||
inbox::{
|
|
||||||
assert_activity_not_local,
|
|
||||||
get_activity_id,
|
|
||||||
inbox_verify_http_signature,
|
|
||||||
is_activity_already_known,
|
|
||||||
receive_for_community::{
|
|
||||||
receive_add_for_community,
|
|
||||||
receive_block_user_for_community,
|
|
||||||
receive_create_for_community,
|
|
||||||
receive_delete_for_community,
|
|
||||||
receive_dislike_for_community,
|
|
||||||
receive_like_for_community,
|
|
||||||
receive_remove_for_community,
|
|
||||||
receive_undo_for_community,
|
|
||||||
receive_update_for_community,
|
|
||||||
},
|
|
||||||
verify_is_addressed_to_public,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
use activitystreams::{
|
|
||||||
activity::{kind::FollowType, ActorAndObject, Follow, Undo},
|
|
||||||
base::AnyBase,
|
|
||||||
prelude::*,
|
|
||||||
};
|
|
||||||
use actix_web::{web, HttpRequest, HttpResponse};
|
|
||||||
use anyhow::{anyhow, Context};
|
|
||||||
use lemmy_api_common::blocking;
|
|
||||||
use lemmy_apub::{
|
|
||||||
check_community_or_site_ban,
|
|
||||||
get_activity_to_and_cc,
|
|
||||||
insert_activity,
|
|
||||||
ActorType,
|
|
||||||
CommunityType,
|
|
||||||
};
|
|
||||||
use lemmy_db_queries::{source::community::Community_, ApubObject, Followable};
|
|
||||||
use lemmy_db_schema::source::{
|
|
||||||
community::{Community, CommunityFollower, CommunityFollowerForm},
|
|
||||||
person::Person,
|
|
||||||
};
|
|
||||||
use lemmy_utils::{location_info, LemmyError};
|
|
||||||
use lemmy_websocket::LemmyContext;
|
|
||||||
use log::info;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use std::fmt::Debug;
|
|
||||||
use url::Url;
|
|
||||||
|
|
||||||
/// Allowed activities for community inbox.
|
|
||||||
#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, Deserialize, Serialize)]
|
|
||||||
#[serde(rename_all = "PascalCase")]
|
|
||||||
pub enum CommunityValidTypes {
|
|
||||||
Follow, // follow request from a person
|
|
||||||
Undo, // unfollow from a person
|
|
||||||
Create, // create post or comment
|
|
||||||
Update, // update post or comment
|
|
||||||
Like, // upvote post or comment
|
|
||||||
Dislike, // downvote post or comment
|
|
||||||
Delete, // post or comment deleted by creator
|
|
||||||
Remove, // post or comment removed by mod or admin, or mod removed from community
|
|
||||||
Add, // mod added to community
|
|
||||||
Block, // user blocked by community
|
|
||||||
}
|
|
||||||
|
|
||||||
pub type CommunityAcceptedActivities = ActorAndObject<CommunityValidTypes>;
|
|
||||||
|
|
||||||
/// Handler for all incoming receive to community inboxes.
|
|
||||||
pub async fn community_inbox(
|
|
||||||
request: HttpRequest,
|
|
||||||
input: web::Json<CommunityAcceptedActivities>,
|
|
||||||
path: web::Path<String>,
|
|
||||||
context: web::Data<LemmyContext>,
|
|
||||||
) -> Result<HttpResponse, LemmyError> {
|
|
||||||
let activity = input.into_inner();
|
|
||||||
// First of all check the http signature
|
|
||||||
let request_counter = &mut 0;
|
|
||||||
let actor = inbox_verify_http_signature(&activity, &context, request, request_counter).await?;
|
|
||||||
|
|
||||||
// Do nothing if we received the same activity before
|
|
||||||
let activity_id = get_activity_id(&activity, &actor.actor_id())?;
|
|
||||||
if is_activity_already_known(context.pool(), &activity_id).await? {
|
|
||||||
return Ok(HttpResponse::Ok().finish());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if the activity is actually meant for us
|
|
||||||
let path = path.into_inner();
|
|
||||||
let community = blocking(context.pool(), move |conn| {
|
|
||||||
Community::read_from_name(conn, &path)
|
|
||||||
})
|
|
||||||
.await??;
|
|
||||||
let to_and_cc = get_activity_to_and_cc(&activity);
|
|
||||||
if !to_and_cc.contains(&community.actor_id()) {
|
|
||||||
return Err(anyhow!("Activity delivered to wrong community").into());
|
|
||||||
}
|
|
||||||
|
|
||||||
assert_activity_not_local(&activity)?;
|
|
||||||
insert_activity(&activity_id, activity.clone(), false, true, context.pool()).await?;
|
|
||||||
|
|
||||||
community_receive_message(
|
|
||||||
activity.clone(),
|
|
||||||
community.clone(),
|
|
||||||
actor.as_ref(),
|
|
||||||
&context,
|
|
||||||
request_counter,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Receives Follow, Undo/Follow, post actions, comment actions (including votes)
|
|
||||||
pub(crate) async fn community_receive_message(
|
|
||||||
activity: CommunityAcceptedActivities,
|
|
||||||
to_community: Community,
|
|
||||||
actor: &dyn ActorType,
|
|
||||||
context: &LemmyContext,
|
|
||||||
request_counter: &mut i32,
|
|
||||||
) -> Result<HttpResponse, LemmyError> {
|
|
||||||
// Only persons can send activities to the community, so we can get the actor as person
|
|
||||||
// unconditionally.
|
|
||||||
let actor_id = actor.actor_id();
|
|
||||||
let person = blocking(context.pool(), move |conn| {
|
|
||||||
Person::read_from_apub_id(conn, &actor_id.into())
|
|
||||||
})
|
|
||||||
.await??;
|
|
||||||
check_community_or_site_ban(&person, to_community.id, context.pool()).await?;
|
|
||||||
|
|
||||||
info!(
|
|
||||||
"Community {} received activity {} from {}",
|
|
||||||
to_community.name,
|
|
||||||
&activity
|
|
||||||
.id_unchecked()
|
|
||||||
.context(location_info!())?
|
|
||||||
.to_string(),
|
|
||||||
&person.actor_id().to_string()
|
|
||||||
);
|
|
||||||
|
|
||||||
let any_base = activity.clone().into_any_base()?;
|
|
||||||
let actor_url = actor.actor_id();
|
|
||||||
let activity_kind = activity.kind().context(location_info!())?;
|
|
||||||
let do_announce = match activity_kind {
|
|
||||||
CommunityValidTypes::Follow => {
|
|
||||||
Box::pin(handle_follow(
|
|
||||||
any_base.clone(),
|
|
||||||
person,
|
|
||||||
&to_community,
|
|
||||||
context,
|
|
||||||
))
|
|
||||||
.await?;
|
|
||||||
false
|
|
||||||
}
|
|
||||||
CommunityValidTypes::Undo => {
|
|
||||||
Box::pin(handle_undo(
|
|
||||||
context,
|
|
||||||
activity.clone(),
|
|
||||||
actor_url,
|
|
||||||
&to_community,
|
|
||||||
request_counter,
|
|
||||||
))
|
|
||||||
.await?
|
|
||||||
}
|
|
||||||
CommunityValidTypes::Create => {
|
|
||||||
Box::pin(receive_create_for_community(
|
|
||||||
context,
|
|
||||||
any_base.clone(),
|
|
||||||
&actor_url,
|
|
||||||
request_counter,
|
|
||||||
))
|
|
||||||
.await?;
|
|
||||||
true
|
|
||||||
}
|
|
||||||
CommunityValidTypes::Update => {
|
|
||||||
Box::pin(receive_update_for_community(
|
|
||||||
context,
|
|
||||||
any_base.clone(),
|
|
||||||
None,
|
|
||||||
&actor_url,
|
|
||||||
request_counter,
|
|
||||||
))
|
|
||||||
.await?;
|
|
||||||
true
|
|
||||||
}
|
|
||||||
CommunityValidTypes::Like => {
|
|
||||||
Box::pin(receive_like_for_community(
|
|
||||||
context,
|
|
||||||
any_base.clone(),
|
|
||||||
&actor_url,
|
|
||||||
request_counter,
|
|
||||||
))
|
|
||||||
.await?;
|
|
||||||
true
|
|
||||||
}
|
|
||||||
CommunityValidTypes::Dislike => {
|
|
||||||
Box::pin(receive_dislike_for_community(
|
|
||||||
context,
|
|
||||||
any_base.clone(),
|
|
||||||
&actor_url,
|
|
||||||
request_counter,
|
|
||||||
))
|
|
||||||
.await?;
|
|
||||||
true
|
|
||||||
}
|
|
||||||
CommunityValidTypes::Delete => {
|
|
||||||
Box::pin(receive_delete_for_community(
|
|
||||||
context,
|
|
||||||
any_base.clone(),
|
|
||||||
None,
|
|
||||||
&actor_url,
|
|
||||||
request_counter,
|
|
||||||
))
|
|
||||||
.await?;
|
|
||||||
true
|
|
||||||
}
|
|
||||||
CommunityValidTypes::Add => {
|
|
||||||
Box::pin(receive_add_for_community(
|
|
||||||
context,
|
|
||||||
any_base.clone(),
|
|
||||||
None,
|
|
||||||
request_counter,
|
|
||||||
))
|
|
||||||
.await?;
|
|
||||||
true
|
|
||||||
}
|
|
||||||
CommunityValidTypes::Remove => {
|
|
||||||
Box::pin(receive_remove_for_community(
|
|
||||||
context,
|
|
||||||
any_base.clone(),
|
|
||||||
None,
|
|
||||||
request_counter,
|
|
||||||
))
|
|
||||||
.await?;
|
|
||||||
true
|
|
||||||
}
|
|
||||||
CommunityValidTypes::Block => {
|
|
||||||
Box::pin(receive_block_user_for_community(
|
|
||||||
context,
|
|
||||||
any_base.clone(),
|
|
||||||
None,
|
|
||||||
request_counter,
|
|
||||||
))
|
|
||||||
.await?;
|
|
||||||
true
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if do_announce {
|
|
||||||
// Check again that the activity is public, just to be sure
|
|
||||||
verify_is_addressed_to_public(&activity)?;
|
|
||||||
let mut object_actor = activity.object().clone().single_xsd_any_uri();
|
|
||||||
// If activity is something like Undo/Block, we need to access activity.object.object
|
|
||||||
if object_actor.is_none() {
|
|
||||||
object_actor = activity
|
|
||||||
.object()
|
|
||||||
.as_one()
|
|
||||||
.map(|a| ActorAndObject::from_any_base(a.to_owned()).ok())
|
|
||||||
.flatten()
|
|
||||||
.flatten()
|
|
||||||
.map(|a: ActorAndObject<CommunityValidTypes>| a.object().to_owned().single_xsd_any_uri())
|
|
||||||
.flatten();
|
|
||||||
}
|
|
||||||
to_community
|
|
||||||
.send_announce(activity.into_any_base()?, object_actor, context)
|
|
||||||
.await?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(HttpResponse::Ok().finish())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Handle a follow request from a remote person, adding the person as follower and returning an
|
|
||||||
/// Accept activity.
|
|
||||||
async fn handle_follow(
|
|
||||||
activity: AnyBase,
|
|
||||||
person: Person,
|
|
||||||
community: &Community,
|
|
||||||
context: &LemmyContext,
|
|
||||||
) -> Result<HttpResponse, LemmyError> {
|
|
||||||
let follow = Follow::from_any_base(activity)?.context(location_info!())?;
|
|
||||||
verify_activity_domains_valid(&follow, &person.actor_id(), false)?;
|
|
||||||
|
|
||||||
let community_follower_form = CommunityFollowerForm {
|
|
||||||
community_id: community.id,
|
|
||||||
person_id: person.id,
|
|
||||||
pending: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
// This will fail if they're already a follower, but ignore the error.
|
|
||||||
blocking(context.pool(), move |conn| {
|
|
||||||
CommunityFollower::follow(conn, &community_follower_form).ok()
|
|
||||||
})
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
community.send_accept_follow(follow, context).await?;
|
|
||||||
|
|
||||||
Ok(HttpResponse::Ok().finish())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn handle_undo(
|
|
||||||
context: &LemmyContext,
|
|
||||||
activity: CommunityAcceptedActivities,
|
|
||||||
actor_url: Url,
|
|
||||||
to_community: &Community,
|
|
||||||
request_counter: &mut i32,
|
|
||||||
) -> Result<bool, LemmyError> {
|
|
||||||
let inner_kind = activity
|
|
||||||
.object()
|
|
||||||
.is_single_kind(&FollowType::Follow.to_string());
|
|
||||||
let any_base = activity.into_any_base()?;
|
|
||||||
if inner_kind {
|
|
||||||
handle_undo_follow(any_base, actor_url, to_community, context).await?;
|
|
||||||
Ok(false)
|
|
||||||
} else {
|
|
||||||
receive_undo_for_community(context, any_base, None, &actor_url, request_counter).await?;
|
|
||||||
Ok(true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Handle `Undo/Follow` from a person, removing the person from followers list.
|
|
||||||
async fn handle_undo_follow(
|
|
||||||
activity: AnyBase,
|
|
||||||
person_url: Url,
|
|
||||||
community: &Community,
|
|
||||||
context: &LemmyContext,
|
|
||||||
) -> Result<(), LemmyError> {
|
|
||||||
let undo = Undo::from_any_base(activity)?.context(location_info!())?;
|
|
||||||
verify_activity_domains_valid(&undo, &person_url, true)?;
|
|
||||||
|
|
||||||
let object = undo.object().to_owned().one().context(location_info!())?;
|
|
||||||
let follow = Follow::from_any_base(object)?.context(location_info!())?;
|
|
||||||
verify_activity_domains_valid(&follow, &person_url, false)?;
|
|
||||||
|
|
||||||
let person = blocking(context.pool(), move |conn| {
|
|
||||||
Person::read_from_apub_id(conn, &person_url.into())
|
|
||||||
})
|
|
||||||
.await??;
|
|
||||||
let community_follower_form = CommunityFollowerForm {
|
|
||||||
community_id: community.id,
|
|
||||||
person_id: person.id,
|
|
||||||
pending: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
// This will fail if they aren't a follower, but ignore the error.
|
|
||||||
blocking(context.pool(), move |conn| {
|
|
||||||
CommunityFollower::unfollow(conn, &community_follower_form).ok()
|
|
||||||
})
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
|
@ -1,153 +0,0 @@
|
||||||
use activitystreams::{
|
|
||||||
activity::ActorAndObjectRefExt,
|
|
||||||
base::{AsBase, BaseExt, Extends},
|
|
||||||
object::AsObject,
|
|
||||||
public,
|
|
||||||
};
|
|
||||||
use actix_web::HttpRequest;
|
|
||||||
use anyhow::{anyhow, Context};
|
|
||||||
use lemmy_api_common::blocking;
|
|
||||||
use lemmy_apub::{
|
|
||||||
check_is_apub_id_valid,
|
|
||||||
extensions::signatures::verify_signature,
|
|
||||||
fetcher::get_or_fetch_and_upsert_actor,
|
|
||||||
get_activity_to_and_cc,
|
|
||||||
ActorType,
|
|
||||||
};
|
|
||||||
use lemmy_db_queries::{
|
|
||||||
source::{activity::Activity_, community::Community_},
|
|
||||||
ApubObject,
|
|
||||||
DbPool,
|
|
||||||
};
|
|
||||||
use lemmy_db_schema::source::{activity::Activity, community::Community, person::Person};
|
|
||||||
use lemmy_utils::{location_info, settings::structs::Settings, LemmyError};
|
|
||||||
use lemmy_websocket::LemmyContext;
|
|
||||||
use serde::Serialize;
|
|
||||||
use std::fmt::Debug;
|
|
||||||
use url::Url;
|
|
||||||
|
|
||||||
pub mod community_inbox;
|
|
||||||
pub mod person_inbox;
|
|
||||||
pub(crate) mod receive_for_community;
|
|
||||||
pub mod shared_inbox;
|
|
||||||
|
|
||||||
pub(crate) fn get_activity_id<T, Kind>(activity: &T, creator_uri: &Url) -> Result<Url, LemmyError>
|
|
||||||
where
|
|
||||||
T: BaseExt<Kind> + Extends<Kind> + Debug,
|
|
||||||
Kind: Serialize,
|
|
||||||
<T as Extends<Kind>>::Error: From<serde_json::Error> + Send + Sync + 'static,
|
|
||||||
{
|
|
||||||
let creator_domain = creator_uri.host_str().context(location_info!())?;
|
|
||||||
let activity_id = activity.id(creator_domain)?;
|
|
||||||
Ok(activity_id.context(location_info!())?.to_owned())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) async fn is_activity_already_known(
|
|
||||||
pool: &DbPool,
|
|
||||||
activity_id: &Url,
|
|
||||||
) -> Result<bool, LemmyError> {
|
|
||||||
let activity_id = activity_id.to_owned().into();
|
|
||||||
let existing = blocking(pool, move |conn| {
|
|
||||||
Activity::read_from_apub_id(conn, &activity_id)
|
|
||||||
})
|
|
||||||
.await?;
|
|
||||||
match existing {
|
|
||||||
Ok(_) => Ok(true),
|
|
||||||
Err(_) => Ok(false),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn verify_is_addressed_to_public<T, Kind>(activity: &T) -> Result<(), LemmyError>
|
|
||||||
where
|
|
||||||
T: AsBase<Kind> + AsObject<Kind> + ActorAndObjectRefExt,
|
|
||||||
{
|
|
||||||
let to_and_cc = get_activity_to_and_cc(activity);
|
|
||||||
if to_and_cc.contains(&public()) {
|
|
||||||
Ok(())
|
|
||||||
} else {
|
|
||||||
Err(anyhow!("Activity is not addressed to public").into())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) async fn inbox_verify_http_signature<T, Kind>(
|
|
||||||
activity: &T,
|
|
||||||
context: &LemmyContext,
|
|
||||||
request: HttpRequest,
|
|
||||||
request_counter: &mut i32,
|
|
||||||
) -> Result<Box<dyn ActorType>, LemmyError>
|
|
||||||
where
|
|
||||||
T: AsObject<Kind> + ActorAndObjectRefExt + Extends<Kind> + AsBase<Kind>,
|
|
||||||
Kind: Serialize,
|
|
||||||
<T as Extends<Kind>>::Error: From<serde_json::Error> + Send + Sync + 'static,
|
|
||||||
{
|
|
||||||
let actor_id = activity
|
|
||||||
.actor()?
|
|
||||||
.to_owned()
|
|
||||||
.single_xsd_any_uri()
|
|
||||||
.context(location_info!())?;
|
|
||||||
check_is_apub_id_valid(&actor_id, false)?;
|
|
||||||
let actor = get_or_fetch_and_upsert_actor(&actor_id, context, request_counter).await?;
|
|
||||||
verify_signature(&request, actor.as_ref())?;
|
|
||||||
Ok(actor)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns true if `to_and_cc` contains at least one local user.
|
|
||||||
pub(crate) async fn is_addressed_to_local_person(
|
|
||||||
to_and_cc: &[Url],
|
|
||||||
pool: &DbPool,
|
|
||||||
) -> Result<bool, LemmyError> {
|
|
||||||
for url in to_and_cc {
|
|
||||||
let url = url.to_owned();
|
|
||||||
let person = blocking(pool, move |conn| {
|
|
||||||
Person::read_from_apub_id(conn, &url.into())
|
|
||||||
})
|
|
||||||
.await?;
|
|
||||||
if let Ok(u) = person {
|
|
||||||
if u.local {
|
|
||||||
return Ok(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// If `to_and_cc` contains the followers collection of a remote community, returns this community
|
|
||||||
/// (like `https://example.com/c/main/followers`)
|
|
||||||
pub(crate) async fn is_addressed_to_community_followers(
|
|
||||||
to_and_cc: &[Url],
|
|
||||||
pool: &DbPool,
|
|
||||||
) -> Result<Option<Community>, LemmyError> {
|
|
||||||
for url in to_and_cc {
|
|
||||||
let url = url.to_owned().into();
|
|
||||||
let community = blocking(pool, move |conn| {
|
|
||||||
// ignore errors here, because the current url might not actually be a followers url
|
|
||||||
Community::read_from_followers_url(conn, &url).ok()
|
|
||||||
})
|
|
||||||
.await?;
|
|
||||||
if let Some(c) = community {
|
|
||||||
if !c.local {
|
|
||||||
return Ok(Some(c));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(None)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(in crate::inbox) fn assert_activity_not_local<T, Kind>(activity: &T) -> Result<(), LemmyError>
|
|
||||||
where
|
|
||||||
T: BaseExt<Kind> + Debug,
|
|
||||||
{
|
|
||||||
let id = activity.id_unchecked().context(location_info!())?;
|
|
||||||
let activity_domain = id.domain().context(location_info!())?;
|
|
||||||
|
|
||||||
if activity_domain == Settings::get().hostname() {
|
|
||||||
return Err(
|
|
||||||
anyhow!(
|
|
||||||
"Error: received activity which was sent by local instance: {:?}",
|
|
||||||
activity
|
|
||||||
)
|
|
||||||
.into(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
|
@ -1,515 +0,0 @@
|
||||||
use crate::{
|
|
||||||
activities::receive::{
|
|
||||||
comment::{receive_create_comment, receive_update_comment},
|
|
||||||
community::{
|
|
||||||
receive_delete_community,
|
|
||||||
receive_remove_community,
|
|
||||||
receive_undo_delete_community,
|
|
||||||
receive_undo_remove_community,
|
|
||||||
},
|
|
||||||
private_message::{
|
|
||||||
receive_create_private_message,
|
|
||||||
receive_delete_private_message,
|
|
||||||
receive_undo_delete_private_message,
|
|
||||||
receive_update_private_message,
|
|
||||||
},
|
|
||||||
receive_unhandled_activity,
|
|
||||||
verify_activity_domains_valid,
|
|
||||||
},
|
|
||||||
inbox::{
|
|
||||||
assert_activity_not_local,
|
|
||||||
get_activity_id,
|
|
||||||
inbox_verify_http_signature,
|
|
||||||
is_activity_already_known,
|
|
||||||
is_addressed_to_community_followers,
|
|
||||||
is_addressed_to_local_person,
|
|
||||||
receive_for_community::{
|
|
||||||
receive_add_for_community,
|
|
||||||
receive_block_user_for_community,
|
|
||||||
receive_create_for_community,
|
|
||||||
receive_delete_for_community,
|
|
||||||
receive_dislike_for_community,
|
|
||||||
receive_like_for_community,
|
|
||||||
receive_remove_for_community,
|
|
||||||
receive_undo_for_community,
|
|
||||||
receive_update_for_community,
|
|
||||||
},
|
|
||||||
verify_is_addressed_to_public,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
use activitystreams::{
|
|
||||||
activity::{Accept, ActorAndObject, Announce, Create, Delete, Follow, Remove, Undo, Update},
|
|
||||||
base::AnyBase,
|
|
||||||
prelude::*,
|
|
||||||
};
|
|
||||||
use actix_web::{web, HttpRequest, HttpResponse};
|
|
||||||
use anyhow::{anyhow, Context};
|
|
||||||
use diesel::NotFound;
|
|
||||||
use lemmy_api_common::blocking;
|
|
||||||
use lemmy_apub::{
|
|
||||||
check_is_apub_id_valid,
|
|
||||||
fetcher::community::get_or_fetch_and_upsert_community,
|
|
||||||
get_activity_to_and_cc,
|
|
||||||
insert_activity,
|
|
||||||
ActorType,
|
|
||||||
};
|
|
||||||
use lemmy_db_queries::{source::person::Person_, ApubObject, Followable};
|
|
||||||
use lemmy_db_schema::source::{
|
|
||||||
community::{Community, CommunityFollower},
|
|
||||||
person::Person,
|
|
||||||
private_message::PrivateMessage,
|
|
||||||
};
|
|
||||||
use lemmy_utils::{location_info, LemmyError};
|
|
||||||
use lemmy_websocket::LemmyContext;
|
|
||||||
use log::info;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use std::fmt::Debug;
|
|
||||||
use strum_macros::EnumString;
|
|
||||||
use url::Url;
|
|
||||||
|
|
||||||
/// Allowed activities for person inbox.
|
|
||||||
#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, Deserialize, Serialize)]
|
|
||||||
#[serde(rename_all = "PascalCase")]
|
|
||||||
pub enum PersonValidTypes {
|
|
||||||
Accept, // community accepted our follow request
|
|
||||||
Create, // create private message
|
|
||||||
Update, // edit private message
|
|
||||||
Delete, // private message or community deleted by creator
|
|
||||||
Undo, // private message or community restored
|
|
||||||
Remove, // community removed by admin
|
|
||||||
Announce, // post, comment or vote in community
|
|
||||||
}
|
|
||||||
|
|
||||||
pub type PersonAcceptedActivities = ActorAndObject<PersonValidTypes>;
|
|
||||||
|
|
||||||
/// Handler for all incoming activities to person inboxes.
|
|
||||||
pub async fn person_inbox(
|
|
||||||
request: HttpRequest,
|
|
||||||
input: web::Json<PersonAcceptedActivities>,
|
|
||||||
path: web::Path<String>,
|
|
||||||
context: web::Data<LemmyContext>,
|
|
||||||
) -> Result<HttpResponse, LemmyError> {
|
|
||||||
let activity = input.into_inner();
|
|
||||||
// First of all check the http signature
|
|
||||||
let request_counter = &mut 0;
|
|
||||||
let actor = inbox_verify_http_signature(&activity, &context, request, request_counter).await?;
|
|
||||||
|
|
||||||
// Do nothing if we received the same activity before
|
|
||||||
let activity_id = get_activity_id(&activity, &actor.actor_id())?;
|
|
||||||
if is_activity_already_known(context.pool(), &activity_id).await? {
|
|
||||||
return Ok(HttpResponse::Ok().finish());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if the activity is actually meant for us
|
|
||||||
let username = path.into_inner();
|
|
||||||
let person = blocking(context.pool(), move |conn| {
|
|
||||||
Person::find_by_name(conn, &username)
|
|
||||||
})
|
|
||||||
.await??;
|
|
||||||
let to_and_cc = get_activity_to_and_cc(&activity);
|
|
||||||
// TODO: we should also accept activities that are sent to community followers
|
|
||||||
if !to_and_cc.contains(&person.actor_id()) {
|
|
||||||
return Err(anyhow!("Activity delivered to wrong person").into());
|
|
||||||
}
|
|
||||||
|
|
||||||
assert_activity_not_local(&activity)?;
|
|
||||||
insert_activity(&activity_id, activity.clone(), false, true, context.pool()).await?;
|
|
||||||
|
|
||||||
person_receive_message(
|
|
||||||
activity.clone(),
|
|
||||||
Some(person.clone()),
|
|
||||||
actor.as_ref(),
|
|
||||||
&context,
|
|
||||||
request_counter,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Receives Accept/Follow, Announce, private messages and community (undo) remove, (undo) delete
|
|
||||||
pub(crate) async fn person_receive_message(
|
|
||||||
activity: PersonAcceptedActivities,
|
|
||||||
to_person: Option<Person>,
|
|
||||||
actor: &dyn ActorType,
|
|
||||||
context: &LemmyContext,
|
|
||||||
request_counter: &mut i32,
|
|
||||||
) -> Result<HttpResponse, LemmyError> {
|
|
||||||
is_for_person_inbox(context, &activity).await?;
|
|
||||||
|
|
||||||
info!(
|
|
||||||
"User received activity {:?} from {}",
|
|
||||||
&activity
|
|
||||||
.id_unchecked()
|
|
||||||
.context(location_info!())?
|
|
||||||
.to_string(),
|
|
||||||
&actor.actor_id().to_string()
|
|
||||||
);
|
|
||||||
|
|
||||||
let any_base = activity.clone().into_any_base()?;
|
|
||||||
let kind = activity.kind().context(location_info!())?;
|
|
||||||
let actor_url = actor.actor_id();
|
|
||||||
match kind {
|
|
||||||
PersonValidTypes::Accept => {
|
|
||||||
receive_accept(
|
|
||||||
context,
|
|
||||||
any_base,
|
|
||||||
actor,
|
|
||||||
to_person.expect("person provided"),
|
|
||||||
request_counter,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
}
|
|
||||||
PersonValidTypes::Announce => {
|
|
||||||
Box::pin(receive_announce(context, any_base, actor, request_counter)).await?
|
|
||||||
}
|
|
||||||
PersonValidTypes::Create => {
|
|
||||||
Box::pin(receive_create(
|
|
||||||
context,
|
|
||||||
any_base,
|
|
||||||
actor_url,
|
|
||||||
request_counter,
|
|
||||||
))
|
|
||||||
.await?
|
|
||||||
}
|
|
||||||
PersonValidTypes::Update => {
|
|
||||||
Box::pin(receive_update(
|
|
||||||
context,
|
|
||||||
any_base,
|
|
||||||
actor_url,
|
|
||||||
request_counter,
|
|
||||||
))
|
|
||||||
.await?
|
|
||||||
}
|
|
||||||
PersonValidTypes::Delete => {
|
|
||||||
Box::pin(receive_delete(
|
|
||||||
context,
|
|
||||||
any_base,
|
|
||||||
&actor_url,
|
|
||||||
request_counter,
|
|
||||||
))
|
|
||||||
.await?
|
|
||||||
}
|
|
||||||
PersonValidTypes::Undo => {
|
|
||||||
Box::pin(receive_undo(context, any_base, &actor_url, request_counter)).await?
|
|
||||||
}
|
|
||||||
PersonValidTypes::Remove => Box::pin(receive_remove(context, any_base, &actor_url)).await?,
|
|
||||||
};
|
|
||||||
|
|
||||||
// TODO: would be logical to move websocket notification code here
|
|
||||||
|
|
||||||
Ok(HttpResponse::Ok().finish())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns true if the activity is addressed directly to one or more local persons, or if it is
|
|
||||||
/// addressed to the followers collection of a remote community, and at least one local person follows
|
|
||||||
/// it.
|
|
||||||
async fn is_for_person_inbox(
|
|
||||||
context: &LemmyContext,
|
|
||||||
activity: &PersonAcceptedActivities,
|
|
||||||
) -> Result<(), LemmyError> {
|
|
||||||
let to_and_cc = get_activity_to_and_cc(activity);
|
|
||||||
// Check if it is addressed directly to any local person
|
|
||||||
if is_addressed_to_local_person(&to_and_cc, context.pool()).await? {
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if it is addressed to any followers collection of a remote community, and that the
|
|
||||||
// community has local followers.
|
|
||||||
let community = is_addressed_to_community_followers(&to_and_cc, context.pool()).await?;
|
|
||||||
if let Some(c) = community {
|
|
||||||
let community_id = c.id;
|
|
||||||
let has_local_followers = blocking(context.pool(), move |conn| {
|
|
||||||
CommunityFollower::has_local_followers(conn, community_id)
|
|
||||||
})
|
|
||||||
.await??;
|
|
||||||
if c.local {
|
|
||||||
return Err(
|
|
||||||
anyhow!("Remote activity cant be addressed to followers of local community").into(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if has_local_followers {
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Err(anyhow!("Not addressed for any local person").into())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Handle accepted follows.
|
|
||||||
async fn receive_accept(
|
|
||||||
context: &LemmyContext,
|
|
||||||
activity: AnyBase,
|
|
||||||
actor: &dyn ActorType,
|
|
||||||
person: Person,
|
|
||||||
request_counter: &mut i32,
|
|
||||||
) -> Result<(), LemmyError> {
|
|
||||||
let accept = Accept::from_any_base(activity)?.context(location_info!())?;
|
|
||||||
verify_activity_domains_valid(&accept, &actor.actor_id(), false)?;
|
|
||||||
|
|
||||||
let object = accept.object().to_owned().one().context(location_info!())?;
|
|
||||||
let follow = Follow::from_any_base(object)?.context(location_info!())?;
|
|
||||||
verify_activity_domains_valid(&follow, &person.actor_id(), false)?;
|
|
||||||
|
|
||||||
let community_uri = accept
|
|
||||||
.actor()?
|
|
||||||
.to_owned()
|
|
||||||
.single_xsd_any_uri()
|
|
||||||
.context(location_info!())?;
|
|
||||||
|
|
||||||
let community =
|
|
||||||
get_or_fetch_and_upsert_community(&community_uri, context, request_counter).await?;
|
|
||||||
|
|
||||||
let community_id = community.id;
|
|
||||||
let person_id = person.id;
|
|
||||||
// This will throw an error if no follow was requested
|
|
||||||
blocking(context.pool(), move |conn| {
|
|
||||||
CommunityFollower::follow_accepted(conn, community_id, person_id)
|
|
||||||
})
|
|
||||||
.await??;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(EnumString)]
|
|
||||||
enum AnnouncableActivities {
|
|
||||||
Create,
|
|
||||||
Update,
|
|
||||||
Like,
|
|
||||||
Dislike,
|
|
||||||
Delete,
|
|
||||||
Remove,
|
|
||||||
Undo,
|
|
||||||
Add,
|
|
||||||
Block,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Takes an announce and passes the inner activity to the appropriate handler.
|
|
||||||
pub async fn receive_announce(
|
|
||||||
context: &LemmyContext,
|
|
||||||
activity: AnyBase,
|
|
||||||
actor: &dyn ActorType,
|
|
||||||
request_counter: &mut i32,
|
|
||||||
) -> Result<(), LemmyError> {
|
|
||||||
let announce = Announce::from_any_base(activity)?.context(location_info!())?;
|
|
||||||
verify_activity_domains_valid(&announce, &actor.actor_id(), false)?;
|
|
||||||
verify_is_addressed_to_public(&announce)?;
|
|
||||||
|
|
||||||
let kind = announce
|
|
||||||
.object()
|
|
||||||
.as_single_kind_str()
|
|
||||||
.and_then(|s| s.parse().ok());
|
|
||||||
let inner_activity = announce
|
|
||||||
.object()
|
|
||||||
.to_owned()
|
|
||||||
.one()
|
|
||||||
.context(location_info!())?;
|
|
||||||
|
|
||||||
let inner_id = inner_activity.id().context(location_info!())?.to_owned();
|
|
||||||
check_is_apub_id_valid(&inner_id, false)?;
|
|
||||||
if is_activity_already_known(context.pool(), &inner_id).await? {
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
use AnnouncableActivities::*;
|
|
||||||
match kind {
|
|
||||||
Some(Create) => {
|
|
||||||
receive_create_for_community(context, inner_activity, &inner_id, request_counter).await
|
|
||||||
}
|
|
||||||
Some(Update) => {
|
|
||||||
receive_update_for_community(
|
|
||||||
context,
|
|
||||||
inner_activity,
|
|
||||||
Some(announce),
|
|
||||||
&inner_id,
|
|
||||||
request_counter,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
Some(Like) => {
|
|
||||||
receive_like_for_community(context, inner_activity, &inner_id, request_counter).await
|
|
||||||
}
|
|
||||||
Some(Dislike) => {
|
|
||||||
receive_dislike_for_community(context, inner_activity, &inner_id, request_counter).await
|
|
||||||
}
|
|
||||||
Some(Delete) => {
|
|
||||||
receive_delete_for_community(
|
|
||||||
context,
|
|
||||||
inner_activity,
|
|
||||||
Some(announce),
|
|
||||||
&inner_id,
|
|
||||||
request_counter,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
Some(Remove) => {
|
|
||||||
receive_remove_for_community(context, inner_activity, Some(announce), request_counter).await
|
|
||||||
}
|
|
||||||
Some(Undo) => {
|
|
||||||
receive_undo_for_community(
|
|
||||||
context,
|
|
||||||
inner_activity,
|
|
||||||
Some(announce),
|
|
||||||
&inner_id,
|
|
||||||
request_counter,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
Some(Add) => {
|
|
||||||
receive_add_for_community(context, inner_activity, Some(announce), request_counter).await
|
|
||||||
}
|
|
||||||
Some(Block) => {
|
|
||||||
receive_block_user_for_community(context, inner_activity, Some(announce), request_counter)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
_ => receive_unhandled_activity(inner_activity),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Receive either a new private message, or a new comment mention. We distinguish them by checking
|
|
||||||
/// whether the activity is public.
|
|
||||||
async fn receive_create(
|
|
||||||
context: &LemmyContext,
|
|
||||||
activity: AnyBase,
|
|
||||||
expected_domain: Url,
|
|
||||||
request_counter: &mut i32,
|
|
||||||
) -> Result<(), LemmyError> {
|
|
||||||
let create = Create::from_any_base(activity)?.context(location_info!())?;
|
|
||||||
verify_activity_domains_valid(&create, &expected_domain, true)?;
|
|
||||||
if verify_is_addressed_to_public(&create).is_ok() {
|
|
||||||
receive_create_comment(create, context, request_counter).await
|
|
||||||
} else {
|
|
||||||
receive_create_private_message(context, create, expected_domain, request_counter).await
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Receive either an updated private message, or an updated comment mention. We distinguish
|
|
||||||
/// them by checking whether the activity is public.
|
|
||||||
async fn receive_update(
|
|
||||||
context: &LemmyContext,
|
|
||||||
activity: AnyBase,
|
|
||||||
expected_domain: Url,
|
|
||||||
request_counter: &mut i32,
|
|
||||||
) -> Result<(), LemmyError> {
|
|
||||||
let update = Update::from_any_base(activity)?.context(location_info!())?;
|
|
||||||
verify_activity_domains_valid(&update, &expected_domain, true)?;
|
|
||||||
if verify_is_addressed_to_public(&update).is_ok() {
|
|
||||||
receive_update_comment(update, context, request_counter).await
|
|
||||||
} else {
|
|
||||||
receive_update_private_message(context, update, expected_domain, request_counter).await
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn receive_delete(
|
|
||||||
context: &LemmyContext,
|
|
||||||
any_base: AnyBase,
|
|
||||||
expected_domain: &Url,
|
|
||||||
request_counter: &mut i32,
|
|
||||||
) -> Result<(), LemmyError> {
|
|
||||||
use CommunityOrPrivateMessage::*;
|
|
||||||
|
|
||||||
let delete = Delete::from_any_base(any_base.clone())?.context(location_info!())?;
|
|
||||||
verify_activity_domains_valid(&delete, expected_domain, true)?;
|
|
||||||
let object_uri = delete
|
|
||||||
.object()
|
|
||||||
.to_owned()
|
|
||||||
.single_xsd_any_uri()
|
|
||||||
.context(location_info!())?;
|
|
||||||
|
|
||||||
match find_community_or_private_message_by_id(context, object_uri).await? {
|
|
||||||
Community(c) => receive_delete_community(context, c).await,
|
|
||||||
PrivateMessage(p) => receive_delete_private_message(context, delete, p, request_counter).await,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn receive_remove(
|
|
||||||
context: &LemmyContext,
|
|
||||||
any_base: AnyBase,
|
|
||||||
expected_domain: &Url,
|
|
||||||
) -> Result<(), LemmyError> {
|
|
||||||
let remove = Remove::from_any_base(any_base.clone())?.context(location_info!())?;
|
|
||||||
verify_activity_domains_valid(&remove, expected_domain, true)?;
|
|
||||||
let object_uri = remove
|
|
||||||
.object()
|
|
||||||
.to_owned()
|
|
||||||
.single_xsd_any_uri()
|
|
||||||
.context(location_info!())?;
|
|
||||||
let community = blocking(context.pool(), move |conn| {
|
|
||||||
Community::read_from_apub_id(conn, &object_uri.into())
|
|
||||||
})
|
|
||||||
.await??;
|
|
||||||
receive_remove_community(context, community).await
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn receive_undo(
|
|
||||||
context: &LemmyContext,
|
|
||||||
any_base: AnyBase,
|
|
||||||
expected_domain: &Url,
|
|
||||||
request_counter: &mut i32,
|
|
||||||
) -> Result<(), LemmyError> {
|
|
||||||
let undo = Undo::from_any_base(any_base)?.context(location_info!())?;
|
|
||||||
verify_activity_domains_valid(&undo, expected_domain, true)?;
|
|
||||||
|
|
||||||
let inner_activity = undo.object().to_owned().one().context(location_info!())?;
|
|
||||||
let kind = inner_activity.kind_str();
|
|
||||||
match kind {
|
|
||||||
Some("Delete") => {
|
|
||||||
let delete = Delete::from_any_base(inner_activity)?.context(location_info!())?;
|
|
||||||
verify_activity_domains_valid(&delete, expected_domain, true)?;
|
|
||||||
let object_uri = delete
|
|
||||||
.object()
|
|
||||||
.to_owned()
|
|
||||||
.single_xsd_any_uri()
|
|
||||||
.context(location_info!())?;
|
|
||||||
use CommunityOrPrivateMessage::*;
|
|
||||||
match find_community_or_private_message_by_id(context, object_uri).await? {
|
|
||||||
Community(c) => receive_undo_delete_community(context, c).await,
|
|
||||||
PrivateMessage(p) => {
|
|
||||||
receive_undo_delete_private_message(context, undo, expected_domain, p, request_counter)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some("Remove") => {
|
|
||||||
let remove = Remove::from_any_base(inner_activity)?.context(location_info!())?;
|
|
||||||
let object_uri = remove
|
|
||||||
.object()
|
|
||||||
.to_owned()
|
|
||||||
.single_xsd_any_uri()
|
|
||||||
.context(location_info!())?;
|
|
||||||
let community = blocking(context.pool(), move |conn| {
|
|
||||||
Community::read_from_apub_id(conn, &object_uri.into())
|
|
||||||
})
|
|
||||||
.await??;
|
|
||||||
receive_undo_remove_community(context, community).await
|
|
||||||
}
|
|
||||||
_ => receive_unhandled_activity(undo),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
enum CommunityOrPrivateMessage {
|
|
||||||
Community(Community),
|
|
||||||
PrivateMessage(PrivateMessage),
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn find_community_or_private_message_by_id(
|
|
||||||
context: &LemmyContext,
|
|
||||||
apub_id: Url,
|
|
||||||
) -> Result<CommunityOrPrivateMessage, LemmyError> {
|
|
||||||
let ap_id = apub_id.to_owned();
|
|
||||||
let community = blocking(context.pool(), move |conn| {
|
|
||||||
Community::read_from_apub_id(conn, &ap_id.into())
|
|
||||||
})
|
|
||||||
.await?;
|
|
||||||
if let Ok(c) = community {
|
|
||||||
return Ok(CommunityOrPrivateMessage::Community(c));
|
|
||||||
}
|
|
||||||
|
|
||||||
let ap_id = apub_id.to_owned();
|
|
||||||
let private_message = blocking(context.pool(), move |conn| {
|
|
||||||
PrivateMessage::read_from_apub_id(conn, &ap_id.into())
|
|
||||||
})
|
|
||||||
.await?;
|
|
||||||
if let Ok(p) = private_message {
|
|
||||||
return Ok(CommunityOrPrivateMessage::PrivateMessage(p));
|
|
||||||
}
|
|
||||||
|
|
||||||
Err(NotFound.into())
|
|
||||||
}
|
|
|
@ -1,802 +0,0 @@
|
||||||
use crate::{
|
|
||||||
activities::receive::{
|
|
||||||
comment::{
|
|
||||||
receive_create_comment,
|
|
||||||
receive_delete_comment,
|
|
||||||
receive_dislike_comment,
|
|
||||||
receive_like_comment,
|
|
||||||
receive_remove_comment,
|
|
||||||
receive_update_comment,
|
|
||||||
},
|
|
||||||
comment_undo::{
|
|
||||||
receive_undo_delete_comment,
|
|
||||||
receive_undo_dislike_comment,
|
|
||||||
receive_undo_like_comment,
|
|
||||||
receive_undo_remove_comment,
|
|
||||||
},
|
|
||||||
community::{
|
|
||||||
receive_remote_mod_delete_community,
|
|
||||||
receive_remote_mod_undo_delete_community,
|
|
||||||
receive_remote_mod_update_community,
|
|
||||||
},
|
|
||||||
post::{
|
|
||||||
receive_create_post,
|
|
||||||
receive_delete_post,
|
|
||||||
receive_dislike_post,
|
|
||||||
receive_like_post,
|
|
||||||
receive_remove_post,
|
|
||||||
receive_update_post,
|
|
||||||
},
|
|
||||||
post_undo::{
|
|
||||||
receive_undo_delete_post,
|
|
||||||
receive_undo_dislike_post,
|
|
||||||
receive_undo_like_post,
|
|
||||||
receive_undo_remove_post,
|
|
||||||
},
|
|
||||||
receive_unhandled_activity,
|
|
||||||
verify_activity_domains_valid,
|
|
||||||
},
|
|
||||||
inbox::verify_is_addressed_to_public,
|
|
||||||
};
|
|
||||||
use activitystreams::{
|
|
||||||
activity::{
|
|
||||||
ActorAndObjectRef,
|
|
||||||
Add,
|
|
||||||
Announce,
|
|
||||||
Block,
|
|
||||||
Create,
|
|
||||||
Delete,
|
|
||||||
Dislike,
|
|
||||||
Like,
|
|
||||||
OptTargetRef,
|
|
||||||
Remove,
|
|
||||||
Undo,
|
|
||||||
Update,
|
|
||||||
},
|
|
||||||
base::AnyBase,
|
|
||||||
object::AsObject,
|
|
||||||
prelude::*,
|
|
||||||
};
|
|
||||||
use anyhow::{anyhow, Context};
|
|
||||||
use diesel::result::Error::NotFound;
|
|
||||||
use lemmy_api_common::blocking;
|
|
||||||
use lemmy_apub::{
|
|
||||||
fetcher::{
|
|
||||||
objects::{get_or_fetch_and_insert_comment, get_or_fetch_and_insert_post},
|
|
||||||
person::get_or_fetch_and_upsert_person,
|
|
||||||
},
|
|
||||||
find_object_by_id,
|
|
||||||
find_post_or_comment_by_id,
|
|
||||||
generate_moderators_url,
|
|
||||||
ActorType,
|
|
||||||
CommunityType,
|
|
||||||
Object,
|
|
||||||
PostOrComment,
|
|
||||||
};
|
|
||||||
use lemmy_db_queries::{
|
|
||||||
source::community::CommunityModerator_,
|
|
||||||
ApubObject,
|
|
||||||
Bannable,
|
|
||||||
Crud,
|
|
||||||
Followable,
|
|
||||||
Joinable,
|
|
||||||
};
|
|
||||||
use lemmy_db_schema::{
|
|
||||||
source::{
|
|
||||||
community::{
|
|
||||||
Community,
|
|
||||||
CommunityFollower,
|
|
||||||
CommunityFollowerForm,
|
|
||||||
CommunityModerator,
|
|
||||||
CommunityModeratorForm,
|
|
||||||
CommunityPersonBan,
|
|
||||||
CommunityPersonBanForm,
|
|
||||||
},
|
|
||||||
person::Person,
|
|
||||||
site::Site,
|
|
||||||
},
|
|
||||||
DbUrl,
|
|
||||||
};
|
|
||||||
use lemmy_db_views_actor::community_view::CommunityView;
|
|
||||||
use lemmy_utils::{location_info, LemmyError};
|
|
||||||
use lemmy_websocket::LemmyContext;
|
|
||||||
use strum_macros::EnumString;
|
|
||||||
use url::Url;
|
|
||||||
|
|
||||||
#[derive(EnumString)]
|
|
||||||
enum PageOrNote {
|
|
||||||
Page,
|
|
||||||
Note,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(EnumString)]
|
|
||||||
enum ObjectTypes {
|
|
||||||
Page,
|
|
||||||
Note,
|
|
||||||
Group,
|
|
||||||
Person,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// This file is for post/comment activities received by the community, and for post/comment
|
|
||||||
/// activities announced by the community and received by the person.
|
|
||||||
|
|
||||||
/// A post or comment being created
|
|
||||||
pub(in crate::inbox) async fn receive_create_for_community(
|
|
||||||
context: &LemmyContext,
|
|
||||||
activity: AnyBase,
|
|
||||||
expected_domain: &Url,
|
|
||||||
request_counter: &mut i32,
|
|
||||||
) -> Result<(), LemmyError> {
|
|
||||||
let create = Create::from_any_base(activity)?.context(location_info!())?;
|
|
||||||
verify_activity_domains_valid(&create, expected_domain, true)?;
|
|
||||||
verify_is_addressed_to_public(&create)?;
|
|
||||||
|
|
||||||
let kind = create
|
|
||||||
.object()
|
|
||||||
.as_single_kind_str()
|
|
||||||
.and_then(|s| s.parse().ok());
|
|
||||||
match kind {
|
|
||||||
Some(ObjectTypes::Page) => receive_create_post(create, context, request_counter).await,
|
|
||||||
Some(ObjectTypes::Note) => receive_create_comment(create, context, request_counter).await,
|
|
||||||
_ => receive_unhandled_activity(create),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A post or comment being edited
|
|
||||||
pub(in crate::inbox) async fn receive_update_for_community(
|
|
||||||
context: &LemmyContext,
|
|
||||||
activity: AnyBase,
|
|
||||||
announce: Option<Announce>,
|
|
||||||
expected_domain: &Url,
|
|
||||||
request_counter: &mut i32,
|
|
||||||
) -> Result<(), LemmyError> {
|
|
||||||
let update = Update::from_any_base(activity.to_owned())?.context(location_info!())?;
|
|
||||||
verify_activity_domains_valid(&update, expected_domain, false)?;
|
|
||||||
verify_is_addressed_to_public(&update)?;
|
|
||||||
verify_modification_actor_instance(&update, &announce, context, request_counter).await?;
|
|
||||||
|
|
||||||
let kind = update
|
|
||||||
.object()
|
|
||||||
.as_single_kind_str()
|
|
||||||
.and_then(|s| s.parse().ok());
|
|
||||||
match kind {
|
|
||||||
Some(ObjectTypes::Page) => {
|
|
||||||
receive_update_post(update, announce, context, request_counter).await
|
|
||||||
}
|
|
||||||
Some(ObjectTypes::Note) => receive_update_comment(update, context, request_counter).await,
|
|
||||||
Some(ObjectTypes::Group) => {
|
|
||||||
receive_remote_mod_update_community(update, context, request_counter).await
|
|
||||||
}
|
|
||||||
_ => receive_unhandled_activity(update),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A post or comment being upvoted
|
|
||||||
pub(in crate::inbox) async fn receive_like_for_community(
|
|
||||||
context: &LemmyContext,
|
|
||||||
activity: AnyBase,
|
|
||||||
expected_domain: &Url,
|
|
||||||
request_counter: &mut i32,
|
|
||||||
) -> Result<(), LemmyError> {
|
|
||||||
let like = Like::from_any_base(activity)?.context(location_info!())?;
|
|
||||||
verify_activity_domains_valid(&like, expected_domain, false)?;
|
|
||||||
verify_is_addressed_to_public(&like)?;
|
|
||||||
|
|
||||||
let object_id = like
|
|
||||||
.object()
|
|
||||||
.as_single_xsd_any_uri()
|
|
||||||
.context(location_info!())?;
|
|
||||||
match fetch_post_or_comment_by_id(object_id, context, request_counter).await? {
|
|
||||||
PostOrComment::Post(post) => receive_like_post(like, *post, context, request_counter).await,
|
|
||||||
PostOrComment::Comment(comment) => {
|
|
||||||
receive_like_comment(like, *comment, context, request_counter).await
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A post or comment being downvoted
|
|
||||||
pub(in crate::inbox) async fn receive_dislike_for_community(
|
|
||||||
context: &LemmyContext,
|
|
||||||
activity: AnyBase,
|
|
||||||
expected_domain: &Url,
|
|
||||||
request_counter: &mut i32,
|
|
||||||
) -> Result<(), LemmyError> {
|
|
||||||
let enable_downvotes = blocking(context.pool(), move |conn| {
|
|
||||||
Site::read(conn, 1).map(|s| s.enable_downvotes)
|
|
||||||
})
|
|
||||||
.await??;
|
|
||||||
if !enable_downvotes {
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
let dislike = Dislike::from_any_base(activity)?.context(location_info!())?;
|
|
||||||
verify_activity_domains_valid(&dislike, expected_domain, false)?;
|
|
||||||
verify_is_addressed_to_public(&dislike)?;
|
|
||||||
|
|
||||||
let object_id = dislike
|
|
||||||
.object()
|
|
||||||
.as_single_xsd_any_uri()
|
|
||||||
.context(location_info!())?;
|
|
||||||
match fetch_post_or_comment_by_id(object_id, context, request_counter).await? {
|
|
||||||
PostOrComment::Post(post) => {
|
|
||||||
receive_dislike_post(dislike, *post, context, request_counter).await
|
|
||||||
}
|
|
||||||
PostOrComment::Comment(comment) => {
|
|
||||||
receive_dislike_comment(dislike, *comment, context, request_counter).await
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A post or comment being deleted by its creator
|
|
||||||
pub(in crate::inbox) async fn receive_delete_for_community(
|
|
||||||
context: &LemmyContext,
|
|
||||||
activity: AnyBase,
|
|
||||||
announce: Option<Announce>,
|
|
||||||
expected_domain: &Url,
|
|
||||||
request_counter: &mut i32,
|
|
||||||
) -> Result<(), LemmyError> {
|
|
||||||
let delete = Delete::from_any_base(activity)?.context(location_info!())?;
|
|
||||||
// TODO: skip this check if action is done by remote mod
|
|
||||||
verify_is_addressed_to_public(&delete)?;
|
|
||||||
verify_modification_actor_instance(&delete, &announce, context, request_counter).await?;
|
|
||||||
|
|
||||||
let object = delete
|
|
||||||
.object()
|
|
||||||
.to_owned()
|
|
||||||
.single_xsd_any_uri()
|
|
||||||
.context(location_info!())?;
|
|
||||||
|
|
||||||
match find_object_by_id(context, object).await {
|
|
||||||
Ok(Object::Post(p)) => {
|
|
||||||
verify_activity_domains_valid(&delete, expected_domain, true)?;
|
|
||||||
receive_delete_post(context, *p).await
|
|
||||||
}
|
|
||||||
Ok(Object::Comment(c)) => {
|
|
||||||
verify_activity_domains_valid(&delete, expected_domain, true)?;
|
|
||||||
receive_delete_comment(context, *c).await
|
|
||||||
}
|
|
||||||
Ok(Object::Community(c)) => {
|
|
||||||
receive_remote_mod_delete_community(delete, *c, context, request_counter).await
|
|
||||||
}
|
|
||||||
// if we dont have the object or dont support its deletion, no need to do anything
|
|
||||||
_ => Ok(()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A post or comment being removed by a mod/admin
|
|
||||||
pub(in crate::inbox) async fn receive_remove_for_community(
|
|
||||||
context: &LemmyContext,
|
|
||||||
remove_any_base: AnyBase,
|
|
||||||
announce: Option<Announce>,
|
|
||||||
request_counter: &mut i32,
|
|
||||||
) -> Result<(), LemmyError> {
|
|
||||||
let remove = Remove::from_any_base(remove_any_base.to_owned())?.context(location_info!())?;
|
|
||||||
let community = extract_community_from_cc(&remove, context).await?;
|
|
||||||
|
|
||||||
verify_mod_activity(&remove, announce, &community, context).await?;
|
|
||||||
verify_is_addressed_to_public(&remove)?;
|
|
||||||
|
|
||||||
if remove.target().is_some() {
|
|
||||||
let remove_mod = remove
|
|
||||||
.object()
|
|
||||||
.as_single_xsd_any_uri()
|
|
||||||
.context(location_info!())?;
|
|
||||||
let remove_mod = get_or_fetch_and_upsert_person(remove_mod, context, request_counter).await?;
|
|
||||||
let form = CommunityModeratorForm {
|
|
||||||
community_id: community.id,
|
|
||||||
person_id: remove_mod.id,
|
|
||||||
};
|
|
||||||
blocking(context.pool(), move |conn| {
|
|
||||||
CommunityModerator::leave(conn, &form)
|
|
||||||
})
|
|
||||||
.await??;
|
|
||||||
community
|
|
||||||
.send_announce(
|
|
||||||
remove_any_base,
|
|
||||||
remove.object().clone().single_xsd_any_uri(),
|
|
||||||
context,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
// TODO: send websocket notification about removed mod
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
// Remove a post or comment
|
|
||||||
else {
|
|
||||||
let object = remove
|
|
||||||
.object()
|
|
||||||
.to_owned()
|
|
||||||
.single_xsd_any_uri()
|
|
||||||
.context(location_info!())?;
|
|
||||||
|
|
||||||
match find_post_or_comment_by_id(context, object).await {
|
|
||||||
Ok(PostOrComment::Post(p)) => receive_remove_post(context, *p).await,
|
|
||||||
Ok(PostOrComment::Comment(c)) => receive_remove_comment(context, *c).await,
|
|
||||||
// if we dont have the object, no need to do anything
|
|
||||||
Err(_) => Ok(()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(EnumString)]
|
|
||||||
enum UndoableActivities {
|
|
||||||
Delete,
|
|
||||||
Remove,
|
|
||||||
Like,
|
|
||||||
Dislike,
|
|
||||||
Block,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A post/comment action being reverted (either a delete, remove, upvote or downvote)
|
|
||||||
pub(in crate::inbox) async fn receive_undo_for_community(
|
|
||||||
context: &LemmyContext,
|
|
||||||
activity: AnyBase,
|
|
||||||
announce: Option<Announce>,
|
|
||||||
expected_domain: &Url,
|
|
||||||
request_counter: &mut i32,
|
|
||||||
) -> Result<(), LemmyError> {
|
|
||||||
let undo = Undo::from_any_base(activity)?.context(location_info!())?;
|
|
||||||
verify_activity_domains_valid(&undo, &expected_domain.to_owned(), true)?;
|
|
||||||
verify_is_addressed_to_public(&undo)?;
|
|
||||||
|
|
||||||
use UndoableActivities::*;
|
|
||||||
match undo
|
|
||||||
.object()
|
|
||||||
.as_single_kind_str()
|
|
||||||
.and_then(|s| s.parse().ok())
|
|
||||||
{
|
|
||||||
Some(Delete) => {
|
|
||||||
receive_undo_delete_for_community(context, undo, expected_domain, request_counter).await
|
|
||||||
}
|
|
||||||
Some(Remove) => {
|
|
||||||
receive_undo_remove_for_community(context, undo, announce, expected_domain).await
|
|
||||||
}
|
|
||||||
Some(Like) => {
|
|
||||||
receive_undo_like_for_community(context, undo, expected_domain, request_counter).await
|
|
||||||
}
|
|
||||||
Some(Dislike) => {
|
|
||||||
receive_undo_dislike_for_community(context, undo, expected_domain, request_counter).await
|
|
||||||
}
|
|
||||||
Some(Block) => {
|
|
||||||
receive_undo_block_user_for_community(
|
|
||||||
context,
|
|
||||||
undo,
|
|
||||||
announce,
|
|
||||||
expected_domain,
|
|
||||||
request_counter,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
_ => receive_unhandled_activity(undo),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A post, comment or community deletion being reverted
|
|
||||||
pub(in crate::inbox) async fn receive_undo_delete_for_community(
|
|
||||||
context: &LemmyContext,
|
|
||||||
undo: Undo,
|
|
||||||
expected_domain: &Url,
|
|
||||||
request_counter: &mut i32,
|
|
||||||
) -> Result<(), LemmyError> {
|
|
||||||
let delete = Delete::from_any_base(undo.object().to_owned().one().context(location_info!())?)?
|
|
||||||
.context(location_info!())?;
|
|
||||||
verify_is_addressed_to_public(&delete)?;
|
|
||||||
|
|
||||||
let object = delete
|
|
||||||
.object()
|
|
||||||
.to_owned()
|
|
||||||
.single_xsd_any_uri()
|
|
||||||
.context(location_info!())?;
|
|
||||||
match find_object_by_id(context, object).await {
|
|
||||||
Ok(Object::Post(p)) => {
|
|
||||||
verify_activity_domains_valid(&delete, expected_domain, true)?;
|
|
||||||
receive_undo_delete_post(context, *p).await
|
|
||||||
}
|
|
||||||
Ok(Object::Comment(c)) => {
|
|
||||||
verify_activity_domains_valid(&delete, expected_domain, true)?;
|
|
||||||
receive_undo_delete_comment(context, *c).await
|
|
||||||
}
|
|
||||||
Ok(Object::Community(c)) => {
|
|
||||||
verify_actor_is_community_mod(&undo, &c, context).await?;
|
|
||||||
receive_remote_mod_undo_delete_community(undo, *c, context, request_counter).await
|
|
||||||
}
|
|
||||||
// if we dont have the object or dont support its deletion, no need to do anything
|
|
||||||
_ => Ok(()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A post or comment removal being reverted
|
|
||||||
pub(in crate::inbox) async fn receive_undo_remove_for_community(
|
|
||||||
context: &LemmyContext,
|
|
||||||
undo: Undo,
|
|
||||||
announce: Option<Announce>,
|
|
||||||
expected_domain: &Url,
|
|
||||||
) -> Result<(), LemmyError> {
|
|
||||||
let remove = Remove::from_any_base(undo.object().to_owned().one().context(location_info!())?)?
|
|
||||||
.context(location_info!())?;
|
|
||||||
verify_activity_domains_valid(&remove, expected_domain, false)?;
|
|
||||||
verify_is_addressed_to_public(&remove)?;
|
|
||||||
verify_undo_remove_actor_instance(&undo, &remove, &announce, context).await?;
|
|
||||||
|
|
||||||
let object = remove
|
|
||||||
.object()
|
|
||||||
.to_owned()
|
|
||||||
.single_xsd_any_uri()
|
|
||||||
.context(location_info!())?;
|
|
||||||
match find_post_or_comment_by_id(context, object).await {
|
|
||||||
Ok(PostOrComment::Post(p)) => receive_undo_remove_post(context, *p).await,
|
|
||||||
Ok(PostOrComment::Comment(c)) => receive_undo_remove_comment(context, *c).await,
|
|
||||||
// if we dont have the object, no need to do anything
|
|
||||||
Err(_) => Ok(()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A post or comment upvote being reverted
|
|
||||||
pub(in crate::inbox) async fn receive_undo_like_for_community(
|
|
||||||
context: &LemmyContext,
|
|
||||||
undo: Undo,
|
|
||||||
expected_domain: &Url,
|
|
||||||
request_counter: &mut i32,
|
|
||||||
) -> Result<(), LemmyError> {
|
|
||||||
let like = Like::from_any_base(undo.object().to_owned().one().context(location_info!())?)?
|
|
||||||
.context(location_info!())?;
|
|
||||||
verify_activity_domains_valid(&like, expected_domain, false)?;
|
|
||||||
verify_is_addressed_to_public(&like)?;
|
|
||||||
|
|
||||||
let object_id = like
|
|
||||||
.object()
|
|
||||||
.as_single_xsd_any_uri()
|
|
||||||
.context(location_info!())?;
|
|
||||||
match fetch_post_or_comment_by_id(object_id, context, request_counter).await? {
|
|
||||||
PostOrComment::Post(post) => {
|
|
||||||
receive_undo_like_post(&like, *post, context, request_counter).await
|
|
||||||
}
|
|
||||||
PostOrComment::Comment(comment) => {
|
|
||||||
receive_undo_like_comment(&like, *comment, context, request_counter).await
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Add a new mod to the community (can only be done by an existing mod).
|
|
||||||
pub(in crate::inbox) async fn receive_add_for_community(
|
|
||||||
context: &LemmyContext,
|
|
||||||
add_any_base: AnyBase,
|
|
||||||
announce: Option<Announce>,
|
|
||||||
request_counter: &mut i32,
|
|
||||||
) -> Result<(), LemmyError> {
|
|
||||||
let add = Add::from_any_base(add_any_base.to_owned())?.context(location_info!())?;
|
|
||||||
let community = extract_community_from_cc(&add, context).await?;
|
|
||||||
|
|
||||||
verify_mod_activity(&add, announce, &community, context).await?;
|
|
||||||
verify_is_addressed_to_public(&add)?;
|
|
||||||
verify_add_remove_moderator_target(&add, &community)?;
|
|
||||||
|
|
||||||
let new_mod = add
|
|
||||||
.object()
|
|
||||||
.as_single_xsd_any_uri()
|
|
||||||
.context(location_info!())?;
|
|
||||||
let new_mod = get_or_fetch_and_upsert_person(new_mod, context, request_counter).await?;
|
|
||||||
|
|
||||||
// If we had to refetch the community while parsing the activity, then the new mod has already
|
|
||||||
// been added. Skip it here as it would result in a duplicate key error.
|
|
||||||
let new_mod_id = new_mod.id;
|
|
||||||
let moderated_communities = blocking(context.pool(), move |conn| {
|
|
||||||
CommunityModerator::get_person_moderated_communities(conn, new_mod_id)
|
|
||||||
})
|
|
||||||
.await??;
|
|
||||||
if !moderated_communities.contains(&community.id) {
|
|
||||||
let form = CommunityModeratorForm {
|
|
||||||
community_id: community.id,
|
|
||||||
person_id: new_mod.id,
|
|
||||||
};
|
|
||||||
blocking(context.pool(), move |conn| {
|
|
||||||
CommunityModerator::join(conn, &form)
|
|
||||||
})
|
|
||||||
.await??;
|
|
||||||
}
|
|
||||||
if community.local {
|
|
||||||
community
|
|
||||||
.send_announce(
|
|
||||||
add_any_base,
|
|
||||||
add.object().clone().single_xsd_any_uri(),
|
|
||||||
context,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
}
|
|
||||||
// TODO: send websocket notification about added mod
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A post or comment downvote being reverted
|
|
||||||
pub(in crate::inbox) async fn receive_undo_dislike_for_community(
|
|
||||||
context: &LemmyContext,
|
|
||||||
undo: Undo,
|
|
||||||
expected_domain: &Url,
|
|
||||||
request_counter: &mut i32,
|
|
||||||
) -> Result<(), LemmyError> {
|
|
||||||
let dislike = Dislike::from_any_base(undo.object().to_owned().one().context(location_info!())?)?
|
|
||||||
.context(location_info!())?;
|
|
||||||
verify_activity_domains_valid(&dislike, expected_domain, false)?;
|
|
||||||
verify_is_addressed_to_public(&dislike)?;
|
|
||||||
|
|
||||||
let object_id = dislike
|
|
||||||
.object()
|
|
||||||
.as_single_xsd_any_uri()
|
|
||||||
.context(location_info!())?;
|
|
||||||
match fetch_post_or_comment_by_id(object_id, context, request_counter).await? {
|
|
||||||
PostOrComment::Post(post) => {
|
|
||||||
receive_undo_dislike_post(&dislike, *post, context, request_counter).await
|
|
||||||
}
|
|
||||||
PostOrComment::Comment(comment) => {
|
|
||||||
receive_undo_dislike_comment(&dislike, *comment, context, request_counter).await
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) async fn receive_block_user_for_community(
|
|
||||||
context: &LemmyContext,
|
|
||||||
block_any_base: AnyBase,
|
|
||||||
announce: Option<Announce>,
|
|
||||||
request_counter: &mut i32,
|
|
||||||
) -> Result<(), LemmyError> {
|
|
||||||
let block = Block::from_any_base(block_any_base.to_owned())?.context(location_info!())?;
|
|
||||||
let community = extract_community_from_cc(&block, context).await?;
|
|
||||||
|
|
||||||
verify_mod_activity(&block, announce, &community, context).await?;
|
|
||||||
verify_is_addressed_to_public(&block)?;
|
|
||||||
|
|
||||||
let blocked_user = block
|
|
||||||
.object()
|
|
||||||
.as_single_xsd_any_uri()
|
|
||||||
.context(location_info!())?;
|
|
||||||
let blocked_user = get_or_fetch_and_upsert_person(blocked_user, context, request_counter).await?;
|
|
||||||
|
|
||||||
let community_user_ban_form = CommunityPersonBanForm {
|
|
||||||
community_id: community.id,
|
|
||||||
person_id: blocked_user.id,
|
|
||||||
};
|
|
||||||
|
|
||||||
blocking(context.pool(), move |conn: &'_ _| {
|
|
||||||
CommunityPersonBan::ban(conn, &community_user_ban_form)
|
|
||||||
})
|
|
||||||
.await??;
|
|
||||||
|
|
||||||
// Also unsubscribe them from the community, if they are subscribed
|
|
||||||
let community_follower_form = CommunityFollowerForm {
|
|
||||||
community_id: community.id,
|
|
||||||
person_id: blocked_user.id,
|
|
||||||
pending: false,
|
|
||||||
};
|
|
||||||
blocking(context.pool(), move |conn: &'_ _| {
|
|
||||||
CommunityFollower::unfollow(conn, &community_follower_form)
|
|
||||||
})
|
|
||||||
.await?
|
|
||||||
.ok();
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) async fn receive_undo_block_user_for_community(
|
|
||||||
context: &LemmyContext,
|
|
||||||
undo: Undo,
|
|
||||||
announce: Option<Announce>,
|
|
||||||
expected_domain: &Url,
|
|
||||||
request_counter: &mut i32,
|
|
||||||
) -> Result<(), LemmyError> {
|
|
||||||
let object = undo.object().clone().one().context(location_info!())?;
|
|
||||||
let block = Block::from_any_base(object)?.context(location_info!())?;
|
|
||||||
let community = extract_community_from_cc(&block, context).await?;
|
|
||||||
|
|
||||||
verify_activity_domains_valid(&block, expected_domain, false)?;
|
|
||||||
verify_is_addressed_to_public(&block)?;
|
|
||||||
verify_undo_remove_actor_instance(&undo, &block, &announce, context).await?;
|
|
||||||
|
|
||||||
let blocked_user = block
|
|
||||||
.object()
|
|
||||||
.as_single_xsd_any_uri()
|
|
||||||
.context(location_info!())?;
|
|
||||||
let blocked_user = get_or_fetch_and_upsert_person(blocked_user, context, request_counter).await?;
|
|
||||||
|
|
||||||
let community_user_ban_form = CommunityPersonBanForm {
|
|
||||||
community_id: community.id,
|
|
||||||
person_id: blocked_user.id,
|
|
||||||
};
|
|
||||||
|
|
||||||
blocking(context.pool(), move |conn: &'_ _| {
|
|
||||||
CommunityPersonBan::unban(conn, &community_user_ban_form)
|
|
||||||
})
|
|
||||||
.await??;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn fetch_post_or_comment_by_id(
|
|
||||||
apub_id: &Url,
|
|
||||||
context: &LemmyContext,
|
|
||||||
request_counter: &mut i32,
|
|
||||||
) -> Result<PostOrComment, LemmyError> {
|
|
||||||
if let Ok(post) = get_or_fetch_and_insert_post(apub_id, context, request_counter).await {
|
|
||||||
return Ok(PostOrComment::Post(Box::new(post)));
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Ok(comment) = get_or_fetch_and_insert_comment(apub_id, context, request_counter).await {
|
|
||||||
return Ok(PostOrComment::Comment(Box::new(comment)));
|
|
||||||
}
|
|
||||||
|
|
||||||
Err(NotFound.into())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Searches the activity's cc field for a Community ID, and returns the community.
|
|
||||||
async fn extract_community_from_cc<T, Kind>(
|
|
||||||
activity: &T,
|
|
||||||
context: &LemmyContext,
|
|
||||||
) -> Result<Community, LemmyError>
|
|
||||||
where
|
|
||||||
T: AsObject<Kind>,
|
|
||||||
{
|
|
||||||
let cc = activity
|
|
||||||
.cc()
|
|
||||||
.map(|c| c.as_many())
|
|
||||||
.flatten()
|
|
||||||
.context(location_info!())?;
|
|
||||||
let community_id = cc
|
|
||||||
.first()
|
|
||||||
.map(|c| c.as_xsd_any_uri())
|
|
||||||
.flatten()
|
|
||||||
.context(location_info!())?;
|
|
||||||
let community_id: DbUrl = community_id.to_owned().into();
|
|
||||||
let community = blocking(context.pool(), move |conn| {
|
|
||||||
Community::read_from_apub_id(conn, &community_id)
|
|
||||||
})
|
|
||||||
.await??;
|
|
||||||
Ok(community)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Checks that a moderation activity was sent by a user who is listed as mod for the community.
|
|
||||||
/// This is only used in the case of remote mods, as local mod actions don't go through the
|
|
||||||
/// community inbox.
|
|
||||||
///
|
|
||||||
/// This method should only be used for activities received by the community, not for activities
|
|
||||||
/// used by community followers.
|
|
||||||
pub(crate) async fn verify_actor_is_community_mod<T, Kind>(
|
|
||||||
activity: &T,
|
|
||||||
community: &Community,
|
|
||||||
context: &LemmyContext,
|
|
||||||
) -> Result<(), LemmyError>
|
|
||||||
where
|
|
||||||
T: ActorAndObjectRef + BaseExt<Kind>,
|
|
||||||
{
|
|
||||||
let actor = activity
|
|
||||||
.actor()?
|
|
||||||
.as_single_xsd_any_uri()
|
|
||||||
.context(location_info!())?
|
|
||||||
.to_owned();
|
|
||||||
let actor = blocking(context.pool(), move |conn| {
|
|
||||||
Person::read_from_apub_id(conn, &actor.into())
|
|
||||||
})
|
|
||||||
.await??;
|
|
||||||
|
|
||||||
// Note: this will also return true for admins in addition to mods, but as we dont know about
|
|
||||||
// remote admins, it doesnt make any difference.
|
|
||||||
let community_id = community.id;
|
|
||||||
let actor_id = actor.id;
|
|
||||||
let is_mod_or_admin = blocking(context.pool(), move |conn| {
|
|
||||||
CommunityView::is_mod_or_admin(conn, actor_id, community_id)
|
|
||||||
})
|
|
||||||
.await?;
|
|
||||||
if !is_mod_or_admin {
|
|
||||||
return Err(anyhow!("Not a mod").into());
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// This method behaves differently, depending if it is called via community inbox (activity
|
|
||||||
/// received by community from a remote user), or via user inbox (activity received by user from
|
|
||||||
/// community). We distinguish the cases by checking if the activity is wrapper in an announce
|
|
||||||
/// (only true when sent from user to community).
|
|
||||||
///
|
|
||||||
/// In the first case, we check that the actor is listed as community mod. In the second case, we
|
|
||||||
/// only check that the announce comes from the same domain as the activity. We trust the
|
|
||||||
/// community's instance to have validated the inner activity correctly. We can't do this validation
|
|
||||||
/// here, because we don't know who the instance admins are. Plus this allows for compatibility with
|
|
||||||
/// software that uses different rules for mod actions.
|
|
||||||
pub(crate) async fn verify_mod_activity<T, Kind>(
|
|
||||||
mod_action: &T,
|
|
||||||
announce: Option<Announce>,
|
|
||||||
community: &Community,
|
|
||||||
context: &LemmyContext,
|
|
||||||
) -> Result<(), LemmyError>
|
|
||||||
where
|
|
||||||
T: ActorAndObjectRef + BaseExt<Kind>,
|
|
||||||
{
|
|
||||||
match announce {
|
|
||||||
None => verify_actor_is_community_mod(mod_action, community, context).await?,
|
|
||||||
Some(a) => verify_activity_domains_valid(&a, &community.actor_id.to_owned().into(), false)?,
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// For Add/Remove community moderator activities, check that the target field actually contains
|
|
||||||
/// /c/community/moderators. Any different values are unsupported.
|
|
||||||
fn verify_add_remove_moderator_target<T, Kind>(
|
|
||||||
activity: &T,
|
|
||||||
community: &Community,
|
|
||||||
) -> Result<(), LemmyError>
|
|
||||||
where
|
|
||||||
T: ActorAndObjectRef + BaseExt<Kind> + OptTargetRef,
|
|
||||||
{
|
|
||||||
let target = activity
|
|
||||||
.target()
|
|
||||||
.map(|t| t.as_single_xsd_any_uri())
|
|
||||||
.flatten()
|
|
||||||
.context(location_info!())?;
|
|
||||||
if target != &generate_moderators_url(&community.actor_id)?.into_inner() {
|
|
||||||
return Err(anyhow!("Unkown target url").into());
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// For activities like Update, Delete or Remove, check that the actor is from the same instance
|
|
||||||
/// as the original object itself (or is a remote mod).
|
|
||||||
///
|
|
||||||
/// Note: This is only needed for mod actions. Normal user actions (edit post, undo vote etc) are
|
|
||||||
/// already verified with `expected_domain`, so this serves as an additional check.
|
|
||||||
async fn verify_modification_actor_instance<T, Kind>(
|
|
||||||
activity: &T,
|
|
||||||
announce: &Option<Announce>,
|
|
||||||
context: &LemmyContext,
|
|
||||||
request_counter: &mut i32,
|
|
||||||
) -> Result<(), LemmyError>
|
|
||||||
where
|
|
||||||
T: ActorAndObjectRef + BaseExt<Kind> + AsObject<Kind>,
|
|
||||||
{
|
|
||||||
let actor_id = activity
|
|
||||||
.actor()?
|
|
||||||
.to_owned()
|
|
||||||
.single_xsd_any_uri()
|
|
||||||
.context(location_info!())?;
|
|
||||||
let object_id = activity
|
|
||||||
.object()
|
|
||||||
.as_one()
|
|
||||||
.map(|o| o.id())
|
|
||||||
.flatten()
|
|
||||||
.context(location_info!())?;
|
|
||||||
let original_id = match fetch_post_or_comment_by_id(object_id, context, request_counter).await {
|
|
||||||
Ok(PostOrComment::Post(p)) => p.ap_id.into_inner(),
|
|
||||||
Ok(PostOrComment::Comment(c)) => c.ap_id.into_inner(),
|
|
||||||
Err(_) => {
|
|
||||||
// We can also receive Update activity from remote mod for local activity
|
|
||||||
let object_id = object_id.to_owned().into();
|
|
||||||
blocking(context.pool(), move |conn| {
|
|
||||||
Community::read_from_apub_id(conn, &object_id)
|
|
||||||
})
|
|
||||||
.await??
|
|
||||||
.actor_id()
|
|
||||||
}
|
|
||||||
};
|
|
||||||
if actor_id.domain() != original_id.domain() {
|
|
||||||
let community = extract_community_from_cc(activity, context).await?;
|
|
||||||
verify_mod_activity(activity, announce.to_owned(), &community, context).await?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) async fn verify_undo_remove_actor_instance<T, Kind>(
|
|
||||||
undo: &Undo,
|
|
||||||
inner: &T,
|
|
||||||
announce: &Option<Announce>,
|
|
||||||
context: &LemmyContext,
|
|
||||||
) -> Result<(), LemmyError>
|
|
||||||
where
|
|
||||||
T: ActorAndObjectRef + BaseExt<Kind> + AsObject<Kind>,
|
|
||||||
{
|
|
||||||
if announce.is_none() {
|
|
||||||
let community = extract_community_from_cc(undo, context).await?;
|
|
||||||
verify_mod_activity(undo, announce.to_owned(), &community, context).await?;
|
|
||||||
verify_mod_activity(inner, announce.to_owned(), &community, context).await?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
|
@ -1,151 +0,0 @@
|
||||||
use crate::inbox::{
|
|
||||||
assert_activity_not_local,
|
|
||||||
community_inbox::{community_receive_message, CommunityAcceptedActivities},
|
|
||||||
get_activity_id,
|
|
||||||
inbox_verify_http_signature,
|
|
||||||
is_activity_already_known,
|
|
||||||
is_addressed_to_community_followers,
|
|
||||||
is_addressed_to_local_person,
|
|
||||||
person_inbox::{person_receive_message, PersonAcceptedActivities},
|
|
||||||
};
|
|
||||||
use activitystreams::{activity::ActorAndObject, prelude::*};
|
|
||||||
use actix_web::{web, HttpRequest, HttpResponse};
|
|
||||||
use anyhow::Context;
|
|
||||||
use lemmy_api_common::blocking;
|
|
||||||
use lemmy_apub::{get_activity_to_and_cc, insert_activity};
|
|
||||||
use lemmy_db_queries::{ApubObject, DbPool};
|
|
||||||
use lemmy_db_schema::source::community::Community;
|
|
||||||
use lemmy_utils::{location_info, LemmyError};
|
|
||||||
use lemmy_websocket::LemmyContext;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use std::fmt::Debug;
|
|
||||||
use url::Url;
|
|
||||||
|
|
||||||
/// Allowed activity types for shared inbox.
|
|
||||||
#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, Deserialize, Serialize)]
|
|
||||||
#[serde(rename_all = "PascalCase")]
|
|
||||||
pub enum ValidTypes {
|
|
||||||
Create,
|
|
||||||
Update,
|
|
||||||
Like,
|
|
||||||
Dislike,
|
|
||||||
Delete,
|
|
||||||
Undo,
|
|
||||||
Remove,
|
|
||||||
Announce,
|
|
||||||
Add,
|
|
||||||
Block,
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: this isnt entirely correct, cause some of these receive are not ActorAndObject,
|
|
||||||
// but it still works due to the anybase conversion
|
|
||||||
pub type AcceptedActivities = ActorAndObject<ValidTypes>;
|
|
||||||
|
|
||||||
/// Handler for all incoming requests to shared inbox.
|
|
||||||
pub async fn shared_inbox(
|
|
||||||
request: HttpRequest,
|
|
||||||
input: web::Json<AcceptedActivities>,
|
|
||||||
context: web::Data<LemmyContext>,
|
|
||||||
) -> Result<HttpResponse, LemmyError> {
|
|
||||||
let activity = input.into_inner();
|
|
||||||
// First of all check the http signature
|
|
||||||
let request_counter = &mut 0;
|
|
||||||
let actor = inbox_verify_http_signature(&activity, &context, request, request_counter).await?;
|
|
||||||
|
|
||||||
// Do nothing if we received the same activity before
|
|
||||||
let actor_id = actor.actor_id();
|
|
||||||
let activity_id = get_activity_id(&activity, &actor_id)?;
|
|
||||||
if is_activity_already_known(context.pool(), &activity_id).await? {
|
|
||||||
return Ok(HttpResponse::Ok().finish());
|
|
||||||
}
|
|
||||||
|
|
||||||
assert_activity_not_local(&activity)?;
|
|
||||||
// Log the activity, so we avoid receiving and parsing it twice. Note that this could still happen
|
|
||||||
// if we receive the same activity twice in very quick succession.
|
|
||||||
insert_activity(&activity_id, activity.clone(), false, true, context.pool()).await?;
|
|
||||||
|
|
||||||
let activity_any_base = activity.clone().into_any_base()?;
|
|
||||||
let mut res: Option<HttpResponse> = None;
|
|
||||||
let to_and_cc = get_activity_to_and_cc(&activity);
|
|
||||||
// Handle community first, so in case the sender is banned by the community, it will error out.
|
|
||||||
// If we handled the person receive first, the activity would be inserted to the database before the
|
|
||||||
// community could check for bans.
|
|
||||||
// Note that an activity can be addressed to a community and to a person (or multiple persons) at the
|
|
||||||
// same time. In this case we still only handle it once, to avoid duplicate websocket
|
|
||||||
// notifications.
|
|
||||||
let community = extract_local_community_from_destinations(&to_and_cc, context.pool()).await?;
|
|
||||||
if let Some(community) = community {
|
|
||||||
let community_activity = CommunityAcceptedActivities::from_any_base(activity_any_base.clone())?
|
|
||||||
.context(location_info!())?;
|
|
||||||
res = Some(
|
|
||||||
Box::pin(community_receive_message(
|
|
||||||
community_activity,
|
|
||||||
community,
|
|
||||||
actor.as_ref(),
|
|
||||||
&context,
|
|
||||||
request_counter,
|
|
||||||
))
|
|
||||||
.await?,
|
|
||||||
);
|
|
||||||
} else if is_addressed_to_local_person(&to_and_cc, context.pool()).await? {
|
|
||||||
let person_activity = PersonAcceptedActivities::from_any_base(activity_any_base.clone())?
|
|
||||||
.context(location_info!())?;
|
|
||||||
// `to_person` is only used for follow activities (which we dont receive here), so no need to pass
|
|
||||||
// it in
|
|
||||||
Box::pin(person_receive_message(
|
|
||||||
person_activity,
|
|
||||||
None,
|
|
||||||
actor.as_ref(),
|
|
||||||
&context,
|
|
||||||
request_counter,
|
|
||||||
))
|
|
||||||
.await?;
|
|
||||||
} else if is_addressed_to_community_followers(&to_and_cc, context.pool())
|
|
||||||
.await?
|
|
||||||
.is_some()
|
|
||||||
{
|
|
||||||
let person_activity = PersonAcceptedActivities::from_any_base(activity_any_base.clone())?
|
|
||||||
.context(location_info!())?;
|
|
||||||
res = Some(
|
|
||||||
Box::pin(person_receive_message(
|
|
||||||
person_activity,
|
|
||||||
None,
|
|
||||||
actor.as_ref(),
|
|
||||||
&context,
|
|
||||||
request_counter,
|
|
||||||
))
|
|
||||||
.await?,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// If none of those, throw an error
|
|
||||||
if let Some(r) = res {
|
|
||||||
Ok(r)
|
|
||||||
} else {
|
|
||||||
Ok(HttpResponse::NotImplemented().finish())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// If `to_and_cc` contains the ID of a local community, return that community, otherwise return
|
|
||||||
/// None.
|
|
||||||
///
|
|
||||||
/// This doesnt handle the case where an activity is addressed to multiple communities (because
|
|
||||||
/// Lemmy doesnt generate such activities).
|
|
||||||
async fn extract_local_community_from_destinations(
|
|
||||||
to_and_cc: &[Url],
|
|
||||||
pool: &DbPool,
|
|
||||||
) -> Result<Option<Community>, LemmyError> {
|
|
||||||
for url in to_and_cc {
|
|
||||||
let url = url.to_owned();
|
|
||||||
let community = blocking(pool, move |conn| {
|
|
||||||
Community::read_from_apub_id(conn, &url.into())
|
|
||||||
})
|
|
||||||
.await?;
|
|
||||||
if let Ok(c) = community {
|
|
||||||
if c.local {
|
|
||||||
return Ok(Some(c));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(None)
|
|
||||||
}
|
|
|
@ -1,4 +1,2 @@
|
||||||
mod activities;
|
mod activities;
|
||||||
mod http;
|
pub mod http;
|
||||||
mod inbox;
|
|
||||||
pub mod routes;
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
Loading…
Reference in a new issue