diff --git a/docker/federation-test/servers.sh b/docker/federation-test/servers.sh index 36f10cd82..b34e8c4ef 100755 --- a/docker/federation-test/servers.sh +++ b/docker/federation-test/servers.sh @@ -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/ diff --git a/docker/federation/docker-compose.yml b/docker/federation/docker-compose.yml index c552d18fd..4e087d104 100644 --- a/docker/federation/docker-compose.yml +++ b/docker/federation/docker-compose.yml @@ -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: diff --git a/docker/travis/docker-compose.yml b/docker/travis/docker-compose.yml index 5248553a4..03b3a7ecf 100644 --- a/docker/travis/docker-compose.yml +++ b/docker/travis/docker-compose.yml @@ -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: diff --git a/server/lemmy_db/src/comment.rs b/server/lemmy_db/src/comment.rs index de6904133..99efde8d7 100644 --- a/server/lemmy_db/src/comment.rs +++ b/server/lemmy_db/src/comment.rs @@ -116,7 +116,10 @@ impl Comment { ) -> Result { 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::(conn) } @@ -127,7 +130,10 @@ impl Comment { ) -> Result { 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::(conn) } diff --git a/server/lemmy_db/src/community.rs b/server/lemmy_db/src/community.rs index c4930d793..2c86f1e75 100644 --- a/server/lemmy_db/src/community.rs +++ b/server/lemmy_db/src/community.rs @@ -107,7 +107,10 @@ impl Community { ) -> Result { 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::(conn) } @@ -118,7 +121,10 @@ impl Community { ) -> Result { 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::(conn) } diff --git a/server/lemmy_db/src/post.rs b/server/lemmy_db/src/post.rs index 5eb9a4723..56ff7474b 100644 --- a/server/lemmy_db/src/post.rs +++ b/server/lemmy_db/src/post.rs @@ -119,7 +119,10 @@ impl Post { ) -> Result { 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::(conn) } @@ -130,7 +133,10 @@ impl Post { ) -> Result { 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::(conn) } diff --git a/server/src/api/comment.rs b/server/src/api/comment.rs index 7dfce473f..e3189d434 100644 --- a/server/src/api/comment.rs +++ b/server/src/api/comment.rs @@ -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 { websocket_info: Option, ) -> Result { 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 { 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 { 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 { 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 { 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 { websocket_info: Option, ) -> Result { 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 { 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 { websocket_info: Option, ) -> Result { 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 { // 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 { websocket_info: Option, ) -> Result { 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 { // 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 { // 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 { _websocket_info: Option, ) -> Result { 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 { 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 { // 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 { _websocket_info: Option, ) -> Result { 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 { } 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 { websocket_info: Option, ) -> Result { 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 { 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 { 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 { 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 { // 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 { websocket_info: Option, ) -> Result { let data: &GetComments = &self.data; - - let user_claims: Option = 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)?; diff --git a/server/src/api/community.rs b/server/src/api/community.rs index e4a8b6e8e..904dfe53c 100644 --- a/server/src/api/community.rs +++ b/server/src/api/community.rs @@ -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 { websocket_info: Option, ) -> Result { let data: &GetCommunity = &self.data; - - let user_id: Option = 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 { _websocket_info: Option, ) -> Result { 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 { 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 { 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 { 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 { 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 { websocket_info: Option, ) -> Result { 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 { .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 { // 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 { websocket_info: Option, ) -> Result { 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 { } 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 { websocket_info: Option, ) -> Result { 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 { 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 { } 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 { _websocket_info: Option, ) -> Result { let data: &ListCommunities = &self.data; - - // For logged in users, you need to get back subscribed, and settings - let user: Option = 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 { _websocket_info: Option, ) -> Result { 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 { 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 { _websocket_info: Option, ) -> Result { 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 { websocket_info: Option, ) -> Result { 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 { }; 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 { websocket_info: Option, ) -> Result { 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 { 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 { // 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 { _websocket_info: Option, ) -> Result { 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 { 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 { // 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 { 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)) }) diff --git a/server/src/api/mod.rs b/server/src/api/mod.rs index 11f958f08..a9aae823a 100644 --- a/server/src/api/mod.rs +++ b/server/src/api/mod.rs @@ -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 { + 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, + pool: &DbPool, +) -> Result, 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) -> 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(()) + } +} diff --git a/server/src/api/post.rs b/server/src/api/post.rs index e346a6c89..b43e4e55c 100644 --- a/server/src/api/post.rs +++ b/server/src/api/post.rs @@ -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 { websocket_info: Option, ) -> Result { 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 { 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 { // 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 { // 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 { websocket_info: Option, ) -> Result { let data: &GetPost = &self.data; - - let user_id: Option = 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 { websocket_info: Option, ) -> Result { let data: &GetPosts = &self.data; - - // For logged in users, you need to get back subscribed, and settings - let user: Option = 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 { websocket_info: Option, ) -> Result { 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 { 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 { } 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 { websocket_info: Option, ) -> Result { 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 { 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 { websocket_info: Option, ) -> Result { 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 { // 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 { websocket_info: Option, ) -> Result { 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 { // 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 { // 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 { websocket_info: Option, ) -> Result { 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 { // 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 { // 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 { websocket_info: Option, ) -> Result { 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 { // 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 { // 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 { _websocket_info: Option, ) -> Result { 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 { } 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)) }) diff --git a/server/src/api/site.rs b/server/src/api/site.rs index adade080e..82cad9610 100644 --- a/server/src/api/site.rs +++ b/server/src/api/site.rs @@ -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 { ) -> Result { 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()), - }; + 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(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 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 { websocket_info: Option, ) -> Result { 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 { 0 }; - // Giving back your user, if you're logged in - let my_user: Option = 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 { Err(e) => debug!("Failed to resolve search query as activitypub ID: {}", e), } - let user_id: Option = 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 { _websocket_info: Option, ) -> Result { 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 { 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 { // 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 { _websocket_info: Option, ) -> Result { 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 { _websocket_info: Option, ) -> Result { 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 = 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()); } diff --git a/server/src/api/user.rs b/server/src/api/user.rs index f9a92cd39..d6746c1a8 100644 --- a/server/src/api/user.rs +++ b/server/src/api/user.rs @@ -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 { }; } - 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 { _websocket_info: Option, ) -> Result { 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 { _websocket_info: Option, ) -> Result { let data: &GetUserDetails = &self.data; - - // For logged in users, you need to get back subscribed, and settings - let user: Option = 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 { 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 { websocket_info: Option, ) -> Result { 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 { // 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 { websocket_info: Option, ) -> Result { 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 { }; 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 { _websocket_info: Option, ) -> Result { 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 { _websocket_info: Option, ) -> Result { 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 { _websocket_info: Option, ) -> Result { 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 { }; 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 { _websocket_info: Option, ) -> Result { 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 { _websocket_info: Option, ) -> Result { 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 { } // 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 { websocket_info: Option, ) -> Result { 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 { websocket_info: Option, ) -> Result { 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 { websocket_info: Option, ) -> Result { 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 { websocket_info: Option, ) -> Result { 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 { _websocket_info: Option, ) -> Result { 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 { async fn perform( &self, - _pool: &DbPool, + pool: &DbPool, websocket_info: Option, ) -> Result { 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 }) } } diff --git a/server/src/apub/activities.rs b/server/src/apub/activities.rs index 9fdfe37fb..b5d6ce464 100644 --- a/server/src/apub/activities.rs +++ b/server/src/apub/activities.rs @@ -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"); diff --git a/server/src/apub/fetcher.rs b/server/src/apub/fetcher.rs index 4425757de..919b0e884 100644 --- a/server/src/apub/fetcher.rs +++ b/server/src/apub/fetcher.rs @@ -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( 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); diff --git a/server/src/apub/inbox/activities/create.rs b/server/src/apub/inbox/activities/create.rs index f8a92c1c6..ceeef0ef7 100644 --- a/server/src/apub/inbox/activities/create.rs +++ b/server/src/apub/inbox/activities/create.rs @@ -39,7 +39,6 @@ pub async fn receive_create( chat_server: ChatServerParam, ) -> Result { 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, diff --git a/server/src/apub/inbox/community_inbox.rs b/server/src/apub/inbox/community_inbox.rs index 69dd2cdf7..37e7c2895 100644 --- a/server/src/apub/inbox/community_inbox.rs +++ b/server/src/apub/inbox/community_inbox.rs @@ -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 { - 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; /// Handler for all incoming activities to community inboxes. pub async fn community_inbox( request: HttpRequest, - input: web::Json, + input: web::Json, path: web::Path, db: DbPoolParam, client: web::Data, _chat_server: ChatServerParam, ) -> Result { - 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 { - 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 { - 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, diff --git a/server/src/apub/inbox/shared_inbox.rs b/server/src/apub/inbox/shared_inbox.rs index 9e0cdb3d8..db44a99da 100644 --- a/server/src/apub/inbox/shared_inbox.rs +++ b/server/src/apub/inbox/shared_inbox.rs @@ -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(activity: &T) -> Url +where + T: AsBase + ActorAndObjectRef + AsObject, +{ + 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( activity: T, user: &User_, diff --git a/server/src/apub/inbox/user_inbox.rs b/server/src/apub/inbox/user_inbox.rs index b46f67027..494fd9f5b 100644 --- a/server/src/apub/inbox/user_inbox.rs +++ b/server/src/apub/inbox/user_inbox.rs @@ -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), - Create(Box), - Update(Box), - Delete(Box), - Undo(Box), +#[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; + /// Handler for all incoming activities to user inboxes. pub async fn user_inbox( request: HttpRequest, - input: web::Json, + input: web::Json, path: web::Path, client: web::Data, - db: DbPoolParam, + pool: DbPoolParam, chat_server: ChatServerParam, ) -> Result { - // 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); - 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 + 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())?; + + 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 { + 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 { - 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(¬e, 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 { - 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(¬e, 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 { - 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(¬e, 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 { + 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(¬e, client, pool).await?; diff --git a/server/src/apub/mod.rs b/server/src/apub/mod.rs index e86032f61..7f39afc75 100644 --- a/server/src/apub/mod.rs +++ b/server/src/apub/mod.rs @@ -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 = Settings::get() + let mut allowed_instances: Vec = 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::>(); + 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()), } } diff --git a/ui/package.json b/ui/package.json index aa803aa4a..4ba3c78b1 100644 --- a/ui/package.json +++ b/ui/package.json @@ -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 index 9f498f8b1..000000000 --- a/ui/src/api_tests/api.spec.ts +++ /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 index 000000000..8852a730d --- /dev/null +++ b/ui/src/api_tests/comment.spec.ts @@ -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 index 000000000..6945e3323 --- /dev/null +++ b/ui/src/api_tests/community.spec.ts @@ -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 index 000000000..2f1f8cd89 --- /dev/null +++ b/ui/src/api_tests/follow.spec.ts @@ -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 index 000000000..f2cb6678e --- /dev/null +++ b/ui/src/api_tests/post.spec.ts @@ -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 index 000000000..4bf3f07a2 --- /dev/null +++ b/ui/src/api_tests/private_message.spec.ts @@ -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 index 000000000..08c4ff23e --- /dev/null +++ b/ui/src/api_tests/shared.ts @@ -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 = 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 = 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 = 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + // 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + // 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 { + 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; +}