From a09c818746f159bbc7ece19ee263607002dbb6a5 Mon Sep 17 00:00:00 2001 From: Dessalines Date: Sun, 3 May 2020 10:00:59 -0400 Subject: [PATCH] Adding federated mod remove actions. --- server/src/api/comment.rs | 6 + server/src/api/community.rs | 6 + server/src/api/post.rs | 6 + server/src/apub/comment.rs | 93 ++++++- server/src/apub/community.rs | 96 ++++++- server/src/apub/fetcher.rs | 39 ++- server/src/apub/mod.rs | 16 +- server/src/apub/post.rs | 86 ++++++ server/src/apub/shared_inbox.rs | 472 +++++++++++++++++++++++++++++++- server/src/apub/user.rs | 8 + ui/src/api_tests/api.spec.ts | 276 +++++++++++++++++++ 11 files changed, 1079 insertions(+), 25 deletions(-) diff --git a/server/src/api/comment.rs b/server/src/api/comment.rs index 1ecedb2c8..2853beb34 100644 --- a/server/src/api/comment.rs +++ b/server/src/api/comment.rs @@ -343,6 +343,12 @@ impl Perform for Oper { } else { updated_comment.send_undo_delete(&user, &conn)?; } + } else if let Some(removed) = data.removed.to_owned() { + if removed { + updated_comment.send_remove(&user, &conn)?; + } else { + updated_comment.send_undo_remove(&user, &conn)?; + } } else { updated_comment.send_update(&user, &conn)?; } diff --git a/server/src/api/community.rs b/server/src/api/community.rs index 71da6712a..9659469b9 100644 --- a/server/src/api/community.rs +++ b/server/src/api/community.rs @@ -386,6 +386,12 @@ impl Perform for Oper { } else { updated_community.send_undo_delete(&user, &conn)?; } + } else if let Some(removed) = data.removed.to_owned() { + if removed { + updated_community.send_remove(&user, &conn)?; + } else { + updated_community.send_undo_remove(&user, &conn)?; + } } let community_view = CommunityView::read(&conn, data.edit_id, Some(user_id))?; diff --git a/server/src/api/post.rs b/server/src/api/post.rs index 55e0612fe..b9c4c0836 100644 --- a/server/src/api/post.rs +++ b/server/src/api/post.rs @@ -547,6 +547,12 @@ impl Perform for Oper { } else { updated_post.send_undo_delete(&user, &conn)?; } + } else if let Some(removed) = data.removed.to_owned() { + if removed { + updated_post.send_remove(&user, &conn)?; + } else { + updated_post.send_undo_remove(&user, &conn)?; + } } else { updated_post.send_update(&user, &conn)?; } diff --git a/server/src/apub/comment.rs b/server/src/apub/comment.rs index 65dd3c19a..872582751 100644 --- a/server/src/apub/comment.rs +++ b/server/src/apub/comment.rs @@ -187,7 +187,7 @@ impl ApubObjectType for Comment { // Insert the sent activity into the activity table let activity_form = activity::ActivityForm { - user_id: self.creator_id, + user_id: creator.id, data: serde_json::to_value(&delete)?, local: true, updated: None, @@ -240,7 +240,7 @@ impl ApubObjectType for Comment { // Insert the sent activity into the activity table let activity_form = activity::ActivityForm { - user_id: self.creator_id, + user_id: creator.id, data: serde_json::to_value(&undo)?, local: true, updated: None, @@ -255,6 +255,95 @@ impl ApubObjectType for Comment { )?; Ok(()) } + + fn send_remove(&self, mod_: &User_, conn: &PgConnection) -> Result<(), Error> { + let note = self.to_apub(&conn)?; + let post = Post::read(&conn, self.post_id)?; + let community = Community::read(&conn, post.community_id)?; + let id = format!("{}/remove/{}", self.ap_id, uuid::Uuid::new_v4()); + let mut remove = Remove::default(); + + populate_object_props( + &mut remove.object_props, + &community.get_followers_url(), + &id, + )?; + + remove + .remove_props + .set_actor_xsd_any_uri(mod_.actor_id.to_owned())? + .set_object_base_box(note)?; + + // Insert the sent activity into the activity table + let activity_form = activity::ActivityForm { + user_id: mod_.id, + data: serde_json::to_value(&remove)?, + local: true, + updated: None, + }; + activity::Activity::create(&conn, &activity_form)?; + + send_activity( + &remove, + &mod_.private_key.as_ref().unwrap(), + &mod_.actor_id, + community.get_follower_inboxes(&conn)?, + )?; + Ok(()) + } + + fn send_undo_remove(&self, mod_: &User_, conn: &PgConnection) -> Result<(), Error> { + let note = self.to_apub(&conn)?; + let post = Post::read(&conn, self.post_id)?; + let community = Community::read(&conn, post.community_id)?; + + // Generate a fake delete activity, with the correct object + let id = format!("{}/remove/{}", self.ap_id, uuid::Uuid::new_v4()); + let mut remove = Remove::default(); + + populate_object_props( + &mut remove.object_props, + &community.get_followers_url(), + &id, + )?; + + remove + .remove_props + .set_actor_xsd_any_uri(mod_.actor_id.to_owned())? + .set_object_base_box(note)?; + + // Undo that fake activity + let undo_id = format!("{}/undo/remove/{}", self.ap_id, uuid::Uuid::new_v4()); + let mut undo = Undo::default(); + + populate_object_props( + &mut undo.object_props, + &community.get_followers_url(), + &undo_id, + )?; + + undo + .undo_props + .set_actor_xsd_any_uri(mod_.actor_id.to_owned())? + .set_object_base_box(remove)?; + + // Insert the sent activity into the activity table + let activity_form = activity::ActivityForm { + user_id: mod_.id, + data: serde_json::to_value(&undo)?, + local: true, + updated: None, + }; + activity::Activity::create(&conn, &activity_form)?; + + send_activity( + &undo, + &mod_.private_key.as_ref().unwrap(), + &mod_.actor_id, + community.get_follower_inboxes(&conn)?, + )?; + Ok(()) + } } impl ApubLikeableType for Comment { diff --git a/server/src/apub/community.rs b/server/src/apub/community.rs index c4d9bf839..3510fbffc 100644 --- a/server/src/apub/community.rs +++ b/server/src/apub/community.rs @@ -14,13 +14,21 @@ impl ToApub for Community { let mut group = Group::default(); let oprops: &mut ObjectProperties = group.as_mut(); - let creator = User_::read(conn, self.creator_id)?; + // The attributed to, is an ordered vector with the creator actor_ids first, + // then the rest of the moderators + // TODO Technically the instance admins can mod the community, but lets + // ignore that for now + let moderators = CommunityModeratorView::for_community(&conn, self.id)? + .into_iter() + .map(|m| m.user_actor_id) + .collect(); + oprops .set_context_xsd_any_uri(context())? .set_id(self.actor_id.to_owned())? .set_name_xsd_string(self.name.to_owned())? .set_published(convert_datetime(self.published))? - .set_attributed_to_xsd_any_uri(creator.actor_id)?; + .set_many_attributed_to_xsd_any_uris(moderators)?; if let Some(u) = self.updated.to_owned() { oprops.set_updated(convert_datetime(u))?; @@ -181,6 +189,83 @@ impl ActorType for Community { Ok(()) } + fn send_remove(&self, mod_: &User_, conn: &PgConnection) -> Result<(), Error> { + let group = self.to_apub(conn)?; + let id = format!("{}/remove/{}", self.actor_id, uuid::Uuid::new_v4()); + + let mut remove = Remove::default(); + populate_object_props(&mut remove.object_props, &self.get_followers_url(), &id)?; + + remove + .remove_props + .set_actor_xsd_any_uri(mod_.actor_id.to_owned())? + .set_object_base_box(group)?; + + // Insert the sent activity into the activity table + let activity_form = activity::ActivityForm { + user_id: mod_.id, + data: serde_json::to_value(&remove)?, + local: true, + updated: None, + }; + activity::Activity::create(&conn, &activity_form)?; + + // Note: For an accept, since it was automatic, no one pushed a button, + // the community was the actor. + // But for delete, the creator is the actor, and does the signing + send_activity( + &remove, + &mod_.private_key.as_ref().unwrap(), + &mod_.actor_id, + self.get_follower_inboxes(&conn)?, + )?; + Ok(()) + } + + fn send_undo_remove(&self, mod_: &User_, conn: &PgConnection) -> Result<(), Error> { + let group = self.to_apub(conn)?; + let id = format!("{}/remove/{}", self.actor_id, uuid::Uuid::new_v4()); + + let mut remove = Remove::default(); + populate_object_props(&mut remove.object_props, &self.get_followers_url(), &id)?; + + remove + .remove_props + .set_actor_xsd_any_uri(mod_.actor_id.to_owned())? + .set_object_base_box(group)?; + + // Undo that fake activity + let undo_id = format!("{}/undo/remove/{}", self.actor_id, uuid::Uuid::new_v4()); + let mut undo = Undo::default(); + + populate_object_props(&mut undo.object_props, &self.get_followers_url(), &undo_id)?; + + undo + .undo_props + .set_actor_xsd_any_uri(mod_.actor_id.to_owned())? + .set_object_base_box(remove)?; + + // Insert the sent activity into the activity table + let activity_form = activity::ActivityForm { + user_id: mod_.id, + data: serde_json::to_value(&undo)?, + local: true, + updated: None, + }; + activity::Activity::create(&conn, &activity_form)?; + + // Note: For an accept, since it was automatic, no one pushed a button, + // the community was the actor. + // But for remove , the creator is the actor, and does the signing + send_activity( + &undo, + &mod_.private_key.as_ref().unwrap(), + &mod_.actor_id, + self.get_follower_inboxes(&conn)?, + )?; + Ok(()) + } + /// For a given community, returns the inboxes of all followers. fn get_follower_inboxes(&self, conn: &PgConnection) -> Result, Error> { Ok( @@ -220,8 +305,11 @@ impl FromApub for CommunityForm { // TODO don't do extra fetching here // let _outbox = fetch_remote_object::(&outbox_uri)?; // let _followers = fetch_remote_object::(&followers_uri)?; - let apub_id = &oprops.get_attributed_to_xsd_any_uri().unwrap().to_string(); - let creator = get_or_fetch_and_upsert_remote_user(&apub_id, conn)?; + let mut creator_and_moderator_uris = oprops.get_many_attributed_to_xsd_any_uris().unwrap(); + let creator = creator_and_moderator_uris + .next() + .map(|c| get_or_fetch_and_upsert_remote_user(&c.to_string(), &conn).unwrap()) + .unwrap(); Ok(CommunityForm { name: oprops.get_name_xsd_string().unwrap().to_string(), diff --git a/server/src/apub/fetcher.rs b/server/src/apub/fetcher.rs index e07e410ba..e581e14d0 100644 --- a/server/src/apub/fetcher.rs +++ b/server/src/apub/fetcher.rs @@ -86,10 +86,10 @@ pub fn get_or_fetch_and_upsert_remote_user( match User_::read_from_actor_id(&conn, &apub_id) { Ok(u) => { // If its older than a day, re-fetch it - // TODO the less than needs to be tested - if u - .last_refreshed_at - .lt(&(naive_now() - chrono::Duration::days(1))) + if !u.local + && u + .last_refreshed_at + .lt(&(naive_now() - chrono::Duration::days(1))) { debug!("Fetching and updating from remote user: {}", apub_id); let person = fetch_remote_object::(&Url::parse(apub_id)?)?; @@ -118,10 +118,10 @@ pub fn get_or_fetch_and_upsert_remote_community( match Community::read_from_actor_id(&conn, &apub_id) { Ok(c) => { // If its older than a day, re-fetch it - // TODO the less than needs to be tested - if c - .last_refreshed_at - .lt(&(naive_now() - chrono::Duration::days(1))) + if !c.local + && c + .last_refreshed_at + .lt(&(naive_now() - chrono::Duration::days(1))) { debug!("Fetching and updating from remote community: {}", apub_id); let group = fetch_remote_object::(&Url::parse(apub_id)?)?; @@ -136,7 +136,28 @@ pub fn get_or_fetch_and_upsert_remote_community( debug!("Fetching and creating remote community: {}", apub_id); let group = fetch_remote_object::(&Url::parse(apub_id)?)?; let cf = CommunityForm::from_apub(&group, conn)?; - Ok(Community::create(conn, &cf)?) + let community = Community::create(conn, &cf)?; + + // Also add the community moderators too + let creator_and_moderator_uris = group + .base + .base + .object_props + .get_many_attributed_to_xsd_any_uris() + .unwrap(); + let creator_and_moderators = creator_and_moderator_uris + .map(|c| get_or_fetch_and_upsert_remote_user(&c.to_string(), &conn).unwrap()) + .collect::>(); + + for mod_ in creator_and_moderators { + let community_moderator_form = CommunityModeratorForm { + community_id: community.id, + user_id: mod_.id, + }; + CommunityModerator::join(&conn, &community_moderator_form)?; + } + + Ok(community) } Err(e) => Err(Error::from(e)), } diff --git a/server/src/apub/mod.rs b/server/src/apub/mod.rs index 1d8605025..3c18a0133 100644 --- a/server/src/apub/mod.rs +++ b/server/src/apub/mod.rs @@ -13,7 +13,7 @@ use crate::api::community::CommunityResponse; use crate::websocket::server::SendCommunityRoomMessage; use activitystreams::object::kind::{NoteType, PageType}; use activitystreams::{ - activity::{Accept, Create, Delete, Dislike, Follow, Like, Undo, Update}, + activity::{Accept, Create, Delete, Dislike, Follow, Like, Remove, Undo, Update}, actor::{properties::ApActorProperties, Actor, Group, Person}, collection::UnorderedCollection, context, @@ -47,13 +47,16 @@ use crate::api::post::PostResponse; use crate::api::site::SearchResponse; use crate::db::comment::{Comment, CommentForm, CommentLike, CommentLikeForm}; use crate::db::comment_view::CommentView; -use crate::db::community::{Community, CommunityFollower, CommunityFollowerForm, CommunityForm}; -use crate::db::community_view::{CommunityFollowerView, CommunityView}; +use crate::db::community::{ + Community, CommunityFollower, CommunityFollowerForm, CommunityForm, CommunityModerator, + CommunityModeratorForm, +}; +use crate::db::community_view::{CommunityFollowerView, CommunityModeratorView, CommunityView}; use crate::db::post::{Post, PostForm, PostLike, PostLikeForm}; use crate::db::post_view::PostView; use crate::db::user::{UserForm, User_}; use crate::db::user_view::UserView; -use crate::db::{activity, Crud, Followable, Likeable, SearchType}; +use crate::db::{activity, Crud, Followable, Joinable, Likeable, SearchType}; use crate::routes::nodeinfo::{NodeInfo, NodeInfoWellKnown}; use crate::routes::{ChatServerParam, DbPoolParam}; use crate::websocket::{ @@ -197,6 +200,8 @@ pub trait ApubObjectType { fn send_update(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error>; fn send_delete(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error>; fn send_undo_delete(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error>; + fn send_remove(&self, mod_: &User_, conn: &PgConnection) -> Result<(), Error>; + fn send_undo_remove(&self, mod_: &User_, conn: &PgConnection) -> Result<(), Error>; } pub trait ApubLikeableType { @@ -239,6 +244,9 @@ pub trait ActorType { fn send_delete(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error>; fn send_undo_delete(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error>; + fn send_remove(&self, mod_: &User_, conn: &PgConnection) -> Result<(), Error>; + fn send_undo_remove(&self, mod_: &User_, conn: &PgConnection) -> Result<(), Error>; + // TODO default because there is no user following yet. #[allow(unused_variables)] /// For a given community, returns the inboxes of all followers. diff --git a/server/src/apub/post.rs b/server/src/apub/post.rs index 5a7383c0e..2d1f1c710 100644 --- a/server/src/apub/post.rs +++ b/server/src/apub/post.rs @@ -263,6 +263,92 @@ impl ApubObjectType for Post { )?; Ok(()) } + + fn send_remove(&self, mod_: &User_, conn: &PgConnection) -> Result<(), Error> { + let page = self.to_apub(conn)?; + let community = Community::read(conn, self.community_id)?; + let id = format!("{}/remove/{}", self.ap_id, uuid::Uuid::new_v4()); + let mut remove = Remove::default(); + + populate_object_props( + &mut remove.object_props, + &community.get_followers_url(), + &id, + )?; + + remove + .remove_props + .set_actor_xsd_any_uri(mod_.actor_id.to_owned())? + .set_object_base_box(page)?; + + // Insert the sent activity into the activity table + let activity_form = activity::ActivityForm { + user_id: mod_.id, + data: serde_json::to_value(&remove)?, + local: true, + updated: None, + }; + activity::Activity::create(&conn, &activity_form)?; + + let community = Community::read(conn, self.community_id)?; + send_activity( + &remove, + &mod_.private_key.as_ref().unwrap(), + &mod_.actor_id, + community.get_follower_inboxes(&conn)?, + )?; + Ok(()) + } + fn send_undo_remove(&self, mod_: &User_, conn: &PgConnection) -> Result<(), Error> { + let page = self.to_apub(conn)?; + let community = Community::read(conn, self.community_id)?; + let id = format!("{}/remove/{}", self.ap_id, uuid::Uuid::new_v4()); + let mut remove = Remove::default(); + + populate_object_props( + &mut remove.object_props, + &community.get_followers_url(), + &id, + )?; + + remove + .remove_props + .set_actor_xsd_any_uri(mod_.actor_id.to_owned())? + .set_object_base_box(page)?; + + // Undo that fake activity + let undo_id = format!("{}/undo/remove/{}", self.ap_id, uuid::Uuid::new_v4()); + let mut undo = Undo::default(); + + populate_object_props( + &mut undo.object_props, + &community.get_followers_url(), + &undo_id, + )?; + + undo + .undo_props + .set_actor_xsd_any_uri(mod_.actor_id.to_owned())? + .set_object_base_box(remove)?; + + // Insert the sent activity into the activity table + let activity_form = activity::ActivityForm { + user_id: mod_.id, + data: serde_json::to_value(&undo)?, + local: true, + updated: None, + }; + activity::Activity::create(&conn, &activity_form)?; + + let community = Community::read(conn, self.community_id)?; + send_activity( + &undo, + &mod_.private_key.as_ref().unwrap(), + &mod_.actor_id, + community.get_follower_inboxes(&conn)?, + )?; + Ok(()) + } } impl ApubLikeableType for Post { diff --git a/server/src/apub/shared_inbox.rs b/server/src/apub/shared_inbox.rs index a9a610200..d77788e55 100644 --- a/server/src/apub/shared_inbox.rs +++ b/server/src/apub/shared_inbox.rs @@ -3,12 +3,13 @@ use super::*; #[serde(untagged)] #[derive(Serialize, Deserialize, Debug)] pub enum SharedAcceptedObjects { - Create(Create), - Update(Update), - Like(Like), - Dislike(Dislike), - Delete(Delete), - Undo(Undo), + Create(Box), + Update(Box), + Like(Box), + Dislike(Box), + Delete(Box), + Undo(Box), + Remove(Box), } impl SharedAcceptedObjects { @@ -20,6 +21,7 @@ impl SharedAcceptedObjects { SharedAcceptedObjects::Dislike(d) => d.dislike_props.get_object_base_box(), SharedAcceptedObjects::Delete(d) => d.delete_props.get_object_base_box(), SharedAcceptedObjects::Undo(d) => d.undo_props.get_object_base_box(), + SharedAcceptedObjects::Remove(r) => r.remove_props.get_object_base_box(), } } } @@ -56,6 +58,9 @@ pub async fn shared_inbox( (SharedAcceptedObjects::Delete(d), Some("Page")) => { receive_delete_post(&d, &request, &conn, chat_server) } + (SharedAcceptedObjects::Remove(r), Some("Page")) => { + receive_remove_post(&r, &request, &conn, chat_server) + } (SharedAcceptedObjects::Create(c), Some("Note")) => { receive_create_comment(&c, &request, &conn, chat_server) } @@ -71,12 +76,21 @@ pub async fn shared_inbox( (SharedAcceptedObjects::Delete(d), Some("Note")) => { receive_delete_comment(&d, &request, &conn, chat_server) } + (SharedAcceptedObjects::Remove(r), Some("Note")) => { + receive_remove_comment(&r, &request, &conn, chat_server) + } (SharedAcceptedObjects::Delete(d), Some("Group")) => { receive_delete_community(&d, &request, &conn, chat_server) } + (SharedAcceptedObjects::Remove(r), Some("Group")) => { + receive_remove_community(&r, &request, &conn, chat_server) + } (SharedAcceptedObjects::Undo(u), Some("Delete")) => { receive_undo_delete(&u, &request, &conn, chat_server) } + (SharedAcceptedObjects::Undo(u), Some("Remove")) => { + receive_undo_remove(&u, &request, &conn, chat_server) + } _ => Err(format_err!("Unknown incoming activity type.")), } } @@ -588,6 +602,75 @@ fn receive_delete_community( Ok(HttpResponse::Ok().finish()) } +fn receive_remove_community( + remove: &Remove, + request: &HttpRequest, + conn: &PgConnection, + chat_server: ChatServerParam, +) -> Result { + let mod_uri = remove + .remove_props + .get_actor_xsd_any_uri() + .unwrap() + .to_string(); + + let group = remove + .remove_props + .get_object_base_box() + .to_owned() + .unwrap() + .to_owned() + .into_concrete::()?; + + let mod_ = get_or_fetch_and_upsert_remote_user(&mod_uri, &conn)?; + verify(request, &mod_.public_key.unwrap())?; + + // Insert the received activity into the activity table + let activity_form = activity::ActivityForm { + user_id: mod_.id, + data: serde_json::to_value(&remove)?, + local: false, + updated: None, + }; + activity::Activity::create(&conn, &activity_form)?; + + let community_actor_id = CommunityForm::from_apub(&group, &conn)?.actor_id; + let community = Community::read_from_actor_id(conn, &community_actor_id)?; + + let community_form = CommunityForm { + name: community.name.to_owned(), + title: community.title.to_owned(), + description: community.description.to_owned(), + category_id: community.category_id, // Note: need to keep this due to foreign key constraint + creator_id: community.creator_id, // Note: need to keep this due to foreign key constraint + removed: Some(true), + published: None, + updated: Some(naive_now()), + deleted: None, + nsfw: community.nsfw, + actor_id: community.actor_id, + local: community.local, + private_key: community.private_key, + public_key: community.public_key, + last_refreshed_at: None, + }; + + Community::update(&conn, community.id, &community_form)?; + + let res = CommunityResponse { + community: CommunityView::read(&conn, community.id, None)?, + }; + + chat_server.do_send(SendCommunityRoomMessage { + op: UserOperation::EditCommunity, + response: res, + community_id: community.id, + my_id: None, + }); + + Ok(HttpResponse::Ok().finish()) +} + fn receive_delete_post( delete: &Delete, request: &HttpRequest, @@ -659,6 +742,77 @@ fn receive_delete_post( Ok(HttpResponse::Ok().finish()) } +fn receive_remove_post( + remove: &Remove, + request: &HttpRequest, + conn: &PgConnection, + chat_server: ChatServerParam, +) -> Result { + let mod_uri = remove + .remove_props + .get_actor_xsd_any_uri() + .unwrap() + .to_string(); + + let page = remove + .remove_props + .get_object_base_box() + .to_owned() + .unwrap() + .to_owned() + .into_concrete::()?; + + let mod_ = get_or_fetch_and_upsert_remote_user(&mod_uri, &conn)?; + verify(request, &mod_.public_key.unwrap())?; + + // Insert the received activity into the activity table + let activity_form = activity::ActivityForm { + user_id: mod_.id, + data: serde_json::to_value(&remove)?, + local: false, + updated: None, + }; + activity::Activity::create(&conn, &activity_form)?; + + let post_ap_id = PostForm::from_apub(&page, conn)?.ap_id; + let post = Post::read_from_apub_id(conn, &post_ap_id)?; + + let post_form = PostForm { + name: post.name.to_owned(), + url: post.url.to_owned(), + body: post.body.to_owned(), + creator_id: post.creator_id.to_owned(), + community_id: post.community_id, + removed: Some(true), + deleted: None, + nsfw: post.nsfw, + locked: None, + stickied: None, + updated: Some(naive_now()), + embed_title: post.embed_title, + embed_description: post.embed_description, + embed_html: post.embed_html, + thumbnail_url: post.thumbnail_url, + ap_id: post.ap_id, + local: post.local, + published: None, + }; + Post::update(&conn, post.id, &post_form)?; + + // Refetch the view + let post_view = PostView::read(&conn, post.id, None)?; + + let res = PostResponse { post: post_view }; + + chat_server.do_send(SendPost { + op: UserOperation::EditPost, + post: res, + my_id: None, + }); + + Ok(HttpResponse::Ok().finish()) +} + fn receive_delete_comment( delete: &Delete, request: &HttpRequest, @@ -727,6 +881,74 @@ fn receive_delete_comment( Ok(HttpResponse::Ok().finish()) } +fn receive_remove_comment( + remove: &Remove, + request: &HttpRequest, + conn: &PgConnection, + chat_server: ChatServerParam, +) -> Result { + let mod_uri = remove + .remove_props + .get_actor_xsd_any_uri() + .unwrap() + .to_string(); + + let note = remove + .remove_props + .get_object_base_box() + .to_owned() + .unwrap() + .to_owned() + .into_concrete::()?; + + let mod_ = get_or_fetch_and_upsert_remote_user(&mod_uri, &conn)?; + verify(request, &mod_.public_key.unwrap())?; + + // Insert the received activity into the activity table + let activity_form = activity::ActivityForm { + user_id: mod_.id, + data: serde_json::to_value(&remove)?, + local: false, + updated: None, + }; + activity::Activity::create(&conn, &activity_form)?; + + let comment_ap_id = CommentForm::from_apub(¬e, &conn)?.ap_id; + let comment = Comment::read_from_apub_id(conn, &comment_ap_id)?; + let comment_form = CommentForm { + content: comment.content.to_owned(), + parent_id: comment.parent_id, + post_id: comment.post_id, + creator_id: comment.creator_id, + removed: Some(true), + deleted: None, + read: None, + published: None, + updated: Some(naive_now()), + ap_id: comment.ap_id, + local: comment.local, + }; + Comment::update(&conn, comment.id, &comment_form)?; + + // Refetch the view + let comment_view = CommentView::read(&conn, comment.id, None)?; + + // TODO get those recipient actor ids from somewhere + let recipient_ids = vec![]; + let res = CommentResponse { + comment: comment_view, + recipient_ids, + }; + + chat_server.do_send(SendComment { + op: UserOperation::EditComment, + comment: res, + my_id: None, + }); + + Ok(HttpResponse::Ok().finish()) +} + fn receive_undo_delete( undo: &Undo, request: &HttpRequest, @@ -757,6 +979,36 @@ fn receive_undo_delete( } } +fn receive_undo_remove( + undo: &Undo, + request: &HttpRequest, + conn: &PgConnection, + chat_server: ChatServerParam, +) -> Result { + let remove = undo + .undo_props + .get_object_base_box() + .to_owned() + .unwrap() + .to_owned() + .into_concrete::()?; + + let type_ = remove + .remove_props + .get_object_base_box() + .to_owned() + .unwrap() + .kind() + .unwrap(); + + match type_ { + "Note" => receive_undo_remove_comment(&remove, &request, &conn, chat_server), + "Page" => receive_undo_remove_post(&remove, &request, &conn, chat_server), + "Group" => receive_undo_remove_community(&remove, &request, &conn, chat_server), + d => Err(format_err!("Undo Delete type {} not supported", d)), + } +} + fn receive_undo_delete_comment( delete: &Delete, request: &HttpRequest, @@ -825,6 +1077,74 @@ fn receive_undo_delete_comment( Ok(HttpResponse::Ok().finish()) } +fn receive_undo_remove_comment( + remove: &Remove, + request: &HttpRequest, + conn: &PgConnection, + chat_server: ChatServerParam, +) -> Result { + let mod_uri = remove + .remove_props + .get_actor_xsd_any_uri() + .unwrap() + .to_string(); + + let note = remove + .remove_props + .get_object_base_box() + .to_owned() + .unwrap() + .to_owned() + .into_concrete::()?; + + let mod_ = get_or_fetch_and_upsert_remote_user(&mod_uri, &conn)?; + verify(request, &mod_.public_key.unwrap())?; + + // Insert the received activity into the activity table + let activity_form = activity::ActivityForm { + user_id: mod_.id, + data: serde_json::to_value(&remove)?, + local: false, + updated: None, + }; + activity::Activity::create(&conn, &activity_form)?; + + let comment_ap_id = CommentForm::from_apub(¬e, &conn)?.ap_id; + let comment = Comment::read_from_apub_id(conn, &comment_ap_id)?; + let comment_form = CommentForm { + content: comment.content.to_owned(), + parent_id: comment.parent_id, + post_id: comment.post_id, + creator_id: comment.creator_id, + removed: Some(false), + deleted: None, + read: None, + published: None, + updated: Some(naive_now()), + ap_id: comment.ap_id, + local: comment.local, + }; + Comment::update(&conn, comment.id, &comment_form)?; + + // Refetch the view + let comment_view = CommentView::read(&conn, comment.id, None)?; + + // TODO get those recipient actor ids from somewhere + let recipient_ids = vec![]; + let res = CommentResponse { + comment: comment_view, + recipient_ids, + }; + + chat_server.do_send(SendComment { + op: UserOperation::EditComment, + comment: res, + my_id: None, + }); + + Ok(HttpResponse::Ok().finish()) +} + fn receive_undo_delete_post( delete: &Delete, request: &HttpRequest, @@ -896,6 +1216,77 @@ fn receive_undo_delete_post( Ok(HttpResponse::Ok().finish()) } +fn receive_undo_remove_post( + remove: &Remove, + request: &HttpRequest, + conn: &PgConnection, + chat_server: ChatServerParam, +) -> Result { + let mod_uri = remove + .remove_props + .get_actor_xsd_any_uri() + .unwrap() + .to_string(); + + let page = remove + .remove_props + .get_object_base_box() + .to_owned() + .unwrap() + .to_owned() + .into_concrete::()?; + + let mod_ = get_or_fetch_and_upsert_remote_user(&mod_uri, &conn)?; + verify(request, &mod_.public_key.unwrap())?; + + // Insert the received activity into the activity table + let activity_form = activity::ActivityForm { + user_id: mod_.id, + data: serde_json::to_value(&remove)?, + local: false, + updated: None, + }; + activity::Activity::create(&conn, &activity_form)?; + + let post_ap_id = PostForm::from_apub(&page, conn)?.ap_id; + let post = Post::read_from_apub_id(conn, &post_ap_id)?; + + let post_form = PostForm { + name: post.name.to_owned(), + url: post.url.to_owned(), + body: post.body.to_owned(), + creator_id: post.creator_id.to_owned(), + community_id: post.community_id, + removed: Some(false), + deleted: None, + nsfw: post.nsfw, + locked: None, + stickied: None, + updated: Some(naive_now()), + embed_title: post.embed_title, + embed_description: post.embed_description, + embed_html: post.embed_html, + thumbnail_url: post.thumbnail_url, + ap_id: post.ap_id, + local: post.local, + published: None, + }; + Post::update(&conn, post.id, &post_form)?; + + // Refetch the view + let post_view = PostView::read(&conn, post.id, None)?; + + let res = PostResponse { post: post_view }; + + chat_server.do_send(SendPost { + op: UserOperation::EditPost, + post: res, + my_id: None, + }); + + Ok(HttpResponse::Ok().finish()) +} + fn receive_undo_delete_community( delete: &Delete, request: &HttpRequest, @@ -964,3 +1355,72 @@ fn receive_undo_delete_community( Ok(HttpResponse::Ok().finish()) } + +fn receive_undo_remove_community( + remove: &Remove, + request: &HttpRequest, + conn: &PgConnection, + chat_server: ChatServerParam, +) -> Result { + let mod_uri = remove + .remove_props + .get_actor_xsd_any_uri() + .unwrap() + .to_string(); + + let group = remove + .remove_props + .get_object_base_box() + .to_owned() + .unwrap() + .to_owned() + .into_concrete::()?; + + let mod_ = get_or_fetch_and_upsert_remote_user(&mod_uri, &conn)?; + verify(request, &mod_.public_key.unwrap())?; + + // Insert the received activity into the activity table + let activity_form = activity::ActivityForm { + user_id: mod_.id, + data: serde_json::to_value(&remove)?, + local: false, + updated: None, + }; + activity::Activity::create(&conn, &activity_form)?; + + let community_actor_id = CommunityForm::from_apub(&group, &conn)?.actor_id; + let community = Community::read_from_actor_id(conn, &community_actor_id)?; + + let community_form = CommunityForm { + name: community.name.to_owned(), + title: community.title.to_owned(), + description: community.description.to_owned(), + category_id: community.category_id, // Note: need to keep this due to foreign key constraint + creator_id: community.creator_id, // Note: need to keep this due to foreign key constraint + removed: Some(false), + published: None, + updated: Some(naive_now()), + deleted: None, + nsfw: community.nsfw, + actor_id: community.actor_id, + local: community.local, + private_key: community.private_key, + public_key: community.public_key, + last_refreshed_at: None, + }; + + Community::update(&conn, community.id, &community_form)?; + + let res = CommunityResponse { + community: CommunityView::read(&conn, community.id, None)?, + }; + + chat_server.do_send(SendCommunityRoomMessage { + op: UserOperation::EditCommunity, + response: res, + community_id: community.id, + my_id: None, + }); + + Ok(HttpResponse::Ok().finish()) +} diff --git a/server/src/apub/user.rs b/server/src/apub/user.rs index b5f47e251..d9c7e86a0 100644 --- a/server/src/apub/user.rs +++ b/server/src/apub/user.rs @@ -98,6 +98,14 @@ impl ActorType for User_ { fn send_undo_delete(&self, _creator: &User_, _conn: &PgConnection) -> Result<(), Error> { unimplemented!() } + + fn send_remove(&self, _creator: &User_, _conn: &PgConnection) -> Result<(), Error> { + unimplemented!() + } + + fn send_undo_remove(&self, _creator: &User_, _conn: &PgConnection) -> Result<(), Error> { + unimplemented!() + } } impl FromApub for UserForm { diff --git a/ui/src/api_tests/api.spec.ts b/ui/src/api_tests/api.spec.ts index e6f7bd864..cc240476c 100644 --- a/ui/src/api_tests/api.spec.ts +++ b/ui/src/api_tests/api.spec.ts @@ -603,6 +603,282 @@ describe('main', () => { expect(getCommunityResAgain.community.deleted).toBe(false); }); }); + + describe('remove things', () => { + test('/u/lemmy_beta removes and unremoves a federated comment, post, and community, lemmy_alpha sees its removed.', async () => { + // Create a test community + let communityName = 'test_community_rem'; + let communityForm: CommunityForm = { + name: communityName, + title: communityName, + category_id: 1, + nsfw: false, + auth: lemmyBetaAuth, + }; + + let createCommunityRes: CommunityResponse = await fetch( + `${lemmyBetaApiUrl}/community`, + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: wrapper(communityForm), + } + ).then(d => d.json()); + + expect(createCommunityRes.community.name).toBe(communityName); + + // Cache it on lemmy_alpha + let searchUrl = `${lemmyAlphaApiUrl}/search?q=http://lemmy_beta:8550/c/${communityName}&type_=All&sort=TopAll`; + let searchResponse: SearchResponse = await fetch(searchUrl, { + method: 'GET', + }).then(d => d.json()); + + let communityOnAlphaId = searchResponse.communities[0].id; + + // Follow it + let followForm: FollowCommunityForm = { + community_id: communityOnAlphaId, + follow: true, + auth: lemmyAlphaAuth, + }; + + let followRes: CommunityResponse = await fetch( + `${lemmyAlphaApiUrl}/community/follow`, + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: wrapper(followForm), + } + ).then(d => d.json()); + + // Make sure the follow response went through + expect(followRes.community.local).toBe(false); + expect(followRes.community.name).toBe(communityName); + + // Lemmy beta creates a test post + let postName = 'A jest test post with remove'; + let createPostForm: PostForm = { + name: postName, + auth: lemmyBetaAuth, + community_id: createCommunityRes.community.id, + creator_id: 2, + nsfw: false, + }; + + let createPostRes: PostResponse = await fetch(`${lemmyBetaApiUrl}/post`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: wrapper(createPostForm), + }).then(d => d.json()); + expect(createPostRes.post.name).toBe(postName); + + // Lemmy beta creates a test comment + let commentContent = 'A jest test federated comment with remove'; + let createCommentForm: CommentForm = { + content: commentContent, + post_id: createPostRes.post.id, + auth: lemmyBetaAuth, + }; + + let createCommentRes: CommentResponse = await fetch( + `${lemmyBetaApiUrl}/comment`, + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: wrapper(createCommentForm), + } + ).then(d => d.json()); + + expect(createCommentRes.comment.content).toBe(commentContent); + + // lemmy_beta removes the comment + let removeCommentForm: CommentForm = { + content: commentContent, + edit_id: createCommentRes.comment.id, + post_id: createPostRes.post.id, + removed: true, + auth: lemmyBetaAuth, + creator_id: createCommentRes.comment.creator_id, + }; + + let removeCommentRes: CommentResponse = await fetch( + `${lemmyBetaApiUrl}/comment`, + { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + }, + body: wrapper(removeCommentForm), + } + ).then(d => d.json()); + expect(removeCommentRes.comment.removed).toBe(true); + + // lemmy_alpha sees that the comment is removed + let getPostUrl = `${lemmyAlphaApiUrl}/post?id=4`; + let getPostRes: GetPostResponse = await fetch(getPostUrl, { + method: 'GET', + }).then(d => d.json()); + expect(getPostRes.comments[0].removed).toBe(true); + + // lemmy_beta undeletes the comment + let unremoveCommentForm: CommentForm = { + content: commentContent, + edit_id: createCommentRes.comment.id, + post_id: createPostRes.post.id, + removed: false, + auth: lemmyBetaAuth, + creator_id: createCommentRes.comment.creator_id, + }; + + let unremoveCommentRes: CommentResponse = await fetch( + `${lemmyBetaApiUrl}/comment`, + { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + }, + body: wrapper(unremoveCommentForm), + } + ).then(d => d.json()); + expect(unremoveCommentRes.comment.removed).toBe(false); + + // lemmy_alpha sees that the comment is undeleted + let getPostUnremoveRes: GetPostResponse = await fetch(getPostUrl, { + method: 'GET', + }).then(d => d.json()); + expect(getPostUnremoveRes.comments[0].removed).toBe(false); + + // lemmy_beta deletes the post + let removePostForm: PostForm = { + name: postName, + edit_id: createPostRes.post.id, + auth: lemmyBetaAuth, + community_id: createPostRes.post.community_id, + creator_id: createPostRes.post.creator_id, + nsfw: false, + removed: true, + }; + + let removePostRes: PostResponse = await fetch(`${lemmyBetaApiUrl}/post`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + }, + body: wrapper(removePostForm), + }).then(d => d.json()); + expect(removePostRes.post.removed).toBe(true); + + // Make sure lemmy_alpha sees the post is deleted + let getPostResAgain: GetPostResponse = await fetch(getPostUrl, { + method: 'GET', + }).then(d => d.json()); + expect(getPostResAgain.post.removed).toBe(true); + + // lemmy_beta unremoves the post + let unremovePostForm: PostForm = { + name: postName, + edit_id: createPostRes.post.id, + auth: lemmyBetaAuth, + community_id: createPostRes.post.community_id, + creator_id: createPostRes.post.creator_id, + nsfw: false, + removed: false, + }; + + let unremovePostRes: PostResponse = await fetch( + `${lemmyBetaApiUrl}/post`, + { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + }, + body: wrapper(unremovePostForm), + } + ).then(d => d.json()); + expect(unremovePostRes.post.removed).toBe(false); + + // Make sure lemmy_alpha sees the post is unremoved + let getPostResAgainTwo: GetPostResponse = await fetch(getPostUrl, { + method: 'GET', + }).then(d => d.json()); + expect(getPostResAgainTwo.post.removed).toBe(false); + + // lemmy_beta deletes the community + let removeCommunityForm: CommunityForm = { + name: communityName, + title: communityName, + category_id: 1, + edit_id: createCommunityRes.community.id, + nsfw: false, + removed: true, + auth: lemmyBetaAuth, + }; + + let removeCommunityRes: CommunityResponse = await fetch( + `${lemmyBetaApiUrl}/community`, + { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + }, + body: wrapper(removeCommunityForm), + } + ).then(d => d.json()); + + // Make sure the delete went through + expect(removeCommunityRes.community.removed).toBe(true); + + // Re-get it from alpha, make sure its removed there too + let getCommunityUrl = `${lemmyAlphaApiUrl}/community?id=${communityOnAlphaId}&auth=${lemmyAlphaAuth}`; + let getCommunityRes: GetCommunityResponse = await fetch(getCommunityUrl, { + method: 'GET', + }).then(d => d.json()); + + expect(getCommunityRes.community.removed).toBe(true); + + // lemmy_beta unremoves the community + let unremoveCommunityForm: CommunityForm = { + name: communityName, + title: communityName, + category_id: 1, + edit_id: createCommunityRes.community.id, + nsfw: false, + removed: false, + auth: lemmyBetaAuth, + }; + + let unremoveCommunityRes: CommunityResponse = await fetch( + `${lemmyBetaApiUrl}/community`, + { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + }, + body: wrapper(unremoveCommunityForm), + } + ).then(d => d.json()); + + // Make sure the delete went through + expect(unremoveCommunityRes.community.removed).toBe(false); + + // Re-get it from alpha, make sure its deleted there too + let getCommunityResAgain: GetCommunityResponse = await fetch( + getCommunityUrl, + { + method: 'GET', + } + ).then(d => d.json()); + expect(getCommunityResAgain.community.removed).toBe(false); + }); + }); }); function wrapper(form: any): string {