]> Untitled Git - lemmy.git/commitdiff
Re-organizing federation tests. #746 #1040 (#1047)
authorDessalines <dessalines@users.noreply.github.com>
Tue, 4 Aug 2020 14:57:37 +0000 (10:57 -0400)
committerGitHub <noreply@github.com>
Tue, 4 Aug 2020 14:57:37 +0000 (10:57 -0400)
* Re-organizing federation tests. #746 #1040

* Add more checks in inbox, plus some refactoring (#76)

Merge branch 'main' into more-inbox-permissions

Move check_community_ban() into helper function

Move slur check into helper functions

Move Claims::decode and site ban check into helper function

Note: this changes behaviour in that site ban is checked in more
places now. we could easily add a boolean parameter
check_for_site_ban to get the previous behaviour back

Rewrite user_inbox and community_inbox in the same way as shared_inbox

Add check against instance allowlist etc in shared_inbox

Co-authored-by: dessalines <dessalines@noreply.yerbamate.dev>
Co-authored-by: Felix Ableitner <me@nutomic.com>
Reviewed-on: https://yerbamate.dev/LemmyNet/lemmy/pulls/76

* Adding verbose to test results.

Co-authored-by: nutomic <nutomic@noreply.yerbamate.dev>
Co-authored-by: dessalines <dessalines@noreply.yerbamate.dev>
Co-authored-by: Felix Ableitner <me@nutomic.com>
27 files changed:
docker/federation-test/servers.sh
docker/federation/docker-compose.yml
docker/travis/docker-compose.yml
server/lemmy_db/src/comment.rs
server/lemmy_db/src/community.rs
server/lemmy_db/src/post.rs
server/src/api/comment.rs
server/src/api/community.rs
server/src/api/mod.rs
server/src/api/post.rs
server/src/api/site.rs
server/src/api/user.rs
server/src/apub/activities.rs
server/src/apub/fetcher.rs
server/src/apub/inbox/activities/create.rs
server/src/apub/inbox/community_inbox.rs
server/src/apub/inbox/shared_inbox.rs
server/src/apub/inbox/user_inbox.rs
server/src/apub/mod.rs
ui/package.json
ui/src/api_tests/api.spec.ts [deleted file]
ui/src/api_tests/comment.spec.ts [new file with mode: 0644]
ui/src/api_tests/community.spec.ts [new file with mode: 0644]
ui/src/api_tests/follow.spec.ts [new file with mode: 0644]
ui/src/api_tests/post.spec.ts [new file with mode: 0644]
ui/src/api_tests/private_message.spec.ts [new file with mode: 0644]
ui/src/api_tests/shared.ts [new file with mode: 0644]

index 36f10cd82e5c43704ab7783b351c5e08bafa6f27..b34e8c4efeaaca1d1971d26791bb51caac8add17 100755 (executable)
@@ -1,6 +1,7 @@
 #!/bin/bash
 set -e
 
+sudo docker-compose --file ../federation/docker-compose.yml --project-directory . down
 sudo rm -rf volumes
 
 pushd ../../server/
index c552d18fd4862a5708bc0ec9f39e427e92fcb5ae..4e087d10441017a0c52c94d020d09641fe58e181 100644 (file)
@@ -39,6 +39,8 @@ services:
       - LEMMY_SETUP__ADMIN_USERNAME=lemmy_alpha
       - LEMMY_SETUP__ADMIN_PASSWORD=lemmy
       - LEMMY_SETUP__SITE_NAME=lemmy-alpha
+      - LEMMY_RATE_LIMIT__POST=99999
+      - LEMMY_RATE_LIMIT__REGISTER=99999
       - RUST_BACKTRACE=1
       - RUST_LOG=debug
     depends_on:
@@ -66,6 +68,8 @@ services:
       - LEMMY_SETUP__ADMIN_USERNAME=lemmy_beta
       - LEMMY_SETUP__ADMIN_PASSWORD=lemmy
       - LEMMY_SETUP__SITE_NAME=lemmy-beta
+      - LEMMY_RATE_LIMIT__POST=99999
+      - LEMMY_RATE_LIMIT__REGISTER=99999
       - RUST_BACKTRACE=1
       - RUST_LOG=debug
     depends_on:
@@ -93,6 +97,8 @@ services:
       - LEMMY_SETUP__ADMIN_USERNAME=lemmy_gamma
       - LEMMY_SETUP__ADMIN_PASSWORD=lemmy
       - LEMMY_SETUP__SITE_NAME=lemmy-gamma
+      - LEMMY_RATE_LIMIT__POST=99999
+      - LEMMY_RATE_LIMIT__REGISTER=99999
       - RUST_BACKTRACE=1
       - RUST_LOG=debug
     depends_on:
index 5248553a4ef59ceec0b003fbf0eba7428a7af74d..03b3a7ecff23ac1a6855aaf45efc60cce99a79ec 100644 (file)
@@ -39,6 +39,8 @@ services:
       - LEMMY_SETUP__ADMIN_USERNAME=lemmy_alpha
       - LEMMY_SETUP__ADMIN_PASSWORD=lemmy
       - LEMMY_SETUP__SITE_NAME=lemmy-alpha
+      - LEMMY_RATE_LIMIT__POST=99999
+      - LEMMY_RATE_LIMIT__REGISTER=99999
       - RUST_BACKTRACE=1
       - RUST_LOG=debug
     depends_on:
@@ -66,6 +68,8 @@ services:
       - LEMMY_SETUP__ADMIN_USERNAME=lemmy_beta
       - LEMMY_SETUP__ADMIN_PASSWORD=lemmy
       - LEMMY_SETUP__SITE_NAME=lemmy-beta
+      - LEMMY_RATE_LIMIT__POST=99999
+      - LEMMY_RATE_LIMIT__REGISTER=99999
       - RUST_BACKTRACE=1
       - RUST_LOG=debug
     depends_on:
@@ -93,6 +97,8 @@ services:
       - LEMMY_SETUP__ADMIN_USERNAME=lemmy_gamma
       - LEMMY_SETUP__ADMIN_PASSWORD=lemmy
       - LEMMY_SETUP__SITE_NAME=lemmy-gamma
+      - LEMMY_RATE_LIMIT__POST=99999
+      - LEMMY_RATE_LIMIT__REGISTER=99999
       - RUST_BACKTRACE=1
       - RUST_LOG=debug
     depends_on:
index de6904133a7a597d893760818dcefb096b2e9793..99efde8d7d1faaadb5430c12ba6c7e46f862da67 100644 (file)
@@ -116,7 +116,10 @@ impl Comment {
   ) -> Result<Self, Error> {
     use crate::schema::comment::dsl::*;
     diesel::update(comment.find(comment_id))
-      .set(deleted.eq(new_deleted))
+      .set((
+        deleted.eq(new_deleted),
+        updated.eq(naive_now())
+      ))
       .get_result::<Self>(conn)
   }
 
@@ -127,7 +130,10 @@ impl Comment {
   ) -> Result<Self, Error> {
     use crate::schema::comment::dsl::*;
     diesel::update(comment.find(comment_id))
-      .set(removed.eq(new_removed))
+      .set((
+        removed.eq(new_removed),
+        updated.eq(naive_now())
+      ))
       .get_result::<Self>(conn)
   }
 
index c4930d7931e958dc3649cc8c9f761adb91e29bf4..2c86f1e755f0031160fca1eb57437ad55d70cfe6 100644 (file)
@@ -107,7 +107,10 @@ impl Community {
   ) -> Result<Self, Error> {
     use crate::schema::community::dsl::*;
     diesel::update(community.find(community_id))
-      .set(deleted.eq(new_deleted))
+      .set((
+        deleted.eq(new_deleted),
+        updated.eq(naive_now())
+      ))
       .get_result::<Self>(conn)
   }
 
@@ -118,7 +121,10 @@ impl Community {
   ) -> Result<Self, Error> {
     use crate::schema::community::dsl::*;
     diesel::update(community.find(community_id))
-      .set(removed.eq(new_removed))
+      .set((
+        removed.eq(new_removed),
+        updated.eq(naive_now())
+      ))
       .get_result::<Self>(conn)
   }
 
index 5eb9a47230626a269ce2796958a5a4dc2c6454bd..56ff7474bdbb6e191b52ef56932235d873804e07 100644 (file)
@@ -119,7 +119,10 @@ impl Post {
   ) -> Result<Self, Error> {
     use crate::schema::post::dsl::*;
     diesel::update(post.find(post_id))
-      .set(deleted.eq(new_deleted))
+      .set((
+        deleted.eq(new_deleted),
+        updated.eq(naive_now())
+      ))
       .get_result::<Self>(conn)
   }
 
@@ -130,7 +133,10 @@ impl Post {
   ) -> Result<Self, Error> {
     use crate::schema::post::dsl::*;
     diesel::update(post.find(post_id))
-      .set(removed.eq(new_removed))
+      .set((
+        removed.eq(new_removed),
+        updated.eq(naive_now())
+      ))
       .get_result::<Self>(conn)
   }
 
index 7dfce473f3e5b6016f6a2999d7df815ec8c9265c..e3189d434f77e6015c3bfc0c1678fab2078acc2e 100644 (file)
@@ -1,5 +1,13 @@
 use crate::{
-  api::{claims::Claims, is_mod_or_admin, APIError, Oper, Perform},
+  api::{
+    check_community_ban,
+    get_user_from_jwt,
+    get_user_from_jwt_opt,
+    is_mod_or_admin,
+    APIError,
+    Oper,
+    Perform,
+  },
   apub::{ApubLikeableType, ApubObjectType},
   blocking,
   websocket::{
@@ -13,7 +21,6 @@ use crate::{
 use lemmy_db::{
   comment::*,
   comment_view::*,
-  community_view::*,
   moderator::*,
   post::*,
   site_view::*,
@@ -123,13 +130,7 @@ impl Perform for Oper<CreateComment> {
     websocket_info: Option<WebsocketInfo>,
   ) -> Result<CommentResponse, LemmyError> {
     let data: &CreateComment = &self.data;
-
-    let claims = match Claims::decode(&data.auth) {
-      Ok(claims) => claims.claims,
-      Err(_e) => return Err(APIError::err("not_logged_in").into()),
-    };
-
-    let user_id = claims.id;
+    let user = get_user_from_jwt(&data.auth, pool).await?;
 
     let content_slurs_removed = remove_slurs(&data.content.to_owned());
 
@@ -137,7 +138,7 @@ impl Perform for Oper<CreateComment> {
       content: content_slurs_removed,
       parent_id: data.parent_id.to_owned(),
       post_id: data.post_id,
-      creator_id: user_id,
+      creator_id: user.id,
       removed: None,
       deleted: None,
       read: None,
@@ -151,18 +152,7 @@ impl Perform for Oper<CreateComment> {
     let post_id = data.post_id;
     let post = blocking(pool, move |conn| Post::read(conn, post_id)).await??;
 
-    let community_id = post.community_id;
-    let is_banned =
-      move |conn: &'_ _| CommunityUserBanView::get(conn, user_id, community_id).is_ok();
-    if blocking(pool, is_banned).await? {
-      return Err(APIError::err("community_ban").into());
-    }
-
-    // Check for a site ban
-    let user = blocking(pool, move |conn| User_::read(&conn, user_id)).await??;
-    if user.banned {
-      return Err(APIError::err("site_ban").into());
-    }
+    check_community_ban(user.id, post.community_id, pool).await?;
 
     // Check if post is locked, no new comments
     if post.locked {
@@ -203,7 +193,7 @@ impl Perform for Oper<CreateComment> {
     let like_form = CommentLikeForm {
       comment_id: inserted_comment.id,
       post_id: data.post_id,
-      user_id,
+      user_id: user.id,
       score: 1,
     };
 
@@ -214,6 +204,7 @@ impl Perform for Oper<CreateComment> {
 
     updated_comment.send_like(&user, &self.client, pool).await?;
 
+    let user_id = user.id;
     let comment_view = blocking(pool, move |conn| {
       CommentView::read(&conn, inserted_comment.id, Some(user_id))
     })
@@ -251,34 +242,16 @@ impl Perform for Oper<EditComment> {
     websocket_info: Option<WebsocketInfo>,
   ) -> Result<CommentResponse, LemmyError> {
     let data: &EditComment = &self.data;
-
-    let claims = match Claims::decode(&data.auth) {
-      Ok(claims) => claims.claims,
-      Err(_e) => return Err(APIError::err("not_logged_in").into()),
-    };
-
-    let user_id = claims.id;
+    let user = get_user_from_jwt(&data.auth, pool).await?;
 
     let edit_id = data.edit_id;
     let orig_comment =
       blocking(pool, move |conn| CommentView::read(&conn, edit_id, None)).await??;
 
-    // Check for a site ban
-    let user = blocking(pool, move |conn| User_::read(conn, user_id)).await??;
-    if user.banned {
-      return Err(APIError::err("site_ban").into());
-    }
-
-    // Check for a community ban
-    let community_id = orig_comment.community_id;
-    let is_banned =
-      move |conn: &'_ _| CommunityUserBanView::get(conn, user_id, community_id).is_ok();
-    if blocking(pool, is_banned).await? {
-      return Err(APIError::err("community_ban").into());
-    }
+    check_community_ban(user.id, orig_comment.community_id, pool).await?;
 
     // Verify that only the creator can edit
-    if user_id != orig_comment.creator_id {
+    if user.id != orig_comment.creator_id {
       return Err(APIError::err("no_comment_edit_allowed").into());
     }
 
@@ -309,6 +282,7 @@ impl Perform for Oper<EditComment> {
       send_local_notifs(mentions, updated_comment, &user, post, pool, false).await?;
 
     let edit_id = data.edit_id;
+    let user_id = user.id;
     let comment_view = blocking(pool, move |conn| {
       CommentView::read(conn, edit_id, Some(user_id))
     })
@@ -346,34 +320,16 @@ impl Perform for Oper<DeleteComment> {
     websocket_info: Option<WebsocketInfo>,
   ) -> Result<CommentResponse, LemmyError> {
     let data: &DeleteComment = &self.data;
-
-    let claims = match Claims::decode(&data.auth) {
-      Ok(claims) => claims.claims,
-      Err(_e) => return Err(APIError::err("not_logged_in").into()),
-    };
-
-    let user_id = claims.id;
+    let user = get_user_from_jwt(&data.auth, pool).await?;
 
     let edit_id = data.edit_id;
     let orig_comment =
       blocking(pool, move |conn| CommentView::read(&conn, edit_id, None)).await??;
 
-    // Check for a site ban
-    let user = blocking(pool, move |conn| User_::read(conn, user_id)).await??;
-    if user.banned {
-      return Err(APIError::err("site_ban").into());
-    }
-
-    // Check for a community ban
-    let community_id = orig_comment.community_id;
-    let is_banned =
-      move |conn: &'_ _| CommunityUserBanView::get(conn, user_id, community_id).is_ok();
-    if blocking(pool, is_banned).await? {
-      return Err(APIError::err("community_ban").into());
-    }
+    check_community_ban(user.id, orig_comment.community_id, pool).await?;
 
     // Verify that only the creator can delete
-    if user_id != orig_comment.creator_id {
+    if user.id != orig_comment.creator_id {
       return Err(APIError::err("no_comment_edit_allowed").into());
     }
 
@@ -401,6 +357,7 @@ impl Perform for Oper<DeleteComment> {
 
     // Refetch it
     let edit_id = data.edit_id;
+    let user_id = user.id;
     let comment_view = blocking(pool, move |conn| {
       CommentView::read(conn, edit_id, Some(user_id))
     })
@@ -445,34 +402,16 @@ impl Perform for Oper<RemoveComment> {
     websocket_info: Option<WebsocketInfo>,
   ) -> Result<CommentResponse, LemmyError> {
     let data: &RemoveComment = &self.data;
-
-    let claims = match Claims::decode(&data.auth) {
-      Ok(claims) => claims.claims,
-      Err(_e) => return Err(APIError::err("not_logged_in").into()),
-    };
-
-    let user_id = claims.id;
+    let user = get_user_from_jwt(&data.auth, pool).await?;
 
     let edit_id = data.edit_id;
     let orig_comment =
       blocking(pool, move |conn| CommentView::read(&conn, edit_id, None)).await??;
 
-    // Check for a site ban
-    let user = blocking(pool, move |conn| User_::read(conn, user_id)).await??;
-    if user.banned {
-      return Err(APIError::err("site_ban").into());
-    }
-
-    // Check for a community ban
-    let community_id = orig_comment.community_id;
-    let is_banned =
-      move |conn: &'_ _| CommunityUserBanView::get(conn, user_id, community_id).is_ok();
-    if blocking(pool, is_banned).await? {
-      return Err(APIError::err("community_ban").into());
-    }
+    check_community_ban(user.id, orig_comment.community_id, pool).await?;
 
     // Verify that only a mod or admin can remove
-    is_mod_or_admin(pool, user_id, community_id).await?;
+    is_mod_or_admin(pool, user.id, orig_comment.community_id).await?;
 
     // Do the remove
     let removed = data.removed;
@@ -487,7 +426,7 @@ impl Perform for Oper<RemoveComment> {
 
     // Mod tables
     let form = ModRemoveCommentForm {
-      mod_user_id: user_id,
+      mod_user_id: user.id,
       comment_id: data.edit_id,
       removed: Some(removed),
       reason: data.reason.to_owned(),
@@ -507,6 +446,7 @@ impl Perform for Oper<RemoveComment> {
 
     // Refetch it
     let edit_id = data.edit_id;
+    let user_id = user.id;
     let comment_view = blocking(pool, move |conn| {
       CommentView::read(conn, edit_id, Some(user_id))
     })
@@ -551,31 +491,13 @@ impl Perform for Oper<MarkCommentAsRead> {
     _websocket_info: Option<WebsocketInfo>,
   ) -> Result<CommentResponse, LemmyError> {
     let data: &MarkCommentAsRead = &self.data;
-
-    let claims = match Claims::decode(&data.auth) {
-      Ok(claims) => claims.claims,
-      Err(_e) => return Err(APIError::err("not_logged_in").into()),
-    };
-
-    let user_id = claims.id;
+    let user = get_user_from_jwt(&data.auth, pool).await?;
 
     let edit_id = data.edit_id;
     let orig_comment =
       blocking(pool, move |conn| CommentView::read(&conn, edit_id, None)).await??;
 
-    // Check for a site ban
-    let user = blocking(pool, move |conn| User_::read(conn, user_id)).await??;
-    if user.banned {
-      return Err(APIError::err("site_ban").into());
-    }
-
-    // Check for a community ban
-    let community_id = orig_comment.community_id;
-    let is_banned =
-      move |conn: &'_ _| CommunityUserBanView::get(conn, user_id, community_id).is_ok();
-    if blocking(pool, is_banned).await? {
-      return Err(APIError::err("community_ban").into());
-    }
+    check_community_ban(user.id, orig_comment.community_id, pool).await?;
 
     // Verify that only the recipient can mark as read
     // Needs to fetch the parent comment / post to get the recipient
@@ -584,14 +506,14 @@ impl Perform for Oper<MarkCommentAsRead> {
       Some(pid) => {
         let parent_comment =
           blocking(pool, move |conn| CommentView::read(&conn, pid, None)).await??;
-        if user_id != parent_comment.creator_id {
+        if user.id != parent_comment.creator_id {
           return Err(APIError::err("no_comment_edit_allowed").into());
         }
       }
       None => {
         let parent_post_id = orig_comment.post_id;
         let parent_post = blocking(pool, move |conn| Post::read(conn, parent_post_id)).await??;
-        if user_id != parent_post.creator_id {
+        if user.id != parent_post.creator_id {
           return Err(APIError::err("no_comment_edit_allowed").into());
         }
       }
@@ -606,6 +528,7 @@ impl Perform for Oper<MarkCommentAsRead> {
 
     // Refetch it
     let edit_id = data.edit_id;
+    let user_id = user.id;
     let comment_view = blocking(pool, move |conn| {
       CommentView::read(conn, edit_id, Some(user_id))
     })
@@ -631,17 +554,11 @@ impl Perform for Oper<SaveComment> {
     _websocket_info: Option<WebsocketInfo>,
   ) -> Result<CommentResponse, LemmyError> {
     let data: &SaveComment = &self.data;
-
-    let claims = match Claims::decode(&data.auth) {
-      Ok(claims) => claims.claims,
-      Err(_e) => return Err(APIError::err("not_logged_in").into()),
-    };
-
-    let user_id = claims.id;
+    let user = get_user_from_jwt(&data.auth, pool).await?;
 
     let comment_saved_form = CommentSavedForm {
       comment_id: data.comment_id,
-      user_id,
+      user_id: user.id,
     };
 
     if data.save {
@@ -657,6 +574,7 @@ impl Perform for Oper<SaveComment> {
     }
 
     let comment_id = data.comment_id;
+    let user_id = user.id;
     let comment_view = blocking(pool, move |conn| {
       CommentView::read(conn, comment_id, Some(user_id))
     })
@@ -680,13 +598,7 @@ impl Perform for Oper<CreateCommentLike> {
     websocket_info: Option<WebsocketInfo>,
   ) -> Result<CommentResponse, LemmyError> {
     let data: &CreateCommentLike = &self.data;
-
-    let claims = match Claims::decode(&data.auth) {
-      Ok(claims) => claims.claims,
-      Err(_e) => return Err(APIError::err("not_logged_in").into()),
-    };
-
-    let user_id = claims.id;
+    let user = get_user_from_jwt(&data.auth, pool).await?;
 
     let mut recipient_ids = Vec::new();
 
@@ -702,21 +614,9 @@ impl Perform for Oper<CreateCommentLike> {
     let orig_comment =
       blocking(pool, move |conn| CommentView::read(&conn, comment_id, None)).await??;
 
-    // Check for a community ban
     let post_id = orig_comment.post_id;
     let post = blocking(pool, move |conn| Post::read(conn, post_id)).await??;
-    let community_id = post.community_id;
-    let is_banned =
-      move |conn: &'_ _| CommunityUserBanView::get(conn, user_id, community_id).is_ok();
-    if blocking(pool, is_banned).await? {
-      return Err(APIError::err("community_ban").into());
-    }
-
-    // Check for a site ban
-    let user = blocking(pool, move |conn| User_::read(conn, user_id)).await??;
-    if user.banned {
-      return Err(APIError::err("site_ban").into());
-    }
+    check_community_ban(user.id, post.community_id, pool).await?;
 
     let comment_id = data.comment_id;
     let comment = blocking(pool, move |conn| Comment::read(conn, comment_id)).await??;
@@ -725,7 +625,7 @@ impl Perform for Oper<CreateCommentLike> {
     match comment.parent_id {
       Some(parent_id) => {
         let parent_comment = blocking(pool, move |conn| Comment::read(conn, parent_id)).await??;
-        if parent_comment.creator_id != user_id {
+        if parent_comment.creator_id != user.id {
           let parent_user = blocking(pool, move |conn| {
             User_::read(conn, parent_comment.creator_id)
           })
@@ -741,7 +641,7 @@ impl Perform for Oper<CreateCommentLike> {
     let like_form = CommentLikeForm {
       comment_id: data.comment_id,
       post_id,
-      user_id,
+      user_id: user.id,
       score: data.score,
     };
 
@@ -769,6 +669,7 @@ impl Perform for Oper<CreateCommentLike> {
 
     // Have to refetch the comment to get the current state
     let comment_id = data.comment_id;
+    let user_id = user.id;
     let liked_comment = blocking(pool, move |conn| {
       CommentView::read(conn, comment_id, Some(user_id))
     })
@@ -806,19 +707,8 @@ impl Perform for Oper<GetComments> {
     websocket_info: Option<WebsocketInfo>,
   ) -> Result<GetCommentsResponse, LemmyError> {
     let data: &GetComments = &self.data;
-
-    let user_claims: Option<Claims> = match &data.auth {
-      Some(auth) => match Claims::decode(&auth) {
-        Ok(claims) => Some(claims.claims),
-        Err(_e) => None,
-      },
-      None => None,
-    };
-
-    let user_id = match &user_claims {
-      Some(claims) => Some(claims.id),
-      None => None,
-    };
+    let user = get_user_from_jwt_opt(&data.auth, pool).await?;
+    let user_id = user.map(|u| u.id);
 
     let type_ = ListingType::from_str(&data.type_)?;
     let sort = SortType::from_str(&data.sort)?;
index e4a8b6e8e6745cefe19fc00feb4b1b0bf24db31c..904dfe53cb28f2f5a644423636ce3233b6651140 100644 (file)
@@ -1,6 +1,6 @@
 use super::*;
 use crate::{
-  api::{claims::Claims, is_admin, is_mod_or_admin, APIError, Oper, Perform},
+  api::{is_admin, is_mod_or_admin, APIError, Oper, Perform},
   apub::ActorType,
   blocking,
   websocket::{
@@ -16,8 +16,6 @@ use lemmy_utils::{
   is_valid_community_name,
   make_apub_endpoint,
   naive_from_unix,
-  slur_check,
-  slurs_vec_to_str,
   EndpointType,
 };
 use serde::{Deserialize, Serialize};
@@ -154,17 +152,8 @@ impl Perform for Oper<GetCommunity> {
     websocket_info: Option<WebsocketInfo>,
   ) -> Result<GetCommunityResponse, LemmyError> {
     let data: &GetCommunity = &self.data;
-
-    let user_id: Option<i32> = match &data.auth {
-      Some(auth) => match Claims::decode(&auth) {
-        Ok(claims) => {
-          let user_id = claims.claims.id;
-          Some(user_id)
-        }
-        Err(_e) => None,
-      },
-      None => None,
-    };
+    let user = get_user_from_jwt_opt(&data.auth, pool).await?;
+    let user_id = user.map(|u| u.id);
 
     let name = data.name.to_owned().unwrap_or_else(|| "main".to_string());
     let community = match data.id {
@@ -234,38 +223,16 @@ impl Perform for Oper<CreateCommunity> {
     _websocket_info: Option<WebsocketInfo>,
   ) -> Result<CommunityResponse, LemmyError> {
     let data: &CreateCommunity = &self.data;
+    let user = get_user_from_jwt(&data.auth, pool).await?;
 
-    let claims = match Claims::decode(&data.auth) {
-      Ok(claims) => claims.claims,
-      Err(_e) => return Err(APIError::err("not_logged_in").into()),
-    };
-
-    if let Err(slurs) = slur_check(&data.name) {
-      return Err(APIError::err(&slurs_vec_to_str(slurs)).into());
-    }
-
-    if let Err(slurs) = slur_check(&data.title) {
-      return Err(APIError::err(&slurs_vec_to_str(slurs)).into());
-    }
-
-    if let Some(description) = &data.description {
-      if let Err(slurs) = slur_check(description) {
-        return Err(APIError::err(&slurs_vec_to_str(slurs)).into());
-      }
-    }
+    check_slurs(&data.name)?;
+    check_slurs(&data.title)?;
+    check_slurs_opt(&data.description)?;
 
     if !is_valid_community_name(&data.name) {
       return Err(APIError::err("invalid_community_name").into());
     }
 
-    let user_id = claims.id;
-
-    // Check for a site ban
-    let user_view = blocking(pool, move |conn| UserView::read(conn, user_id)).await??;
-    if user_view.banned {
-      return Err(APIError::err("site_ban").into());
-    }
-
     // Double check for duplicate community actor_ids
     let actor_id = make_apub_endpoint(EndpointType::Community, &data.name).to_string();
     let actor_id_cloned = actor_id.to_owned();
@@ -285,7 +252,7 @@ impl Perform for Oper<CreateCommunity> {
       title: data.title.to_owned(),
       description: data.description.to_owned(),
       category_id: data.category_id,
-      creator_id: user_id,
+      creator_id: user.id,
       removed: None,
       deleted: None,
       nsfw: data.nsfw,
@@ -306,7 +273,7 @@ impl Perform for Oper<CreateCommunity> {
 
     let community_moderator_form = CommunityModeratorForm {
       community_id: inserted_community.id,
-      user_id,
+      user_id: user.id,
     };
 
     let join = move |conn: &'_ _| CommunityModerator::join(conn, &community_moderator_form);
@@ -316,7 +283,7 @@ impl Perform for Oper<CreateCommunity> {
 
     let community_follower_form = CommunityFollowerForm {
       community_id: inserted_community.id,
-      user_id,
+      user_id: user.id,
     };
 
     let follow = move |conn: &'_ _| CommunityFollower::follow(conn, &community_follower_form);
@@ -324,6 +291,7 @@ impl Perform for Oper<CreateCommunity> {
       return Err(APIError::err("community_follower_already_exists").into());
     }
 
+    let user_id = user.id;
     let community_view = blocking(pool, move |conn| {
       CommunityView::read(conn, inserted_community.id, Some(user_id))
     })
@@ -345,29 +313,10 @@ impl Perform for Oper<EditCommunity> {
     websocket_info: Option<WebsocketInfo>,
   ) -> Result<CommunityResponse, LemmyError> {
     let data: &EditCommunity = &self.data;
+    let user = get_user_from_jwt(&data.auth, pool).await?;
 
-    if let Err(slurs) = slur_check(&data.title) {
-      return Err(APIError::err(&slurs_vec_to_str(slurs)).into());
-    }
-
-    if let Some(description) = &data.description {
-      if let Err(slurs) = slur_check(description) {
-        return Err(APIError::err(&slurs_vec_to_str(slurs)).into());
-      }
-    }
-
-    let claims = match Claims::decode(&data.auth) {
-      Ok(claims) => claims.claims,
-      Err(_e) => return Err(APIError::err("not_logged_in").into()),
-    };
-
-    let user_id = claims.id;
-
-    // Check for a site ban
-    let user = blocking(pool, move |conn| User_::read(conn, user_id)).await??;
-    if user.banned {
-      return Err(APIError::err("site_ban").into());
-    }
+    check_slurs(&data.title)?;
+    check_slurs_opt(&data.description)?;
 
     // Verify its a mod (only mods can edit it)
     let edit_id = data.edit_id;
@@ -376,7 +325,7 @@ impl Perform for Oper<EditCommunity> {
         .map(|v| v.into_iter().map(|m| m.user_id).collect())
     })
     .await??;
-    if !mods.contains(&user_id) {
+    if !mods.contains(&user.id) {
       return Err(APIError::err("not_a_moderator").into());
     }
 
@@ -415,6 +364,7 @@ impl Perform for Oper<EditCommunity> {
     // process for communities and users
 
     let edit_id = data.edit_id;
+    let user_id = user.id;
     let community_view = blocking(pool, move |conn| {
       CommunityView::read(conn, edit_id, Some(user_id))
     })
@@ -440,24 +390,12 @@ impl Perform for Oper<DeleteCommunity> {
     websocket_info: Option<WebsocketInfo>,
   ) -> Result<CommunityResponse, LemmyError> {
     let data: &DeleteCommunity = &self.data;
-
-    let claims = match Claims::decode(&data.auth) {
-      Ok(claims) => claims.claims,
-      Err(_e) => return Err(APIError::err("not_logged_in").into()),
-    };
-
-    let user_id = claims.id;
-
-    // Check for a site ban
-    let user = blocking(pool, move |conn| User_::read(conn, user_id)).await??;
-    if user.banned {
-      return Err(APIError::err("site_ban").into());
-    }
+    let user = get_user_from_jwt(&data.auth, pool).await?;
 
     // Verify its the creator (only a creator can delete the community)
     let edit_id = data.edit_id;
     let read_community = blocking(pool, move |conn| Community::read(conn, edit_id)).await??;
-    if read_community.creator_id != user_id {
+    if read_community.creator_id != user.id {
       return Err(APIError::err("no_community_edit_allowed").into());
     }
 
@@ -485,6 +423,7 @@ impl Perform for Oper<DeleteCommunity> {
     }
 
     let edit_id = data.edit_id;
+    let user_id = user.id;
     let community_view = blocking(pool, move |conn| {
       CommunityView::read(conn, edit_id, Some(user_id))
     })
@@ -510,22 +449,10 @@ impl Perform for Oper<RemoveCommunity> {
     websocket_info: Option<WebsocketInfo>,
   ) -> Result<CommunityResponse, LemmyError> {
     let data: &RemoveCommunity = &self.data;
-
-    let claims = match Claims::decode(&data.auth) {
-      Ok(claims) => claims.claims,
-      Err(_e) => return Err(APIError::err("not_logged_in").into()),
-    };
-
-    let user_id = claims.id;
-
-    // Check for a site ban
-    let user = blocking(pool, move |conn| User_::read(conn, user_id)).await??;
-    if user.banned {
-      return Err(APIError::err("site_ban").into());
-    }
+    let user = get_user_from_jwt(&data.auth, pool).await?;
 
     // Verify its an admin (only an admin can remove a community)
-    is_admin(pool, user_id).await?;
+    is_admin(pool, user.id).await?;
 
     // Do the remove
     let edit_id = data.edit_id;
@@ -545,7 +472,7 @@ impl Perform for Oper<RemoveCommunity> {
       None => None,
     };
     let form = ModRemoveCommunityForm {
-      mod_user_id: user_id,
+      mod_user_id: user.id,
       community_id: data.edit_id,
       removed: Some(removed),
       reason: data.reason.to_owned(),
@@ -565,6 +492,7 @@ impl Perform for Oper<RemoveCommunity> {
     }
 
     let edit_id = data.edit_id;
+    let user_id = user.id;
     let community_view = blocking(pool, move |conn| {
       CommunityView::read(conn, edit_id, Some(user_id))
     })
@@ -590,19 +518,7 @@ impl Perform for Oper<ListCommunities> {
     _websocket_info: Option<WebsocketInfo>,
   ) -> Result<ListCommunitiesResponse, LemmyError> {
     let data: &ListCommunities = &self.data;
-
-    // For logged in users, you need to get back subscribed, and settings
-    let user: Option<User_> = match &data.auth {
-      Some(auth) => match Claims::decode(&auth) {
-        Ok(claims) => {
-          let user_id = claims.claims.id;
-          let user = blocking(pool, move |conn| User_::read(conn, user_id)).await??;
-          Some(user)
-        }
-        Err(_e) => None,
-      },
-      None => None,
-    };
+    let user = get_user_from_jwt_opt(&data.auth, pool).await?;
 
     let user_id = match &user {
       Some(user) => Some(user.id),
@@ -644,19 +560,13 @@ impl Perform for Oper<FollowCommunity> {
     _websocket_info: Option<WebsocketInfo>,
   ) -> Result<CommunityResponse, LemmyError> {
     let data: &FollowCommunity = &self.data;
-
-    let claims = match Claims::decode(&data.auth) {
-      Ok(claims) => claims.claims,
-      Err(_e) => return Err(APIError::err("not_logged_in").into()),
-    };
-
-    let user_id = claims.id;
+    let user = get_user_from_jwt(&data.auth, pool).await?;
 
     let community_id = data.community_id;
     let community = blocking(pool, move |conn| Community::read(conn, community_id)).await??;
     let community_follower_form = CommunityFollowerForm {
       community_id: data.community_id,
-      user_id,
+      user_id: user.id,
     };
 
     if community.local {
@@ -672,29 +582,25 @@ impl Perform for Oper<FollowCommunity> {
           return Err(APIError::err("community_follower_already_exists").into());
         }
       }
+    } else if data.follow {
+      // Dont actually add to the community followers here, because you need
+      // to wait for the accept
+      user
+        .send_follow(&community.actor_id, &self.client, pool)
+        .await?;
     } else {
-      let user = blocking(pool, move |conn| User_::read(conn, user_id)).await??;
-
-      if data.follow {
-        // Dont actually add to the community followers here, because you need
-        // to wait for the accept
-        user
-          .send_follow(&community.actor_id, &self.client, pool)
-          .await?;
-      } else {
-        user
-          .send_unfollow(&community.actor_id, &self.client, pool)
-          .await?;
-        let unfollow =
-          move |conn: &'_ _| CommunityFollower::unfollow(conn, &community_follower_form);
-        if blocking(pool, unfollow).await?.is_err() {
-          return Err(APIError::err("community_follower_already_exists").into());
-        }
+      user
+        .send_unfollow(&community.actor_id, &self.client, pool)
+        .await?;
+      let unfollow = move |conn: &'_ _| CommunityFollower::unfollow(conn, &community_follower_form);
+      if blocking(pool, unfollow).await?.is_err() {
+        return Err(APIError::err("community_follower_already_exists").into());
       }
-      // TODO: this needs to return a "pending" state, until Accept is received from the remote server
     }
+    // TODO: this needs to return a "pending" state, until Accept is received from the remote server
 
     let community_id = data.community_id;
+    let user_id = user.id;
     let community_view = blocking(pool, move |conn| {
       CommunityView::read(conn, community_id, Some(user_id))
     })
@@ -716,14 +622,9 @@ impl Perform for Oper<GetFollowedCommunities> {
     _websocket_info: Option<WebsocketInfo>,
   ) -> Result<GetFollowedCommunitiesResponse, LemmyError> {
     let data: &GetFollowedCommunities = &self.data;
+    let user = get_user_from_jwt(&data.auth, pool).await?;
 
-    let claims = match Claims::decode(&data.auth) {
-      Ok(claims) => claims.claims,
-      Err(_e) => return Err(APIError::err("not_logged_in").into()),
-    };
-
-    let user_id = claims.id;
-
+    let user_id = user.id;
     let communities = match blocking(pool, move |conn| {
       CommunityFollowerView::for_user(conn, user_id)
     })
@@ -748,18 +649,12 @@ impl Perform for Oper<BanFromCommunity> {
     websocket_info: Option<WebsocketInfo>,
   ) -> Result<BanFromCommunityResponse, LemmyError> {
     let data: &BanFromCommunity = &self.data;
-
-    let claims = match Claims::decode(&data.auth) {
-      Ok(claims) => claims.claims,
-      Err(_e) => return Err(APIError::err("not_logged_in").into()),
-    };
-
-    let user_id = claims.id;
+    let user = get_user_from_jwt(&data.auth, pool).await?;
 
     let community_id = data.community_id;
 
     // Verify that only mods or admins can ban
-    is_mod_or_admin(pool, user_id, community_id).await?;
+    is_mod_or_admin(pool, user.id, community_id).await?;
 
     let community_user_ban_form = CommunityUserBanForm {
       community_id: data.community_id,
@@ -786,7 +681,7 @@ impl Perform for Oper<BanFromCommunity> {
     };
 
     let form = ModBanFromCommunityForm {
-      mod_user_id: user_id,
+      mod_user_id: user.id,
       other_user_id: data.user_id,
       community_id: data.community_id,
       reason: data.reason.to_owned(),
@@ -826,13 +721,7 @@ impl Perform for Oper<AddModToCommunity> {
     websocket_info: Option<WebsocketInfo>,
   ) -> Result<AddModToCommunityResponse, LemmyError> {
     let data: &AddModToCommunity = &self.data;
-
-    let claims = match Claims::decode(&data.auth) {
-      Ok(claims) => claims.claims,
-      Err(_e) => return Err(APIError::err("not_logged_in").into()),
-    };
-
-    let user_id = claims.id;
+    let user = get_user_from_jwt(&data.auth, pool).await?;
 
     let community_moderator_form = CommunityModeratorForm {
       community_id: data.community_id,
@@ -842,7 +731,7 @@ impl Perform for Oper<AddModToCommunity> {
     let community_id = data.community_id;
 
     // Verify that only mods or admins can add mod
-    is_mod_or_admin(pool, user_id, community_id).await?;
+    is_mod_or_admin(pool, user.id, community_id).await?;
 
     if data.added {
       let join = move |conn: &'_ _| CommunityModerator::join(conn, &community_moderator_form);
@@ -858,7 +747,7 @@ impl Perform for Oper<AddModToCommunity> {
 
     // Mod tables
     let form = ModAddCommunityForm {
-      mod_user_id: user_id,
+      mod_user_id: user.id,
       other_user_id: data.user_id,
       community_id: data.community_id,
       removed: Some(!data.added),
@@ -896,13 +785,7 @@ impl Perform for Oper<TransferCommunity> {
     _websocket_info: Option<WebsocketInfo>,
   ) -> Result<GetCommunityResponse, LemmyError> {
     let data: &TransferCommunity = &self.data;
-
-    let claims = match Claims::decode(&data.auth) {
-      Ok(claims) => claims.claims,
-      Err(_e) => return Err(APIError::err("not_logged_in").into()),
-    };
-
-    let user_id = claims.id;
+    let user = get_user_from_jwt(&data.auth, pool).await?;
 
     let community_id = data.community_id;
     let read_community = blocking(pool, move |conn| Community::read(conn, community_id)).await??;
@@ -917,7 +800,7 @@ impl Perform for Oper<TransferCommunity> {
     admins.insert(0, creator_user);
 
     // Make sure user is the creator, or an admin
-    if user_id != read_community.creator_id && !admins.iter().map(|a| a.id).any(|x| x == user_id) {
+    if user.id != read_community.creator_id && !admins.iter().map(|a| a.id).any(|x| x == user.id) {
       return Err(APIError::err("not_an_admin").into());
     }
 
@@ -962,7 +845,7 @@ impl Perform for Oper<TransferCommunity> {
 
     // Mod tables
     let form = ModAddCommunityForm {
-      mod_user_id: user_id,
+      mod_user_id: user.id,
       other_user_id: data.user_id,
       community_id: data.community_id,
       removed: Some(false),
@@ -970,6 +853,7 @@ impl Perform for Oper<TransferCommunity> {
     blocking(pool, move |conn| ModAddCommunity::create(conn, &form)).await??;
 
     let community_id = data.community_id;
+    let user_id = user.id;
     let community_view = match blocking(pool, move |conn| {
       CommunityView::read(conn, community_id, Some(user_id))
     })
index 11f958f087d6c92982e94ad1855ea1fd18c4b84c..a9aae823a5b53cee0c930ef58b25b9750375a7f9 100644 (file)
@@ -1,4 +1,4 @@
-use crate::{blocking, websocket::WebsocketInfo, DbPool, LemmyError};
+use crate::{api::claims::Claims, blocking, websocket::WebsocketInfo, DbPool, LemmyError};
 use actix_web::client::Client;
 use lemmy_db::{
   community::*,
@@ -9,6 +9,7 @@ use lemmy_db::{
   user_view::*,
   Crud,
 };
+use lemmy_utils::{slur_check, slurs_vec_to_str};
 use thiserror::Error;
 
 pub mod claims;
@@ -75,3 +76,56 @@ pub async fn is_admin(pool: &DbPool, user_id: i32) -> Result<(), LemmyError> {
   }
   Ok(())
 }
+
+pub(in crate::api) async fn get_user_from_jwt(
+  jwt: &str,
+  pool: &DbPool,
+) -> Result<User_, LemmyError> {
+  let claims = match Claims::decode(&jwt) {
+    Ok(claims) => claims.claims,
+    Err(_e) => return Err(APIError::err("not_logged_in").into()),
+  };
+  let user_id = claims.id;
+  let user = blocking(pool, move |conn| User_::read(conn, user_id)).await??;
+  // Check for a site ban
+  if user.banned {
+    return Err(APIError::err("site_ban").into());
+  }
+  Ok(user)
+}
+
+pub(in crate::api) async fn get_user_from_jwt_opt(
+  jwt: &Option<String>,
+  pool: &DbPool,
+) -> Result<Option<User_>, LemmyError> {
+  match jwt {
+    Some(jwt) => Ok(Some(get_user_from_jwt(jwt, pool).await?)),
+    None => Ok(None),
+  }
+}
+
+pub(in crate::api) fn check_slurs(text: &str) -> Result<(), APIError> {
+  if let Err(slurs) = slur_check(text) {
+    Err(APIError::err(&slurs_vec_to_str(slurs)))
+  } else {
+    Ok(())
+  }
+}
+pub(in crate::api) fn check_slurs_opt(text: &Option<String>) -> Result<(), APIError> {
+  match text {
+    Some(t) => check_slurs(t),
+    None => Ok(()),
+  }
+}
+pub(in crate::api) async fn check_community_ban(
+  user_id: i32,
+  community_id: i32,
+  pool: &DbPool,
+) -> Result<(), LemmyError> {
+  let is_banned = move |conn: &'_ _| CommunityUserBanView::get(conn, user_id, community_id).is_ok();
+  if blocking(pool, is_banned).await? {
+    Err(APIError::err("community_ban").into())
+  } else {
+    Ok(())
+  }
+}
index e346a6c89c7528f032ef9c70f13a6af9d7dc12e4..b43e4e55cf6b2a8a5c0ab6ea972b2ebdfd1c9e8d 100644 (file)
@@ -1,5 +1,15 @@
 use crate::{
-  api::{claims::Claims, is_mod_or_admin, APIError, Oper, Perform},
+  api::{
+    check_community_ban,
+    check_slurs,
+    check_slurs_opt,
+    get_user_from_jwt,
+    get_user_from_jwt_opt,
+    is_mod_or_admin,
+    APIError,
+    Oper,
+    Perform,
+  },
   apub::{ApubLikeableType, ApubObjectType},
   blocking,
   fetch_iframely_and_pictrs_data,
@@ -19,20 +29,13 @@ use lemmy_db::{
   post::*,
   post_view::*,
   site_view::*,
-  user::*,
   Crud,
   Likeable,
   ListingType,
   Saveable,
   SortType,
 };
-use lemmy_utils::{
-  is_valid_post_title,
-  make_apub_endpoint,
-  slur_check,
-  slurs_vec_to_str,
-  EndpointType,
-};
+use lemmy_utils::{is_valid_post_title, make_apub_endpoint, EndpointType};
 use serde::{Deserialize, Serialize};
 use std::str::FromStr;
 use url::Url;
@@ -146,41 +149,16 @@ impl Perform for Oper<CreatePost> {
     websocket_info: Option<WebsocketInfo>,
   ) -> Result<PostResponse, LemmyError> {
     let data: &CreatePost = &self.data;
+    let user = get_user_from_jwt(&data.auth, pool).await?;
 
-    let claims = match Claims::decode(&data.auth) {
-      Ok(claims) => claims.claims,
-      Err(_e) => return Err(APIError::err("not_logged_in").into()),
-    };
-
-    if let Err(slurs) = slur_check(&data.name) {
-      return Err(APIError::err(&slurs_vec_to_str(slurs)).into());
-    }
-
-    if let Some(body) = &data.body {
-      if let Err(slurs) = slur_check(body) {
-        return Err(APIError::err(&slurs_vec_to_str(slurs)).into());
-      }
-    }
+    check_slurs(&data.name)?;
+    check_slurs_opt(&data.body)?;
 
     if !is_valid_post_title(&data.name) {
       return Err(APIError::err("invalid_post_title").into());
     }
 
-    let user_id = claims.id;
-
-    // Check for a community ban
-    let community_id = data.community_id;
-    let is_banned =
-      move |conn: &'_ _| CommunityUserBanView::get(conn, user_id, community_id).is_ok();
-    if blocking(pool, is_banned).await? {
-      return Err(APIError::err("community_ban").into());
-    }
-
-    // Check for a site ban
-    let user = blocking(pool, move |conn| User_::read(conn, user_id)).await??;
-    if user.banned {
-      return Err(APIError::err("site_ban").into());
-    }
+    check_community_ban(user.id, data.community_id, pool).await?;
 
     if let Some(url) = data.url.as_ref() {
       match Url::parse(url) {
@@ -198,7 +176,7 @@ impl Perform for Oper<CreatePost> {
       url: data.url.to_owned(),
       body: data.body.to_owned(),
       community_id: data.community_id,
-      creator_id: user_id,
+      creator_id: user.id,
       removed: None,
       deleted: None,
       nsfw: data.nsfw,
@@ -244,7 +222,7 @@ impl Perform for Oper<CreatePost> {
     // They like their own post by default
     let like_form = PostLikeForm {
       post_id: inserted_post.id,
-      user_id,
+      user_id: user.id,
       score: 1,
     };
 
@@ -258,7 +236,7 @@ impl Perform for Oper<CreatePost> {
     // Refetch the view
     let inserted_post_id = inserted_post.id;
     let post_view = match blocking(pool, move |conn| {
-      PostView::read(conn, inserted_post_id, Some(user_id))
+      PostView::read(conn, inserted_post_id, Some(user.id))
     })
     .await?
     {
@@ -290,17 +268,8 @@ impl Perform for Oper<GetPost> {
     websocket_info: Option<WebsocketInfo>,
   ) -> Result<GetPostResponse, LemmyError> {
     let data: &GetPost = &self.data;
-
-    let user_id: Option<i32> = match &data.auth {
-      Some(auth) => match Claims::decode(&auth) {
-        Ok(claims) => {
-          let user_id = claims.claims.id;
-          Some(user_id)
-        }
-        Err(_e) => None,
-      },
-      None => None,
-    };
+    let user = get_user_from_jwt_opt(&data.auth, pool).await?;
+    let user_id = user.map(|u| u.id);
 
     let id = data.id;
     let post_view = match blocking(pool, move |conn| PostView::read(conn, id, user_id)).await? {
@@ -369,19 +338,7 @@ impl Perform for Oper<GetPosts> {
     websocket_info: Option<WebsocketInfo>,
   ) -> Result<GetPostsResponse, LemmyError> {
     let data: &GetPosts = &self.data;
-
-    // For logged in users, you need to get back subscribed, and settings
-    let user: Option<User_> = match &data.auth {
-      Some(auth) => match Claims::decode(&auth) {
-        Ok(claims) => {
-          let user_id = claims.claims.id;
-          let user = blocking(pool, move |conn| User_::read(conn, user_id)).await??;
-          Some(user)
-        }
-        Err(_e) => None,
-      },
-      None => None,
-    };
+    let user = get_user_from_jwt_opt(&data.auth, pool).await?;
 
     let user_id = match &user {
       Some(user) => Some(user.id),
@@ -446,13 +403,7 @@ impl Perform for Oper<CreatePostLike> {
     websocket_info: Option<WebsocketInfo>,
   ) -> Result<PostResponse, LemmyError> {
     let data: &CreatePostLike = &self.data;
-
-    let claims = match Claims::decode(&data.auth) {
-      Ok(claims) => claims.claims,
-      Err(_e) => return Err(APIError::err("not_logged_in").into()),
-    };
-
-    let user_id = claims.id;
+    let user = get_user_from_jwt(&data.auth, pool).await?;
 
     // Don't do a downvote if site has downvotes disabled
     if data.score == -1 {
@@ -466,22 +417,11 @@ impl Perform for Oper<CreatePostLike> {
     let post_id = data.post_id;
     let post = blocking(pool, move |conn| Post::read(conn, post_id)).await??;
 
-    let community_id = post.community_id;
-    let is_banned =
-      move |conn: &'_ _| CommunityUserBanView::get(conn, user_id, community_id).is_ok();
-    if blocking(pool, is_banned).await? {
-      return Err(APIError::err("community_ban").into());
-    }
-
-    // Check for a site ban
-    let user = blocking(pool, move |conn| User_::read(conn, user_id)).await??;
-    if user.banned {
-      return Err(APIError::err("site_ban").into());
-    }
+    check_community_ban(user.id, post.community_id, pool).await?;
 
     let like_form = PostLikeForm {
       post_id: data.post_id,
-      user_id,
+      user_id: user.id,
       score: data.score,
     };
 
@@ -508,6 +448,7 @@ impl Perform for Oper<CreatePostLike> {
     }
 
     let post_id = data.post_id;
+    let user_id = user.id;
     let post_view = match blocking(pool, move |conn| {
       PostView::read(conn, post_id, Some(user_id))
     })
@@ -541,47 +482,22 @@ impl Perform for Oper<EditPost> {
     websocket_info: Option<WebsocketInfo>,
   ) -> Result<PostResponse, LemmyError> {
     let data: &EditPost = &self.data;
+    let user = get_user_from_jwt(&data.auth, pool).await?;
 
-    if let Err(slurs) = slur_check(&data.name) {
-      return Err(APIError::err(&slurs_vec_to_str(slurs)).into());
-    }
-
-    if let Some(body) = &data.body {
-      if let Err(slurs) = slur_check(body) {
-        return Err(APIError::err(&slurs_vec_to_str(slurs)).into());
-      }
-    }
+    check_slurs(&data.name)?;
+    check_slurs_opt(&data.body)?;
 
     if !is_valid_post_title(&data.name) {
       return Err(APIError::err("invalid_post_title").into());
     }
 
-    let claims = match Claims::decode(&data.auth) {
-      Ok(claims) => claims.claims,
-      Err(_e) => return Err(APIError::err("not_logged_in").into()),
-    };
-
-    let user_id = claims.id;
-
     let edit_id = data.edit_id;
     let orig_post = blocking(pool, move |conn| Post::read(conn, edit_id)).await??;
 
-    // Check for a community ban
-    let community_id = orig_post.community_id;
-    let is_banned =
-      move |conn: &'_ _| CommunityUserBanView::get(conn, user_id, community_id).is_ok();
-    if blocking(pool, is_banned).await? {
-      return Err(APIError::err("community_ban").into());
-    }
-
-    // Check for a site ban
-    let user = blocking(pool, move |conn| User_::read(conn, user_id)).await??;
-    if user.banned {
-      return Err(APIError::err("site_ban").into());
-    }
+    check_community_ban(user.id, orig_post.community_id, pool).await?;
 
     // Verify that only the creator can edit
-    if !Post::is_post_creator(user_id, orig_post.creator_id) {
+    if !Post::is_post_creator(user.id, orig_post.creator_id) {
       return Err(APIError::err("no_post_edit_allowed").into());
     }
 
@@ -630,7 +546,7 @@ impl Perform for Oper<EditPost> {
 
     let edit_id = data.edit_id;
     let post_view = blocking(pool, move |conn| {
-      PostView::read(conn, edit_id, Some(user_id))
+      PostView::read(conn, edit_id, Some(user.id))
     })
     .await??;
 
@@ -658,33 +574,15 @@ impl Perform for Oper<DeletePost> {
     websocket_info: Option<WebsocketInfo>,
   ) -> Result<PostResponse, LemmyError> {
     let data: &DeletePost = &self.data;
-
-    let claims = match Claims::decode(&data.auth) {
-      Ok(claims) => claims.claims,
-      Err(_e) => return Err(APIError::err("not_logged_in").into()),
-    };
-
-    let user_id = claims.id;
+    let user = get_user_from_jwt(&data.auth, pool).await?;
 
     let edit_id = data.edit_id;
     let orig_post = blocking(pool, move |conn| Post::read(conn, edit_id)).await??;
 
-    // Check for a site ban
-    let user = blocking(pool, move |conn| User_::read(conn, user_id)).await??;
-    if user.banned {
-      return Err(APIError::err("site_ban").into());
-    }
-
-    // Check for a community ban
-    let community_id = orig_post.community_id;
-    let is_banned =
-      move |conn: &'_ _| CommunityUserBanView::get(conn, user_id, community_id).is_ok();
-    if blocking(pool, is_banned).await? {
-      return Err(APIError::err("community_ban").into());
-    }
+    check_community_ban(user.id, orig_post.community_id, pool).await?;
 
     // Verify that only the creator can delete
-    if !Post::is_post_creator(user_id, orig_post.creator_id) {
+    if !Post::is_post_creator(user.id, orig_post.creator_id) {
       return Err(APIError::err("no_post_edit_allowed").into());
     }
 
@@ -708,7 +606,7 @@ impl Perform for Oper<DeletePost> {
     // Refetch the post
     let edit_id = data.edit_id;
     let post_view = blocking(pool, move |conn| {
-      PostView::read(conn, edit_id, Some(user_id))
+      PostView::read(conn, edit_id, Some(user.id))
     })
     .await??;
 
@@ -736,33 +634,15 @@ impl Perform for Oper<RemovePost> {
     websocket_info: Option<WebsocketInfo>,
   ) -> Result<PostResponse, LemmyError> {
     let data: &RemovePost = &self.data;
-
-    let claims = match Claims::decode(&data.auth) {
-      Ok(claims) => claims.claims,
-      Err(_e) => return Err(APIError::err("not_logged_in").into()),
-    };
-
-    let user_id = claims.id;
+    let user = get_user_from_jwt(&data.auth, pool).await?;
 
     let edit_id = data.edit_id;
     let orig_post = blocking(pool, move |conn| Post::read(conn, edit_id)).await??;
 
-    // Check for a site ban
-    let user = blocking(pool, move |conn| User_::read(conn, user_id)).await??;
-    if user.banned {
-      return Err(APIError::err("site_ban").into());
-    }
-
-    // Check for a community ban
-    let community_id = orig_post.community_id;
-    let is_banned =
-      move |conn: &'_ _| CommunityUserBanView::get(conn, user_id, community_id).is_ok();
-    if blocking(pool, is_banned).await? {
-      return Err(APIError::err("community_ban").into());
-    }
+    check_community_ban(user.id, orig_post.community_id, pool).await?;
 
     // Verify that only the mods can remove
-    is_mod_or_admin(pool, user_id, community_id).await?;
+    is_mod_or_admin(pool, user.id, orig_post.community_id).await?;
 
     // Update the post
     let edit_id = data.edit_id;
@@ -774,7 +654,7 @@ impl Perform for Oper<RemovePost> {
 
     // Mod tables
     let form = ModRemovePostForm {
-      mod_user_id: user_id,
+      mod_user_id: user.id,
       post_id: data.edit_id,
       removed: Some(removed),
       reason: data.reason.to_owned(),
@@ -792,6 +672,7 @@ impl Perform for Oper<RemovePost> {
 
     // Refetch the post
     let edit_id = data.edit_id;
+    let user_id = user.id;
     let post_view = blocking(pool, move |conn| {
       PostView::read(conn, edit_id, Some(user_id))
     })
@@ -821,33 +702,15 @@ impl Perform for Oper<LockPost> {
     websocket_info: Option<WebsocketInfo>,
   ) -> Result<PostResponse, LemmyError> {
     let data: &LockPost = &self.data;
-
-    let claims = match Claims::decode(&data.auth) {
-      Ok(claims) => claims.claims,
-      Err(_e) => return Err(APIError::err("not_logged_in").into()),
-    };
-
-    let user_id = claims.id;
+    let user = get_user_from_jwt(&data.auth, pool).await?;
 
     let edit_id = data.edit_id;
     let orig_post = blocking(pool, move |conn| Post::read(conn, edit_id)).await??;
 
-    // Check for a site ban
-    let user = blocking(pool, move |conn| User_::read(conn, user_id)).await??;
-    if user.banned {
-      return Err(APIError::err("site_ban").into());
-    }
-
-    // Check for a community ban
-    let community_id = orig_post.community_id;
-    let is_banned =
-      move |conn: &'_ _| CommunityUserBanView::get(conn, user_id, community_id).is_ok();
-    if blocking(pool, is_banned).await? {
-      return Err(APIError::err("community_ban").into());
-    }
+    check_community_ban(user.id, orig_post.community_id, pool).await?;
 
     // Verify that only the mods can lock
-    is_mod_or_admin(pool, user_id, community_id).await?;
+    is_mod_or_admin(pool, user.id, orig_post.community_id).await?;
 
     // Update the post
     let edit_id = data.edit_id;
@@ -857,7 +720,7 @@ impl Perform for Oper<LockPost> {
 
     // Mod tables
     let form = ModLockPostForm {
-      mod_user_id: user_id,
+      mod_user_id: user.id,
       post_id: data.edit_id,
       locked: Some(locked),
     };
@@ -869,7 +732,7 @@ impl Perform for Oper<LockPost> {
     // Refetch the post
     let edit_id = data.edit_id;
     let post_view = blocking(pool, move |conn| {
-      PostView::read(conn, edit_id, Some(user_id))
+      PostView::read(conn, edit_id, Some(user.id))
     })
     .await??;
 
@@ -897,33 +760,15 @@ impl Perform for Oper<StickyPost> {
     websocket_info: Option<WebsocketInfo>,
   ) -> Result<PostResponse, LemmyError> {
     let data: &StickyPost = &self.data;
-
-    let claims = match Claims::decode(&data.auth) {
-      Ok(claims) => claims.claims,
-      Err(_e) => return Err(APIError::err("not_logged_in").into()),
-    };
-
-    let user_id = claims.id;
+    let user = get_user_from_jwt(&data.auth, pool).await?;
 
     let edit_id = data.edit_id;
     let orig_post = blocking(pool, move |conn| Post::read(conn, edit_id)).await??;
 
-    // Check for a site ban
-    let user = blocking(pool, move |conn| User_::read(conn, user_id)).await??;
-    if user.banned {
-      return Err(APIError::err("site_ban").into());
-    }
-
-    // Check for a community ban
-    let community_id = orig_post.community_id;
-    let is_banned =
-      move |conn: &'_ _| CommunityUserBanView::get(conn, user_id, community_id).is_ok();
-    if blocking(pool, is_banned).await? {
-      return Err(APIError::err("community_ban").into());
-    }
+    check_community_ban(user.id, orig_post.community_id, pool).await?;
 
     // Verify that only the mods can sticky
-    is_mod_or_admin(pool, user_id, community_id).await?;
+    is_mod_or_admin(pool, user.id, orig_post.community_id).await?;
 
     // Update the post
     let edit_id = data.edit_id;
@@ -935,7 +780,7 @@ impl Perform for Oper<StickyPost> {
 
     // Mod tables
     let form = ModStickyPostForm {
-      mod_user_id: user_id,
+      mod_user_id: user.id,
       post_id: data.edit_id,
       stickied: Some(stickied),
     };
@@ -948,7 +793,7 @@ impl Perform for Oper<StickyPost> {
     // Refetch the post
     let edit_id = data.edit_id;
     let post_view = blocking(pool, move |conn| {
-      PostView::read(conn, edit_id, Some(user_id))
+      PostView::read(conn, edit_id, Some(user.id))
     })
     .await??;
 
@@ -976,17 +821,11 @@ impl Perform for Oper<SavePost> {
     _websocket_info: Option<WebsocketInfo>,
   ) -> Result<PostResponse, LemmyError> {
     let data: &SavePost = &self.data;
-
-    let claims = match Claims::decode(&data.auth) {
-      Ok(claims) => claims.claims,
-      Err(_e) => return Err(APIError::err("not_logged_in").into()),
-    };
-
-    let user_id = claims.id;
+    let user = get_user_from_jwt(&data.auth, pool).await?;
 
     let post_saved_form = PostSavedForm {
       post_id: data.post_id,
-      user_id,
+      user_id: user.id,
     };
 
     if data.save {
@@ -1002,6 +841,7 @@ impl Perform for Oper<SavePost> {
     }
 
     let post_id = data.post_id;
+    let user_id = user.id;
     let post_view = blocking(pool, move |conn| {
       PostView::read(conn, post_id, Some(user_id))
     })
index adade080eea8f650e849265b8bac13e76bf581f3..82cad9610047d904991f0b8b8571f0b63404dbca 100644 (file)
@@ -1,6 +1,15 @@
 use super::user::Register;
 use crate::{
-  api::{claims::Claims, is_admin, APIError, Oper, Perform},
+  api::{
+    check_slurs,
+    check_slurs_opt,
+    get_user_from_jwt,
+    get_user_from_jwt_opt,
+    is_admin,
+    APIError,
+    Oper,
+    Perform,
+  },
   apub::fetcher::search_by_apub_id,
   blocking,
   version,
@@ -24,7 +33,7 @@ use lemmy_db::{
   SearchType,
   SortType,
 };
-use lemmy_utils::{settings::Settings, slur_check, slurs_vec_to_str};
+use lemmy_utils::settings::Settings;
 use log::{debug, info};
 use serde::{Deserialize, Serialize};
 use std::str::FromStr;
@@ -243,30 +252,18 @@ impl Perform for Oper<CreateSite> {
   ) -> Result<SiteResponse, LemmyError> {
     let data: &CreateSite = &self.data;
 
-    let claims = match Claims::decode(&data.auth) {
-      Ok(claims) => claims.claims,
-      Err(_e) => return Err(APIError::err("not_logged_in").into()),
-    };
-
-    if let Err(slurs) = slur_check(&data.name) {
-      return Err(APIError::err(&slurs_vec_to_str(slurs)).into());
-    }
-
-    if let Some(description) = &data.description {
-      if let Err(slurs) = slur_check(description) {
-        return Err(APIError::err(&slurs_vec_to_str(slurs)).into());
-      }
-    }
+    let user = get_user_from_jwt(&data.auth, pool).await?;
 
-    let user_id = claims.id;
+    check_slurs(&data.name)?;
+    check_slurs_opt(&data.description)?;
 
     // Make sure user is an admin
-    is_admin(pool, user_id).await?;
+    is_admin(pool, user.id).await?;
 
     let site_form = SiteForm {
       name: data.name.to_owned(),
       description: data.description.to_owned(),
-      creator_id: user_id,
+      creator_id: user.id,
       enable_downvotes: data.enable_downvotes,
       open_registration: data.open_registration,
       enable_nsfw: data.enable_nsfw,
@@ -293,26 +290,13 @@ impl Perform for Oper<EditSite> {
     websocket_info: Option<WebsocketInfo>,
   ) -> Result<SiteResponse, LemmyError> {
     let data: &EditSite = &self.data;
+    let user = get_user_from_jwt(&data.auth, pool).await?;
 
-    let claims = match Claims::decode(&data.auth) {
-      Ok(claims) => claims.claims,
-      Err(_e) => return Err(APIError::err("not_logged_in").into()),
-    };
-
-    if let Err(slurs) = slur_check(&data.name) {
-      return Err(APIError::err(&slurs_vec_to_str(slurs)).into());
-    }
-
-    if let Some(description) = &data.description {
-      if let Err(slurs) = slur_check(description) {
-        return Err(APIError::err(&slurs_vec_to_str(slurs)).into());
-      }
-    }
-
-    let user_id = claims.id;
+    check_slurs(&data.name)?;
+    check_slurs_opt(&data.description)?;
 
     // Make sure user is an admin
-    is_admin(pool, user_id).await?;
+    is_admin(pool, user.id).await?;
 
     let found_site = blocking(pool, move |conn| Site::read(conn, 1)).await??;
 
@@ -421,21 +405,12 @@ impl Perform for Oper<GetSite> {
       0
     };
 
-    // Giving back your user, if you're logged in
-    let my_user: Option<User_> = match &data.auth {
-      Some(auth) => match Claims::decode(&auth) {
-        Ok(claims) => {
-          let user_id = claims.claims.id;
-          let mut user = blocking(pool, move |conn| User_::read(conn, user_id)).await??;
-          user.password_encrypted = "".to_string();
-          user.private_key = None;
-          user.public_key = None;
-          Some(user)
-        }
-        Err(_e) => None,
-      },
-      None => None,
-    };
+    let my_user = get_user_from_jwt_opt(&data.auth, pool).await?.map(|mut u| {
+      u.password_encrypted = "".to_string();
+      u.private_key = None;
+      u.public_key = None;
+      u
+    });
 
     Ok(GetSiteResponse {
       site: site_view,
@@ -466,16 +441,8 @@ impl Perform for Oper<Search> {
       Err(e) => debug!("Failed to resolve search query as activitypub ID: {}", e),
     }
 
-    let user_id: Option<i32> = match &data.auth {
-      Some(auth) => match Claims::decode(&auth) {
-        Ok(claims) => {
-          let user_id = claims.claims.id;
-          Some(user_id)
-        }
-        Err(_e) => None,
-      },
-      None => None,
-    };
+    let user = get_user_from_jwt_opt(&data.auth, pool).await?;
+    let user_id = user.map(|u| u.id);
 
     let type_ = SearchType::from_str(&data.type_)?;
 
@@ -630,14 +597,8 @@ impl Perform for Oper<TransferSite> {
     _websocket_info: Option<WebsocketInfo>,
   ) -> Result<GetSiteResponse, LemmyError> {
     let data: &TransferSite = &self.data;
+    let mut user = get_user_from_jwt(&data.auth, pool).await?;
 
-    let claims = match Claims::decode(&data.auth) {
-      Ok(claims) => claims.claims,
-      Err(_e) => return Err(APIError::err("not_logged_in").into()),
-    };
-
-    let user_id = claims.id;
-    let mut user = blocking(pool, move |conn| User_::read(conn, user_id)).await??;
     // TODO add a User_::read_safe() for this.
     user.password_encrypted = "".to_string();
     user.private_key = None;
@@ -646,7 +607,7 @@ impl Perform for Oper<TransferSite> {
     let read_site = blocking(pool, move |conn| Site::read(conn, 1)).await??;
 
     // Make sure user is the creator
-    if read_site.creator_id != user_id {
+    if read_site.creator_id != user.id {
       return Err(APIError::err("not_an_admin").into());
     }
 
@@ -667,7 +628,7 @@ impl Perform for Oper<TransferSite> {
 
     // Mod tables
     let form = ModAddForm {
-      mod_user_id: user_id,
+      mod_user_id: user.id,
       other_user_id: data.user_id,
       removed: Some(false),
     };
@@ -707,16 +668,10 @@ impl Perform for Oper<GetSiteConfig> {
     _websocket_info: Option<WebsocketInfo>,
   ) -> Result<GetSiteConfigResponse, LemmyError> {
     let data: &GetSiteConfig = &self.data;
-
-    let claims = match Claims::decode(&data.auth) {
-      Ok(claims) => claims.claims,
-      Err(_e) => return Err(APIError::err("not_logged_in").into()),
-    };
-
-    let user_id = claims.id;
+    let user = get_user_from_jwt(&data.auth, pool).await?;
 
     // Only let admins read this
-    is_admin(pool, user_id).await?;
+    is_admin(pool, user.id).await?;
 
     let config_hjson = Settings::read_config_file()?;
 
@@ -734,19 +689,13 @@ impl Perform for Oper<SaveSiteConfig> {
     _websocket_info: Option<WebsocketInfo>,
   ) -> Result<GetSiteConfigResponse, LemmyError> {
     let data: &SaveSiteConfig = &self.data;
-
-    let claims = match Claims::decode(&data.auth) {
-      Ok(claims) => claims.claims,
-      Err(_e) => return Err(APIError::err("not_logged_in").into()),
-    };
-
-    let user_id = claims.id;
+    let user = get_user_from_jwt(&data.auth, pool).await?;
 
     // Only let admins read this
     let admins = blocking(pool, move |conn| UserView::admins(conn)).await??;
     let admin_ids: Vec<i32> = admins.into_iter().map(|m| m.id).collect();
 
-    if !admin_ids.contains(&user_id) {
+    if !admin_ids.contains(&user.id) {
       return Err(APIError::err("not_an_admin").into());
     }
 
index f9a92cd39e86c03ad75daf8f0dbe056fe11ff2e3..d6746c1a879ebee15ea3ad8b4461fc79d0b005a6 100644 (file)
@@ -1,5 +1,14 @@
 use crate::{
-  api::{claims::Claims, is_admin, APIError, Oper, Perform},
+  api::{
+    check_slurs,
+    claims::Claims,
+    get_user_from_jwt,
+    get_user_from_jwt_opt,
+    is_admin,
+    APIError,
+    Oper,
+    Perform,
+  },
   apub::ApubObjectType,
   blocking,
   captcha_espeak_wav_base64,
@@ -47,8 +56,6 @@ use lemmy_utils::{
   remove_slurs,
   send_email,
   settings::Settings,
-  slur_check,
-  slurs_vec_to_str,
   EndpointType,
 };
 use log::error;
@@ -366,9 +373,7 @@ impl Perform for Oper<Register> {
       };
     }
 
-    if let Err(slurs) = slur_check(&data.username) {
-      return Err(APIError::err(&slurs_vec_to_str(slurs)).into());
-    }
+    check_slurs(&data.username)?;
 
     // Make sure there are no admins
     let any_admins = blocking(pool, move |conn| {
@@ -543,14 +548,9 @@ impl Perform for Oper<SaveUserSettings> {
     _websocket_info: Option<WebsocketInfo>,
   ) -> Result<LoginResponse, LemmyError> {
     let data: &SaveUserSettings = &self.data;
+    let user = get_user_from_jwt(&data.auth, pool).await?;
 
-    let claims = match Claims::decode(&data.auth) {
-      Ok(claims) => claims.claims,
-      Err(_e) => return Err(APIError::err("not_logged_in").into()),
-    };
-
-    let user_id = claims.id;
-
+    let user_id = user.id;
     let read_user = blocking(pool, move |conn| User_::read(conn, user_id)).await??;
 
     let email = match &data.email {
@@ -665,24 +665,7 @@ impl Perform for Oper<GetUserDetails> {
     _websocket_info: Option<WebsocketInfo>,
   ) -> Result<GetUserDetailsResponse, LemmyError> {
     let data: &GetUserDetails = &self.data;
-
-    // For logged in users, you need to get back subscribed, and settings
-    let user: Option<User_> = match &data.auth {
-      Some(auth) => match Claims::decode(&auth) {
-        Ok(claims) => {
-          let user_id = claims.claims.id;
-          let user = blocking(pool, move |conn| User_::read(conn, user_id)).await??;
-          Some(user)
-        }
-        Err(_e) => None,
-      },
-      None => None,
-    };
-
-    let user_id = match &user {
-      Some(user) => Some(user.id),
-      None => None,
-    };
+    let user = get_user_from_jwt_opt(&data.auth, pool).await?;
 
     let show_nsfw = match &user {
       Some(user) => user.show_nsfw,
@@ -712,6 +695,7 @@ impl Perform for Oper<GetUserDetails> {
     let limit = data.limit;
     let saved_only = data.saved_only;
     let community_id = data.community_id;
+    let user_id = user.map(|u| u.id);
     let (posts, comments) = blocking(pool, move |conn| {
       let mut posts_query = PostQueryBuilder::create(conn)
         .sort(&sort)
@@ -780,16 +764,10 @@ impl Perform for Oper<AddAdmin> {
     websocket_info: Option<WebsocketInfo>,
   ) -> Result<AddAdminResponse, LemmyError> {
     let data: &AddAdmin = &self.data;
-
-    let claims = match Claims::decode(&data.auth) {
-      Ok(claims) => claims.claims,
-      Err(_e) => return Err(APIError::err("not_logged_in").into()),
-    };
-
-    let user_id = claims.id;
+    let user = get_user_from_jwt(&data.auth, pool).await?;
 
     // Make sure user is an admin
-    is_admin(pool, user_id).await?;
+    is_admin(pool, user.id).await?;
 
     let added = data.added;
     let added_user_id = data.user_id;
@@ -800,7 +778,7 @@ impl Perform for Oper<AddAdmin> {
 
     // Mod tables
     let form = ModAddForm {
-      mod_user_id: user_id,
+      mod_user_id: user.id,
       other_user_id: data.user_id,
       removed: Some(!data.added),
     };
@@ -839,16 +817,10 @@ impl Perform for Oper<BanUser> {
     websocket_info: Option<WebsocketInfo>,
   ) -> Result<BanUserResponse, LemmyError> {
     let data: &BanUser = &self.data;
-
-    let claims = match Claims::decode(&data.auth) {
-      Ok(claims) => claims.claims,
-      Err(_e) => return Err(APIError::err("not_logged_in").into()),
-    };
-
-    let user_id = claims.id;
+    let user = get_user_from_jwt(&data.auth, pool).await?;
 
     // Make sure user is an admin
-    is_admin(pool, user_id).await?;
+    is_admin(pool, user.id).await?;
 
     let ban = data.ban;
     let banned_user_id = data.user_id;
@@ -864,7 +836,7 @@ impl Perform for Oper<BanUser> {
     };
 
     let form = ModBanForm {
-      mod_user_id: user_id,
+      mod_user_id: user.id,
       other_user_id: data.user_id,
       reason: data.reason.to_owned(),
       banned: Some(data.ban),
@@ -903,19 +875,14 @@ impl Perform for Oper<GetReplies> {
     _websocket_info: Option<WebsocketInfo>,
   ) -> Result<GetRepliesResponse, LemmyError> {
     let data: &GetReplies = &self.data;
-
-    let claims = match Claims::decode(&data.auth) {
-      Ok(claims) => claims.claims,
-      Err(_e) => return Err(APIError::err("not_logged_in").into()),
-    };
-
-    let user_id = claims.id;
+    let user = get_user_from_jwt(&data.auth, pool).await?;
 
     let sort = SortType::from_str(&data.sort)?;
 
     let page = data.page;
     let limit = data.limit;
     let unread_only = data.unread_only;
+    let user_id = user.id;
     let replies = blocking(pool, move |conn| {
       ReplyQueryBuilder::create(conn, user_id)
         .sort(&sort)
@@ -940,19 +907,14 @@ impl Perform for Oper<GetUserMentions> {
     _websocket_info: Option<WebsocketInfo>,
   ) -> Result<GetUserMentionsResponse, LemmyError> {
     let data: &GetUserMentions = &self.data;
-
-    let claims = match Claims::decode(&data.auth) {
-      Ok(claims) => claims.claims,
-      Err(_e) => return Err(APIError::err("not_logged_in").into()),
-    };
-
-    let user_id = claims.id;
+    let user = get_user_from_jwt(&data.auth, pool).await?;
 
     let sort = SortType::from_str(&data.sort)?;
 
     let page = data.page;
     let limit = data.limit;
     let unread_only = data.unread_only;
+    let user_id = user.id;
     let mentions = blocking(pool, move |conn| {
       UserMentionQueryBuilder::create(conn, user_id)
         .sort(&sort)
@@ -977,19 +939,13 @@ impl Perform for Oper<MarkUserMentionAsRead> {
     _websocket_info: Option<WebsocketInfo>,
   ) -> Result<UserMentionResponse, LemmyError> {
     let data: &MarkUserMentionAsRead = &self.data;
-
-    let claims = match Claims::decode(&data.auth) {
-      Ok(claims) => claims.claims,
-      Err(_e) => return Err(APIError::err("not_logged_in").into()),
-    };
-
-    let user_id = claims.id;
+    let user = get_user_from_jwt(&data.auth, pool).await?;
 
     let user_mention_id = data.user_mention_id;
     let read_user_mention =
       blocking(pool, move |conn| UserMention::read(conn, user_mention_id)).await??;
 
-    if user_id != read_user_mention.recipient_id {
+    if user.id != read_user_mention.recipient_id {
       return Err(APIError::err("couldnt_update_comment").into());
     }
 
@@ -1001,6 +957,7 @@ impl Perform for Oper<MarkUserMentionAsRead> {
     };
 
     let user_mention_id = read_user_mention.id;
+    let user_id = user.id;
     let user_mention_view = blocking(pool, move |conn| {
       UserMentionView::read(conn, user_mention_id, user_id)
     })
@@ -1022,14 +979,9 @@ impl Perform for Oper<MarkAllAsRead> {
     _websocket_info: Option<WebsocketInfo>,
   ) -> Result<GetRepliesResponse, LemmyError> {
     let data: &MarkAllAsRead = &self.data;
+    let user = get_user_from_jwt(&data.auth, pool).await?;
 
-    let claims = match Claims::decode(&data.auth) {
-      Ok(claims) => claims.claims,
-      Err(_e) => return Err(APIError::err("not_logged_in").into()),
-    };
-
-    let user_id = claims.id;
-
+    let user_id = user.id;
     let replies = blocking(pool, move |conn| {
       ReplyQueryBuilder::create(conn, user_id)
         .unread_only(true)
@@ -1076,15 +1028,7 @@ impl Perform for Oper<DeleteAccount> {
     _websocket_info: Option<WebsocketInfo>,
   ) -> Result<LoginResponse, LemmyError> {
     let data: &DeleteAccount = &self.data;
-
-    let claims = match Claims::decode(&data.auth) {
-      Ok(claims) => claims.claims,
-      Err(_e) => return Err(APIError::err("not_logged_in").into()),
-    };
-
-    let user_id = claims.id;
-
-    let user = blocking(pool, move |conn| User_::read(conn, user_id)).await??;
+    let user = get_user_from_jwt(&data.auth, pool).await?;
 
     // Verify the password
     let valid: bool = verify(&data.password, &user.password_encrypted).unwrap_or(false);
@@ -1093,6 +1037,7 @@ impl Perform for Oper<DeleteAccount> {
     }
 
     // Comments
+    let user_id = user.id;
     let comments = blocking(pool, move |conn| {
       CommentQueryBuilder::create(conn)
         .for_creator_id(user_id)
@@ -1230,27 +1175,15 @@ impl Perform for Oper<CreatePrivateMessage> {
     websocket_info: Option<WebsocketInfo>,
   ) -> Result<PrivateMessageResponse, LemmyError> {
     let data: &CreatePrivateMessage = &self.data;
-
-    let claims = match Claims::decode(&data.auth) {
-      Ok(claims) => claims.claims,
-      Err(_e) => return Err(APIError::err("not_logged_in").into()),
-    };
-
-    let user_id = claims.id;
+    let user = get_user_from_jwt(&data.auth, pool).await?;
 
     let hostname = &format!("https://{}", Settings::get().hostname);
 
-    // Check for a site ban
-    let user = blocking(pool, move |conn| User_::read(conn, user_id)).await??;
-    if user.banned {
-      return Err(APIError::err("site_ban").into());
-    }
-
     let content_slurs_removed = remove_slurs(&data.content.to_owned());
 
     let private_message_form = PrivateMessageForm {
       content: content_slurs_removed.to_owned(),
-      creator_id: user_id,
+      creator_id: user.id,
       recipient_id: data.recipient_id,
       deleted: None,
       read: None,
@@ -1341,25 +1274,13 @@ impl Perform for Oper<EditPrivateMessage> {
     websocket_info: Option<WebsocketInfo>,
   ) -> Result<PrivateMessageResponse, LemmyError> {
     let data: &EditPrivateMessage = &self.data;
-
-    let claims = match Claims::decode(&data.auth) {
-      Ok(claims) => claims.claims,
-      Err(_e) => return Err(APIError::err("not_logged_in").into()),
-    };
-
-    let user_id = claims.id;
-
-    // Check for a site ban
-    let user = blocking(pool, move |conn| User_::read(conn, user_id)).await??;
-    if user.banned {
-      return Err(APIError::err("site_ban").into());
-    }
+    let user = get_user_from_jwt(&data.auth, pool).await?;
 
     // Checking permissions
     let edit_id = data.edit_id;
     let orig_private_message =
       blocking(pool, move |conn| PrivateMessage::read(conn, edit_id)).await??;
-    if user_id != orig_private_message.creator_id {
+    if user.id != orig_private_message.creator_id {
       return Err(APIError::err("no_private_message_edit_allowed").into());
     }
 
@@ -1409,25 +1330,13 @@ impl Perform for Oper<DeletePrivateMessage> {
     websocket_info: Option<WebsocketInfo>,
   ) -> Result<PrivateMessageResponse, LemmyError> {
     let data: &DeletePrivateMessage = &self.data;
-
-    let claims = match Claims::decode(&data.auth) {
-      Ok(claims) => claims.claims,
-      Err(_e) => return Err(APIError::err("not_logged_in").into()),
-    };
-
-    let user_id = claims.id;
-
-    // Check for a site ban
-    let user = blocking(pool, move |conn| User_::read(conn, user_id)).await??;
-    if user.banned {
-      return Err(APIError::err("site_ban").into());
-    }
+    let user = get_user_from_jwt(&data.auth, pool).await?;
 
     // Checking permissions
     let edit_id = data.edit_id;
     let orig_private_message =
       blocking(pool, move |conn| PrivateMessage::read(conn, edit_id)).await??;
-    if user_id != orig_private_message.creator_id {
+    if user.id != orig_private_message.creator_id {
       return Err(APIError::err("no_private_message_edit_allowed").into());
     }
 
@@ -1483,25 +1392,13 @@ impl Perform for Oper<MarkPrivateMessageAsRead> {
     websocket_info: Option<WebsocketInfo>,
   ) -> Result<PrivateMessageResponse, LemmyError> {
     let data: &MarkPrivateMessageAsRead = &self.data;
-
-    let claims = match Claims::decode(&data.auth) {
-      Ok(claims) => claims.claims,
-      Err(_e) => return Err(APIError::err("not_logged_in").into()),
-    };
-
-    let user_id = claims.id;
-
-    // Check for a site ban
-    let user = blocking(pool, move |conn| User_::read(conn, user_id)).await??;
-    if user.banned {
-      return Err(APIError::err("site_ban").into());
-    }
+    let user = get_user_from_jwt(&data.auth, pool).await?;
 
     // Checking permissions
     let edit_id = data.edit_id;
     let orig_private_message =
       blocking(pool, move |conn| PrivateMessage::read(conn, edit_id)).await??;
-    if user_id != orig_private_message.recipient_id {
+    if user.id != orig_private_message.recipient_id {
       return Err(APIError::err("couldnt_update_private_message").into());
     }
 
@@ -1548,13 +1445,8 @@ impl Perform for Oper<GetPrivateMessages> {
     _websocket_info: Option<WebsocketInfo>,
   ) -> Result<PrivateMessagesResponse, LemmyError> {
     let data: &GetPrivateMessages = &self.data;
-
-    let claims = match Claims::decode(&data.auth) {
-      Ok(claims) => claims.claims,
-      Err(_e) => return Err(APIError::err("not_logged_in").into()),
-    };
-
-    let user_id = claims.id;
+    let user = get_user_from_jwt(&data.auth, pool).await?;
+    let user_id = user.id;
 
     let page = data.page;
     let limit = data.limit;
@@ -1578,24 +1470,21 @@ impl Perform for Oper<UserJoin> {
 
   async fn perform(
     &self,
-    _pool: &DbPool,
+    pool: &DbPool,
     websocket_info: Option<WebsocketInfo>,
   ) -> Result<UserJoinResponse, LemmyError> {
     let data: &UserJoin = &self.data;
-
-    let claims = match Claims::decode(&data.auth) {
-      Ok(claims) => claims.claims,
-      Err(_e) => return Err(APIError::err("not_logged_in").into()),
-    };
-
-    let user_id = claims.id;
+    let user = get_user_from_jwt(&data.auth, pool).await?;
 
     if let Some(ws) = websocket_info {
       if let Some(id) = ws.id {
-        ws.chatserver.do_send(JoinUserRoom { user_id, id });
+        ws.chatserver.do_send(JoinUserRoom {
+          user_id: user.id,
+          id,
+        });
       }
     }
 
-    Ok(UserJoinResponse { user_id })
+    Ok(UserJoinResponse { user_id: user.id })
   }
 }
index 9fdfe37fb57c7925fdc15ac0eeff33439e4c8a1a..b5d6ce46476e9d4a1d2ee63a42ac138385139db8 100644 (file)
@@ -1,9 +1,9 @@
 use crate::{
   apub::{
+    check_is_apub_id_valid,
     community::do_announce,
     extensions::signatures::sign,
     insert_activity,
-    is_apub_id_valid,
     ActorType,
   },
   request::retry_custom,
@@ -50,10 +50,7 @@ pub async fn send_activity(
 
   for t in to {
     let to_url = Url::parse(&t)?;
-    if !is_apub_id_valid(&to_url) {
-      debug!("Not sending activity to {} (invalid or blocklisted)", t);
-      continue;
-    }
+    check_is_apub_id_valid(&to_url)?;
 
     let res = retry_custom(|| async {
       let request = client.post(&t).header("Content-Type", "application/json");
index 4425757de24289f416402e3ef840cfc7cb9eac71..919b0e884aa9a2eb82b0824473ffd49c29e8b613 100644 (file)
@@ -1,7 +1,7 @@
 use crate::{
   api::site::SearchResponse,
   apub::{
-    is_apub_id_valid,
+    check_is_apub_id_valid,
     ActorType,
     FromApub,
     GroupExt,
@@ -66,9 +66,7 @@ pub async fn fetch_remote_object<Response>(
 where
   Response: for<'de> Deserialize<'de>,
 {
-  if !is_apub_id_valid(&url) {
-    return Err(anyhow!("Activitypub uri invalid or blocked: {}", url).into());
-  }
+  check_is_apub_id_valid(&url)?;
 
   let timeout = Duration::from_secs(60);
 
index f8a92c1c688ad3ae7c9279f021ed44c1eeb10c55..ceeef0ef76bfe2acba82ffc7ba430208cd5c6ae7 100644 (file)
@@ -39,7 +39,6 @@ pub async fn receive_create(
   chat_server: ChatServerParam,
 ) -> Result<HttpResponse, LemmyError> {
   let create = Create::from_any_base(activity)?.unwrap();
-  dbg!(create.object().as_single_kind_str());
   match create.object().as_single_kind_str() {
     Some("Page") => receive_create_post(create, client, pool, chat_server).await,
     Some("Note") => receive_create_comment(create, client, pool, chat_server).await,
index 69dd2cdf7ced2a36bb5b4cee0904d918c0243cf8..37e7c2895b8efc1970aac4db94bf643981eb6387 100644 (file)
@@ -1,7 +1,8 @@
 use crate::{
   apub::{
+    check_is_apub_id_valid,
     extensions::signatures::verify,
-    fetcher::{get_or_fetch_and_upsert_community, get_or_fetch_and_upsert_user},
+    fetcher::get_or_fetch_and_upsert_user,
     insert_activity,
     ActorType,
   },
@@ -10,7 +11,8 @@ use crate::{
   LemmyError,
 };
 use activitystreams::{
-  activity::{Follow, Undo},
+  activity::{ActorAndObject, Follow, Undo},
+  base::AnyBase,
   prelude::*,
 };
 use actix_web::{client::Client, web, HttpRequest, HttpResponse};
@@ -21,37 +23,28 @@ use lemmy_db::{
   Followable,
 };
 use log::debug;
-use serde::Deserialize;
+use serde::{Deserialize, Serialize};
 use std::fmt::Debug;
 
-#[serde(untagged)]
-#[derive(Deserialize, Debug)]
-pub enum CommunityAcceptedObjects {
-  Follow(Follow),
-  Undo(Undo),
+#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, Deserialize, Serialize)]
+#[serde(rename_all = "PascalCase")]
+pub enum ValidTypes {
+  Follow,
+  Undo,
 }
 
-impl CommunityAcceptedObjects {
-  fn follow(&self) -> Result<Follow, LemmyError> {
-    match self {
-      CommunityAcceptedObjects::Follow(f) => Ok(f.to_owned()),
-      CommunityAcceptedObjects::Undo(u) => {
-        Ok(Follow::from_any_base(u.object().as_one().unwrap().to_owned())?.unwrap())
-      }
-    }
-  }
-}
+pub type AcceptedActivities = ActorAndObject<ValidTypes>;
 
 /// Handler for all incoming activities to community inboxes.
 pub async fn community_inbox(
   request: HttpRequest,
-  input: web::Json<CommunityAcceptedObjects>,
+  input: web::Json<AcceptedActivities>,
   path: web::Path<String>,
   db: DbPoolParam,
   client: web::Data<Client>,
   _chat_server: ChatServerParam,
 ) -> Result<HttpResponse, LemmyError> {
-  let input = input.into_inner();
+  let activity = input.into_inner();
 
   let path = path.into_inner();
   let community = blocking(&db, move |conn| Community::read_from_name(&conn, &path)).await??;
@@ -67,34 +60,35 @@ pub async fn community_inbox(
   }
   debug!(
     "Community {} received activity {:?}",
-    &community.name, &input
+    &community.name, &activity
   );
-  let follow = input.follow()?;
-  let user_uri = follow.actor()?.as_single_xsd_any_uri().unwrap();
-  let community_uri = follow.object().as_single_xsd_any_uri().unwrap();
+  let user_uri = activity.actor()?.as_single_xsd_any_uri().unwrap();
+  check_is_apub_id_valid(user_uri)?;
 
   let user = get_or_fetch_and_upsert_user(&user_uri, &client, &db).await?;
-  let community = get_or_fetch_and_upsert_community(community_uri, &client, &db).await?;
 
   verify(&request, &user)?;
 
-  match input {
-    CommunityAcceptedObjects::Follow(f) => handle_follow(f, user, community, &client, db).await,
-    CommunityAcceptedObjects::Undo(u) => handle_undo_follow(u, user, community, db).await,
+  insert_activity(user.id, activity.clone(), false, &db).await?;
+
+  let any_base = activity.clone().into_any_base()?;
+  let kind = activity.kind().unwrap();
+  match kind {
+    ValidTypes::Follow => handle_follow(any_base, user, community, &client, db).await,
+    ValidTypes::Undo => handle_undo_follow(any_base, user, community, db).await,
   }
 }
 
 /// Handle a follow request from a remote user, adding it to the local database and returning an
 /// Accept activity.
 async fn handle_follow(
-  follow: Follow,
+  activity: AnyBase,
   user: User_,
   community: Community,
   client: &Client,
   db: DbPoolParam,
 ) -> Result<HttpResponse, LemmyError> {
-  insert_activity(user.id, follow.clone(), false, &db).await?;
-
+  let follow = Follow::from_any_base(activity)?.unwrap();
   let community_follower_form = CommunityFollowerForm {
     community_id: community.id,
     user_id: user.id,
@@ -112,12 +106,12 @@ async fn handle_follow(
 }
 
 async fn handle_undo_follow(
-  undo: Undo,
+  activity: AnyBase,
   user: User_,
   community: Community,
   db: DbPoolParam,
 ) -> Result<HttpResponse, LemmyError> {
-  insert_activity(user.id, undo, false, &db).await?;
+  let _undo = Undo::from_any_base(activity)?.unwrap();
 
   let community_follower_form = CommunityFollowerForm {
     community_id: community.id,
index 9e0cdb3d84a37c677fb1d1c7e0d688bc1fbd00d3..db44a99da42f7314af160a45e089ef7cb74a921c 100644 (file)
@@ -1,5 +1,6 @@
 use crate::{
   apub::{
+    check_is_apub_id_valid,
     community::do_announce,
     extensions::signatures::verify,
     fetcher::{
@@ -32,11 +33,11 @@ use activitystreams::{
 use actix_web::{client::Client, web, HttpRequest, HttpResponse};
 use lemmy_db::user::User_;
 use log::debug;
-use serde::Serialize;
+use serde::{Deserialize, Serialize};
 use std::fmt::Debug;
 use url::Url;
 
-#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, serde::Deserialize, serde::Serialize)]
+#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, Deserialize, Serialize)]
 #[serde(rename_all = "PascalCase")]
 pub enum ValidTypes {
   Create,
@@ -67,16 +68,19 @@ pub async fn shared_inbox(
   debug!("Shared inbox received activity: {}", json);
 
   let sender = &activity.actor()?.to_owned().single_xsd_any_uri().unwrap();
-
   // TODO: pass this actor in instead of using get_user_from_activity()
   let actor = get_or_fetch_and_upsert_actor(sender, &client, &pool).await?;
+
+  let community = get_community_id_from_activity(&activity).await;
+
+  check_is_apub_id_valid(sender)?;
+  check_is_apub_id_valid(&community)?;
   verify(&request, actor.as_ref())?;
 
   insert_activity(actor.user_id(), activity.clone(), false, &pool).await?;
 
   let any_base = activity.clone().into_any_base()?;
   let kind = activity.kind().unwrap();
-  dbg!(kind);
   match kind {
     ValidTypes::Announce => receive_announce(any_base, &client, &pool, chat_server).await,
     ValidTypes::Create => receive_create(any_base, &client, &pool, chat_server).await,
@@ -112,6 +116,15 @@ where
   get_or_fetch_and_upsert_user(&user_uri, client, pool).await
 }
 
+pub(in crate::apub::inbox) async fn get_community_id_from_activity<T, A>(activity: &T) -> Url
+where
+  T: AsBase<A> + ActorAndObjectRef + AsObject<A>,
+{
+  let cc = activity.cc().unwrap();
+  let cc = cc.as_many().unwrap();
+  cc.first().unwrap().as_xsd_any_uri().unwrap().to_owned()
+}
+
 pub(in crate::apub::inbox) async fn announce_if_community_is_local<T, Kind>(
   activity: T,
   user: &User_,
index b46f670276ea0ee4f1c1df3c613aa618a464a8c4..494fd9f5bb0ba1ada07260809e1c8fd24b077a29 100644 (file)
@@ -1,8 +1,9 @@
 use crate::{
   api::user::PrivateMessageResponse,
   apub::{
+    check_is_apub_id_valid,
     extensions::signatures::verify,
-    fetcher::{get_or_fetch_and_upsert_community, get_or_fetch_and_upsert_user},
+    fetcher::{get_or_fetch_and_upsert_actor, get_or_fetch_and_upsert_community},
     insert_activity,
     FromApub,
   },
@@ -13,7 +14,8 @@ use crate::{
   LemmyError,
 };
 use activitystreams::{
-  activity::{Accept, Create, Delete, Undo, Update},
+  activity::{Accept, ActorAndObject, Create, Delete, Undo, Update},
+  base::AnyBase,
   object::Note,
   prelude::*,
 };
@@ -28,68 +30,76 @@ use lemmy_db::{
   Followable,
 };
 use log::debug;
-use serde::Deserialize;
+use serde::{Deserialize, Serialize};
 use std::fmt::Debug;
 
-#[serde(untagged)]
-#[derive(Deserialize, Debug)]
-pub enum UserAcceptedObjects {
-  Accept(Box<Accept>),
-  Create(Box<Create>),
-  Update(Box<Update>),
-  Delete(Box<Delete>),
-  Undo(Box<Undo>),
+#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, Deserialize, Serialize)]
+#[serde(rename_all = "PascalCase")]
+pub enum ValidTypes {
+  Accept,
+  Create,
+  Update,
+  Delete,
+  Undo,
 }
 
+pub type AcceptedActivities = ActorAndObject<ValidTypes>;
+
 /// Handler for all incoming activities to user inboxes.
 pub async fn user_inbox(
   request: HttpRequest,
-  input: web::Json<UserAcceptedObjects>,
+  input: web::Json<AcceptedActivities>,
   path: web::Path<String>,
   client: web::Data<Client>,
-  db: DbPoolParam,
+  pool: DbPoolParam,
   chat_server: ChatServerParam,
 ) -> Result<HttpResponse, LemmyError> {
-  // TODO: would be nice if we could do the signature check here, but we cant access the actor property
-  let input = input.into_inner();
+  let activity = input.into_inner();
   let username = path.into_inner();
-  debug!("User {} received activity: {:?}", &username, &input);
+  debug!("User {} received activity: {:?}", &username, &activity);
+
+  let actor_uri = activity.actor()?.as_single_xsd_any_uri().unwrap();
+
+  check_is_apub_id_valid(actor_uri)?;
+
+  let actor = get_or_fetch_and_upsert_actor(actor_uri, &client, &pool).await?;
+  verify(&request, actor.as_ref())?;
 
-  match input {
-    UserAcceptedObjects::Accept(a) => receive_accept(*a, &request, &username, &client, &db).await,
-    UserAcceptedObjects::Create(c) => {
-      receive_create_private_message(*c, &request, &client, &db, chat_server).await
+  insert_activity(actor.user_id(), activity.clone(), false, &pool).await?;
+
+  let any_base = activity.clone().into_any_base()?;
+  let kind = activity.kind().unwrap();
+  match kind {
+    ValidTypes::Accept => receive_accept(any_base, username, &client, &pool).await,
+    ValidTypes::Create => {
+      receive_create_private_message(any_base, &client, &pool, chat_server).await
     }
-    UserAcceptedObjects::Update(u) => {
-      receive_update_private_message(*u, &request, &client, &db, chat_server).await
+    ValidTypes::Update => {
+      receive_update_private_message(any_base, &client, &pool, chat_server).await
     }
-    UserAcceptedObjects::Delete(d) => {
-      receive_delete_private_message(*d, &request, &client, &db, chat_server).await
+    ValidTypes::Delete => {
+      receive_delete_private_message(any_base, &client, &pool, chat_server).await
     }
-    UserAcceptedObjects::Undo(u) => {
-      receive_undo_delete_private_message(*u, &request, &client, &db, chat_server).await
+    ValidTypes::Undo => {
+      receive_undo_delete_private_message(any_base, &client, &pool, chat_server).await
     }
   }
 }
 
 /// Handle accepted follows.
 async fn receive_accept(
-  accept: Accept,
-  request: &HttpRequest,
-  username: &str,
+  activity: AnyBase,
+  username: String,
   client: &Client,
   pool: &DbPool,
 ) -> Result<HttpResponse, LemmyError> {
+  let accept = Accept::from_any_base(activity)?.unwrap();
   let community_uri = accept.actor()?.to_owned().single_xsd_any_uri().unwrap();
 
   let community = get_or_fetch_and_upsert_community(&community_uri, client, pool).await?;
-  verify(request, &community)?;
 
-  let username = username.to_owned();
   let user = blocking(pool, move |conn| User_::read_from_name(conn, &username)).await??;
 
-  insert_activity(community.creator_id, accept, false, pool).await?;
-
   // Now you need to add this to the community follower
   let community_follower_form = CommunityFollowerForm {
     community_id: community.id,
@@ -98,29 +108,23 @@ async fn receive_accept(
 
   // This will fail if they're already a follower
   blocking(pool, move |conn| {
-    CommunityFollower::follow(conn, &community_follower_form)
+    CommunityFollower::follow(conn, &community_follower_form).ok()
   })
-  .await??;
+  .await?;
 
   // TODO: make sure that we actually requested a follow
   Ok(HttpResponse::Ok().finish())
 }
 
 async fn receive_create_private_message(
-  create: Create,
-  request: &HttpRequest,
+  activity: AnyBase,
   client: &Client,
   pool: &DbPool,
   chat_server: ChatServerParam,
 ) -> Result<HttpResponse, LemmyError> {
-  let user_uri = &create.actor()?.to_owned().single_xsd_any_uri().unwrap();
+  let create = Create::from_any_base(activity)?.unwrap();
   let note = Note::from_any_base(create.object().as_one().unwrap().to_owned())?.unwrap();
 
-  let user = get_or_fetch_and_upsert_user(user_uri, client, pool).await?;
-  verify(request, &user)?;
-
-  insert_activity(user.id, create, false, pool).await?;
-
   let private_message = PrivateMessageForm::from_apub(&note, client, pool).await?;
 
   let inserted_private_message = blocking(pool, move |conn| {
@@ -148,20 +152,14 @@ async fn receive_create_private_message(
 }
 
 async fn receive_update_private_message(
-  update: Update,
-  request: &HttpRequest,
+  activity: AnyBase,
   client: &Client,
   pool: &DbPool,
   chat_server: ChatServerParam,
 ) -> Result<HttpResponse, LemmyError> {
-  let user_uri = &update.actor()?.to_owned().single_xsd_any_uri().unwrap();
+  let update = Update::from_any_base(activity)?.unwrap();
   let note = Note::from_any_base(update.object().as_one().unwrap().to_owned())?.unwrap();
 
-  let user = get_or_fetch_and_upsert_user(&user_uri, client, pool).await?;
-  verify(request, &user)?;
-
-  insert_activity(user.id, update, false, pool).await?;
-
   let private_message_form = PrivateMessageForm::from_apub(&note, client, pool).await?;
 
   let private_message_ap_id = private_message_form.ap_id.clone();
@@ -197,20 +195,14 @@ async fn receive_update_private_message(
 }
 
 async fn receive_delete_private_message(
-  delete: Delete,
-  request: &HttpRequest,
+  activity: AnyBase,
   client: &Client,
   pool: &DbPool,
   chat_server: ChatServerParam,
 ) -> Result<HttpResponse, LemmyError> {
-  let user_uri = &delete.actor()?.to_owned().single_xsd_any_uri().unwrap();
+  let delete = Delete::from_any_base(activity)?.unwrap();
   let note = Note::from_any_base(delete.object().as_one().unwrap().to_owned())?.unwrap();
 
-  let user = get_or_fetch_and_upsert_user(&user_uri, client, pool).await?;
-  verify(request, &user)?;
-
-  insert_activity(user.id, delete, false, pool).await?;
-
   let private_message_form = PrivateMessageForm::from_apub(&note, client, pool).await?;
 
   let private_message_ap_id = private_message_form.ap_id;
@@ -258,20 +250,14 @@ async fn receive_delete_private_message(
 }
 
 async fn receive_undo_delete_private_message(
-  undo: Undo,
-  request: &HttpRequest,
+  activity: AnyBase,
   client: &Client,
   pool: &DbPool,
   chat_server: ChatServerParam,
 ) -> Result<HttpResponse, LemmyError> {
+  let undo = Undo::from_any_base(activity)?.unwrap();
   let delete = Delete::from_any_base(undo.object().as_one().unwrap().to_owned())?.unwrap();
   let note = Note::from_any_base(delete.object().as_one().unwrap().to_owned())?.unwrap();
-  let user_uri = &delete.actor()?.to_owned().single_xsd_any_uri().unwrap();
-
-  let user = get_or_fetch_and_upsert_user(&user_uri, client, pool).await?;
-  verify(request, &user)?;
-
-  insert_activity(user.id, delete, false, pool).await?;
 
   let private_message = PrivateMessageForm::from_apub(&note, client, pool).await?;
 
index e86032f616ad6caf1ff03f05a4eba4ba4b9461bd..7f39afc7559ef7534a489de381d00a693f87e5e6 100644 (file)
@@ -63,33 +63,34 @@ where
 }
 
 // Checks if the ID has a valid format, correct scheme, and is in the allowed instance list.
-fn is_apub_id_valid(apub_id: &Url) -> bool {
-  debug!("Checking {}", apub_id);
+fn check_is_apub_id_valid(apub_id: &Url) -> Result<(), LemmyError> {
   if apub_id.scheme() != get_apub_protocol_string() {
-    debug!("invalid scheme: {:?}", apub_id.scheme());
-    return false;
+    return Err(anyhow!("invalid apub id scheme: {:?}", apub_id.scheme()).into());
   }
 
-  let allowed_instances: Vec<String> = Settings::get()
+  let mut allowed_instances: Vec<String> = Settings::get()
     .federation
     .allowed_instances
     .split(',')
     .map(|d| d.to_string())
     .collect();
+  // need to allow this explicitly because apub activities might contain objects from our local
+  // instance. replace is needed to remove the port in our federation test setup.
+  let settings = Settings::get();
+  let local_instance = settings.hostname.split(':').collect::<Vec<&str>>();
+  allowed_instances.push(local_instance.first().unwrap().to_string());
+
   match apub_id.domain() {
     Some(d) => {
       let contains = allowed_instances.contains(&d.to_owned());
 
       if !contains {
-        debug!("{} not in {:?}", d, allowed_instances);
+        return Err(anyhow!("{} not in federation allowlist", d).into());
       }
 
-      contains
-    }
-    None => {
-      debug!("missing domain");
-      false
+      Ok(())
     }
+    None => Err(anyhow!("federation allowlist is empty").into()),
   }
 }
 
index aa803aa4ab34b333436f2f142908fbacc2cb516b..4ba3c78b1c8509ce6cb59df17cef334a65f7eaa1 100644 (file)
@@ -6,7 +6,7 @@
   "license": "AGPL-3.0-or-later",
   "main": "index.js",
   "scripts": {
-    "api-test": "jest src/api_tests/api.spec.ts",
+    "api-test": "jest src/api_tests/ -i --verbose",
     "build": "node fuse prod",
     "lint": "tsc --noEmit && eslint --report-unused-disable-directives --ext .js,.ts,.tsx src",
     "prebuild": "node generate_translations.js",
diff --git a/ui/src/api_tests/api.spec.ts b/ui/src/api_tests/api.spec.ts
deleted file mode 100644 (file)
index 9f498f8..0000000
+++ /dev/null
@@ -1,1570 +0,0 @@
-import fetch from 'node-fetch';
-
-import {
-  LoginForm,
-  LoginResponse,
-  PostForm,
-  DeletePostForm,
-  RemovePostForm,
-  StickyPostForm,
-  LockPostForm,
-  PostResponse,
-  SearchResponse,
-  FollowCommunityForm,
-  CommunityResponse,
-  GetFollowedCommunitiesResponse,
-  GetPostResponse,
-  CommentForm,
-  DeleteCommentForm,
-  RemoveCommentForm,
-  CommentResponse,
-  CommunityForm,
-  DeleteCommunityForm,
-  RemoveCommunityForm,
-  GetCommunityResponse,
-  CommentLikeForm,
-  CreatePostLikeForm,
-  PrivateMessageForm,
-  EditPrivateMessageForm,
-  DeletePrivateMessageForm,
-  PrivateMessageResponse,
-  PrivateMessagesResponse,
-  GetUserMentionsResponse,
-} from '../interfaces';
-
-let lemmyAlphaUrl = 'http://localhost:8540';
-let lemmyAlphaApiUrl = `${lemmyAlphaUrl}/api/v1`;
-let lemmyAlphaAuth: string;
-
-let lemmyBetaUrl = 'http://localhost:8550';
-let lemmyBetaApiUrl = `${lemmyBetaUrl}/api/v1`;
-let lemmyBetaAuth: string;
-
-let lemmyGammaUrl = 'http://localhost:8560';
-let lemmyGammaApiUrl = `${lemmyGammaUrl}/api/v1`;
-let lemmyGammaAuth: string;
-
-// Workaround for tests being run before beforeAll() is finished
-// https://github.com/facebook/jest/issues/9527#issuecomment-592406108
-describe('main', () => {
-  beforeAll(async () => {
-    console.log('Logging in as lemmy_alpha');
-    let form: LoginForm = {
-      username_or_email: 'lemmy_alpha',
-      password: 'lemmy',
-    };
-
-    let res: LoginResponse = await fetch(`${lemmyAlphaApiUrl}/user/login`, {
-      method: 'POST',
-      headers: {
-        'Content-Type': 'application/json',
-      },
-      body: wrapper(form),
-    }).then(d => d.json());
-
-    lemmyAlphaAuth = res.jwt;
-
-    console.log('Logging in as lemmy_beta');
-    let formB = {
-      username_or_email: 'lemmy_beta',
-      password: 'lemmy',
-    };
-
-    let resB: LoginResponse = await fetch(`${lemmyBetaApiUrl}/user/login`, {
-      method: 'POST',
-      headers: {
-        'Content-Type': 'application/json',
-      },
-      body: wrapper(formB),
-    }).then(d => d.json());
-
-    lemmyBetaAuth = resB.jwt;
-
-    console.log('Logging in as lemmy_gamma');
-    let formC = {
-      username_or_email: 'lemmy_gamma',
-      password: 'lemmy',
-    };
-
-    let resG: LoginResponse = await fetch(`${lemmyGammaApiUrl}/user/login`, {
-      method: 'POST',
-      headers: {
-        'Content-Type': 'application/json',
-      },
-      body: wrapper(formC),
-    }).then(d => d.json());
-
-    lemmyGammaAuth = resG.jwt;
-  });
-
-  describe('post_search', () => {
-    test('Create test post on alpha and fetch it on beta', async () => {
-      let name = 'A jest test post';
-      let postForm: PostForm = {
-        name,
-        auth: lemmyAlphaAuth,
-        community_id: 2,
-        nsfw: false,
-      };
-
-      let createPostRes: PostResponse = await fetch(
-        `${lemmyAlphaApiUrl}/post`,
-        {
-          method: 'POST',
-          headers: {
-            'Content-Type': 'application/json',
-          },
-          body: wrapper(postForm),
-        }
-      ).then(d => d.json());
-      expect(createPostRes.post.name).toBe(name);
-
-      let searchUrl = `${lemmyBetaApiUrl}/search?q=${createPostRes.post.ap_id}&type_=All&sort=TopAll`;
-      let searchResponse: SearchResponse = await fetch(searchUrl, {
-        method: 'GET',
-      }).then(d => d.json());
-
-      // TODO: check more fields
-      expect(searchResponse.posts[0].name).toBe(name);
-    });
-  });
-
-  describe('follow_accept', () => {
-    test('/u/lemmy_alpha follows and accepts lemmy-beta/c/main', async () => {
-      // Make sure lemmy-beta/c/main is cached on lemmy_alpha
-      // Use short-hand search url
-      let searchUrl = `${lemmyAlphaApiUrl}/search?q=!main@lemmy-beta:8550&type_=All&sort=TopAll`;
-
-      let searchResponse: SearchResponse = await fetch(searchUrl, {
-        method: 'GET',
-      }).then(d => d.json());
-
-      expect(searchResponse.communities[0].name).toBe('main');
-
-      let followForm: FollowCommunityForm = {
-        community_id: searchResponse.communities[0].id,
-        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('main');
-
-      // Check that you are subscribed to it locally
-      let followedCommunitiesUrl = `${lemmyAlphaApiUrl}/user/followed_communities?&auth=${lemmyAlphaAuth}`;
-      let followedCommunitiesRes: GetFollowedCommunitiesResponse = await fetch(
-        followedCommunitiesUrl,
-        {
-          method: 'GET',
-        }
-      ).then(d => d.json());
-
-      expect(followedCommunitiesRes.communities[1].community_local).toBe(false);
-
-      // Test out unfollowing
-      let unfollowForm: FollowCommunityForm = {
-        community_id: searchResponse.communities[0].id,
-        follow: false,
-        auth: lemmyAlphaAuth,
-      };
-
-      let unfollowRes: CommunityResponse = await fetch(
-        `${lemmyAlphaApiUrl}/community/follow`,
-        {
-          method: 'POST',
-          headers: {
-            'Content-Type': 'application/json',
-          },
-          body: wrapper(unfollowForm),
-        }
-      ).then(d => d.json());
-      expect(unfollowRes.community.local).toBe(false);
-
-      // Check that you are unsubscribed to it locally
-      let followedCommunitiesResAgain: GetFollowedCommunitiesResponse = await fetch(
-        followedCommunitiesUrl,
-        {
-          method: 'GET',
-        }
-      ).then(d => d.json());
-
-      expect(followedCommunitiesResAgain.communities.length).toBe(1);
-
-      // Follow again, for other tests
-      let followResAgain: 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(followResAgain.community.local).toBe(false);
-      expect(followResAgain.community.name).toBe('main');
-
-      // Also make G follow B
-
-      // Use short-hand search url
-      let searchUrlG = `${lemmyGammaApiUrl}/search?q=!main@lemmy-beta:8550&type_=All&sort=TopAll`;
-
-      let searchResponseG: SearchResponse = await fetch(searchUrlG, {
-        method: 'GET',
-      }).then(d => d.json());
-
-      expect(searchResponseG.communities[0].name).toBe('main');
-
-      let followFormG: FollowCommunityForm = {
-        community_id: searchResponseG.communities[0].id,
-        follow: true,
-        auth: lemmyGammaAuth,
-      };
-
-      let followResG: CommunityResponse = await fetch(
-        `${lemmyGammaApiUrl}/community/follow`,
-        {
-          method: 'POST',
-          headers: {
-            'Content-Type': 'application/json',
-          },
-          body: wrapper(followFormG),
-        }
-      ).then(d => d.json());
-
-      // Make sure the follow response went through
-      expect(followResG.community.local).toBe(false);
-      expect(followResG.community.name).toBe('main');
-
-      // Check that you are subscribed to it locally
-      let followedCommunitiesUrlG = `${lemmyGammaApiUrl}/user/followed_communities?&auth=${lemmyGammaAuth}`;
-      let followedCommunitiesResG: GetFollowedCommunitiesResponse = await fetch(
-        followedCommunitiesUrlG,
-        {
-          method: 'GET',
-        }
-      ).then(d => d.json());
-
-      expect(followedCommunitiesResG.communities[1].community_local).toBe(
-        false
-      );
-    });
-  });
-
-  describe('create test post', () => {
-    test('/u/lemmy_alpha creates a post on /c/lemmy_beta/main, its on both instances', async () => {
-      let name = 'A jest test federated post';
-      let postForm: PostForm = {
-        name,
-        auth: lemmyAlphaAuth,
-        community_id: 3,
-        nsfw: false,
-      };
-
-      let createResponse: PostResponse = await fetch(
-        `${lemmyAlphaApiUrl}/post`,
-        {
-          method: 'POST',
-          headers: {
-            'Content-Type': 'application/json',
-          },
-          body: wrapper(postForm),
-        }
-      ).then(d => d.json());
-
-      let unlikePostForm: CreatePostLikeForm = {
-        post_id: createResponse.post.id,
-        score: 0,
-        auth: lemmyAlphaAuth,
-      };
-      expect(createResponse.post.name).toBe(name);
-      expect(createResponse.post.community_local).toBe(false);
-      expect(createResponse.post.creator_local).toBe(true);
-      expect(createResponse.post.score).toBe(1);
-
-      let unlikePostRes: PostResponse = await fetch(
-        `${lemmyAlphaApiUrl}/post/like`,
-        {
-          method: 'POST',
-          headers: {
-            'Content-Type': 'application/json',
-          },
-          body: wrapper(unlikePostForm),
-        }
-      ).then(d => d.json());
-      expect(unlikePostRes.post.score).toBe(0);
-
-      let getPostUrl = `${lemmyBetaApiUrl}/post?id=2`;
-      let getPostRes: GetPostResponse = await fetch(getPostUrl, {
-        method: 'GET',
-      }).then(d => d.json());
-
-      expect(getPostRes.post.name).toBe(name);
-      expect(getPostRes.post.community_local).toBe(true);
-      expect(getPostRes.post.creator_local).toBe(false);
-      expect(getPostRes.post.score).toBe(0);
-    });
-  });
-
-  describe('update test post', () => {
-    test('/u/lemmy_alpha updates a post on /c/lemmy_beta/main, the update is on both', async () => {
-      let name = 'A jest test federated post, updated';
-      let postForm: PostForm = {
-        name,
-        edit_id: 2,
-        auth: lemmyAlphaAuth,
-        community_id: 3,
-        nsfw: false,
-      };
-
-      let updateResponse: PostResponse = await fetch(
-        `${lemmyAlphaApiUrl}/post`,
-        {
-          method: 'PUT',
-          headers: {
-            'Content-Type': 'application/json',
-          },
-          body: wrapper(postForm),
-        }
-      ).then(d => d.json());
-
-      expect(updateResponse.post.name).toBe(name);
-      expect(updateResponse.post.community_local).toBe(false);
-      expect(updateResponse.post.creator_local).toBe(true);
-
-      let stickyPostForm: StickyPostForm = {
-        edit_id: 2,
-        stickied: true,
-        auth: lemmyAlphaAuth,
-      };
-
-      let stickyRes: PostResponse = await fetch(
-        `${lemmyAlphaApiUrl}/post/sticky`,
-        {
-          method: 'POST',
-          headers: {
-            'Content-Type': 'application/json',
-          },
-          body: wrapper(stickyPostForm),
-        }
-      ).then(d => d.json());
-
-      expect(stickyRes.post.name).toBe(name);
-      expect(stickyRes.post.stickied).toBe(true);
-
-      // Fetch from B
-      let getPostUrl = `${lemmyBetaApiUrl}/post?id=2`;
-      let getPostRes: GetPostResponse = await fetch(getPostUrl, {
-        method: 'GET',
-      }).then(d => d.json());
-
-      expect(getPostRes.post.name).toBe(name);
-      expect(getPostRes.post.community_local).toBe(true);
-      expect(getPostRes.post.creator_local).toBe(false);
-      expect(getPostRes.post.stickied).toBe(true);
-
-      let lockPostForm: LockPostForm = {
-        edit_id: 2,
-        locked: true,
-        auth: lemmyAlphaAuth,
-      };
-
-      let lockedRes: PostResponse = await fetch(
-        `${lemmyAlphaApiUrl}/post/lock`,
-        {
-          method: 'POST',
-          headers: {
-            'Content-Type': 'application/json',
-          },
-          body: wrapper(lockPostForm),
-        }
-      ).then(d => d.json());
-
-      expect(lockedRes.post.name).toBe(name);
-      expect(lockedRes.post.locked).toBe(true);
-
-      // Fetch from B to make sure its locked
-      getPostRes = await fetch(getPostUrl, {
-        method: 'GET',
-      }).then(d => d.json());
-      expect(getPostRes.post.locked).toBe(true);
-
-      // Create a test comment on a locked post, it should be undefined
-      // since it shouldn't get created.
-      let content = 'A rejected comment on a locked post';
-      let commentForm: CommentForm = {
-        content,
-        post_id: 2,
-        auth: lemmyAlphaAuth,
-      };
-
-      let createResponse: CommentResponse = await fetch(
-        `${lemmyAlphaApiUrl}/comment`,
-        {
-          method: 'POST',
-          headers: {
-            'Content-Type': 'application/json',
-          },
-          body: wrapper(commentForm),
-        }
-      ).then(d => d.json());
-
-      expect(createResponse['error']).toBe('locked');
-
-      // Unlock the post for later actions
-      let unlockPostForm: LockPostForm = {
-        edit_id: 2,
-        locked: false,
-        auth: lemmyAlphaAuth,
-      };
-
-      let unlockedRes: PostResponse = await fetch(
-        `${lemmyAlphaApiUrl}/post/lock`,
-        {
-          method: 'POST',
-          headers: {
-            'Content-Type': 'application/json',
-          },
-          body: wrapper(unlockPostForm),
-        }
-      ).then(d => d.json());
-
-      expect(unlockedRes.post.name).toBe(name);
-      expect(unlockedRes.post.locked).toBe(false);
-    });
-  });
-
-  describe('create test comment', () => {
-    test('/u/lemmy_alpha creates a comment on /c/lemmy_beta/main, its on both instances', async () => {
-      let content = 'A jest test federated comment';
-      let commentForm: CommentForm = {
-        content,
-        post_id: 2,
-        auth: lemmyAlphaAuth,
-      };
-
-      let createResponse: CommentResponse = await fetch(
-        `${lemmyAlphaApiUrl}/comment`,
-        {
-          method: 'POST',
-          headers: {
-            'Content-Type': 'application/json',
-          },
-          body: wrapper(commentForm),
-        }
-      ).then(d => d.json());
-
-      expect(createResponse.comment.content).toBe(content);
-      expect(createResponse.comment.community_local).toBe(false);
-      expect(createResponse.comment.creator_local).toBe(true);
-      expect(createResponse.comment.score).toBe(1);
-
-      // Do an unlike, to test it
-      let unlikeCommentForm: CommentLikeForm = {
-        comment_id: createResponse.comment.id,
-        score: 0,
-        auth: lemmyAlphaAuth,
-      };
-
-      let unlikeCommentRes: CommentResponse = await fetch(
-        `${lemmyAlphaApiUrl}/comment/like`,
-        {
-          method: 'POST',
-          headers: {
-            'Content-Type': 'application/json',
-          },
-          body: wrapper(unlikeCommentForm),
-        }
-      ).then(d => d.json());
-
-      expect(unlikeCommentRes.comment.score).toBe(0);
-
-      let getPostUrl = `${lemmyBetaApiUrl}/post?id=2`;
-      let getPostRes: GetPostResponse = await fetch(getPostUrl, {
-        method: 'GET',
-      }).then(d => d.json());
-
-      expect(getPostRes.comments[0].content).toBe(content);
-      expect(getPostRes.comments[0].community_local).toBe(true);
-      expect(getPostRes.comments[0].creator_local).toBe(false);
-      expect(getPostRes.comments[0].score).toBe(0);
-
-      // Now do beta replying to that comment, as a child comment
-      let contentBeta = 'A child federated comment from beta';
-      let commentFormBeta: CommentForm = {
-        content: contentBeta,
-        post_id: getPostRes.post.id,
-        parent_id: getPostRes.comments[0].id,
-        auth: lemmyBetaAuth,
-      };
-
-      let createResponseBeta: CommentResponse = await fetch(
-        `${lemmyBetaApiUrl}/comment`,
-        {
-          method: 'POST',
-          headers: {
-            'Content-Type': 'application/json',
-          },
-          body: wrapper(commentFormBeta),
-        }
-      ).then(d => d.json());
-
-      expect(createResponseBeta.comment.content).toBe(contentBeta);
-      expect(createResponseBeta.comment.community_local).toBe(true);
-      expect(createResponseBeta.comment.creator_local).toBe(true);
-      expect(createResponseBeta.comment.parent_id).toBe(1);
-      expect(createResponseBeta.comment.score).toBe(1);
-
-      // Make sure lemmy alpha sees that new child comment from beta
-      let getPostUrlAlpha = `${lemmyAlphaApiUrl}/post?id=2`;
-      let getPostResAlpha: GetPostResponse = await fetch(getPostUrlAlpha, {
-        method: 'GET',
-      }).then(d => d.json());
-
-      // The newest show up first
-      expect(getPostResAlpha.comments[0].content).toBe(contentBeta);
-      expect(getPostResAlpha.comments[0].community_local).toBe(false);
-      expect(getPostResAlpha.comments[0].creator_local).toBe(false);
-      expect(getPostResAlpha.comments[0].score).toBe(1);
-
-      // Lemmy alpha responds to their own comment, but mentions lemmy beta.
-      // Make sure lemmy beta gets that in their inbox.
-      let mentionContent = 'A test mention of @lemmy_beta@lemmy-beta:8550';
-      let mentionCommentForm: CommentForm = {
-        content: mentionContent,
-        post_id: 2,
-        parent_id: createResponse.comment.id,
-        auth: lemmyAlphaAuth,
-      };
-
-      let createMentionRes: CommentResponse = await fetch(
-        `${lemmyAlphaApiUrl}/comment`,
-        {
-          method: 'POST',
-          headers: {
-            'Content-Type': 'application/json',
-          },
-          body: wrapper(mentionCommentForm),
-        }
-      ).then(d => d.json());
-
-      expect(createMentionRes.comment.content).toBe(mentionContent);
-      expect(createMentionRes.comment.community_local).toBe(false);
-      expect(createMentionRes.comment.creator_local).toBe(true);
-      expect(createMentionRes.comment.score).toBe(1);
-
-      // Make sure lemmy beta sees that new mention
-      let getMentionUrl = `${lemmyBetaApiUrl}/user/mention?sort=New&unread_only=false&auth=${lemmyBetaAuth}`;
-      let getMentionsRes: GetUserMentionsResponse = await fetch(getMentionUrl, {
-        method: 'GET',
-      }).then(d => d.json());
-
-      // The newest show up first
-      expect(getMentionsRes.mentions[0].content).toBe(mentionContent);
-      expect(getMentionsRes.mentions[0].community_local).toBe(true);
-      expect(getMentionsRes.mentions[0].creator_local).toBe(false);
-      expect(getMentionsRes.mentions[0].score).toBe(1);
-    });
-  });
-
-  describe('update test comment', () => {
-    test('/u/lemmy_alpha updates a comment on /c/lemmy_beta/main, its on both instances', async () => {
-      let content = 'A jest test federated comment update';
-      let commentForm: CommentForm = {
-        content,
-        post_id: 2,
-        edit_id: 1,
-        auth: lemmyAlphaAuth,
-        creator_id: 2,
-      };
-
-      let updateResponse: CommentResponse = await fetch(
-        `${lemmyAlphaApiUrl}/comment`,
-        {
-          method: 'PUT',
-          headers: {
-            'Content-Type': 'application/json',
-          },
-          body: wrapper(commentForm),
-        }
-      ).then(d => d.json());
-
-      expect(updateResponse.comment.content).toBe(content);
-      expect(updateResponse.comment.community_local).toBe(false);
-      expect(updateResponse.comment.creator_local).toBe(true);
-
-      let getPostUrl = `${lemmyBetaApiUrl}/post?id=2`;
-      let getPostRes: GetPostResponse = await fetch(getPostUrl, {
-        method: 'GET',
-      }).then(d => d.json());
-
-      expect(getPostRes.comments[2].content).toBe(content);
-      expect(getPostRes.comments[2].community_local).toBe(true);
-      expect(getPostRes.comments[2].creator_local).toBe(false);
-    });
-  });
-
-  describe('federated comment like', () => {
-    test('/u/lemmy_beta likes a comment from /u/lemmy_alpha, the like is on both instances', async () => {
-      // Do a like, to test it (its also been unliked, so its at 0)
-      let likeCommentForm: CommentLikeForm = {
-        comment_id: 1,
-        score: 1,
-        auth: lemmyBetaAuth,
-      };
-
-      let likeCommentRes: CommentResponse = await fetch(
-        `${lemmyBetaApiUrl}/comment/like`,
-        {
-          method: 'POST',
-          headers: {
-            'Content-Type': 'application/json',
-          },
-          body: wrapper(likeCommentForm),
-        }
-      ).then(d => d.json());
-
-      expect(likeCommentRes.comment.score).toBe(1);
-
-      let getPostUrl = `${lemmyAlphaApiUrl}/post?id=2`;
-      let getPostRes: GetPostResponse = await fetch(getPostUrl, {
-        method: 'GET',
-      }).then(d => d.json());
-
-      expect(getPostRes.comments[2].score).toBe(1);
-    });
-  });
-
-  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 = {
-        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,
-        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: DeleteCommentForm = {
-        edit_id: createCommentRes.comment.id,
-        deleted: true,
-        auth: lemmyBetaAuth,
-      };
-
-      let deleteCommentRes: CommentResponse = await fetch(
-        `${lemmyBetaApiUrl}/comment/delete`,
-        {
-          method: 'POST',
-          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 undeletes the comment
-      let undeleteCommentForm: DeleteCommentForm = {
-        edit_id: createCommentRes.comment.id,
-        deleted: false,
-        auth: lemmyBetaAuth,
-      };
-
-      let undeleteCommentRes: CommentResponse = await fetch(
-        `${lemmyBetaApiUrl}/comment/delete`,
-        {
-          method: 'POST',
-          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: DeletePostForm = {
-        edit_id: createPostRes.post.id,
-        deleted: true,
-        auth: lemmyBetaAuth,
-      };
-
-      let deletePostRes: PostResponse = await fetch(
-        `${lemmyBetaApiUrl}/post/delete`,
-        {
-          method: 'POST',
-          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 undeletes the post
-      let undeletePostForm: DeletePostForm = {
-        edit_id: createPostRes.post.id,
-        deleted: false,
-        auth: lemmyBetaAuth,
-      };
-
-      let undeletePostRes: PostResponse = await fetch(
-        `${lemmyBetaApiUrl}/post/delete`,
-        {
-          method: 'POST',
-          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: DeleteCommunityForm = {
-        edit_id: createCommunityRes.community.id,
-        deleted: true,
-        auth: lemmyBetaAuth,
-      };
-
-      let deleteResponse: CommunityResponse = await fetch(
-        `${lemmyBetaApiUrl}/community/delete`,
-        {
-          method: 'POST',
-          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);
-
-      // lemmy_beta undeletes the community
-      let undeleteCommunityForm: DeleteCommunityForm = {
-        edit_id: createCommunityRes.community.id,
-        deleted: false,
-        auth: lemmyBetaAuth,
-      };
-
-      let undeleteCommunityRes: CommunityResponse = await fetch(
-        `${lemmyBetaApiUrl}/community/delete`,
-        {
-          method: 'POST',
-          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);
-    });
-  });
-
-  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,
-        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: RemoveCommentForm = {
-        edit_id: createCommentRes.comment.id,
-        removed: true,
-        auth: lemmyBetaAuth,
-      };
-
-      let removeCommentRes: CommentResponse = await fetch(
-        `${lemmyBetaApiUrl}/comment/remove`,
-        {
-          method: 'POST',
-          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: RemoveCommentForm = {
-        edit_id: createCommentRes.comment.id,
-        removed: false,
-        auth: lemmyBetaAuth,
-      };
-
-      let unremoveCommentRes: CommentResponse = await fetch(
-        `${lemmyBetaApiUrl}/comment/remove`,
-        {
-          method: 'POST',
-          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: RemovePostForm = {
-        edit_id: createPostRes.post.id,
-        removed: true,
-        auth: lemmyBetaAuth,
-      };
-
-      let removePostRes: PostResponse = await fetch(
-        `${lemmyBetaApiUrl}/post/remove`,
-        {
-          method: 'POST',
-          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: RemovePostForm = {
-        edit_id: createPostRes.post.id,
-        removed: false,
-        auth: lemmyBetaAuth,
-      };
-
-      let unremovePostRes: PostResponse = await fetch(
-        `${lemmyBetaApiUrl}/post/remove`,
-        {
-          method: 'POST',
-          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 removes the community
-      let removeCommunityForm: RemoveCommunityForm = {
-        edit_id: createCommunityRes.community.id,
-        removed: true,
-        auth: lemmyBetaAuth,
-      };
-
-      let removeCommunityRes: CommunityResponse = await fetch(
-        `${lemmyBetaApiUrl}/community/remove`,
-        {
-          method: 'POST',
-          headers: {
-            'Content-Type': 'application/json',
-          },
-          body: wrapper(removeCommunityForm),
-        }
-      ).then(d => d.json());
-
-      // Make sure the remove 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: RemoveCommunityForm = {
-        edit_id: createCommunityRes.community.id,
-        removed: false,
-        auth: lemmyBetaAuth,
-      };
-
-      let unremoveCommunityRes: CommunityResponse = await fetch(
-        `${lemmyBetaApiUrl}/community/remove`,
-        {
-          method: 'POST',
-          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);
-    });
-  });
-
-  describe('private message', () => {
-    test('/u/lemmy_alpha creates/updates/deletes/undeletes a private_message to /u/lemmy_beta, its on both instances', async () => {
-      let content = 'A jest test federated private message';
-      let privateMessageForm: PrivateMessageForm = {
-        content,
-        recipient_id: 3,
-        auth: lemmyAlphaAuth,
-      };
-
-      let createRes: PrivateMessageResponse = await fetch(
-        `${lemmyAlphaApiUrl}/private_message`,
-        {
-          method: 'POST',
-          headers: {
-            'Content-Type': 'application/json',
-          },
-          body: wrapper(privateMessageForm),
-        }
-      ).then(d => d.json());
-      expect(createRes.message.content).toBe(content);
-      expect(createRes.message.local).toBe(true);
-      expect(createRes.message.creator_local).toBe(true);
-      expect(createRes.message.recipient_local).toBe(false);
-
-      // Get it from beta
-      let getPrivateMessagesUrl = `${lemmyBetaApiUrl}/private_message/list?auth=${lemmyBetaAuth}&unread_only=false`;
-
-      let getPrivateMessagesRes: PrivateMessagesResponse = await fetch(
-        getPrivateMessagesUrl,
-        {
-          method: 'GET',
-        }
-      ).then(d => d.json());
-
-      expect(getPrivateMessagesRes.messages[0].content).toBe(content);
-      expect(getPrivateMessagesRes.messages[0].local).toBe(false);
-      expect(getPrivateMessagesRes.messages[0].creator_local).toBe(false);
-      expect(getPrivateMessagesRes.messages[0].recipient_local).toBe(true);
-
-      // lemmy alpha updates the private message
-      let updatedContent = 'A jest test federated private message edited';
-      let updatePrivateMessageForm: EditPrivateMessageForm = {
-        content: updatedContent,
-        edit_id: createRes.message.id,
-        auth: lemmyAlphaAuth,
-      };
-
-      let updateRes: PrivateMessageResponse = await fetch(
-        `${lemmyAlphaApiUrl}/private_message`,
-        {
-          method: 'PUT',
-          headers: {
-            'Content-Type': 'application/json',
-          },
-          body: wrapper(updatePrivateMessageForm),
-        }
-      ).then(d => d.json());
-
-      expect(updateRes.message.content).toBe(updatedContent);
-
-      // Fetch from beta again
-      let getPrivateMessagesUpdatedRes: PrivateMessagesResponse = await fetch(
-        getPrivateMessagesUrl,
-        {
-          method: 'GET',
-        }
-      ).then(d => d.json());
-
-      expect(getPrivateMessagesUpdatedRes.messages[0].content).toBe(
-        updatedContent
-      );
-
-      // lemmy alpha deletes the private message
-      let deletePrivateMessageForm: DeletePrivateMessageForm = {
-        deleted: true,
-        edit_id: createRes.message.id,
-        auth: lemmyAlphaAuth,
-      };
-
-      let deleteRes: PrivateMessageResponse = await fetch(
-        `${lemmyAlphaApiUrl}/private_message/delete`,
-        {
-          method: 'POST',
-          headers: {
-            'Content-Type': 'application/json',
-          },
-          body: wrapper(deletePrivateMessageForm),
-        }
-      ).then(d => d.json());
-
-      expect(deleteRes.message.deleted).toBe(true);
-
-      // Fetch from beta again
-      let getPrivateMessagesDeletedRes: PrivateMessagesResponse = await fetch(
-        getPrivateMessagesUrl,
-        {
-          method: 'GET',
-        }
-      ).then(d => d.json());
-
-      // The GetPrivateMessages filters out deleted,
-      // even though they are in the actual database.
-      // no reason to show them
-      expect(getPrivateMessagesDeletedRes.messages.length).toBe(0);
-
-      // lemmy alpha undeletes the private message
-      let undeletePrivateMessageForm: DeletePrivateMessageForm = {
-        deleted: false,
-        edit_id: createRes.message.id,
-        auth: lemmyAlphaAuth,
-      };
-
-      let undeleteRes: PrivateMessageResponse = await fetch(
-        `${lemmyAlphaApiUrl}/private_message/delete`,
-        {
-          method: 'POST',
-          headers: {
-            'Content-Type': 'application/json',
-          },
-          body: wrapper(undeletePrivateMessageForm),
-        }
-      ).then(d => d.json());
-
-      expect(undeleteRes.message.deleted).toBe(false);
-
-      // Fetch from beta again
-      let getPrivateMessagesUnDeletedRes: PrivateMessagesResponse = await fetch(
-        getPrivateMessagesUrl,
-        {
-          method: 'GET',
-        }
-      ).then(d => d.json());
-
-      expect(getPrivateMessagesUnDeletedRes.messages[0].deleted).toBe(false);
-    });
-  });
-
-  describe('comment_search', () => {
-    test('Create comment on alpha and search it', async () => {
-      let content = 'A jest test federated comment for search';
-      let commentForm: CommentForm = {
-        content,
-        post_id: 1,
-        auth: lemmyAlphaAuth,
-      };
-
-      let createResponse: CommentResponse = await fetch(
-        `${lemmyAlphaApiUrl}/comment`,
-        {
-          method: 'POST',
-          headers: {
-            'Content-Type': 'application/json',
-          },
-          body: wrapper(commentForm),
-        }
-      ).then(d => d.json());
-
-      let searchUrl = `${lemmyBetaApiUrl}/search?q=${createResponse.comment.ap_id}&type_=All&sort=TopAll`;
-      let searchResponse: SearchResponse = await fetch(searchUrl, {
-        method: 'GET',
-      }).then(d => d.json());
-
-      // TODO: check more fields
-      expect(searchResponse.comments[0].content).toBe(content);
-    });
-  });
-
-  describe('announce', () => {
-    test('A and G subscribe to B (center) A does action, it gets announced to G', async () => {
-      // A and G are already subscribed to B earlier.
-      //
-      let postName = 'A jest test post for announce';
-      let createPostForm: PostForm = {
-        name: postName,
-        auth: lemmyAlphaAuth,
-        community_id: 2,
-        nsfw: false,
-      };
-
-      let createPostRes: PostResponse = await fetch(
-        `${lemmyAlphaApiUrl}/post`,
-        {
-          method: 'POST',
-          headers: {
-            'Content-Type': 'application/json',
-          },
-          body: wrapper(createPostForm),
-        }
-      ).then(d => d.json());
-      expect(createPostRes.post.name).toBe(postName);
-
-      // Make sure that post got announced to Gamma
-      let searchUrl = `${lemmyGammaApiUrl}/search?q=${createPostRes.post.ap_id}&type_=All&sort=TopAll`;
-      let searchResponse: SearchResponse = await fetch(searchUrl, {
-        method: 'GET',
-      }).then(d => d.json());
-      let postId = searchResponse.posts[0].id;
-      expect(searchResponse.posts[0].name).toBe(postName);
-
-      // Create a test comment on Gamma, make sure it gets announced to alpha
-      let commentContent =
-        'A jest test federated comment announce, lets mention @lemmy_beta@lemmy-beta:8550';
-
-      let commentForm: CommentForm = {
-        content: commentContent,
-        post_id: postId,
-        auth: lemmyGammaAuth,
-      };
-
-      let createCommentRes: CommentResponse = await fetch(
-        `${lemmyGammaApiUrl}/comment`,
-        {
-          method: 'POST',
-          headers: {
-            'Content-Type': 'application/json',
-          },
-          body: wrapper(commentForm),
-        }
-      ).then(d => d.json());
-
-      expect(createCommentRes.comment.content).toBe(commentContent);
-      expect(createCommentRes.comment.community_local).toBe(false);
-      expect(createCommentRes.comment.creator_local).toBe(true);
-      expect(createCommentRes.comment.score).toBe(1);
-
-      // Get the post from alpha, make sure it has gamma's comment
-      let getPostUrl = `${lemmyAlphaApiUrl}/post?id=5`;
-      let getPostRes: GetPostResponse = await fetch(getPostUrl, {
-        method: 'GET',
-      }).then(d => d.json());
-
-      expect(getPostRes.comments[0].content).toBe(commentContent);
-      expect(getPostRes.comments[0].community_local).toBe(true);
-      expect(getPostRes.comments[0].creator_local).toBe(false);
-      expect(getPostRes.comments[0].score).toBe(1);
-    });
-  });
-
-  describe('fetch inreplytos', () => {
-    test('A is unsubbed from B, B makes a post, and some embedded comments, A subs to B, B updates the lowest level comment, A fetches both the post and all the inreplyto comments for that post.', async () => {
-      // Check that A is subscribed to B
-      let followedCommunitiesUrl = `${lemmyAlphaApiUrl}/user/followed_communities?&auth=${lemmyAlphaAuth}`;
-      let followedCommunitiesRes: GetFollowedCommunitiesResponse = await fetch(
-        followedCommunitiesUrl,
-        {
-          method: 'GET',
-        }
-      ).then(d => d.json());
-      expect(followedCommunitiesRes.communities[1].community_local).toBe(false);
-
-      // A unsubs from B (communities ids 3-5)
-      for (let i = 3; i <= 5; i++) {
-        let unfollowForm: FollowCommunityForm = {
-          community_id: i,
-          follow: false,
-          auth: lemmyAlphaAuth,
-        };
-
-        let unfollowRes: CommunityResponse = await fetch(
-          `${lemmyAlphaApiUrl}/community/follow`,
-          {
-            method: 'POST',
-            headers: {
-              'Content-Type': 'application/json',
-            },
-            body: wrapper(unfollowForm),
-          }
-        ).then(d => d.json());
-        expect(unfollowRes.community.local).toBe(false);
-      }
-
-      // Check that you are unsubscribed from all of them locally
-      let followedCommunitiesResAgain: GetFollowedCommunitiesResponse = await fetch(
-        followedCommunitiesUrl,
-        {
-          method: 'GET',
-        }
-      ).then(d => d.json());
-      expect(followedCommunitiesResAgain.communities.length).toBe(1);
-
-      // B creates a post, and two comments, should be invisible to A
-      let betaPostName = 'Test post on B, invisible to A at first';
-      let postForm: PostForm = {
-        name: betaPostName,
-        auth: lemmyBetaAuth,
-        community_id: 2,
-        nsfw: false,
-      };
-
-      let createPostRes: PostResponse = await fetch(`${lemmyBetaApiUrl}/post`, {
-        method: 'POST',
-        headers: {
-          'Content-Type': 'application/json',
-        },
-        body: wrapper(postForm),
-      }).then(d => d.json());
-      expect(createPostRes.post.name).toBe(betaPostName);
-
-      // B creates a comment, then a child one of that.
-      let parentCommentContent = 'An invisible top level comment from beta';
-      let createParentCommentForm: CommentForm = {
-        content: parentCommentContent,
-        post_id: createPostRes.post.id,
-        auth: lemmyBetaAuth,
-      };
-
-      let createParentCommentRes: CommentResponse = await fetch(
-        `${lemmyBetaApiUrl}/comment`,
-        {
-          method: 'POST',
-          headers: {
-            'Content-Type': 'application/json',
-          },
-          body: wrapper(createParentCommentForm),
-        }
-      ).then(d => d.json());
-      expect(createParentCommentRes.comment.content).toBe(parentCommentContent);
-
-      let childCommentContent = 'An invisible child comment from beta';
-      let createChildCommentForm: CommentForm = {
-        content: childCommentContent,
-        parent_id: createParentCommentRes.comment.id,
-        post_id: createPostRes.post.id,
-        auth: lemmyBetaAuth,
-      };
-
-      let createChildCommentRes: CommentResponse = await fetch(
-        `${lemmyBetaApiUrl}/comment`,
-        {
-          method: 'POST',
-          headers: {
-            'Content-Type': 'application/json',
-          },
-          body: wrapper(createChildCommentForm),
-        }
-      ).then(d => d.json());
-      expect(createChildCommentRes.comment.content).toBe(childCommentContent);
-
-      // Follow again, for other tests
-      let searchUrl = `${lemmyAlphaApiUrl}/search?q=!main@lemmy-beta:8550&type_=All&sort=TopAll`;
-
-      let searchResponse: SearchResponse = await fetch(searchUrl, {
-        method: 'GET',
-      }).then(d => d.json());
-
-      expect(searchResponse.communities[0].name).toBe('main');
-
-      let followForm: FollowCommunityForm = {
-        community_id: searchResponse.communities[0].id,
-        follow: true,
-        auth: lemmyAlphaAuth,
-      };
-
-      let followResAgain: 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(followResAgain.community.local).toBe(false);
-      expect(followResAgain.community.name).toBe('main');
-
-      let updatedCommentContent = 'An update child comment from beta';
-      let updatedCommentForm: CommentForm = {
-        content: updatedCommentContent,
-        post_id: createPostRes.post.id,
-        edit_id: createChildCommentRes.comment.id,
-        auth: lemmyBetaAuth,
-        creator_id: 2,
-      };
-
-      let updateResponse: CommentResponse = await fetch(
-        `${lemmyBetaApiUrl}/comment`,
-        {
-          method: 'PUT',
-          headers: {
-            'Content-Type': 'application/json',
-          },
-          body: wrapper(updatedCommentForm),
-        }
-      ).then(d => d.json());
-      expect(updateResponse.comment.content).toBe(updatedCommentContent);
-
-      // Make sure that A picked up the post, parent comment, and child comment
-      let getPostUrl = `${lemmyAlphaApiUrl}/post?id=6`;
-      let getPostRes: GetPostResponse = await fetch(getPostUrl, {
-        method: 'GET',
-      }).then(d => d.json());
-
-      expect(getPostRes.post.name).toBe(betaPostName);
-      expect(getPostRes.comments[1].content).toBe(parentCommentContent);
-      expect(getPostRes.comments[0].content).toBe(updatedCommentContent);
-      expect(getPostRes.post.community_local).toBe(false);
-      expect(getPostRes.post.creator_local).toBe(false);
-    });
-  });
-});
-
-function wrapper(form: any): string {
-  return JSON.stringify(form);
-}
diff --git a/ui/src/api_tests/comment.spec.ts b/ui/src/api_tests/comment.spec.ts
new file mode 100644 (file)
index 0000000..8852a73
--- /dev/null
@@ -0,0 +1,308 @@
+import {
+  alpha,
+  beta,
+  gamma,
+  setupLogins,
+  createPost,
+  getPost,
+  searchComment,
+  likeComment,
+  followBeta,
+  searchForBetaCommunity,
+  createComment,
+  updateComment,
+  deleteComment,
+  removeComment,
+  getMentions,
+  searchPost,
+  unfollowRemotes,
+} from './shared';
+
+import { PostResponse } from '../interfaces';
+
+let postRes: PostResponse;
+
+beforeAll(async () => {
+  await setupLogins();
+  await followBeta(alpha);
+  await followBeta(gamma);
+  let search = await searchForBetaCommunity(alpha);
+  postRes = await createPost(
+    alpha,
+    search.communities.filter(c => c.local == false)[0].id
+  );
+});
+
+afterAll(async () => {
+  await unfollowRemotes(alpha);
+  await unfollowRemotes(gamma);
+});
+
+test('Create a comment', async () => {
+  let commentRes = await createComment(alpha, postRes.post.id);
+  expect(commentRes.comment.content).toBeDefined();
+  expect(commentRes.comment.community_local).toBe(false);
+  expect(commentRes.comment.creator_local).toBe(true);
+  expect(commentRes.comment.score).toBe(1);
+
+  // Make sure that comment is liked on beta
+  let searchBeta = await searchComment(beta, commentRes.comment);
+  let betaComment = searchBeta.comments[0];
+  expect(betaComment).toBeDefined();
+  expect(betaComment.community_local).toBe(true);
+  expect(betaComment.creator_local).toBe(false);
+  expect(betaComment.score).toBe(1);
+});
+
+test('Update a comment', async () => {
+  let commentRes = await createComment(alpha, postRes.post.id);
+  let updateCommentRes = await updateComment(alpha, commentRes.comment.id);
+  expect(updateCommentRes.comment.content).toBe(
+    'A jest test federated comment update'
+  );
+  expect(updateCommentRes.comment.community_local).toBe(false);
+  expect(updateCommentRes.comment.creator_local).toBe(true);
+
+  // Make sure that post is updated on beta
+  let searchBeta = await searchComment(beta, commentRes.comment);
+  let betaComment = searchBeta.comments[0];
+  expect(betaComment.content).toBe('A jest test federated comment update');
+});
+
+test('Delete a comment', async () => {
+  let commentRes = await createComment(alpha, postRes.post.id);
+  let deleteCommentRes = await deleteComment(
+    alpha,
+    true,
+    commentRes.comment.id
+  );
+  expect(deleteCommentRes.comment.deleted).toBe(true);
+
+  // Make sure that comment is deleted on beta
+  // The search doesnt work below, because it returns a tombstone / http::gone
+  // let searchBeta = await searchComment(beta, commentRes.comment);
+  // console.log(searchBeta);
+  // let betaComment = searchBeta.comments[0];
+  // Create a fake post, just to get the previous new post id
+  let createdBetaPostJustToGetId = await createPost(beta, 2);
+  let betaPost = await getPost(beta, createdBetaPostJustToGetId.post.id - 1);
+  let betaComment = betaPost.comments[0];
+  expect(betaComment.deleted).toBe(true);
+
+  let undeleteCommentRes = await deleteComment(
+    alpha,
+    false,
+    commentRes.comment.id
+  );
+  expect(undeleteCommentRes.comment.deleted).toBe(false);
+
+  // Make sure that comment is undeleted on beta
+  let searchBeta2 = await searchComment(beta, commentRes.comment);
+  let betaComment2 = searchBeta2.comments[0];
+  expect(betaComment2.deleted).toBe(false);
+});
+
+test('Remove a comment', async () => {
+  let commentRes = await createComment(alpha, postRes.post.id);
+  let removeCommentRes = await removeComment(
+    alpha,
+    true,
+    commentRes.comment.id
+  );
+  expect(removeCommentRes.comment.removed).toBe(true);
+
+  // Make sure that comment is removed on beta
+  let searchBeta = await searchComment(beta, commentRes.comment);
+  let betaComment = searchBeta.comments[0];
+  expect(betaComment.removed).toBe(true);
+
+  let unremoveCommentRes = await removeComment(
+    alpha,
+    false,
+    commentRes.comment.id
+  );
+  expect(unremoveCommentRes.comment.removed).toBe(false);
+
+  // Make sure that comment is unremoved on beta
+  let searchBeta2 = await searchComment(beta, commentRes.comment);
+  let betaComment2 = searchBeta2.comments[0];
+  expect(betaComment2.removed).toBe(false);
+});
+
+test('Unlike a comment', async () => {
+  let commentRes = await createComment(alpha, postRes.post.id);
+  let unlike = await likeComment(alpha, 0, commentRes.comment);
+  expect(unlike.comment.score).toBe(0);
+
+  // Make sure that post is unliked on beta
+  let searchBeta = await searchComment(beta, commentRes.comment);
+  let betaComment = searchBeta.comments[0];
+  expect(betaComment).toBeDefined();
+  expect(betaComment.community_local).toBe(true);
+  expect(betaComment.creator_local).toBe(false);
+  expect(betaComment.score).toBe(0);
+});
+
+test('Federated comment like', async () => {
+  let commentRes = await createComment(alpha, postRes.post.id);
+
+  // Find the comment on beta
+  let searchBeta = await searchComment(beta, commentRes.comment);
+  let betaComment = searchBeta.comments[0];
+
+  let like = await likeComment(beta, 1, betaComment);
+  expect(like.comment.score).toBe(2);
+
+  // Get the post from alpha, check the likes
+  let post = await getPost(alpha, postRes.post.id);
+  expect(post.comments[0].score).toBe(2);
+});
+
+test('Reply to a comment', async () => {
+  // Create a comment on alpha, find it on beta
+  let commentRes = await createComment(alpha, postRes.post.id);
+  let searchBeta = await searchComment(beta, commentRes.comment);
+  let betaComment = searchBeta.comments[0];
+
+  // find that comment id on beta
+
+  // Reply from beta
+  let replyRes = await createComment(beta, betaComment.post_id, betaComment.id);
+  expect(replyRes.comment.content).toBeDefined();
+  expect(replyRes.comment.community_local).toBe(true);
+  expect(replyRes.comment.creator_local).toBe(true);
+  expect(replyRes.comment.parent_id).toBe(betaComment.id);
+  expect(replyRes.comment.score).toBe(1);
+
+  // Make sure that comment is seen on alpha
+  // TODO not sure why, but a searchComment back to alpha, for the ap_id of betas
+  // comment, isn't working.
+  // let searchAlpha = await searchComment(alpha, replyRes.comment);
+  let post = await getPost(alpha, postRes.post.id);
+  let alphaComment = post.comments[0];
+  expect(alphaComment.content).toBeDefined();
+  expect(alphaComment.parent_id).toBe(post.comments[1].id);
+  expect(alphaComment.community_local).toBe(false);
+  expect(alphaComment.creator_local).toBe(false);
+  expect(alphaComment.score).toBe(1);
+});
+
+test('Mention beta', async () => {
+  // Create a mention on alpha
+  let mentionContent = 'A test mention of @lemmy_beta@lemmy-beta:8550';
+  let commentRes = await createComment(alpha, postRes.post.id);
+  let mentionRes = await createComment(
+    alpha,
+    postRes.post.id,
+    commentRes.comment.id,
+    mentionContent
+  );
+  expect(mentionRes.comment.content).toBeDefined();
+  expect(mentionRes.comment.community_local).toBe(false);
+  expect(mentionRes.comment.creator_local).toBe(true);
+  expect(mentionRes.comment.score).toBe(1);
+
+  let mentionsRes = await getMentions(beta);
+  expect(mentionsRes.mentions[0].content).toBeDefined();
+  expect(mentionsRes.mentions[0].community_local).toBe(true);
+  expect(mentionsRes.mentions[0].creator_local).toBe(false);
+  expect(mentionsRes.mentions[0].score).toBe(1);
+});
+
+test('Comment Search', async () => {
+  let commentRes = await createComment(alpha, postRes.post.id);
+  let searchBeta = await searchComment(beta, commentRes.comment);
+  expect(searchBeta.comments[0].ap_id).toBe(commentRes.comment.ap_id);
+});
+
+test('A and G subscribe to B (center) A posts, G mentions B, it gets announced to A', async () => {
+  // Create a local post
+  let alphaPost = await createPost(alpha, 2);
+  expect(alphaPost.post.community_local).toBe(true);
+
+  // Make sure gamma sees it
+  let search = await searchPost(gamma, alphaPost.post);
+  let gammaPost = search.posts[0];
+
+  let commentContent =
+    'A jest test federated comment announce, lets mention @lemmy_beta@lemmy-beta:8550';
+  let commentRes = await createComment(
+    gamma,
+    gammaPost.id,
+    undefined,
+    commentContent
+  );
+  expect(commentRes.comment.content).toBe(commentContent);
+  expect(commentRes.comment.community_local).toBe(false);
+  expect(commentRes.comment.creator_local).toBe(true);
+  expect(commentRes.comment.score).toBe(1);
+
+  // Make sure alpha sees it
+  let alphaPost2 = await getPost(alpha, alphaPost.post.id);
+  expect(alphaPost2.comments[0].content).toBe(commentContent);
+  expect(alphaPost2.comments[0].community_local).toBe(true);
+  expect(alphaPost2.comments[0].creator_local).toBe(false);
+  expect(alphaPost2.comments[0].score).toBe(1);
+
+  // Make sure beta has mentions
+  let mentionsRes = await getMentions(beta);
+  expect(mentionsRes.mentions[0].content).toBe(commentContent);
+  expect(mentionsRes.mentions[0].community_local).toBe(false);
+  expect(mentionsRes.mentions[0].creator_local).toBe(false);
+  // TODO this is failing because fetchInReplyTos aren't getting score
+  // expect(mentionsRes.mentions[0].score).toBe(1);
+});
+
+test('Fetch in_reply_tos: A is unsubbed from B, B makes a post, and some embedded comments, A subs to B, B updates the lowest level comment, A fetches both the post and all the inreplyto comments for that post.', async () => {
+  // Unfollow all remote communities
+  let followed = await unfollowRemotes(alpha);
+  expect(
+    followed.communities.filter(c => c.community_local == false).length
+  ).toBe(0);
+
+  // B creates a post, and two comments, should be invisible to A
+  let postRes = await createPost(beta, 2);
+  expect(postRes.post.name).toBeDefined();
+
+  let parentCommentContent = 'An invisible top level comment from beta';
+  let parentCommentRes = await createComment(
+    beta,
+    postRes.post.id,
+    undefined,
+    parentCommentContent
+  );
+  expect(parentCommentRes.comment.content).toBe(parentCommentContent);
+
+  // B creates a comment, then a child one of that.
+  let childCommentContent = 'An invisible child comment from beta';
+  let childCommentRes = await createComment(
+    beta,
+    postRes.post.id,
+    parentCommentRes.comment.id,
+    childCommentContent
+  );
+  expect(childCommentRes.comment.content).toBe(childCommentContent);
+
+  // Follow beta again
+  let follow = await followBeta(alpha);
+  expect(follow.community.local).toBe(false);
+  expect(follow.community.name).toBe('main');
+
+  // An update to the child comment on beta, should push the post, parent, and child to alpha now
+  let updatedCommentContent = 'An update child comment from beta';
+  let updateRes = await updateComment(
+    beta,
+    childCommentRes.comment.id,
+    updatedCommentContent
+  );
+  expect(updateRes.comment.content).toBe(updatedCommentContent);
+
+  // Get the post from alpha
+  let createFakeAlphaPostToGetId = await createPost(alpha, 2);
+  let alphaPost = await getPost(alpha, createFakeAlphaPostToGetId.post.id - 1);
+  expect(alphaPost.post.name).toBeDefined();
+  expect(alphaPost.comments[1].content).toBe(parentCommentContent);
+  expect(alphaPost.comments[0].content).toBe(updatedCommentContent);
+  expect(alphaPost.post.community_local).toBe(false);
+  expect(alphaPost.post.creator_local).toBe(false);
+});
diff --git a/ui/src/api_tests/community.spec.ts b/ui/src/api_tests/community.spec.ts
new file mode 100644 (file)
index 0000000..6945e33
--- /dev/null
@@ -0,0 +1,88 @@
+import {
+  alpha,
+  beta,
+  setupLogins,
+  searchForBetaCommunity,
+  createCommunity,
+  deleteCommunity,
+  removeCommunity,
+} from './shared';
+
+beforeAll(async () => {
+  await setupLogins();
+});
+
+test('Create community', async () => {
+  let communityRes = await createCommunity(alpha);
+  expect(communityRes.community.name).toBeDefined();
+
+  // A dupe check
+  let prevName = communityRes.community.name;
+  let communityRes2 = await createCommunity(alpha, prevName);
+  expect(communityRes2['error']).toBe('community_already_exists');
+});
+
+test('Delete community', async () => {
+  let communityRes = await createCommunity(beta);
+  let deleteCommunityRes = await deleteCommunity(
+    beta,
+    true,
+    communityRes.community.id
+  );
+  expect(deleteCommunityRes.community.deleted).toBe(true);
+
+  // Make sure it got deleted on A
+  let search = await searchForBetaCommunity(alpha);
+  let communityA = search.communities[0];
+  // TODO this fails currently, because no updates are pushed
+  // expect(communityA.deleted).toBe(true);
+
+  // Undelete
+  let undeleteCommunityRes = await deleteCommunity(
+    beta,
+    false,
+    communityRes.community.id
+  );
+  expect(undeleteCommunityRes.community.deleted).toBe(false);
+
+  // Make sure it got undeleted on A
+  let search2 = await searchForBetaCommunity(alpha);
+  let communityA2 = search2.communities[0];
+  // TODO this fails currently, because no updates are pushed
+  // expect(communityA2.deleted).toBe(false);
+});
+
+test('Remove community', async () => {
+  let communityRes = await createCommunity(beta);
+  let removeCommunityRes = await removeCommunity(
+    beta,
+    true,
+    communityRes.community.id
+  );
+  expect(removeCommunityRes.community.removed).toBe(true);
+
+  // Make sure it got removed on A
+  let search = await searchForBetaCommunity(alpha);
+  let communityA = search.communities[0];
+  // TODO this fails currently, because no updates are pushed
+  // expect(communityA.removed).toBe(true);
+
+  // unremove
+  let unremoveCommunityRes = await removeCommunity(
+    beta,
+    false,
+    communityRes.community.id
+  );
+  expect(unremoveCommunityRes.community.removed).toBe(false);
+
+  // Make sure it got unremoved on A
+  let search2 = await searchForBetaCommunity(alpha);
+  let communityA2 = search2.communities[0];
+  // TODO this fails currently, because no updates are pushed
+  // expect(communityA2.removed).toBe(false);
+});
+
+test('Search for beta community', async () => {
+  let search = await searchForBetaCommunity(alpha);
+  expect(search.communities[0].name).toBe('main');
+});
diff --git a/ui/src/api_tests/follow.spec.ts b/ui/src/api_tests/follow.spec.ts
new file mode 100644 (file)
index 0000000..2f1f8cd
--- /dev/null
@@ -0,0 +1,40 @@
+import {
+  alpha,
+  setupLogins,
+  searchForBetaCommunity,
+  followCommunity,
+  checkFollowedCommunities,
+  unfollowRemotes,
+} from './shared';
+
+beforeAll(async () => {
+  await setupLogins();
+});
+
+afterAll(async () => {
+  await unfollowRemotes(alpha);
+});
+
+test('Follow federated community', async () => {
+  let search = await searchForBetaCommunity(alpha); // TODO sometimes this is returning null?
+  let follow = await followCommunity(alpha, true, search.communities[0].id);
+
+  // Make sure the follow response went through
+  expect(follow.community.local).toBe(false);
+  expect(follow.community.name).toBe('main');
+
+  // Check it from local
+  let followCheck = await checkFollowedCommunities(alpha);
+  let remoteCommunityId = followCheck.communities.filter(
+    c => c.community_local == false
+  )[0].community_id;
+  expect(remoteCommunityId).toBeDefined();
+
+  // Test an unfollow
+  let unfollow = await followCommunity(alpha, false, remoteCommunityId);
+  expect(unfollow.community.local).toBe(false);
+
+  // Make sure you are unsubbed locally
+  let unfollowCheck = await checkFollowedCommunities(alpha);
+  expect(unfollowCheck.communities.length).toBeGreaterThanOrEqual(1);
+});
diff --git a/ui/src/api_tests/post.spec.ts b/ui/src/api_tests/post.spec.ts
new file mode 100644 (file)
index 0000000..f2cb667
--- /dev/null
@@ -0,0 +1,192 @@
+import {
+  alpha,
+  beta,
+  gamma,
+  setupLogins,
+  createPost,
+  updatePost,
+  stickyPost,
+  lockPost,
+  searchPost,
+  likePost,
+  followBeta,
+  searchForBetaCommunity,
+  createComment,
+  deletePost,
+  removePost,
+  getPost,
+  unfollowRemotes,
+} from './shared';
+
+beforeAll(async () => {
+  await setupLogins();
+  await followBeta(alpha);
+  await followBeta(gamma);
+});
+
+afterAll(async () => {
+  await unfollowRemotes(alpha);
+  await unfollowRemotes(gamma);
+});
+
+test('Create a post', async () => {
+  let search = await searchForBetaCommunity(alpha);
+  let postRes = await createPost(alpha, search.communities[0].id);
+  expect(postRes.post).toBeDefined();
+  expect(postRes.post.community_local).toBe(false);
+  expect(postRes.post.creator_local).toBe(true);
+  expect(postRes.post.score).toBe(1);
+
+  // Make sure that post is liked on beta
+  let searchBeta = await searchPost(beta, postRes.post);
+  let betaPost = searchBeta.posts[0];
+
+  expect(betaPost).toBeDefined();
+  expect(betaPost.community_local).toBe(true);
+  expect(betaPost.creator_local).toBe(false);
+  expect(betaPost.score).toBe(1);
+});
+
+test('Unlike a post', async () => {
+  let search = await searchForBetaCommunity(alpha);
+  let postRes = await createPost(alpha, search.communities[0].id);
+  let unlike = await likePost(alpha, 0, postRes.post);
+  expect(unlike.post.score).toBe(0);
+
+  // Make sure that post is unliked on beta
+  let searchBeta = await searchPost(beta, postRes.post);
+  let betaPost = searchBeta.posts[0];
+
+  expect(betaPost).toBeDefined();
+  expect(betaPost.community_local).toBe(true);
+  expect(betaPost.creator_local).toBe(false);
+  expect(betaPost.score).toBe(0);
+});
+
+test('Update a post', async () => {
+  let search = await searchForBetaCommunity(alpha);
+  let postRes = await createPost(alpha, search.communities[0].id);
+
+  let updatedPost = await updatePost(alpha, postRes.post);
+  expect(updatedPost.post.name).toBe('A jest test federated post, updated');
+  expect(updatedPost.post.community_local).toBe(false);
+  expect(updatedPost.post.creator_local).toBe(true);
+});
+
+test('Sticky a post', async () => {
+  let search = await searchForBetaCommunity(alpha);
+  let postRes = await createPost(alpha, search.communities[0].id);
+
+  let stickiedPostRes = await stickyPost(alpha, true, postRes.post);
+  expect(stickiedPostRes.post.stickied).toBe(true);
+
+  // Make sure that post is stickied on beta
+  let searchBeta = await searchPost(beta, postRes.post);
+  let betaPost = searchBeta.posts[0];
+  expect(betaPost.community_local).toBe(true);
+  expect(betaPost.creator_local).toBe(false);
+  expect(betaPost.stickied).toBe(true);
+
+  // Unsticky a post
+  let unstickiedPost = await stickyPost(alpha, false, postRes.post);
+  expect(unstickiedPost.post.stickied).toBe(false);
+
+  // Make sure that post is unstickied on beta
+  let searchBeta2 = await searchPost(beta, postRes.post);
+  let betaPost2 = searchBeta2.posts[0];
+  expect(betaPost2.community_local).toBe(true);
+  expect(betaPost2.creator_local).toBe(false);
+  expect(betaPost2.stickied).toBe(false);
+});
+
+test('Lock a post', async () => {
+  let search = await searchForBetaCommunity(alpha);
+  let postRes = await createPost(alpha, search.communities[0].id);
+
+  let lockedPostRes = await lockPost(alpha, true, postRes.post);
+  expect(lockedPostRes.post.locked).toBe(true);
+
+  // Make sure that post is locked on beta
+  let searchBeta = await searchPost(beta, postRes.post);
+  let betaPost = searchBeta.posts[0];
+  expect(betaPost.community_local).toBe(true);
+  expect(betaPost.creator_local).toBe(false);
+  expect(betaPost.locked).toBe(true);
+
+  // Try to make a new comment there, on alpha
+  let comment = await createComment(alpha, postRes.post.id);
+  expect(comment['error']).toBe('locked');
+
+  // Try to create a new comment, on beta
+  let commentBeta = await createComment(beta, betaPost.id);
+  expect(commentBeta['error']).toBe('locked');
+
+  // Unlock a post
+  let unlockedPost = await lockPost(alpha, false, postRes.post);
+  expect(unlockedPost.post.locked).toBe(false);
+
+  // Make sure that post is unlocked on beta
+  let searchBeta2 = await searchPost(beta, postRes.post);
+  let betaPost2 = searchBeta2.posts[0];
+  expect(betaPost2.community_local).toBe(true);
+  expect(betaPost2.creator_local).toBe(false);
+  expect(betaPost2.locked).toBe(false);
+});
+
+test('Delete a post', async () => {
+  let search = await searchForBetaCommunity(alpha);
+  let postRes = await createPost(alpha, search.communities[0].id);
+
+  let deletedPost = await deletePost(alpha, true, postRes.post);
+  expect(deletedPost.post.deleted).toBe(true);
+
+  // Make sure lemmy beta sees post is deleted
+  let createFakeBetaPostToGetId = (await createPost(beta, 2)).post.id - 1;
+  let betaPost = await getPost(beta, createFakeBetaPostToGetId);
+  expect(betaPost.post.deleted).toBe(true);
+
+  // Undelete
+  let undeletedPost = await deletePost(alpha, false, postRes.post);
+  expect(undeletedPost.post.deleted).toBe(false);
+
+  // Make sure lemmy beta sees post is undeleted
+  let betaPost2 = await getPost(beta, createFakeBetaPostToGetId);
+  expect(betaPost2.post.deleted).toBe(false);
+});
+
+test('Remove a post', async () => {
+  let search = await searchForBetaCommunity(alpha);
+  let postRes = await createPost(alpha, search.communities[0].id);
+
+  let removedPost = await removePost(alpha, true, postRes.post);
+  expect(removedPost.post.removed).toBe(true);
+
+  // Make sure lemmy beta sees post is removed
+  let createFakeBetaPostToGetId = (await createPost(beta, 2)).post.id - 1;
+  let betaPost = await getPost(beta, createFakeBetaPostToGetId);
+  expect(betaPost.post.removed).toBe(true);
+
+  // Undelete
+  let undeletedPost = await removePost(alpha, false, postRes.post);
+  expect(undeletedPost.post.removed).toBe(false);
+
+  // Make sure lemmy beta sees post is undeleted
+  let betaPost2 = await getPost(beta, createFakeBetaPostToGetId);
+  expect(betaPost2.post.removed).toBe(false);
+});
+
+test('Search for a post', async () => {
+  let search = await searchForBetaCommunity(alpha);
+  let postRes = await createPost(alpha, search.communities[0].id);
+  let searchBeta = await searchPost(beta, postRes.post);
+
+  expect(searchBeta.posts[0].name).toBeDefined();
+});
+
+test('A and G subscribe to B (center) A posts, it gets announced to G', async () => {
+  let search = await searchForBetaCommunity(alpha);
+  let postRes = await createPost(alpha, search.communities[0].id);
+
+  let search2 = await searchPost(gamma, postRes.post);
+  expect(search2.posts[0].name).toBeDefined();
+});
diff --git a/ui/src/api_tests/private_message.spec.ts b/ui/src/api_tests/private_message.spec.ts
new file mode 100644 (file)
index 0000000..4bf3f07
--- /dev/null
@@ -0,0 +1,71 @@
+import {
+  alpha,
+  beta,
+  setupLogins,
+  followBeta,
+  createPrivateMessage,
+  updatePrivateMessage,
+  listPrivateMessages,
+  deletePrivateMessage,
+  unfollowRemotes,
+} from './shared';
+
+let recipient_id: number;
+
+beforeAll(async () => {
+  await setupLogins();
+  recipient_id = (await followBeta(alpha)).community.creator_id;
+});
+
+afterAll(async () => {
+  await unfollowRemotes(alpha);
+});
+
+test('Create a private message', async () => {
+  let pmRes = await createPrivateMessage(alpha, recipient_id);
+  expect(pmRes.message.content).toBeDefined();
+  expect(pmRes.message.local).toBe(true);
+  expect(pmRes.message.creator_local).toBe(true);
+  expect(pmRes.message.recipient_local).toBe(false);
+
+  let betaPms = await listPrivateMessages(beta);
+  expect(betaPms.messages[0].content).toBeDefined();
+  expect(betaPms.messages[0].local).toBe(false);
+  expect(betaPms.messages[0].creator_local).toBe(false);
+  expect(betaPms.messages[0].recipient_local).toBe(true);
+});
+
+test('Update a private message', async () => {
+  let updatedContent = 'A jest test federated private message edited';
+
+  let pmRes = await createPrivateMessage(alpha, recipient_id);
+  let pmUpdated = await updatePrivateMessage(alpha, pmRes.message.id);
+  expect(pmUpdated.message.content).toBe(updatedContent);
+
+  let betaPms = await listPrivateMessages(beta);
+  expect(betaPms.messages[0].content).toBe(updatedContent);
+});
+
+test('Delete a private message', async () => {
+  let pmRes = await createPrivateMessage(alpha, recipient_id);
+  let betaPms1 = await listPrivateMessages(beta);
+  let deletedPmRes = await deletePrivateMessage(alpha, true, pmRes.message.id);
+  expect(deletedPmRes.message.deleted).toBe(true);
+
+  // The GetPrivateMessages filters out deleted,
+  // even though they are in the actual database.
+  // no reason to show them
+  let betaPms2 = await listPrivateMessages(beta);
+  expect(betaPms2.messages.length).toBe(betaPms1.messages.length - 1);
+
+  // Undelete
+  let undeletedPmRes = await deletePrivateMessage(
+    alpha,
+    false,
+    pmRes.message.id
+  );
+  expect(undeletedPmRes.message.deleted).toBe(false);
+
+  let betaPms3 = await listPrivateMessages(beta);
+  expect(betaPms3.messages.length).toBe(betaPms1.messages.length);
+});
diff --git a/ui/src/api_tests/shared.ts b/ui/src/api_tests/shared.ts
new file mode 100644 (file)
index 0000000..08c4ff2
--- /dev/null
@@ -0,0 +1,675 @@
+import fetch from 'node-fetch';
+
+import {
+  LoginForm,
+  LoginResponse,
+  Post,
+  PostForm,
+  Comment,
+  DeletePostForm,
+  RemovePostForm,
+  StickyPostForm,
+  LockPostForm,
+  PostResponse,
+  SearchResponse,
+  FollowCommunityForm,
+  CommunityResponse,
+  GetFollowedCommunitiesResponse,
+  GetPostResponse,
+  CommentForm,
+  DeleteCommentForm,
+  RemoveCommentForm,
+  CommentResponse,
+  CommunityForm,
+  DeleteCommunityForm,
+  RemoveCommunityForm,
+  CommentLikeForm,
+  CreatePostLikeForm,
+  PrivateMessageForm,
+  EditPrivateMessageForm,
+  DeletePrivateMessageForm,
+  PrivateMessageResponse,
+  PrivateMessagesResponse,
+  GetUserMentionsResponse,
+} from '../interfaces';
+
+export interface API {
+  url: string;
+  auth?: string;
+}
+
+function apiUrl(api: API) {
+  return `${api.url}/api/v1`;
+}
+
+export let alpha: API = {
+  url: 'http://localhost:8540',
+};
+
+export let beta: API = {
+  url: 'http://localhost:8550',
+};
+
+export let gamma: API = {
+  url: 'http://localhost:8560',
+};
+
+export async function setupLogins() {
+  let form: LoginForm = {
+    username_or_email: 'lemmy_alpha',
+    password: 'lemmy',
+  };
+
+  let resA: Promise<LoginResponse> = fetch(`${apiUrl(alpha)}/user/login`, {
+    method: 'POST',
+    headers: {
+      'Content-Type': 'application/json',
+    },
+    body: wrapper(form),
+  }).then(d => d.json());
+
+  let formB = {
+    username_or_email: 'lemmy_beta',
+    password: 'lemmy',
+  };
+
+  let resB: Promise<LoginResponse> = fetch(`${apiUrl(beta)}/user/login`, {
+    method: 'POST',
+    headers: {
+      'Content-Type': 'application/json',
+    },
+    body: wrapper(formB),
+  }).then(d => d.json());
+
+  let formC = {
+    username_or_email: 'lemmy_gamma',
+    password: 'lemmy',
+  };
+
+  let resG: Promise<LoginResponse> = fetch(`${apiUrl(gamma)}/user/login`, {
+    method: 'POST',
+    headers: {
+      'Content-Type': 'application/json',
+    },
+    body: wrapper(formC),
+  }).then(d => d.json());
+
+  let res = await Promise.all([resA, resB, resG]);
+  alpha.auth = res[0].jwt;
+  beta.auth = res[1].jwt;
+  gamma.auth = res[2].jwt;
+}
+
+export async function createPost(
+  api: API,
+  community_id: number
+): Promise<PostResponse> {
+  let name = 'A jest test post';
+  let postForm: PostForm = {
+    name,
+    auth: api.auth,
+    community_id,
+    nsfw: false,
+  };
+
+  let createPostRes: PostResponse = await fetch(`${apiUrl(api)}/post`, {
+    method: 'POST',
+    headers: {
+      'Content-Type': 'application/json',
+    },
+    body: wrapper(postForm),
+  }).then(d => d.json());
+  return createPostRes;
+}
+
+export async function updatePost(api: API, post: Post): Promise<PostResponse> {
+  let name = 'A jest test federated post, updated';
+  let postForm: PostForm = {
+    name,
+    edit_id: post.id,
+    auth: api.auth,
+    nsfw: false,
+  };
+
+  let updateResponse: PostResponse = await fetch(`${apiUrl(api)}/post`, {
+    method: 'PUT',
+    headers: {
+      'Content-Type': 'application/json',
+    },
+    body: wrapper(postForm),
+  }).then(d => d.json());
+  return updateResponse;
+}
+
+export async function deletePost(
+  api: API,
+  deleted: boolean,
+  post: Post
+): Promise<PostResponse> {
+  let deletePostForm: DeletePostForm = {
+    edit_id: post.id,
+    deleted: deleted,
+    auth: api.auth,
+  };
+
+  let deletePostRes: PostResponse = await fetch(`${apiUrl(api)}/post/delete`, {
+    method: 'POST',
+    headers: {
+      'Content-Type': 'application/json',
+    },
+    body: wrapper(deletePostForm),
+  }).then(d => d.json());
+  return deletePostRes;
+}
+
+export async function removePost(
+  api: API,
+  removed: boolean,
+  post: Post
+): Promise<PostResponse> {
+  let removePostForm: RemovePostForm = {
+    edit_id: post.id,
+    removed,
+    auth: api.auth,
+  };
+
+  let removePostRes: PostResponse = await fetch(`${apiUrl(api)}/post/remove`, {
+    method: 'POST',
+    headers: {
+      'Content-Type': 'application/json',
+    },
+    body: wrapper(removePostForm),
+  }).then(d => d.json());
+  return removePostRes;
+}
+
+export async function stickyPost(
+  api: API,
+  stickied: boolean,
+  post: Post
+): Promise<PostResponse> {
+  let stickyPostForm: StickyPostForm = {
+    edit_id: post.id,
+    stickied,
+    auth: api.auth,
+  };
+
+  let stickyRes: PostResponse = await fetch(`${apiUrl(api)}/post/sticky`, {
+    method: 'POST',
+    headers: {
+      'Content-Type': 'application/json',
+    },
+    body: wrapper(stickyPostForm),
+  }).then(d => d.json());
+
+  return stickyRes;
+}
+
+export async function lockPost(
+  api: API,
+  locked: boolean,
+  post: Post
+): Promise<PostResponse> {
+  let lockPostForm: LockPostForm = {
+    edit_id: post.id,
+    locked,
+    auth: api.auth,
+  };
+
+  let lockRes: PostResponse = await fetch(`${apiUrl(api)}/post/lock`, {
+    method: 'POST',
+    headers: {
+      'Content-Type': 'application/json',
+    },
+    body: wrapper(lockPostForm),
+  }).then(d => d.json());
+
+  return lockRes;
+}
+
+export async function searchPost(
+  api: API,
+  post: Post
+): Promise<SearchResponse> {
+  let searchUrl = `${apiUrl(api)}/search?q=${post.ap_id}&type_=All&sort=TopAll`;
+  let searchResponse: SearchResponse = await fetch(searchUrl, {
+    method: 'GET',
+  }).then(d => d.json());
+  return searchResponse;
+}
+
+export async function getPost(
+  api: API,
+  post_id: number
+): Promise<GetPostResponse> {
+  let getPostUrl = `${apiUrl(api)}/post?id=${post_id}`;
+  let getPostRes: GetPostResponse = await fetch(getPostUrl, {
+    method: 'GET',
+  }).then(d => d.json());
+
+  return getPostRes;
+}
+
+export async function searchComment(
+  api: API,
+  comment: Comment
+): Promise<SearchResponse> {
+  let searchUrl = `${apiUrl(api)}/search?q=${
+    comment.ap_id
+  }&type_=All&sort=TopAll`;
+  let searchResponse: SearchResponse = await fetch(searchUrl, {
+    method: 'GET',
+  }).then(d => d.json());
+  return searchResponse;
+}
+
+export async function searchForBetaCommunity(
+  api: API
+): Promise<SearchResponse> {
+  // Make sure lemmy-beta/c/main is cached on lemmy_alpha
+  // Use short-hand search url
+  let searchUrl = `${apiUrl(
+    api
+  )}/search?q=!main@lemmy-beta:8550&type_=All&sort=TopAll`;
+
+  let searchResponse: SearchResponse = await fetch(searchUrl, {
+    method: 'GET',
+  }).then(d => d.json());
+  return searchResponse;
+}
+
+export async function followCommunity(
+  api: API,
+  follow: boolean,
+  community_id: number
+): Promise<CommunityResponse> {
+  let followForm: FollowCommunityForm = {
+    community_id,
+    follow,
+    auth: api.auth,
+  };
+
+  let followRes: CommunityResponse = await fetch(
+    `${apiUrl(api)}/community/follow`,
+    {
+      method: 'POST',
+      headers: {
+        'Content-Type': 'application/json',
+      },
+      body: wrapper(followForm),
+    }
+  )
+    .then(d => d.json())
+    .catch(_e => {});
+
+  return followRes;
+}
+
+export async function checkFollowedCommunities(
+  api: API
+): Promise<GetFollowedCommunitiesResponse> {
+  let followedCommunitiesUrl = `${apiUrl(
+    api
+  )}/user/followed_communities?&auth=${api.auth}`;
+  let followedCommunitiesRes: GetFollowedCommunitiesResponse = await fetch(
+    followedCommunitiesUrl,
+    {
+      method: 'GET',
+    }
+  ).then(d => d.json());
+  return followedCommunitiesRes;
+}
+
+export async function likePost(
+  api: API,
+  score: number,
+  post: Post
+): Promise<PostResponse> {
+  let likePostForm: CreatePostLikeForm = {
+    post_id: post.id,
+    score: score,
+    auth: api.auth,
+  };
+
+  let likePostRes: PostResponse = await fetch(`${apiUrl(api)}/post/like`, {
+    method: 'POST',
+    headers: {
+      'Content-Type': 'application/json',
+    },
+    body: wrapper(likePostForm),
+  }).then(d => d.json());
+
+  return likePostRes;
+}
+
+export async function createComment(
+  api: API,
+  post_id: number,
+  parent_id?: number,
+  content = 'a jest test comment'
+): Promise<CommentResponse> {
+  let commentForm: CommentForm = {
+    content,
+    post_id,
+    parent_id,
+    auth: api.auth,
+  };
+
+  let createResponse: CommentResponse = await fetch(`${apiUrl(api)}/comment`, {
+    method: 'POST',
+    headers: {
+      'Content-Type': 'application/json',
+    },
+    body: wrapper(commentForm),
+  }).then(d => d.json());
+  return createResponse;
+}
+
+export async function updateComment(
+  api: API,
+  edit_id: number,
+  content = 'A jest test federated comment update'
+): Promise<CommentResponse> {
+  let commentForm: CommentForm = {
+    content,
+    edit_id,
+    auth: api.auth,
+  };
+
+  let updateResponse: CommentResponse = await fetch(`${apiUrl(api)}/comment`, {
+    method: 'PUT',
+    headers: {
+      'Content-Type': 'application/json',
+    },
+    body: wrapper(commentForm),
+  }).then(d => d.json());
+  return updateResponse;
+}
+
+export async function deleteComment(
+  api: API,
+  deleted: boolean,
+  edit_id: number
+): Promise<CommentResponse> {
+  let deleteCommentForm: DeleteCommentForm = {
+    edit_id,
+    deleted,
+    auth: api.auth,
+  };
+
+  let deleteCommentRes: CommentResponse = await fetch(
+    `${apiUrl(api)}/comment/delete`,
+    {
+      method: 'POST',
+      headers: {
+        'Content-Type': 'application/json',
+      },
+      body: wrapper(deleteCommentForm),
+    }
+  ).then(d => d.json());
+  return deleteCommentRes;
+}
+
+export async function removeComment(
+  api: API,
+  removed: boolean,
+  edit_id: number
+): Promise<CommentResponse> {
+  let removeCommentForm: RemoveCommentForm = {
+    edit_id,
+    removed,
+    auth: api.auth,
+  };
+
+  let removeCommentRes: CommentResponse = await fetch(
+    `${apiUrl(api)}/comment/remove`,
+    {
+      method: 'POST',
+      headers: {
+        'Content-Type': 'application/json',
+      },
+      body: wrapper(removeCommentForm),
+    }
+  ).then(d => d.json());
+  return removeCommentRes;
+}
+
+export async function getMentions(api: API): Promise<GetUserMentionsResponse> {
+  let getMentionUrl = `${apiUrl(
+    api
+  )}/user/mention?sort=New&unread_only=false&auth=${api.auth}`;
+  let getMentionsRes: GetUserMentionsResponse = await fetch(getMentionUrl, {
+    method: 'GET',
+  }).then(d => d.json());
+  return getMentionsRes;
+}
+
+export async function likeComment(
+  api: API,
+  score: number,
+  comment: Comment
+): Promise<CommentResponse> {
+  let likeCommentForm: CommentLikeForm = {
+    comment_id: comment.id,
+    score,
+    auth: api.auth,
+  };
+
+  let likeCommentRes: CommentResponse = await fetch(
+    `${apiUrl(api)}/comment/like`,
+    {
+      method: 'POST',
+      headers: {
+        'Content-Type': 'application/json',
+      },
+      body: wrapper(likeCommentForm),
+    }
+  ).then(d => d.json());
+  return likeCommentRes;
+}
+
+export async function createCommunity(
+  api: API,
+  name_: string = randomString(5)
+): Promise<CommunityResponse> {
+  let communityForm: CommunityForm = {
+    name: name_,
+    title: name_,
+    category_id: 1,
+    nsfw: false,
+    auth: api.auth,
+  };
+
+  let createCommunityRes: CommunityResponse = await fetch(
+    `${apiUrl(api)}/community`,
+    {
+      method: 'POST',
+      headers: {
+        'Content-Type': 'application/json',
+      },
+      body: wrapper(communityForm),
+    }
+  ).then(d => d.json());
+  return createCommunityRes;
+}
+
+export async function deleteCommunity(
+  api: API,
+  deleted: boolean,
+  edit_id: number
+): Promise<CommunityResponse> {
+  let deleteCommunityForm: DeleteCommunityForm = {
+    edit_id,
+    deleted,
+    auth: api.auth,
+  };
+
+  let deleteResponse: CommunityResponse = await fetch(
+    `${apiUrl(api)}/community/delete`,
+    {
+      method: 'POST',
+      headers: {
+        'Content-Type': 'application/json',
+      },
+      body: wrapper(deleteCommunityForm),
+    }
+  ).then(d => d.json());
+  return deleteResponse;
+}
+
+export async function removeCommunity(
+  api: API,
+  removed: boolean,
+  edit_id: number
+): Promise<CommunityResponse> {
+  let removeCommunityForm: RemoveCommunityForm = {
+    edit_id,
+    removed,
+    auth: api.auth,
+  };
+
+  let removeResponse: CommunityResponse = await fetch(
+    `${apiUrl(api)}/community/remove`,
+    {
+      method: 'POST',
+      headers: {
+        'Content-Type': 'application/json',
+      },
+      body: wrapper(removeCommunityForm),
+    }
+  ).then(d => d.json());
+  return removeResponse;
+}
+
+export async function createPrivateMessage(
+  api: API,
+  recipient_id: number
+): Promise<PrivateMessageResponse> {
+  let content = 'A jest test federated private message';
+  let privateMessageForm: PrivateMessageForm = {
+    content,
+    recipient_id,
+    auth: api.auth,
+  };
+
+  let createRes: PrivateMessageResponse = await fetch(
+    `${apiUrl(api)}/private_message`,
+    {
+      method: 'POST',
+      headers: {
+        'Content-Type': 'application/json',
+      },
+      body: wrapper(privateMessageForm),
+    }
+  ).then(d => d.json());
+  return createRes;
+}
+
+export async function updatePrivateMessage(
+  api: API,
+  edit_id: number
+): Promise<PrivateMessageResponse> {
+  let updatedContent = 'A jest test federated private message edited';
+  let updatePrivateMessageForm: EditPrivateMessageForm = {
+    content: updatedContent,
+    edit_id,
+    auth: api.auth,
+  };
+
+  let updateRes: PrivateMessageResponse = await fetch(
+    `${apiUrl(api)}/private_message`,
+    {
+      method: 'PUT',
+      headers: {
+        'Content-Type': 'application/json',
+      },
+      body: wrapper(updatePrivateMessageForm),
+    }
+  ).then(d => d.json());
+  return updateRes;
+}
+
+export async function deletePrivateMessage(
+  api: API,
+  deleted: boolean,
+  edit_id: number
+): Promise<PrivateMessageResponse> {
+  let deletePrivateMessageForm: DeletePrivateMessageForm = {
+    deleted,
+    edit_id,
+    auth: api.auth,
+  };
+
+  let deleteRes: PrivateMessageResponse = await fetch(
+    `${apiUrl(api)}/private_message/delete`,
+    {
+      method: 'POST',
+      headers: {
+        'Content-Type': 'application/json',
+      },
+      body: wrapper(deletePrivateMessageForm),
+    }
+  ).then(d => d.json());
+
+  return deleteRes;
+}
+
+export async function listPrivateMessages(
+  api: API
+): Promise<PrivateMessagesResponse> {
+  let getPrivateMessagesUrl = `${apiUrl(api)}/private_message/list?auth=${
+    api.auth
+  }&unread_only=false&limit=999`;
+
+  let getPrivateMessagesRes: PrivateMessagesResponse = await fetch(
+    getPrivateMessagesUrl,
+    {
+      method: 'GET',
+    }
+  ).then(d => d.json());
+  return getPrivateMessagesRes;
+}
+
+export async function unfollowRemotes(
+  api: API
+): Promise<GetFollowedCommunitiesResponse> {
+  // Unfollow all remote communities
+  let followed = await checkFollowedCommunities(api);
+  let remoteFollowed = followed.communities.filter(
+    c => c.community_local == false
+  );
+  for (let cu of remoteFollowed) {
+    await followCommunity(api, false, cu.community_id);
+  }
+  let followed2 = await checkFollowedCommunities(api);
+  return followed2;
+}
+
+export async function followBeta(api: API): Promise<CommunityResponse> {
+  await unfollowRemotes(api);
+
+  // Cache it
+  let search = await searchForBetaCommunity(api);
+
+  // Unfollow first
+  let follow = await followCommunity(
+    api,
+    true,
+    search.communities.filter(c => c.local == false)[0].id
+  );
+  return follow;
+}
+
+export function wrapper(form: any): string {
+  return JSON.stringify(form);
+}
+
+function randomString(length: number): string {
+  var result = '';
+  var characters = 'abcdefghijklmnopqrstuvwxyz0123456789_';
+  var charactersLength = characters.length;
+  for (var i = 0; i < length; i++) {
+    result += characters.charAt(Math.floor(Math.random() * charactersLength));
+  }
+  return result;
+}