]> Untitled Git - lemmy.git/commitdiff
Adding federated community, comment, and post deletes.
authorDessalines <tyhou13@gmx.com>
Fri, 1 May 2020 14:07:38 +0000 (10:07 -0400)
committerDessalines <tyhou13@gmx.com>
Fri, 1 May 2020 14:07:38 +0000 (10:07 -0400)
- Unit tests added too.
- No undeletes working yet.

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
server/src/db/community.rs
ui/src/api_tests/api.spec.ts

index 17b52d2f54d73ef7c996f535cdde3af75efec32d..961ef0c1123eb025a9606405d0b59cb4c79ba062 100644 (file)
@@ -337,14 +337,14 @@ impl Perform for Oper<EditComment> {
       Err(_e) => return Err(APIError::err("couldnt_update_comment").into()),
     };
 
-    updated_comment.send_update(&user, &conn)?;
-
     if let Some(deleted) = data.deleted.to_owned() {
       if deleted {
         updated_comment.send_delete(&user, &conn)?;
       } else {
         // TODO: undo delete
       }
+    } else {
+      updated_comment.send_update(&user, &conn)?;
     }
 
     let mut recipient_ids = Vec::new();
index d7f16c50c27223affd7a069fb9918530ea2ce7dc..a08424317a4fcdb5d3223337d5f96c1587585b69 100644 (file)
@@ -321,7 +321,8 @@ impl Perform for Oper<EditCommunity> {
     let conn = pool.get()?;
 
     // Check for a site ban
-    if UserView::read(&conn, user_id)?.banned {
+    let user = User_::read(&conn, user_id)?;
+    if user.banned {
       return Err(APIError::err("site_ban").into());
     }
 
@@ -381,7 +382,7 @@ impl Perform for Oper<EditCommunity> {
 
     if let Some(deleted) = data.deleted.to_owned() {
       if deleted {
-        updated_community.send_delete(&conn)?;
+        updated_community.send_delete(&user, &conn)?;
       } else {
         // TODO: undo delete
       }
@@ -709,7 +710,7 @@ impl Perform for Oper<TransferCommunity> {
       title: read_community.title,
       description: read_community.description,
       category_id: read_community.category_id,
-      creator_id: data.user_id,
+      creator_id: data.user_id, // This makes the new user the community creator
       removed: None,
       deleted: None,
       nsfw: read_community.nsfw,
index 56c1337373c524fc0602941145740b4b6bcb136e..3d2df4631eca0a3ff1a5c5ec3b83692c10ba8bea 100644 (file)
@@ -541,14 +541,14 @@ impl Perform for Oper<EditPost> {
       ModStickyPost::create(&conn, &form)?;
     }
 
-    updated_post.send_update(&user, &conn)?;
-
     if let Some(deleted) = data.deleted.to_owned() {
       if deleted {
         updated_post.send_delete(&user, &conn)?;
       } else {
         // TODO: undo delete
       }
+    } else {
+      updated_post.send_update(&user, &conn)?;
     }
 
     let post_view = PostView::read(&conn, data.edit_id, Some(user_id))?;
index 14a0995652b2cf985b01576736c50c1ae4acea32..b30334772ac4bfdc690e14520bc2aa7aea915d31 100644 (file)
@@ -35,11 +35,14 @@ impl ToApub for Comment {
 
     Ok(comment)
   }
-}
 
-impl ToTombstone for Comment {
   fn to_tombstone(&self) -> Result<Tombstone, Error> {
-    create_tombstone(self.deleted, &self.ap_id, self.published, self.updated, NoteType.to_string())
+    create_tombstone(
+      self.deleted,
+      &self.ap_id,
+      self.updated,
+      NoteType.to_string(),
+    )
   }
 }
 
@@ -164,13 +167,23 @@ impl ApubObjectType for Comment {
     Ok(())
   }
 
-  // TODO: this code is literally copied from post.rs
-  fn send_delete(&self, actor: &User_, conn: &PgConnection) -> Result<(), Error> {
+  fn send_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)?;
+    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(actor.actor_id.to_owned())?
-      .set_object_base_box(BaseBox::from_concrete(self.to_tombstone()?)?)?;
+      .set_actor_xsd_any_uri(creator.actor_id.to_owned())?
+      .set_object_base_box(note)?;
 
     // Insert the sent activity into the activity table
     let activity_form = activity::ActivityForm {
@@ -181,12 +194,10 @@ impl ApubObjectType for Comment {
     };
     activity::Activity::create(&conn, &activity_form)?;
 
-    let post = Post::read(conn, self.post_id)?;
-    let community = Community::read(conn, post.community_id)?;
     send_activity(
       &delete,
-      &actor.private_key.to_owned().unwrap(),
-      &actor.actor_id,
+      &creator.private_key.as_ref().unwrap(),
+      &creator.actor_id,
       community.get_follower_inboxes(&conn)?,
     )?;
     Ok(())
index 0bfb95d2b2c2ecc63359a5f3ead7d7795969e270..336aa24f03646013c86e6ab941826e80889e73e4 100644 (file)
@@ -46,11 +46,14 @@ impl ToApub for Community {
 
     Ok(group.extend(actor_props).extend(self.get_public_key_ext()))
   }
-}
 
-impl ToTombstone for Community {
   fn to_tombstone(&self) -> Result<Tombstone, Error> {
-    create_tombstone(self.deleted, &self.actor_id, self.published, self.updated, GroupType.to_string())
+    create_tombstone(
+      self.deleted,
+      &self.actor_id,
+      self.updated,
+      GroupType.to_string(),
+    )
   }
 }
 
@@ -101,12 +104,17 @@ impl ActorType for Community {
     Ok(())
   }
 
-  fn send_delete(&self, conn: &PgConnection) -> Result<(), Error> {
+  fn send_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(self.actor_id.to_owned())?
-      .set_object_base_box(BaseBox::from_concrete(self.to_tombstone()?)?)?;
+      .set_actor_xsd_any_uri(creator.actor_id.to_owned())?
+      .set_object_base_box(group)?;
 
     // Insert the sent activity into the activity table
     let activity_form = activity::ActivityForm {
@@ -117,10 +125,13 @@ impl ActorType for Community {
     };
     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(
       &delete,
-      &self.private_key.to_owned().unwrap(),
-      &self.actor_id,
+      &creator.private_key.as_ref().unwrap(),
+      &creator.actor_id,
       self.get_follower_inboxes(&conn)?,
     )?;
     Ok(())
index 03c43bdc23b09f3ecdc3c5a16cbcd2723068633d..b56d6744fba632aa358d158b67efc89665f31fe2 100644 (file)
@@ -9,6 +9,9 @@ pub mod signatures;
 pub mod user;
 pub mod user_inbox;
 
+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},
   actor::{properties::ApActorProperties, Actor, Group, Person},
@@ -19,9 +22,6 @@ use activitystreams::{
   object::{properties::ObjectProperties, Note, Page, Tombstone},
   public, BaseBox,
 };
-use activitystreams::object::kind::{NoteType, PageType};
-use crate::api::community::CommunityResponse;
-use crate::websocket::server::SendCommunityRoomMessage;
 use actix_web::body::Body;
 use actix_web::web::Path;
 use actix_web::{web, HttpRequest, HttpResponse, Result};
@@ -155,29 +155,29 @@ fn is_apub_id_valid(apub_id: &Url) -> bool {
 pub trait ToApub {
   type Response;
   fn to_apub(&self, conn: &PgConnection) -> Result<Self::Response, Error>;
+  fn to_tombstone(&self) -> Result<Tombstone, Error>;
 }
 
 fn create_tombstone(
   deleted: bool,
   object_id: &str,
-  published: NaiveDateTime,
   updated: Option<NaiveDateTime>,
   former_type: String,
 ) -> Result<Tombstone, Error> {
   if deleted {
-    let mut tombstone = Tombstone::default();
-    // TODO: might want to include deleted time as well
-    tombstone
-      .object_props
-      .set_id(object_id)?
-      .set_published(convert_datetime(published))?;
     if let Some(updated) = updated {
+      let mut tombstone = Tombstone::default();
+      tombstone.object_props.set_id(object_id)?;
       tombstone
-        .object_props
-        .set_updated(convert_datetime(updated))?;
+        .tombstone_props
+        .set_former_type_xsd_string(former_type)?
+        .set_deleted(convert_datetime(updated))?;
+      Ok(tombstone)
+    } else {
+      Err(format_err!(
+        "Cant convert to tombstone because updated time was None."
+      ))
     }
-    tombstone.tombstone_props.set_former_type_xsd_string(former_type)?;
-    Ok(tombstone)
   } else {
     Err(format_err!(
       "Cant convert object to tombstone if it wasnt deleted"
@@ -185,10 +185,6 @@ fn create_tombstone(
   }
 }
 
-pub trait ToTombstone {
-  fn to_tombstone(&self) -> Result<Tombstone, Error>;
-}
-
 pub trait FromApub {
   type ApubType;
   fn from_apub(apub: &Self::ApubType, conn: &PgConnection) -> Result<Self, Error>
@@ -199,7 +195,7 @@ pub trait FromApub {
 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, actor: &User_, conn: &PgConnection) -> Result<(), Error>;
+  fn send_delete(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error>;
 }
 
 pub trait ApubLikeableType {
@@ -238,7 +234,7 @@ pub trait ActorType {
     Err(format_err!("Accept not implemented."))
   }
 
-  fn send_delete(&self, conn: &PgConnection) -> Result<(), Error>;
+  fn send_delete(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error>;
 
   // TODO default because there is no user following yet.
   #[allow(unused_variables)]
index 356141819d347f7b4c2fcc5c03df13d0671d5d28..2c8bce722fd11c21deef9a5b1e1a5af568a0bd6d 100644 (file)
@@ -57,11 +57,14 @@ impl ToApub for Post {
 
     Ok(page)
   }
-}
 
-impl ToTombstone for Post {
   fn to_tombstone(&self) -> Result<Tombstone, Error> {
-    create_tombstone(self.deleted, &self.ap_id, self.published, self.updated, PageType.to_string())
+    create_tombstone(
+      self.deleted,
+      &self.ap_id,
+      self.updated,
+      PageType.to_string(),
+    )
   }
 }
 
@@ -174,12 +177,22 @@ impl ApubObjectType for Post {
     Ok(())
   }
 
-  fn send_delete(&self, actor: &User_, conn: &PgConnection) -> Result<(), Error> {
+  fn send_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(actor.actor_id.to_owned())?
-      .set_object_base_box(BaseBox::from_concrete(self.to_tombstone()?)?)?;
+      .set_actor_xsd_any_uri(creator.actor_id.to_owned())?
+      .set_object_base_box(page)?;
 
     // Insert the sent activity into the activity table
     let activity_form = activity::ActivityForm {
@@ -193,8 +206,8 @@ impl ApubObjectType for Post {
     let community = Community::read(conn, self.community_id)?;
     send_activity(
       &delete,
-      &actor.private_key.to_owned().unwrap(),
-      &actor.actor_id,
+      &creator.private_key.as_ref().unwrap(),
+      &creator.actor_id,
       community.get_follower_inboxes(&conn)?,
     )?;
     Ok(())
index 7d3826f22f54f58c0113378764d430e5fbe8abe5..ea03c9e6be0d01dda1ba16af828c1241e3d72406 100644 (file)
@@ -51,6 +51,9 @@ pub async fn shared_inbox(
     (SharedAcceptedObjects::Dislike(d), Some("Page")) => {
       receive_dislike_post(&d, &request, &conn, chat_server)
     }
+    (SharedAcceptedObjects::Delete(d), Some("Page")) => {
+      receive_delete_post(&d, &request, &conn, chat_server)
+    }
     (SharedAcceptedObjects::Create(c), Some("Note")) => {
       receive_create_comment(&c, &request, &conn, chat_server)
     }
@@ -63,8 +66,11 @@ pub async fn shared_inbox(
     (SharedAcceptedObjects::Dislike(d), Some("Note")) => {
       receive_dislike_comment(&d, &request, &conn, chat_server)
     }
-    (SharedAcceptedObjects::Delete(d), Some("Tombstone")) => {
-      receive_delete(&d, &request, &conn, chat_server)
+    (SharedAcceptedObjects::Delete(d), Some("Note")) => {
+      receive_delete_comment(&d, &request, &conn, chat_server)
+    }
+    (SharedAcceptedObjects::Delete(d), Some("Group")) => {
+      receive_delete_community(&d, &request, &conn, chat_server)
     }
     _ => Err(format_err!("Unknown incoming activity type.")),
   }
@@ -508,58 +514,60 @@ fn receive_dislike_comment(
   Ok(HttpResponse::Ok().finish())
 }
 
-fn receive_delete(
+fn receive_delete_community(
   delete: &Delete,
   request: &HttpRequest,
   conn: &PgConnection,
   chat_server: ChatServerParam,
 ) -> Result<HttpResponse, Error> {
-  let tombstone = delete
+  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::<Tombstone>()?;
-  let former_type = tombstone.tombstone_props.get_former_type_xsd_string().unwrap().to_string();
-  // TODO: handle these
-  match former_type.as_str() {
-    "Group" => {},
-    d => return Err(format_err!("Delete type {} not supported", d)),
-  }
-  let community_apub_id = tombstone.object_props.get_id().unwrap().to_string();
+    .into_concrete::<GroupExt>()?;
 
-  let community = Community::read_from_actor_id(conn, &community_apub_id)?;
-  verify(request, &community.public_key.clone().unwrap())?;
+  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: community.creator_id,
+    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: "".to_string(),
-    title: "".to_string(),
-    description: None,
+    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: None,
+    updated: Some(naive_now()),
     deleted: Some(true),
-    nsfw: false,
+    nsfw: community.nsfw,
     actor_id: community.actor_id,
-    local: false,
-    private_key: None,
+    local: community.local,
+    private_key: community.private_key,
     public_key: community.public_key,
-    last_refreshed_at: Some(community.last_refreshed_at),
+    last_refreshed_at: None,
   };
 
-  Community::update(conn, community.id, &community_form)?;
+  Community::update(&conn, community.id, &community_form)?;
 
   let res = CommunityResponse {
     community: CommunityView::read(&conn, community.id, None)?,
@@ -574,3 +582,142 @@ fn receive_delete(
 
   Ok(HttpResponse::Ok().finish())
 }
+
+fn receive_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(true),
+    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,
+  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(true),
+    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())
+}
index 7426efd5e9dc525381caebd056a446b0adf6ebc8..0d0bc8f2be4ac56efc5d5d4cb55d8dc9a49c0328 100644 (file)
@@ -43,6 +43,9 @@ impl ToApub for User_ {
 
     Ok(person.extend(actor_props).extend(self.get_public_key_ext()))
   }
+  fn to_tombstone(&self) -> Result<Tombstone, Error> {
+    unimplemented!()
+  }
 }
 
 impl ActorType for User_ {
@@ -88,7 +91,7 @@ impl ActorType for User_ {
     Ok(())
   }
 
-  fn send_delete(&self, _conn: &PgConnection) -> Result<(), Error> {
+  fn send_delete(&self, _creator: &User_, _conn: &PgConnection) -> Result<(), Error> {
     unimplemented!()
   }
 }
index 301fce0322a5ea256b5169952f39f149bde3ba99..0f324de29d168a078766d8f12b5e94dcd41c4c5a 100644 (file)
@@ -22,6 +22,7 @@ pub struct Community {
   pub last_refreshed_at: chrono::NaiveDateTime,
 }
 
+// TODO add better delete, remove, lock actions here.
 #[derive(Insertable, AsChangeset, Clone, Serialize, Deserialize, Debug)]
 #[table_name = "community"]
 pub struct CommunityForm {
index ffc33888bb3ec8ed4e28c413ba3f5006d99b9448..3e5546e5edffa3e101089dcd76a2cce936a1869e 100644 (file)
@@ -13,6 +13,9 @@ import {
   GetPostResponse,
   CommentForm,
   CommentResponse,
+  CommunityForm,
+  GetCommunityForm,
+  GetCommunityResponse,
 } from '../interfaces';
 
 let lemmyAlphaUrl = 'http://localhost:8540';
@@ -324,6 +327,191 @@ describe('main', () => {
       expect(getPostRes.comments[1].creator_local).toBe(false);
     });
   });
+
+  describe('delete community', () => {
+    test('/u/lemmy_beta deletes a federated comment, post, and community, lemmy_alpha sees its deleted.', async () => {
+      // Create a test community
+      let communityName = 'test_community';
+      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 delete';
+      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 delete';
+      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 deletes the comment
+      let deleteCommentForm: CommentForm = {
+        content: commentContent,
+        edit_id: createCommentRes.comment.id,
+        post_id: createPostRes.post.id,
+        deleted: true,
+        auth: lemmyBetaAuth,
+        creator_id: createCommentRes.comment.creator_id,
+      };
+
+      let deleteCommentRes: CommentResponse = await fetch(
+        `${lemmyBetaApiUrl}/comment`,
+        {
+          method: 'PUT',
+          headers: {
+            'Content-Type': 'application/json',
+          },
+          body: wrapper(deleteCommentForm),
+        }
+      ).then(d => d.json());
+      expect(deleteCommentRes.comment.deleted).toBe(true);
+
+      // lemmy_alpha sees that the comment is deleted
+      let getPostUrl = `${lemmyAlphaApiUrl}/post?id=3`;
+      let getPostRes: GetPostResponse = await fetch(getPostUrl, {
+        method: 'GET',
+      }).then(d => d.json());
+      expect(getPostRes.comments[0].deleted).toBe(true);
+
+      // lemmy_beta deletes the post
+      let deletePostForm: 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: true,
+      };
+
+      let deletePostRes: PostResponse = await fetch(`${lemmyBetaApiUrl}/post`, {
+        method: 'PUT',
+        headers: {
+          'Content-Type': 'application/json',
+        },
+        body: wrapper(deletePostForm),
+      }).then(d => d.json());
+      expect(deletePostRes.post.deleted).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.deleted).toBe(true);
+
+      // lemmy_beta deletes the community
+      let deleteCommunityForm: CommunityForm = {
+        name: communityName,
+        title: communityName,
+        category_id: 1,
+        edit_id: createCommunityRes.community.id,
+        nsfw: false,
+        deleted: true,
+        auth: lemmyBetaAuth,
+      };
+
+      let deleteResponse: CommunityResponse = await fetch(
+        `${lemmyBetaApiUrl}/community`,
+        {
+          method: 'PUT',
+          headers: {
+            'Content-Type': 'application/json',
+          },
+          body: wrapper(deleteCommunityForm),
+        }
+      ).then(d => d.json());
+
+      // Make sure the delete went through
+      expect(deleteResponse.community.deleted).toBe(true);
+
+      // Re-get it from alpha, make sure its deleted 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.deleted).toBe(true);
+    });
+  });
 });
 
 function wrapper(form: any): string {