]> Untitled Git - lemmy.git/commitdiff
Add undos for delete community, post, and comment.
authorDessalines <tyhou13@gmx.com>
Fri, 1 May 2020 19:01:29 +0000 (15:01 -0400)
committerDessalines <tyhou13@gmx.com>
Fri, 1 May 2020 19:01:29 +0000 (15:01 -0400)
server/src/api/comment.rs
server/src/api/community.rs
server/src/api/post.rs
server/src/apub/comment.rs
server/src/apub/community.rs
server/src/apub/mod.rs
server/src/apub/post.rs
server/src/apub/shared_inbox.rs
server/src/apub/user.rs
ui/src/api_tests/api.spec.ts

index 961ef0c1123eb025a9606405d0b59cb4c79ba062..1ecedb2c888d61153b67385e11715cf5541af958 100644 (file)
@@ -341,7 +341,7 @@ impl Perform for Oper<EditComment> {
       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)?;
index a08424317a4fcdb5d3223337d5f96c1587585b69..71da6712a26f39c9a81ab5d88f0b7a1d146936a8 100644 (file)
@@ -384,7 +384,7 @@ impl Perform for Oper<EditCommunity> {
       if deleted {
         updated_community.send_delete(&user, &conn)?;
       } else {
-        // TODO: undo delete
+        updated_community.send_undo_delete(&user, &conn)?;
       }
     }
 
index 3d2df4631eca0a3ff1a5c5ec3b83692c10ba8bea..55e0612fe9b912b45963dbae91aef03e734cc9a0 100644 (file)
@@ -545,7 +545,7 @@ impl Perform for Oper<EditPost> {
       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)?;
index b30334772ac4bfdc690e14520bc2aa7aea915d31..65dd3c19a955aec4eba7d87ea61803ddddd10499 100644 (file)
@@ -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 {
index 336aa24f03646013c86e6ab941826e80889e73e4..c4d9bf839c4c37e4294b43fdd843d6da47f934fa 100644 (file)
@@ -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<Vec<String>, Error> {
     Ok(
index b56d6744fba632aa358d158b67efc89665f31fe2..1d86050251cb6ed934d11859253b67aad5f5ba03 100644 (file)
@@ -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)]
index 2c8bce722fd11c21deef9a5b1e1a5af568a0bd6d..5a7383c0e87584367d0467df6d0bcfcd4cba3f6d 100644 (file)
@@ -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 {
index ea03c9e6be0d01dda1ba16af828c1241e3d72406..a9a61020042c4b74bdad34e61d75df9dc290a639 100644 (file)
@@ -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<HttpResponse, Error> {
+  let delete = undo
+    .undo_props
+    .get_object_base_box()
+    .to_owned()
+    .unwrap()
+    .to_owned()
+    .into_concrete::<Delete>()?;
+
+  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<HttpResponse, Error> {
+  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::<Note>()?;
+
+  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(&note, &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<HttpResponse, Error> {
+  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::<Page>()?;
+
+  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<HttpResponse, Error> {
+  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::<GroupExt>()?;
+
+  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())
+}
index 0d0bc8f2be4ac56efc5d5d4cb55d8dc9a49c0328..b5f47e251dcb1197175320d916e68d3383ac6e90 100644 (file)
@@ -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 {
index 3e5546e5edffa3e101089dcd76a2cce936a1869e..e6f7bd86497ac27e1ac3c22832eb85bc9a587bc7 100644 (file)
@@ -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);
     });
   });
 });