From 8358bdbe36da7b7f4074653bd89869b6d251e6ca Mon Sep 17 00:00:00 2001 From: Dessalines Date: Fri, 1 May 2020 15:01:29 -0400 Subject: [PATCH] Add undos for delete community, post, and comment. --- server/src/api/comment.rs | 2 +- server/src/api/community.rs | 2 +- server/src/api/post.rs | 2 +- server/src/apub/comment.rs | 53 +++++++ server/src/apub/community.rs | 44 ++++++ server/src/apub/mod.rs | 5 +- server/src/apub/post.rs | 51 +++++++ server/src/apub/shared_inbox.rs | 243 ++++++++++++++++++++++++++++++++ server/src/apub/user.rs | 4 + ui/src/api_tests/api.spec.ts | 95 ++++++++++++- 10 files changed, 495 insertions(+), 6 deletions(-) diff --git a/server/src/api/comment.rs b/server/src/api/comment.rs index 961ef0c11..1ecedb2c8 100644 --- a/server/src/api/comment.rs +++ b/server/src/api/comment.rs @@ -341,7 +341,7 @@ impl Perform for Oper { if deleted { updated_comment.send_delete(&user, &conn)?; } else { - // TODO: undo delete + updated_comment.send_undo_delete(&user, &conn)?; } } else { updated_comment.send_update(&user, &conn)?; diff --git a/server/src/api/community.rs b/server/src/api/community.rs index a08424317..71da6712a 100644 --- a/server/src/api/community.rs +++ b/server/src/api/community.rs @@ -384,7 +384,7 @@ impl Perform for Oper { if deleted { updated_community.send_delete(&user, &conn)?; } else { - // TODO: undo delete + updated_community.send_undo_delete(&user, &conn)?; } } diff --git a/server/src/api/post.rs b/server/src/api/post.rs index 3d2df4631..55e0612fe 100644 --- a/server/src/api/post.rs +++ b/server/src/api/post.rs @@ -545,7 +545,7 @@ impl Perform for Oper { if deleted { updated_post.send_delete(&user, &conn)?; } else { - // TODO: undo delete + updated_post.send_undo_delete(&user, &conn)?; } } else { updated_post.send_update(&user, &conn)?; diff --git a/server/src/apub/comment.rs b/server/src/apub/comment.rs index b30334772..65dd3c19a 100644 --- a/server/src/apub/comment.rs +++ b/server/src/apub/comment.rs @@ -202,6 +202,59 @@ impl ApubObjectType for Comment { )?; Ok(()) } + + fn send_undo_delete(&self, creator: &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!("{}/delete/{}", self.ap_id, uuid::Uuid::new_v4()); + let mut delete = Delete::default(); + + populate_object_props( + &mut delete.object_props, + &community.get_followers_url(), + &id, + )?; + + delete + .delete_props + .set_actor_xsd_any_uri(creator.actor_id.to_owned())? + .set_object_base_box(note)?; + + // Undo that fake activity + let undo_id = format!("{}/undo/delete/{}", 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(creator.actor_id.to_owned())? + .set_object_base_box(delete)?; + + // Insert the sent activity into the activity table + let activity_form = activity::ActivityForm { + user_id: self.creator_id, + data: serde_json::to_value(&undo)?, + local: true, + updated: None, + }; + activity::Activity::create(&conn, &activity_form)?; + + send_activity( + &undo, + &creator.private_key.as_ref().unwrap(), + &creator.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 336aa24f0..c4d9bf839 100644 --- a/server/src/apub/community.rs +++ b/server/src/apub/community.rs @@ -137,6 +137,50 @@ impl ActorType for Community { Ok(()) } + fn send_undo_delete(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error> { + let group = self.to_apub(conn)?; + let id = format!("{}/delete/{}", self.actor_id, uuid::Uuid::new_v4()); + + let mut delete = Delete::default(); + populate_object_props(&mut delete.object_props, &self.get_followers_url(), &id)?; + + delete + .delete_props + .set_actor_xsd_any_uri(creator.actor_id.to_owned())? + .set_object_base_box(group)?; + + // Undo that fake activity + let undo_id = format!("{}/undo/delete/{}", 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(creator.actor_id.to_owned())? + .set_object_base_box(delete)?; + + // Insert the sent activity into the activity table + let activity_form = activity::ActivityForm { + user_id: self.creator_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 delete, the creator is the actor, and does the signing + send_activity( + &undo, + &creator.private_key.as_ref().unwrap(), + &creator.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( diff --git a/server/src/apub/mod.rs b/server/src/apub/mod.rs index b56d6744f..1d8605025 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, Update}, + activity::{Accept, Create, Delete, Dislike, Follow, Like, Undo, Update}, actor::{properties::ApActorProperties, Actor, Group, Person}, collection::UnorderedCollection, context, @@ -196,11 +196,13 @@ pub trait ApubObjectType { fn send_create(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error>; 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>; } pub trait ApubLikeableType { fn send_like(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error>; fn send_dislike(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error>; + // TODO add send_undo_like / undo_dislike } pub fn get_shared_inbox(actor_id: &str) -> String { @@ -235,6 +237,7 @@ pub trait ActorType { } fn send_delete(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error>; + fn send_undo_delete(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error>; // TODO default because there is no user following yet. #[allow(unused_variables)] diff --git a/server/src/apub/post.rs b/server/src/apub/post.rs index 2c8bce722..5a7383c0e 100644 --- a/server/src/apub/post.rs +++ b/server/src/apub/post.rs @@ -212,6 +212,57 @@ impl ApubObjectType for Post { )?; Ok(()) } + + fn send_undo_delete(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error> { + let page = self.to_apub(conn)?; + let community = Community::read(conn, self.community_id)?; + let id = format!("{}/delete/{}", self.ap_id, uuid::Uuid::new_v4()); + let mut delete = Delete::default(); + + populate_object_props( + &mut delete.object_props, + &community.get_followers_url(), + &id, + )?; + + delete + .delete_props + .set_actor_xsd_any_uri(creator.actor_id.to_owned())? + .set_object_base_box(page)?; + + // Undo that fake activity + let undo_id = format!("{}/undo/delete/{}", 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(creator.actor_id.to_owned())? + .set_object_base_box(delete)?; + + // Insert the sent activity into the activity table + let activity_form = activity::ActivityForm { + user_id: self.creator_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, + &creator.private_key.as_ref().unwrap(), + &creator.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 ea03c9e6b..a9a610200 100644 --- a/server/src/apub/shared_inbox.rs +++ b/server/src/apub/shared_inbox.rs @@ -8,6 +8,7 @@ pub enum SharedAcceptedObjects { Like(Like), Dislike(Dislike), Delete(Delete), + Undo(Undo), } impl SharedAcceptedObjects { @@ -18,6 +19,7 @@ impl SharedAcceptedObjects { SharedAcceptedObjects::Like(l) => l.like_props.get_object_base_box(), 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(), } } } @@ -72,6 +74,9 @@ pub async fn shared_inbox( (SharedAcceptedObjects::Delete(d), Some("Group")) => { receive_delete_community(&d, &request, &conn, chat_server) } + (SharedAcceptedObjects::Undo(u), Some("Delete")) => { + receive_undo_delete(&u, &request, &conn, chat_server) + } _ => Err(format_err!("Unknown incoming activity type.")), } } @@ -721,3 +726,241 @@ fn receive_delete_comment( Ok(HttpResponse::Ok().finish()) } + +fn receive_undo_delete( + undo: &Undo, + request: &HttpRequest, + conn: &PgConnection, + chat_server: ChatServerParam, +) -> Result { + let delete = undo + .undo_props + .get_object_base_box() + .to_owned() + .unwrap() + .to_owned() + .into_concrete::()?; + + let type_ = delete + .delete_props + .get_object_base_box() + .to_owned() + .unwrap() + .kind() + .unwrap(); + + match type_ { + "Note" => receive_undo_delete_comment(&delete, &request, &conn, chat_server), + "Page" => receive_undo_delete_post(&delete, &request, &conn, chat_server), + "Group" => receive_undo_delete_community(&delete, &request, &conn, chat_server), + d => Err(format_err!("Undo Delete type {} not supported", d)), + } +} + +fn receive_undo_delete_comment( + delete: &Delete, + request: &HttpRequest, + conn: &PgConnection, + chat_server: ChatServerParam, +) -> Result { + let user_uri = delete + .delete_props + .get_actor_xsd_any_uri() + .unwrap() + .to_string(); + + let note = delete + .delete_props + .get_object_base_box() + .to_owned() + .unwrap() + .to_owned() + .into_concrete::()?; + + let user = get_or_fetch_and_upsert_remote_user(&user_uri, &conn)?; + verify(request, &user.public_key.unwrap())?; + + // Insert the received activity into the activity table + let activity_form = activity::ActivityForm { + user_id: user.id, + data: serde_json::to_value(&delete)?, + 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: None, + deleted: Some(false), + 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, + conn: &PgConnection, + chat_server: ChatServerParam, +) -> Result { + let user_uri = delete + .delete_props + .get_actor_xsd_any_uri() + .unwrap() + .to_string(); + + let page = delete + .delete_props + .get_object_base_box() + .to_owned() + .unwrap() + .to_owned() + .into_concrete::()?; + + let user = get_or_fetch_and_upsert_remote_user(&user_uri, &conn)?; + verify(request, &user.public_key.unwrap())?; + + // Insert the received activity into the activity table + let activity_form = activity::ActivityForm { + user_id: user.id, + data: serde_json::to_value(&delete)?, + 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: None, + deleted: Some(false), + 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, + conn: &PgConnection, + chat_server: ChatServerParam, +) -> Result { + let user_uri = delete + .delete_props + .get_actor_xsd_any_uri() + .unwrap() + .to_string(); + + let group = delete + .delete_props + .get_object_base_box() + .to_owned() + .unwrap() + .to_owned() + .into_concrete::()?; + + let user = get_or_fetch_and_upsert_remote_user(&user_uri, &conn)?; + verify(request, &user.public_key.unwrap())?; + + // Insert the received activity into the activity table + let activity_form = activity::ActivityForm { + user_id: user.id, + data: serde_json::to_value(&delete)?, + 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: None, + published: None, + updated: Some(naive_now()), + deleted: Some(false), + 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 0d0bc8f2b..b5f47e251 100644 --- a/server/src/apub/user.rs +++ b/server/src/apub/user.rs @@ -94,6 +94,10 @@ impl ActorType for User_ { fn send_delete(&self, _creator: &User_, _conn: &PgConnection) -> Result<(), Error> { unimplemented!() } + + fn send_undo_delete(&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 3e5546e5e..e6f7bd864 100644 --- a/ui/src/api_tests/api.spec.ts +++ b/ui/src/api_tests/api.spec.ts @@ -328,8 +328,8 @@ describe('main', () => { }); }); - describe('delete community', () => { - test('/u/lemmy_beta deletes a federated comment, post, and community, lemmy_alpha sees its deleted.', async () => { + describe('delete things', () => { + test('/u/lemmy_beta deletes and undeletes a federated comment, post, and community, lemmy_alpha sees its deleted.', async () => { // Create a test community let communityName = 'test_community'; let communityForm: CommunityForm = { @@ -452,6 +452,34 @@ describe('main', () => { }).then(d => d.json()); expect(getPostRes.comments[0].deleted).toBe(true); + // lemmy_beta undeletes the comment + let undeleteCommentForm: CommentForm = { + content: commentContent, + edit_id: createCommentRes.comment.id, + post_id: createPostRes.post.id, + deleted: false, + auth: lemmyBetaAuth, + creator_id: createCommentRes.comment.creator_id, + }; + + let undeleteCommentRes: CommentResponse = await fetch( + `${lemmyBetaApiUrl}/comment`, + { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + }, + body: wrapper(undeleteCommentForm), + } + ).then(d => d.json()); + expect(undeleteCommentRes.comment.deleted).toBe(false); + + // lemmy_alpha sees that the comment is undeleted + let getPostUndeleteRes: GetPostResponse = await fetch(getPostUrl, { + method: 'GET', + }).then(d => d.json()); + expect(getPostUndeleteRes.comments[0].deleted).toBe(false); + // lemmy_beta deletes the post let deletePostForm: PostForm = { name: postName, @@ -478,6 +506,35 @@ describe('main', () => { }).then(d => d.json()); expect(getPostResAgain.post.deleted).toBe(true); + // lemmy_beta undeletes the post + let undeletePostForm: PostForm = { + name: postName, + edit_id: createPostRes.post.id, + auth: lemmyBetaAuth, + community_id: createPostRes.post.community_id, + creator_id: createPostRes.post.creator_id, + nsfw: false, + deleted: false, + }; + + let undeletePostRes: PostResponse = await fetch( + `${lemmyBetaApiUrl}/post`, + { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + }, + body: wrapper(undeletePostForm), + } + ).then(d => d.json()); + expect(undeletePostRes.post.deleted).toBe(false); + + // Make sure lemmy_alpha sees the post is undeleted + let getPostResAgainTwo: GetPostResponse = await fetch(getPostUrl, { + method: 'GET', + }).then(d => d.json()); + expect(getPostResAgainTwo.post.deleted).toBe(false); + // lemmy_beta deletes the community let deleteCommunityForm: CommunityForm = { name: communityName, @@ -510,6 +567,40 @@ describe('main', () => { }).then(d => d.json()); expect(getCommunityRes.community.deleted).toBe(true); + + // lemmy_beta undeletes the community + let undeleteCommunityForm: CommunityForm = { + name: communityName, + title: communityName, + category_id: 1, + edit_id: createCommunityRes.community.id, + nsfw: false, + deleted: false, + auth: lemmyBetaAuth, + }; + + let undeleteCommunityRes: CommunityResponse = await fetch( + `${lemmyBetaApiUrl}/community`, + { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + }, + body: wrapper(undeleteCommunityForm), + } + ).then(d => d.json()); + + // Make sure the delete went through + expect(undeleteCommunityRes.community.deleted).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.deleted).toBe(false); }); }); });