From 2016afc9dbdd305364bf2e70270682ad5bfdc82d Mon Sep 17 00:00:00 2001 From: Dessalines Date: Thu, 19 Aug 2021 16:54:15 -0400 Subject: [PATCH] User / community blocking. Fixes #426 (#1604) * A first pass at user / community blocking. #426 * Adding unit tests for person and community block. * Moving migration * Fixing creator_blocked for comment queries, added tests. * Don't let a person block themselves * Fix post creator_blocked * Adding creator_blocked to PersonMentionView * Moving blocked and follows to MyUserInfo * Rename to local_user_view * Add moderates to MyUserInfo * Adding BlockCommunityResponse * Fixing name, and check_person_block * Fixing tests. * Using type in Blockable trait. * Changing recipient to target, adding unfollow to block action. --- api_tests/package.json | 2 +- api_tests/src/comment.spec.ts | 4 +- api_tests/src/follow.spec.ts | 10 +- api_tests/src/shared.ts | 31 ++--- api_tests/src/user.spec.ts | 15 ++- api_tests/yarn.lock | 8 +- crates/api/src/comment.rs | 8 ++ crates/api/src/community.rs | 62 ++++++++++ crates/api/src/lib.rs | 7 +- crates/api/src/local_user.rs | 81 +++++++++---- crates/api/src/post.rs | 3 + crates/api/src/site.rs | 5 +- crates/api_common/src/community.rs | 12 +- crates/api_common/src/lib.rs | 15 +++ crates/api_common/src/person.rs | 17 ++- crates/api_common/src/site.rs | 20 +++- crates/api_crud/src/comment/create.rs | 7 ++ crates/api_crud/src/comment/update.rs | 1 + crates/api_crud/src/private_message/create.rs | 3 + crates/api_crud/src/site/read.rs | 53 ++++++++- crates/api_crud/src/user/read.rs | 11 -- crates/db_queries/src/lib.rs | 10 ++ .../db_queries/src/source/community_block.rs | 25 ++++ crates/db_queries/src/source/mod.rs | 2 + crates/db_queries/src/source/person_block.rs | 50 ++++++++ crates/db_schema/src/lib.rs | 6 + crates/db_schema/src/schema.rs | 25 ++++ .../db_schema/src/source/community_block.rs | 25 ++++ crates/db_schema/src/source/mod.rs | 2 + crates/db_schema/src/source/person_block.rs | 18 +++ crates/db_views/src/comment_view.rs | 96 ++++++++++++++- crates/db_views/src/post_view.rs | 111 ++++++++++++++++-- .../src/community_block_view.rs | 49 ++++++++ crates/db_views_actor/src/community_view.rs | 34 +++++- crates/db_views_actor/src/lib.rs | 2 + .../db_views_actor/src/person_block_view.rs | 46 ++++++++ .../db_views_actor/src/person_mention_view.rs | 25 +++- crates/websocket/src/lib.rs | 3 +- .../down.sql | 2 + .../up.sql | 15 +++ src/api_routes.rs | 6 +- 41 files changed, 808 insertions(+), 119 deletions(-) create mode 100644 crates/db_queries/src/source/community_block.rs create mode 100644 crates/db_queries/src/source/person_block.rs create mode 100644 crates/db_schema/src/source/community_block.rs create mode 100644 crates/db_schema/src/source/person_block.rs create mode 100644 crates/db_views_actor/src/community_block_view.rs create mode 100644 crates/db_views_actor/src/person_block_view.rs create mode 100644 migrations/2021-08-04-223559_create_user_community_block/down.sql create mode 100644 migrations/2021-08-04-223559_create_user_community_block/up.sql diff --git a/api_tests/package.json b/api_tests/package.json index b34737daa..e8e4cc091 100644 --- a/api_tests/package.json +++ b/api_tests/package.json @@ -16,7 +16,7 @@ "eslint": "^7.30.0", "eslint-plugin-jane": "^9.0.3", "jest": "^27.0.6", - "lemmy-js-client": "0.11.0-rc.3", + "lemmy-js-client": "0.11.4-rc.9", "node-fetch": "^2.6.1", "prettier": "^2.3.2", "ts-jest": "^27.0.3", diff --git a/api_tests/src/comment.spec.ts b/api_tests/src/comment.spec.ts index 68dbcf7c1..309cfd133 100644 --- a/api_tests/src/comment.spec.ts +++ b/api_tests/src/comment.spec.ts @@ -332,9 +332,9 @@ test('A and G subscribe to B (center) A posts, G mentions B, it gets announced t 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); + let site = await unfollowRemotes(alpha); expect( - followed.communities.filter(c => c.community.local == false).length + site.my_user.follows.filter(c => c.community.local == false).length ).toBe(0); // B creates a post, and two comments, should be invisible to A diff --git a/api_tests/src/follow.spec.ts b/api_tests/src/follow.spec.ts index 0749439c0..369a772a1 100644 --- a/api_tests/src/follow.spec.ts +++ b/api_tests/src/follow.spec.ts @@ -4,8 +4,8 @@ import { setupLogins, searchForBetaCommunity, followCommunity, - checkFollowedCommunities, unfollowRemotes, + getSite, } from './shared'; beforeAll(async () => { @@ -29,8 +29,8 @@ test('Follow federated community', async () => { expect(follow.community_view.community.name).toBe('main'); // Check it from local - let followCheck = await checkFollowedCommunities(alpha); - let remoteCommunityId = followCheck.communities.find( + let site = await getSite(alpha); + let remoteCommunityId = site.my_user.follows.find( c => c.community.local == false ).community.id; expect(remoteCommunityId).toBeDefined(); @@ -40,6 +40,6 @@ test('Follow federated community', async () => { expect(unfollow.community_view.community.local).toBe(false); // Make sure you are unsubbed locally - let unfollowCheck = await checkFollowedCommunities(alpha); - expect(unfollowCheck.communities.length).toBeGreaterThanOrEqual(1); + let siteUnfollowCheck = await getSite(alpha); + expect(siteUnfollowCheck.my_user.follows.length).toBeGreaterThanOrEqual(1); }); diff --git a/api_tests/src/shared.ts b/api_tests/src/shared.ts index df1624b8f..2de66c05b 100644 --- a/api_tests/src/shared.ts +++ b/api_tests/src/shared.ts @@ -12,7 +12,6 @@ import { SearchResponse, FollowCommunity, CommunityResponse, - GetFollowedCommunitiesResponse, GetPostResponse, Register, Comment, @@ -30,7 +29,6 @@ import { CreatePostLike, EditPrivateMessage, DeletePrivateMessage, - GetFollowedCommunities, GetPrivateMessages, GetSite, GetPost, @@ -336,15 +334,6 @@ export async function followCommunity( return api.client.followCommunity(form); } -export async function checkFollowedCommunities( - api: API -): Promise { - let form: GetFollowedCommunities = { - auth: api.auth, - }; - return api.client.getFollowedCommunities(form); -} - export async function likePost( api: API, score: number, @@ -543,8 +532,7 @@ export async function registerUser( } export async function saveUserSettingsBio( - api: API, - auth: string + api: API ): Promise { let form: SaveUserSettings = { show_nsfw: true, @@ -555,7 +543,7 @@ export async function saveUserSettingsBio( show_avatars: true, send_notifications_to_email: false, bio: 'a changed bio', - auth, + auth: api.auth, }; return saveUserSettings(api, form); } @@ -568,11 +556,10 @@ export async function saveUserSettings( } export async function getSite( - api: API, - auth: string + api: API ): Promise { let form: GetSite = { - auth, + auth: api.auth, }; return api.client.getSite(form); } @@ -590,17 +577,17 @@ export async function listPrivateMessages( export async function unfollowRemotes( api: API -): Promise { +): Promise { // Unfollow all remote communities - let followed = await checkFollowedCommunities(api); - let remoteFollowed = followed.communities.filter( + let site = await getSite(api); + let remoteFollowed = site.my_user.follows.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; + let siteRes = await getSite(api); + return siteRes; } export async function followBeta(api: API): Promise { diff --git a/api_tests/src/user.spec.ts b/api_tests/src/user.spec.ts index a10876cf5..acbe8fe15 100644 --- a/api_tests/src/user.spec.ts +++ b/api_tests/src/user.spec.ts @@ -14,12 +14,11 @@ import { ListingType, } from 'lemmy-js-client'; -let auth: string; let apShortname: string; function assertUserFederation(userOne: PersonViewSafe, userTwo: PersonViewSafe) { expect(userOne.person.name).toBe(userTwo.person.name); - expect(userOne.person.preferred_username).toBe(userTwo.person.preferred_username); + expect(userOne.person.display_name).toBe(userTwo.person.display_name); expect(userOne.person.bio).toBe(userTwo.person.bio); expect(userOne.person.actor_id).toBe(userTwo.person.actor_id); expect(userOne.person.avatar).toBe(userTwo.person.avatar); @@ -30,11 +29,11 @@ function assertUserFederation(userOne: PersonViewSafe, userTwo: PersonViewSafe) test('Create user', async () => { let userRes = await registerUser(alpha); expect(userRes.jwt).toBeDefined(); - auth = userRes.jwt; - - let site = await getSite(alpha, auth); + alpha.auth = userRes.jwt; + + let site = await getSite(alpha); expect(site.my_user).toBeDefined(); - apShortname = `@${site.my_user.person.name}@lemmy-alpha:8541`; + apShortname = `@${site.my_user.local_user_view.person.name}@lemmy-alpha:8541`; }); test('Set some user settings, check that they are federated', async () => { @@ -49,11 +48,11 @@ test('Set some user settings, check that they are federated', async () => { lang: '', avatar, banner, - preferred_username: 'user321', + display_name: 'user321', show_avatars: false, send_notifications_to_email: false, bio, - auth, + auth: alpha.auth, }; await saveUserSettings(alpha, form); diff --git a/api_tests/yarn.lock b/api_tests/yarn.lock index a5be0f695..370873b1e 100644 --- a/api_tests/yarn.lock +++ b/api_tests/yarn.lock @@ -3076,10 +3076,10 @@ language-tags@^1.0.5: dependencies: language-subtag-registry "~0.3.2" -lemmy-js-client@0.11.0-rc.3: - version "0.11.0-rc.3" - resolved "https://registry.yarnpkg.com/lemmy-js-client/-/lemmy-js-client-0.11.0-rc.3.tgz#dd4727ca4d16fe9593368725aacfd9e7a8d52548" - integrity sha512-16mgl+TS1+0UHiY+ZKPuqHfbrn93h8K8tJ+kKJ1pjT2WhG23o0B8dLahG1jDQPG+dkXpR6PZxYudMYjuO8WvjQ== +lemmy-js-client@0.11.4-rc.9: + version "0.11.4-rc.9" + resolved "https://registry.yarnpkg.com/lemmy-js-client/-/lemmy-js-client-0.11.4-rc.9.tgz#f7b3c73691e4c1600daf3840d22d9cfbddc5f363" + integrity sha512-zP8JxWzQU+yuyE8cMG0GzR8aR3lJ++G5zzbynsXwDevzAZXhOm0ObNNtJiA3Q5msStFVKVYa3GwZxBv4XiYshw== leven@^3.1.0: version "3.1.0" diff --git a/crates/api/src/comment.rs b/crates/api/src/comment.rs index 06510251f..00e136adb 100644 --- a/crates/api/src/comment.rs +++ b/crates/api/src/comment.rs @@ -4,6 +4,7 @@ use lemmy_api_common::{ blocking, check_community_ban, check_downvotes_enabled, + check_person_block, comment::*, get_local_user_view_from_jwt, }; @@ -151,6 +152,13 @@ impl Perform for CreateCommentLike { ) .await?; + check_person_block( + local_user_view.person.id, + orig_comment.get_recipient_id(), + context.pool(), + ) + .await?; + // Add parent user to recipients let recipient_id = orig_comment.get_recipient_id(); if let Ok(local_recipient) = blocking(context.pool(), move |conn| { diff --git a/crates/api/src/community.rs b/crates/api/src/community.rs index 18a4dc461..5aefd1443 100644 --- a/crates/api/src/community.rs +++ b/crates/api/src/community.rs @@ -18,6 +18,7 @@ use lemmy_apub::{ use lemmy_db_queries::{ source::{comment::Comment_, community::CommunityModerator_, post::Post_}, Bannable, + Blockable, Crud, Followable, Joinable, @@ -25,6 +26,7 @@ use lemmy_db_queries::{ use lemmy_db_schema::source::{ comment::Comment, community::*, + community_block::{CommunityBlock, CommunityBlockForm}, moderator::*, person::Person, post::Post, @@ -107,6 +109,66 @@ impl Perform for FollowCommunity { } } +#[async_trait::async_trait(?Send)] +impl Perform for BlockCommunity { + type Response = BlockCommunityResponse; + + async fn perform( + &self, + context: &Data, + _websocket_id: Option, + ) -> Result { + let data: &BlockCommunity = self; + let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?; + + let community_id = data.community_id; + let person_id = local_user_view.person.id; + let community_block_form = CommunityBlockForm { + person_id, + community_id, + }; + + if data.block { + let block = move |conn: &'_ _| CommunityBlock::block(conn, &community_block_form); + if blocking(context.pool(), block).await?.is_err() { + return Err(ApiError::err("community_block_already_exists").into()); + } + + // Also, unfollow the community, and send a federated unfollow + let community_follower_form = CommunityFollowerForm { + community_id: data.community_id, + person_id, + pending: false, + }; + blocking(context.pool(), move |conn: &'_ _| { + CommunityFollower::unfollow(conn, &community_follower_form) + }) + .await? + .ok(); + let community = blocking(context.pool(), move |conn| { + Community::read(conn, community_id) + }) + .await??; + UndoFollowCommunity::send(&local_user_view.person, &community, context).await?; + } else { + let unblock = move |conn: &'_ _| CommunityBlock::unblock(conn, &community_block_form); + if blocking(context.pool(), unblock).await?.is_err() { + return Err(ApiError::err("community_block_already_exists").into()); + } + } + + let community_view = blocking(context.pool(), move |conn| { + CommunityView::read(conn, community_id, Some(person_id)) + }) + .await??; + + Ok(BlockCommunityResponse { + blocked: data.block, + community_view, + }) + } +} + #[async_trait::async_trait(?Send)] impl Perform for BanFromCommunity { type Response = BanFromCommunityResponse; diff --git a/crates/api/src/lib.rs b/crates/api/src/lib.rs index 1c194b311..bf3a813b3 100644 --- a/crates/api/src/lib.rs +++ b/crates/api/src/lib.rs @@ -39,6 +39,9 @@ pub async fn match_websocket_operation( UserOperation::GetReplies => do_websocket_operation::(context, id, op, data).await, UserOperation::AddAdmin => do_websocket_operation::(context, id, op, data).await, UserOperation::BanPerson => do_websocket_operation::(context, id, op, data).await, + UserOperation::BlockPerson => { + do_websocket_operation::(context, id, op, data).await + } UserOperation::GetPersonMentions => { do_websocket_operation::(context, id, op, data).await } @@ -95,8 +98,8 @@ pub async fn match_websocket_operation( UserOperation::FollowCommunity => { do_websocket_operation::(context, id, op, data).await } - UserOperation::GetFollowedCommunities => { - do_websocket_operation::(context, id, op, data).await + UserOperation::BlockCommunity => { + do_websocket_operation::(context, id, op, data).await } UserOperation::BanFromCommunity => { do_websocket_operation::(context, id, op, data).await diff --git a/crates/api/src/local_user.rs b/crates/api/src/local_user.rs index 8eee3c1c4..510ef5adb 100644 --- a/crates/api/src/local_user.rs +++ b/crates/api/src/local_user.rs @@ -7,7 +7,6 @@ use chrono::Duration; use lemmy_api_common::{ blocking, collect_moderated_communities, - community::{GetFollowedCommunities, GetFollowedCommunitiesResponse}, get_local_user_view_from_jwt, is_admin, password_length_check, @@ -27,6 +26,7 @@ use lemmy_db_queries::{ post::Post_, private_message::PrivateMessage_, }, + Blockable, Crud, SortType, }; @@ -39,6 +39,7 @@ use lemmy_db_schema::{ moderator::*, password_reset_request::*, person::*, + person_block::{PersonBlock, PersonBlockForm}, person_mention::*, post::Post, private_message::PrivateMessage, @@ -52,7 +53,6 @@ use lemmy_db_views::{ post_report_view::PostReportView, }; use lemmy_db_views_actor::{ - community_follower_view::CommunityFollowerView, community_moderator_view::CommunityModeratorView, person_mention_view::{PersonMentionQueryBuilder, PersonMentionView}, person_view::PersonViewSafe, @@ -471,6 +471,59 @@ impl Perform for BanPerson { } } +#[async_trait::async_trait(?Send)] +impl Perform for BlockPerson { + type Response = BlockPersonResponse; + + async fn perform( + &self, + context: &Data, + _websocket_id: Option, + ) -> Result { + let data: &BlockPerson = self; + let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?; + + let target_id = data.person_id; + let person_id = local_user_view.person.id; + + // Don't let a person block themselves + if target_id == person_id { + return Err(ApiError::err("cant_block_yourself").into()); + } + + let person_block_form = PersonBlockForm { + person_id, + target_id, + }; + + if data.block { + let block = move |conn: &'_ _| PersonBlock::block(conn, &person_block_form); + if blocking(context.pool(), block).await?.is_err() { + return Err(ApiError::err("person_block_already_exists").into()); + } + } else { + let unblock = move |conn: &'_ _| PersonBlock::unblock(conn, &person_block_form); + if blocking(context.pool(), unblock).await?.is_err() { + return Err(ApiError::err("person_block_already_exists").into()); + } + } + + // TODO does any federated stuff need to be done here? + + let person_view = blocking(context.pool(), move |conn| { + PersonViewSafe::read(conn, target_id) + }) + .await??; + + let res = BlockPersonResponse { + person_view, + blocked: data.block, + }; + + Ok(res) + } +} + #[async_trait::async_trait(?Send)] impl Perform for GetReplies { type Response = GetRepliesResponse; @@ -778,27 +831,3 @@ impl Perform for GetReportCount { Ok(res) } } - -#[async_trait::async_trait(?Send)] -impl Perform for GetFollowedCommunities { - type Response = GetFollowedCommunitiesResponse; - - async fn perform( - &self, - context: &Data, - _websocket_id: Option, - ) -> Result { - let data: &GetFollowedCommunities = self; - let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?; - - let person_id = local_user_view.person.id; - let communities = blocking(context.pool(), move |conn| { - CommunityFollowerView::for_person(conn, person_id) - }) - .await? - .map_err(|_| ApiError::err("system_err_login"))?; - - // Return the jwt - Ok(GetFollowedCommunitiesResponse { communities }) - } -} diff --git a/crates/api/src/post.rs b/crates/api/src/post.rs index 1cb6c587e..f0f025e26 100644 --- a/crates/api/src/post.rs +++ b/crates/api/src/post.rs @@ -4,6 +4,7 @@ use lemmy_api_common::{ blocking, check_community_ban, check_downvotes_enabled, + check_person_block, get_local_user_view_from_jwt, is_mod_or_admin, mark_post_as_read, @@ -48,6 +49,8 @@ impl Perform for CreatePostLike { check_community_ban(local_user_view.person.id, post.community_id, context.pool()).await?; + check_person_block(local_user_view.person.id, post.creator_id, context.pool()).await?; + let like_form = PostLikeForm { post_id: data.post_id, person_id: local_user_view.person.id, diff --git a/crates/api/src/site.rs b/crates/api/src/site.rs index 7b6783a70..221b9870f 100644 --- a/crates/api/src/site.rs +++ b/crates/api/src/site.rs @@ -4,7 +4,6 @@ use anyhow::Context; use lemmy_api_common::{ blocking, build_federated_instances, - get_local_user_settings_view_from_jwt, get_local_user_view_from_jwt, get_local_user_view_from_jwt_opt, is_admin, @@ -422,15 +421,13 @@ impl Perform for TransferSite { let banned = blocking(context.pool(), move |conn| PersonViewSafe::banned(conn)).await??; let federated_instances = build_federated_instances(context.pool()).await?; - let my_user = Some(get_local_user_settings_view_from_jwt(&data.auth, context.pool()).await?); - Ok(GetSiteResponse { site_view: Some(site_view), admins, banned, online: 0, version: version::VERSION.to_string(), - my_user, + my_user: None, federated_instances, }) } diff --git a/crates/api_common/src/community.rs b/crates/api_common/src/community.rs index ab4e9d2a1..68ba2ff68 100644 --- a/crates/api_common/src/community.rs +++ b/crates/api_common/src/community.rs @@ -1,6 +1,5 @@ use lemmy_db_schema::{CommunityId, PersonId}; use lemmy_db_views_actor::{ - community_follower_view::CommunityFollowerView, community_moderator_view::CommunityModeratorView, community_view::CommunityView, person_view::PersonViewSafe, @@ -117,13 +116,16 @@ pub struct FollowCommunity { } #[derive(Deserialize)] -pub struct GetFollowedCommunities { +pub struct BlockCommunity { + pub community_id: CommunityId, + pub block: bool, pub auth: String, } -#[derive(Serialize)] -pub struct GetFollowedCommunitiesResponse { - pub communities: Vec, +#[derive(Serialize, Clone)] +pub struct BlockCommunityResponse { + pub community_view: CommunityView, + pub blocked: bool, } #[derive(Deserialize)] diff --git a/crates/api_common/src/lib.rs b/crates/api_common/src/lib.rs index 1f62a8f3e..e7d5181bc 100644 --- a/crates/api_common/src/lib.rs +++ b/crates/api_common/src/lib.rs @@ -10,6 +10,7 @@ use diesel::PgConnection; use lemmy_db_queries::{ source::{ community::{CommunityModerator_, Community_}, + person_block::PersonBlock_, site::Site_, }, Crud, @@ -21,6 +22,7 @@ use lemmy_db_schema::{ comment::Comment, community::{Community, CommunityModerator}, person::Person, + person_block::PersonBlock, person_mention::{PersonMention, PersonMentionForm}, post::{Post, PostRead, PostReadForm}, site::Site, @@ -353,6 +355,19 @@ pub async fn check_community_ban( } } +pub async fn check_person_block( + my_id: PersonId, + potential_blocker_id: PersonId, + pool: &DbPool, +) -> Result<(), LemmyError> { + let is_blocked = move |conn: &'_ _| PersonBlock::read(conn, potential_blocker_id, my_id).is_ok(); + if blocking(pool, is_blocked).await? { + Err(ApiError::err("person_block").into()) + } else { + Ok(()) + } +} + pub async fn check_downvotes_enabled(score: i16, pool: &DbPool) -> Result<(), LemmyError> { if score == -1 { let site = blocking(pool, move |conn| Site::read_simple(conn)).await??; diff --git a/crates/api_common/src/person.rs b/crates/api_common/src/person.rs index c6745c69d..a63e55d7e 100644 --- a/crates/api_common/src/person.rs +++ b/crates/api_common/src/person.rs @@ -4,7 +4,6 @@ use lemmy_db_views::{ private_message_view::PrivateMessageView, }; use lemmy_db_views_actor::{ - community_follower_view::CommunityFollowerView, community_moderator_view::CommunityModeratorView, person_mention_view::PersonMentionView, person_view::PersonViewSafe, @@ -96,10 +95,9 @@ pub struct GetPersonDetails { #[derive(Serialize)] pub struct GetPersonDetailsResponse { pub person_view: PersonViewSafe, - pub follows: Vec, - pub moderates: Vec, pub comments: Vec, pub posts: Vec, + pub moderates: Vec, } #[derive(Serialize)] @@ -145,6 +143,19 @@ pub struct BanPersonResponse { pub banned: bool, } +#[derive(Deserialize)] +pub struct BlockPerson { + pub person_id: PersonId, + pub block: bool, + pub auth: String, +} + +#[derive(Serialize, Clone)] +pub struct BlockPersonResponse { + pub person_view: PersonViewSafe, + pub blocked: bool, +} + #[derive(Deserialize)] pub struct GetReplies { pub sort: Option, diff --git a/crates/api_common/src/site.rs b/crates/api_common/src/site.rs index 796549860..af63854d3 100644 --- a/crates/api_common/src/site.rs +++ b/crates/api_common/src/site.rs @@ -5,7 +5,14 @@ use lemmy_db_views::{ post_view::PostView, site_view::SiteView, }; -use lemmy_db_views_actor::{community_view::CommunityView, person_view::PersonViewSafe}; +use lemmy_db_views_actor::{ + community_block_view::CommunityBlockView, + community_follower_view::CommunityFollowerView, + community_moderator_view::CommunityModeratorView, + community_view::CommunityView, + person_block_view::PersonBlockView, + person_view::PersonViewSafe, +}; use lemmy_db_views_moderator::{ mod_add_community_view::ModAddCommunityView, mod_add_view::ModAddView, @@ -110,10 +117,19 @@ pub struct GetSiteResponse { pub banned: Vec, pub online: usize, pub version: String, - pub my_user: Option, + pub my_user: Option, pub federated_instances: Option, // Federation may be disabled } +#[derive(Serialize)] +pub struct MyUserInfo { + pub local_user_view: LocalUserSettingsView, + pub follows: Vec, + pub moderates: Vec, + pub community_blocks: Vec, + pub person_blocks: Vec, +} + #[derive(Deserialize)] pub struct TransferSite { pub person_id: PersonId, diff --git a/crates/api_crud/src/comment/create.rs b/crates/api_crud/src/comment/create.rs index a479969a7..c25966c7d 100644 --- a/crates/api_crud/src/comment/create.rs +++ b/crates/api_crud/src/comment/create.rs @@ -3,6 +3,7 @@ use actix_web::web::Data; use lemmy_api_common::{ blocking, check_community_ban, + check_person_block, comment::*, get_local_user_view_from_jwt, get_post, @@ -49,6 +50,8 @@ impl PerformCrud for CreateComment { check_community_ban(local_user_view.person.id, community_id, context.pool()).await?; + check_person_block(local_user_view.person.id, post.creator_id, context.pool()).await?; + // Check if post is locked, no new comments if post.locked { return Err(ApiError::err("locked").into()); @@ -60,6 +63,10 @@ impl PerformCrud for CreateComment { let parent = blocking(context.pool(), move |conn| Comment::read(conn, parent_id)) .await? .map_err(|_| ApiError::err("couldnt_create_comment"))?; + + check_person_block(local_user_view.person.id, parent.creator_id, context.pool()).await?; + + // Strange issue where sometimes the post ID is incorrect if parent.post_id != post_id { return Err(ApiError::err("couldnt_create_comment").into()); } diff --git a/crates/api_crud/src/comment/update.rs b/crates/api_crud/src/comment/update.rs index ee2e7e28e..8a5e8f0ed 100644 --- a/crates/api_crud/src/comment/update.rs +++ b/crates/api_crud/src/comment/update.rs @@ -40,6 +40,7 @@ impl PerformCrud for EditComment { }) .await??; + // TODO is this necessary? It should really only need to check on create check_community_ban( local_user_view.person.id, orig_comment.community.id, diff --git a/crates/api_crud/src/private_message/create.rs b/crates/api_crud/src/private_message/create.rs index aa0bc8f4c..18ef3835d 100644 --- a/crates/api_crud/src/private_message/create.rs +++ b/crates/api_crud/src/private_message/create.rs @@ -2,6 +2,7 @@ use crate::PerformCrud; use actix_web::web::Data; use lemmy_api_common::{ blocking, + check_person_block, get_local_user_view_from_jwt, person::{CreatePrivateMessage, PrivateMessageResponse}, send_email_to_user, @@ -34,6 +35,8 @@ impl PerformCrud for CreatePrivateMessage { let content_slurs_removed = remove_slurs(&data.content.to_owned()); + check_person_block(local_user_view.person.id, data.recipient_id, context.pool()).await?; + let private_message_form = PrivateMessageForm { content: content_slurs_removed.to_owned(), creator_id: local_user_view.person.id, diff --git a/crates/api_crud/src/site/read.rs b/crates/api_crud/src/site/read.rs index 8b2df7117..cfbaf07a5 100644 --- a/crates/api_crud/src/site/read.rs +++ b/crates/api_crud/src/site/read.rs @@ -8,8 +8,14 @@ use lemmy_api_common::{ site::*, }; use lemmy_db_views::site_view::SiteView; -use lemmy_db_views_actor::person_view::PersonViewSafe; -use lemmy_utils::{settings::structs::Settings, version, ConnectionId, LemmyError}; +use lemmy_db_views_actor::{ + community_block_view::CommunityBlockView, + community_follower_view::CommunityFollowerView, + community_moderator_view::CommunityModeratorView, + person_block_view::PersonBlockView, + person_view::PersonViewSafe, +}; +use lemmy_utils::{settings::structs::Settings, version, ApiError, ConnectionId, LemmyError}; use lemmy_websocket::{messages::GetUsersOnline, LemmyContext}; use log::info; @@ -83,7 +89,48 @@ impl PerformCrud for GetSite { .await .unwrap_or(1); - let my_user = get_local_user_settings_view_from_jwt_opt(&data.auth, context.pool()).await?; + // Build the local user + let my_user = if let Some(local_user_view) = + get_local_user_settings_view_from_jwt_opt(&data.auth, context.pool()).await? + { + let person_id = local_user_view.person.id; + let follows = blocking(context.pool(), move |conn| { + CommunityFollowerView::for_person(conn, person_id) + }) + .await? + .map_err(|_| ApiError::err("system_err_login"))?; + + let person_id = local_user_view.person.id; + let community_blocks = blocking(context.pool(), move |conn| { + CommunityBlockView::for_person(conn, person_id) + }) + .await? + .map_err(|_| ApiError::err("system_err_login"))?; + + let person_id = local_user_view.person.id; + let person_blocks = blocking(context.pool(), move |conn| { + PersonBlockView::for_person(conn, person_id) + }) + .await? + .map_err(|_| ApiError::err("system_err_login"))?; + + let moderates = blocking(context.pool(), move |conn| { + CommunityModeratorView::for_person(conn, person_id) + }) + .await? + .map_err(|_| ApiError::err("system_err_login"))?; + + Some(MyUserInfo { + local_user_view, + follows, + moderates, + community_blocks, + person_blocks, + }) + } else { + None + }; + let federated_instances = build_federated_instances(context.pool()).await?; Ok(GetSiteResponse { diff --git a/crates/api_crud/src/user/read.rs b/crates/api_crud/src/user/read.rs index ff8db0a7e..4cfd90ab5 100644 --- a/crates/api_crud/src/user/read.rs +++ b/crates/api_crud/src/user/read.rs @@ -6,7 +6,6 @@ use lemmy_db_queries::{from_opt_str_to_opt_enum, ApubObject, SortType}; use lemmy_db_schema::source::person::*; use lemmy_db_views::{comment_view::CommentQueryBuilder, post_view::PostQueryBuilder}; use lemmy_db_views_actor::{ - community_follower_view::CommunityFollowerView, community_moderator_view::CommunityModeratorView, person_view::PersonViewSafe, }; @@ -103,15 +102,6 @@ impl PerformCrud for GetPersonDetails { }) .await??; - let mut follows = vec![]; - if let Some(pid) = person_id { - if pid == person_details_id { - follows = blocking(context.pool(), move |conn| { - CommunityFollowerView::for_person(conn, person_details_id) - }) - .await??; - } - }; let moderates = blocking(context.pool(), move |conn| { CommunityModeratorView::for_person(conn, person_details_id) }) @@ -120,7 +110,6 @@ impl PerformCrud for GetPersonDetails { // Return the jwt Ok(GetPersonDetailsResponse { person_view, - follows, moderates, comments, posts, diff --git a/crates/db_queries/src/lib.rs b/crates/db_queries/src/lib.rs index fb5b8cd5d..e62124b18 100644 --- a/crates/db_queries/src/lib.rs +++ b/crates/db_queries/src/lib.rs @@ -108,6 +108,16 @@ pub trait Saveable { Self: Sized; } +pub trait Blockable { + type Form; + fn block(conn: &PgConnection, form: &Self::Form) -> Result + where + Self: Sized; + fn unblock(conn: &PgConnection, form: &Self::Form) -> Result + where + Self: Sized; +} + pub trait Readable { type Form; fn mark_as_read(conn: &PgConnection, form: &Self::Form) -> Result diff --git a/crates/db_queries/src/source/community_block.rs b/crates/db_queries/src/source/community_block.rs new file mode 100644 index 000000000..346e7f3ac --- /dev/null +++ b/crates/db_queries/src/source/community_block.rs @@ -0,0 +1,25 @@ +use crate::Blockable; +use diesel::{dsl::*, result::Error, *}; +use lemmy_db_schema::source::community_block::{CommunityBlock, CommunityBlockForm}; + +impl Blockable for CommunityBlock { + type Form = CommunityBlockForm; + fn block(conn: &PgConnection, community_block_form: &Self::Form) -> Result { + use lemmy_db_schema::schema::community_block::dsl::*; + insert_into(community_block) + .values(community_block_form) + .on_conflict((person_id, community_id)) + .do_update() + .set(community_block_form) + .get_result::(conn) + } + fn unblock(conn: &PgConnection, community_block_form: &Self::Form) -> Result { + use lemmy_db_schema::schema::community_block::dsl::*; + diesel::delete( + community_block + .filter(person_id.eq(community_block_form.person_id)) + .filter(community_id.eq(community_block_form.community_id)), + ) + .execute(conn) + } +} diff --git a/crates/db_queries/src/source/mod.rs b/crates/db_queries/src/source/mod.rs index db928bd4f..8b82d31a7 100644 --- a/crates/db_queries/src/source/mod.rs +++ b/crates/db_queries/src/source/mod.rs @@ -2,10 +2,12 @@ pub mod activity; pub mod comment; pub mod comment_report; pub mod community; +pub mod community_block; pub mod local_user; pub mod moderator; pub mod password_reset_request; pub mod person; +pub mod person_block; pub mod person_mention; pub mod post; pub mod post_report; diff --git a/crates/db_queries/src/source/person_block.rs b/crates/db_queries/src/source/person_block.rs new file mode 100644 index 000000000..d4505519c --- /dev/null +++ b/crates/db_queries/src/source/person_block.rs @@ -0,0 +1,50 @@ +use crate::Blockable; +use diesel::{dsl::*, result::Error, *}; +use lemmy_db_schema::{ + source::person_block::{PersonBlock, PersonBlockForm}, + PersonId, +}; + +pub trait PersonBlock_ { + fn read( + conn: &PgConnection, + person_id: PersonId, + target_id: PersonId, + ) -> Result; +} + +impl PersonBlock_ for PersonBlock { + fn read( + conn: &PgConnection, + for_person_id: PersonId, + for_recipient_id: PersonId, + ) -> Result { + use lemmy_db_schema::schema::person_block::dsl::*; + person_block + .filter(person_id.eq(for_person_id)) + .filter(target_id.eq(for_recipient_id)) + .first::(conn) + } +} + +impl Blockable for PersonBlock { + type Form = PersonBlockForm; + fn block(conn: &PgConnection, person_block_form: &PersonBlockForm) -> Result { + use lemmy_db_schema::schema::person_block::dsl::*; + insert_into(person_block) + .values(person_block_form) + .on_conflict((person_id, target_id)) + .do_update() + .set(person_block_form) + .get_result::(conn) + } + fn unblock(conn: &PgConnection, person_block_form: &Self::Form) -> Result { + use lemmy_db_schema::schema::person_block::dsl::*; + diesel::delete( + person_block + .filter(person_id.eq(person_block_form.person_id)) + .filter(target_id.eq(person_block_form.target_id)), + ) + .execute(conn) + } +} diff --git a/crates/db_schema/src/lib.rs b/crates/db_schema/src/lib.rs index 3c8abaf79..ba49a4cd8 100644 --- a/crates/db_schema/src/lib.rs +++ b/crates/db_schema/src/lib.rs @@ -67,6 +67,12 @@ impl fmt::Display for PrivateMessageId { #[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, Serialize, Deserialize, DieselNewType)] pub struct PersonMentionId(i32); +#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, Serialize, Deserialize, DieselNewType)] +pub struct PersonBlockId(i32); + +#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, Serialize, Deserialize, DieselNewType)] +pub struct CommunityBlockId(i32); + #[repr(transparent)] #[derive(Clone, PartialEq, Serialize, Deserialize, Debug, AsExpression, FromSqlRow)] #[sql_type = "Text"] diff --git a/crates/db_schema/src/schema.rs b/crates/db_schema/src/schema.rs index ad1ce2b35..4c92fffba 100644 --- a/crates/db_schema/src/schema.rs +++ b/crates/db_schema/src/schema.rs @@ -465,6 +465,24 @@ table! { } } +table! { + person_block (id) { + id -> Int4, + person_id -> Int4, + target_id -> Int4, + published -> Timestamp, + } +} + +table! { + community_block (id) { + id -> Int4, + person_id -> Int4, + community_id -> Int4, + published -> Timestamp, + } +} + // These are necessary since diesel doesn't have self joins / aliases table! { comment_alias_1 (id) { @@ -542,6 +560,9 @@ joinable!(comment -> person_alias_1 (creator_id)); joinable!(post_report -> person_alias_2 (resolver_id)); joinable!(comment_report -> person_alias_2 (resolver_id)); +joinable!(person_block -> person (person_id)); +joinable!(person_block -> person_alias_1 (target_id)); + joinable!(comment -> person (creator_id)); joinable!(comment -> post (post_id)); joinable!(comment_aggregates -> comment (comment_id)); @@ -552,6 +573,8 @@ joinable!(comment_report -> comment (comment_id)); joinable!(comment_saved -> comment (comment_id)); joinable!(comment_saved -> person (person_id)); joinable!(community_aggregates -> community (community_id)); +joinable!(community_block -> community (community_id)); +joinable!(community_block -> person (person_id)); joinable!(community_follower -> community (community_id)); joinable!(community_follower -> person (person_id)); joinable!(community_moderator -> community (community_id)); @@ -594,6 +617,7 @@ allow_tables_to_appear_in_same_query!( activity, comment, comment_aggregates, + community_block, comment_like, comment_report, comment_saved, @@ -617,6 +641,7 @@ allow_tables_to_appear_in_same_query!( person, person_aggregates, person_ban, + person_block, person_mention, post, post_aggregates, diff --git a/crates/db_schema/src/source/community_block.rs b/crates/db_schema/src/source/community_block.rs new file mode 100644 index 000000000..6aa630b8b --- /dev/null +++ b/crates/db_schema/src/source/community_block.rs @@ -0,0 +1,25 @@ +use crate::{ + schema::community_block, + source::community::Community, + CommunityBlockId, + CommunityId, + PersonId, +}; +use serde::Serialize; + +#[derive(Clone, Queryable, Associations, Identifiable, PartialEq, Debug, Serialize)] +#[table_name = "community_block"] +#[belongs_to(Community)] +pub struct CommunityBlock { + pub id: CommunityBlockId, + pub person_id: PersonId, + pub community_id: CommunityId, + pub published: chrono::NaiveDateTime, +} + +#[derive(Insertable, AsChangeset)] +#[table_name = "community_block"] +pub struct CommunityBlockForm { + pub person_id: PersonId, + pub community_id: CommunityId, +} diff --git a/crates/db_schema/src/source/mod.rs b/crates/db_schema/src/source/mod.rs index db928bd4f..8b82d31a7 100644 --- a/crates/db_schema/src/source/mod.rs +++ b/crates/db_schema/src/source/mod.rs @@ -2,10 +2,12 @@ pub mod activity; pub mod comment; pub mod comment_report; pub mod community; +pub mod community_block; pub mod local_user; pub mod moderator; pub mod password_reset_request; pub mod person; +pub mod person_block; pub mod person_mention; pub mod post; pub mod post_report; diff --git a/crates/db_schema/src/source/person_block.rs b/crates/db_schema/src/source/person_block.rs new file mode 100644 index 000000000..cedf7de18 --- /dev/null +++ b/crates/db_schema/src/source/person_block.rs @@ -0,0 +1,18 @@ +use crate::{schema::person_block, PersonBlockId, PersonId}; +use serde::Serialize; + +#[derive(Clone, Queryable, Associations, Identifiable, PartialEq, Debug, Serialize)] +#[table_name = "person_block"] +pub struct PersonBlock { + pub id: PersonBlockId, + pub person_id: PersonId, + pub target_id: PersonId, + pub published: chrono::NaiveDateTime, +} + +#[derive(Insertable, AsChangeset)] +#[table_name = "person_block"] +pub struct PersonBlockForm { + pub person_id: PersonId, + pub target_id: PersonId, +} diff --git a/crates/db_views/src/comment_view.rs b/crates/db_views/src/comment_view.rs index 3449141d0..2183fa31e 100644 --- a/crates/db_views/src/comment_view.rs +++ b/crates/db_views/src/comment_view.rs @@ -18,16 +18,19 @@ use lemmy_db_schema::{ comment_like, comment_saved, community, + community_block, community_follower, community_person_ban, person, person_alias_1, + person_block, post, }, source::{ comment::{Comment, CommentAlias1, CommentSaved}, community::{Community, CommunityFollower, CommunityPersonBan, CommunitySafe}, person::{Person, PersonAlias1, PersonSafe, PersonSafeAlias1}, + person_block::PersonBlock, post::Post, }, CommentId, @@ -49,6 +52,7 @@ pub struct CommentView { pub creator_banned_from_community: bool, // Left Join to CommunityPersonBan pub subscribed: bool, // Left join to CommunityFollower pub saved: bool, // Left join to CommentSaved + pub creator_blocked: bool, // Left join to PersonBlock pub my_vote: Option, // Left join to CommentLike } @@ -63,6 +67,7 @@ type CommentViewTuple = ( Option, Option, Option, + Option, Option, ); @@ -86,6 +91,7 @@ impl CommentView { creator_banned_from_community, subscribed, saved, + creator_blocked, comment_like, ) = comment::table .find(comment_id) @@ -117,6 +123,13 @@ impl CommentView { .and(comment_saved::person_id.eq(person_id_join)), ), ) + .left_join( + person_block::table.on( + comment::creator_id + .eq(person_block::target_id) + .and(person_block::person_id.eq(person_id_join)), + ), + ) .left_join( comment_like::table.on( comment::id @@ -135,6 +148,7 @@ impl CommentView { community_person_ban::all_columns.nullable(), community_follower::all_columns.nullable(), comment_saved::all_columns.nullable(), + person_block::all_columns.nullable(), comment_like::score.nullable(), )) .first::(conn)?; @@ -157,6 +171,7 @@ impl CommentView { creator_banned_from_community: creator_banned_from_community.is_some(), subscribed: subscribed.is_some(), saved: saved.is_some(), + creator_blocked: creator_blocked.is_some(), my_vote, }) } @@ -315,6 +330,20 @@ impl<'a> CommentQueryBuilder<'a> { .and(comment_saved::person_id.eq(person_id_join)), ), ) + .left_join( + person_block::table.on( + comment::creator_id + .eq(person_block::target_id) + .and(person_block::person_id.eq(person_id_join)), + ), + ) + .left_join( + community_block::table.on( + community::id + .eq(community_block::community_id) + .and(community_block::person_id.eq(person_id_join)), + ), + ) .left_join( comment_like::table.on( comment::id @@ -333,6 +362,7 @@ impl<'a> CommentQueryBuilder<'a> { community_person_ban::all_columns.nullable(), community_follower::all_columns.nullable(), comment_saved::all_columns.nullable(), + person_block::all_columns.nullable(), comment_like::score.nullable(), )) .into_boxed(); @@ -413,6 +443,12 @@ impl<'a> CommentQueryBuilder<'a> { .order_by(comment_aggregates::score.desc()), }; + // Don't show blocked communities or persons + if self.my_person_id.is_some() { + query = query.filter(community_block::person_id.is_null()); + query = query.filter(person_block::person_id.is_null()); + } + let (limit, offset) = limit_and_offset(self.page, self.limit); // Note: deleted and removed comments are done on the front side @@ -440,7 +476,8 @@ impl ViewToVec for CommentView { creator_banned_from_community: a.7.is_some(), subscribed: a.8.is_some(), saved: a.9.is_some(), - my_vote: a.10, + creator_blocked: a.10.is_some(), + my_vote: a.11, }) .collect::>() } @@ -452,10 +489,17 @@ mod tests { use lemmy_db_queries::{ aggregates::comment_aggregates::CommentAggregates, establish_unpooled_connection, + Blockable, Crud, Likeable, }; - use lemmy_db_schema::source::{comment::*, community::*, person::*, post::*}; + use lemmy_db_schema::source::{ + comment::*, + community::*, + person::*, + person_block::PersonBlockForm, + post::*, + }; use serial_test::serial; #[test] @@ -470,6 +514,13 @@ mod tests { let inserted_person = Person::create(&conn, &new_person).unwrap(); + let new_person_2 = PersonForm { + name: "sara".into(), + ..PersonForm::default() + }; + + let inserted_person_2 = Person::create(&conn, &new_person_2).unwrap(); + let new_community = CommunityForm { name: "test community 5".to_string(), title: "nada".to_owned(), @@ -496,6 +547,32 @@ mod tests { let inserted_comment = Comment::create(&conn, &comment_form).unwrap(); + let comment_form_2 = CommentForm { + content: "A test blocked comment".into(), + creator_id: inserted_person_2.id, + post_id: inserted_post.id, + parent_id: Some(inserted_comment.id), + ..CommentForm::default() + }; + + let inserted_comment_2 = Comment::create(&conn, &comment_form_2).unwrap(); + + let timmy_blocks_sara_form = PersonBlockForm { + person_id: inserted_person.id, + target_id: inserted_person_2.id, + }; + + let inserted_block = PersonBlock::block(&conn, &timmy_blocks_sara_form).unwrap(); + + let expected_block = PersonBlock { + id: inserted_block.id, + person_id: inserted_person.id, + target_id: inserted_person_2.id, + published: inserted_block.published, + }; + + assert_eq!(expected_block, inserted_block); + let comment_like_form = CommentLikeForm { comment_id: inserted_comment.id, post_id: inserted_post.id, @@ -512,6 +589,7 @@ mod tests { my_vote: None, subscribed: false, saved: false, + creator_blocked: false, comment: Comment { id: inserted_comment.id, content: "A test comment 32".into(), @@ -606,20 +684,32 @@ mod tests { .list() .unwrap(); + let read_comment_from_blocked_person = + CommentView::read(&conn, inserted_comment_2.id, Some(inserted_person.id)).unwrap(); + let like_removed = CommentLike::remove(&conn, inserted_person.id, inserted_comment.id).unwrap(); let num_deleted = Comment::delete(&conn, inserted_comment.id).unwrap(); + Comment::delete(&conn, inserted_comment_2.id).unwrap(); Post::delete(&conn, inserted_post.id).unwrap(); Community::delete(&conn, inserted_community.id).unwrap(); Person::delete(&conn, inserted_person.id).unwrap(); + Person::delete(&conn, inserted_person_2.id).unwrap(); + + // Make sure its 1, not showing the blocked comment + assert_eq!(1, read_comment_views_with_person.len()); assert_eq!( expected_comment_view_no_person, - read_comment_views_no_person[0] + read_comment_views_no_person[1] ); assert_eq!( expected_comment_view_with_person, read_comment_views_with_person[0] ); + + // Make sure block set the creator blocked + assert_eq!(true, read_comment_from_blocked_person.creator_blocked); + assert_eq!(1, num_deleted); assert_eq!(1, like_removed); } diff --git a/crates/db_views/src/post_view.rs b/crates/db_views/src/post_view.rs index 34847dcde..ec537ad94 100644 --- a/crates/db_views/src/post_view.rs +++ b/crates/db_views/src/post_view.rs @@ -13,9 +13,11 @@ use lemmy_db_queries::{ use lemmy_db_schema::{ schema::{ community, + community_block, community_follower, community_person_ban, person, + person_block, post, post_aggregates, post_like, @@ -25,6 +27,7 @@ use lemmy_db_schema::{ source::{ community::{Community, CommunityFollower, CommunityPersonBan, CommunitySafe}, person::{Person, PersonSafe}, + person_block::PersonBlock, post::{Post, PostRead, PostSaved}, }, CommunityId, @@ -42,10 +45,11 @@ pub struct PostView { pub community: CommunitySafe, pub creator_banned_from_community: bool, // Left Join to CommunityPersonBan pub counts: PostAggregates, - pub subscribed: bool, // Left join to CommunityFollower - pub saved: bool, // Left join to PostSaved - pub read: bool, // Left join to PostRead - pub my_vote: Option, // Left join to PostLike + pub subscribed: bool, // Left join to CommunityFollower + pub saved: bool, // Left join to PostSaved + pub read: bool, // Left join to PostRead + pub creator_blocked: bool, // Left join to PersonBlock + pub my_vote: Option, // Left join to PostLike } type PostViewTuple = ( @@ -57,6 +61,7 @@ type PostViewTuple = ( Option, Option, Option, + Option, Option, ); @@ -78,6 +83,7 @@ impl PostView { follower, saved, read, + creator_blocked, post_like, ) = post::table .find(post_id) @@ -112,6 +118,13 @@ impl PostView { .and(post_read::person_id.eq(person_id_join)), ), ) + .left_join( + person_block::table.on( + post::creator_id + .eq(person_block::target_id) + .and(person_block::person_id.eq(person_id_join)), + ), + ) .left_join( post_like::table.on( post::id @@ -128,6 +141,7 @@ impl PostView { community_follower::all_columns.nullable(), post_saved::all_columns.nullable(), post_read::all_columns.nullable(), + person_block::all_columns.nullable(), post_like::score.nullable(), )) .first::(conn)?; @@ -149,6 +163,7 @@ impl PostView { subscribed: follower.is_some(), saved: saved.is_some(), read: read.is_some(), + creator_blocked: creator_blocked.is_some(), my_vote, }) } @@ -301,6 +316,20 @@ impl<'a> PostQueryBuilder<'a> { .and(post_read::person_id.eq(person_id_join)), ), ) + .left_join( + person_block::table.on( + post::creator_id + .eq(person_block::target_id) + .and(person_block::person_id.eq(person_id_join)), + ), + ) + .left_join( + community_block::table.on( + community::id + .eq(community_block::community_id) + .and(community_block::person_id.eq(person_id_join)), + ), + ) .left_join( post_like::table.on( post::id @@ -317,6 +346,7 @@ impl<'a> PostQueryBuilder<'a> { community_follower::all_columns.nullable(), post_saved::all_columns.nullable(), post_read::all_columns.nullable(), + person_block::all_columns.nullable(), post_like::score.nullable(), )) .into_boxed(); @@ -377,6 +407,12 @@ impl<'a> PostQueryBuilder<'a> { query = query.filter(post_saved::id.is_not_null()); }; + // Don't show blocked communities or persons + if self.my_person_id.is_some() { + query = query.filter(community_block::person_id.is_null()); + query = query.filter(person_block::person_id.is_null()); + } + query = match self.sort.unwrap_or(SortType::Hot) { SortType::Active => query .then_order_by( @@ -440,7 +476,8 @@ impl ViewToVec for PostView { subscribed: a.5.is_some(), saved: a.6.is_some(), read: a.7.is_some(), - my_vote: a.8, + creator_blocked: a.8.is_some(), + my_vote: a.9, }) .collect::>() } @@ -452,12 +489,19 @@ mod tests { use lemmy_db_queries::{ aggregates::post_aggregates::PostAggregates, establish_unpooled_connection, + Blockable, Crud, Likeable, ListingType, SortType, }; - use lemmy_db_schema::source::{community::*, person::*, post::*}; + use lemmy_db_schema::source::{ + community::*, + community_block::{CommunityBlock, CommunityBlockForm}, + person::*, + person_block::{PersonBlock, PersonBlockForm}, + post::*, + }; use serial_test::serial; #[test] @@ -493,6 +537,32 @@ mod tests { let inserted_community = Community::create(&conn, &new_community).unwrap(); + // Test a person block, make sure the post query doesn't include their post + let blocked_person = PersonForm { + name: person_name.to_owned(), + ..PersonForm::default() + }; + + let inserted_blocked_person = Person::create(&conn, &blocked_person).unwrap(); + + let post_from_blocked_person = PostForm { + name: "blocked_person_post".to_string(), + creator_id: inserted_blocked_person.id, + community_id: inserted_community.id, + ..PostForm::default() + }; + + Post::create(&conn, &post_from_blocked_person).unwrap(); + + // block that person + let person_block = PersonBlockForm { + person_id: inserted_person.id, + target_id: inserted_blocked_person.id, + }; + + PersonBlock::block(&conn, &person_block).unwrap(); + + // A sample post let new_post = PostForm { name: post_name.to_owned(), creator_id: inserted_person.id, @@ -623,7 +693,24 @@ mod tests { subscribed: false, read: false, saved: false, + creator_blocked: false, + }; + + // Test a community block + let community_block = CommunityBlockForm { + person_id: inserted_person.id, + community_id: inserted_community.id, }; + CommunityBlock::block(&conn, &community_block).unwrap(); + + let read_post_listings_with_person_after_block = PostQueryBuilder::create(&conn) + .listing_type(ListingType::Community) + .sort(SortType::New) + .show_bot_accounts(false) + .community_id(inserted_community.id) + .my_person_id(inserted_person.id) + .list() + .unwrap(); // TODO More needs to be added here let mut expected_post_listing_with_user = expected_post_listing_no_person.to_owned(); @@ -631,9 +718,12 @@ mod tests { let like_removed = PostLike::remove(&conn, inserted_person.id, inserted_post.id).unwrap(); let num_deleted = Post::delete(&conn, inserted_post.id).unwrap(); + PersonBlock::unblock(&conn, &person_block).unwrap(); + CommunityBlock::unblock(&conn, &community_block).unwrap(); Community::delete(&conn, inserted_community.id).unwrap(); Person::delete(&conn, inserted_person.id).unwrap(); Person::delete(&conn, inserted_bot.id).unwrap(); + Person::delete(&conn, inserted_blocked_person.id).unwrap(); // The with user assert_eq!( @@ -645,7 +735,7 @@ mod tests { read_post_listing_with_person ); - // Should be only one person, IE the bot post should be missing + // Should be only one person, IE the bot post, and blocked should be missing assert_eq!(1, read_post_listings_with_person.len()); // Without the user @@ -655,8 +745,11 @@ mod tests { ); assert_eq!(expected_post_listing_no_person, read_post_listing_no_person); - // Should be 2 posts, with the bot post - assert_eq!(2, read_post_listings_no_person.len()); + // Should be 2 posts, with the bot post, and the blocked + assert_eq!(3, read_post_listings_no_person.len()); + + // Should be 0 posts after the community block + assert_eq!(0, read_post_listings_with_person_after_block.len()); assert_eq!(expected_post_like, inserted_post_like); assert_eq!(1, like_removed); diff --git a/crates/db_views_actor/src/community_block_view.rs b/crates/db_views_actor/src/community_block_view.rs new file mode 100644 index 000000000..5e24bcaab --- /dev/null +++ b/crates/db_views_actor/src/community_block_view.rs @@ -0,0 +1,49 @@ +use diesel::{result::Error, *}; +use lemmy_db_queries::{ToSafe, ViewToVec}; +use lemmy_db_schema::{ + schema::{community, community_block, person}, + source::{ + community::{Community, CommunitySafe}, + person::{Person, PersonSafe}, + }, + PersonId, +}; +use serde::Serialize; + +#[derive(Debug, Serialize, Clone)] +pub struct CommunityBlockView { + pub person: PersonSafe, + pub community: CommunitySafe, +} + +type CommunityBlockViewTuple = (PersonSafe, CommunitySafe); + +impl CommunityBlockView { + pub fn for_person(conn: &PgConnection, person_id: PersonId) -> Result, Error> { + let res = community_block::table + .inner_join(person::table) + .inner_join(community::table) + .select(( + Person::safe_columns_tuple(), + Community::safe_columns_tuple(), + )) + .filter(community_block::person_id.eq(person_id)) + .order_by(community_block::published) + .load::(conn)?; + + Ok(Self::from_tuple_to_vec(res)) + } +} + +impl ViewToVec for CommunityBlockView { + type DbTuple = CommunityBlockViewTuple; + fn from_tuple_to_vec(items: Vec) -> Vec { + items + .iter() + .map(|a| Self { + person: a.0.to_owned(), + community: a.1.to_owned(), + }) + .collect::>() + } +} diff --git a/crates/db_views_actor/src/community_view.rs b/crates/db_views_actor/src/community_view.rs index fc582857a..8e64a5f9e 100644 --- a/crates/db_views_actor/src/community_view.rs +++ b/crates/db_views_actor/src/community_view.rs @@ -12,8 +12,11 @@ use lemmy_db_queries::{ ViewToVec, }; use lemmy_db_schema::{ - schema::{community, community_aggregates, community_follower}, - source::community::{Community, CommunityFollower, CommunitySafe}, + schema::{community, community_aggregates, community_block, community_follower}, + source::{ + community::{Community, CommunityFollower, CommunitySafe}, + community_block::CommunityBlock, + }, CommunityId, PersonId, }; @@ -23,6 +26,7 @@ use serde::Serialize; pub struct CommunityView { pub community: CommunitySafe, pub subscribed: bool, + pub blocked: bool, pub counts: CommunityAggregates, } @@ -30,6 +34,7 @@ type CommunityViewTuple = ( CommunitySafe, CommunityAggregates, Option, + Option, ); impl CommunityView { @@ -41,7 +46,7 @@ impl CommunityView { // The left join below will return None in this case let person_id_join = my_person_id.unwrap_or(PersonId(-1)); - let (community, counts, follower) = community::table + let (community, counts, follower, blocked) = community::table .find(community_id) .inner_join(community_aggregates::table) .left_join( @@ -51,16 +56,25 @@ impl CommunityView { .and(community_follower::person_id.eq(person_id_join)), ), ) + .left_join( + community_block::table.on( + community::id + .eq(community_block::community_id) + .and(community_block::person_id.eq(person_id_join)), + ), + ) .select(( Community::safe_columns_tuple(), community_aggregates::all_columns, community_follower::all_columns.nullable(), + community_block::all_columns.nullable(), )) .first::(conn)?; Ok(CommunityView { community, subscribed: follower.is_some(), + blocked: blocked.is_some(), counts, }) } @@ -165,10 +179,18 @@ impl<'a> CommunityQueryBuilder<'a> { .and(community_follower::person_id.eq(person_id_join)), ), ) + .left_join( + community_block::table.on( + community::id + .eq(community_block::community_id) + .and(community_block::person_id.eq(person_id_join)), + ), + ) .select(( Community::safe_columns_tuple(), community_aggregates::all_columns, community_follower::all_columns.nullable(), + community_block::all_columns.nullable(), )) .into_boxed(); @@ -210,6 +232,11 @@ impl<'a> CommunityQueryBuilder<'a> { }; } + // Don't show blocked communities + if self.my_person_id.is_some() { + query = query.filter(community_block::person_id.is_null()); + } + let (limit, offset) = limit_and_offset(self.page, self.limit); let res = query .limit(limit) @@ -231,6 +258,7 @@ impl ViewToVec for CommunityView { community: a.0.to_owned(), counts: a.1.to_owned(), subscribed: a.2.is_some(), + blocked: a.3.is_some(), }) .collect::>() } diff --git a/crates/db_views_actor/src/lib.rs b/crates/db_views_actor/src/lib.rs index 5d5203c56..6411feeed 100644 --- a/crates/db_views_actor/src/lib.rs +++ b/crates/db_views_actor/src/lib.rs @@ -1,6 +1,8 @@ +pub mod community_block_view; pub mod community_follower_view; pub mod community_moderator_view; pub mod community_person_ban_view; pub mod community_view; +pub mod person_block_view; pub mod person_mention_view; pub mod person_view; diff --git a/crates/db_views_actor/src/person_block_view.rs b/crates/db_views_actor/src/person_block_view.rs new file mode 100644 index 000000000..5eb18589b --- /dev/null +++ b/crates/db_views_actor/src/person_block_view.rs @@ -0,0 +1,46 @@ +use diesel::{result::Error, *}; +use lemmy_db_queries::{ToSafe, ViewToVec}; +use lemmy_db_schema::{ + schema::{person, person_alias_1, person_block}, + source::person::{Person, PersonAlias1, PersonSafe, PersonSafeAlias1}, + PersonId, +}; +use serde::Serialize; + +#[derive(Debug, Serialize, Clone)] +pub struct PersonBlockView { + pub person: PersonSafe, + pub target: PersonSafeAlias1, +} + +type PersonBlockViewTuple = (PersonSafe, PersonSafeAlias1); + +impl PersonBlockView { + pub fn for_person(conn: &PgConnection, person_id: PersonId) -> Result, Error> { + let res = person_block::table + .inner_join(person::table) + .inner_join(person_alias_1::table) // TODO I dont know if this will be smart abt the column + .select(( + Person::safe_columns_tuple(), + PersonAlias1::safe_columns_tuple(), + )) + .filter(person_block::person_id.eq(person_id)) + .order_by(person_block::published) + .load::(conn)?; + + Ok(Self::from_tuple_to_vec(res)) + } +} + +impl ViewToVec for PersonBlockView { + type DbTuple = PersonBlockViewTuple; + fn from_tuple_to_vec(items: Vec) -> Vec { + items + .iter() + .map(|a| Self { + person: a.0.to_owned(), + target: a.1.to_owned(), + }) + .collect::>() + } +} diff --git a/crates/db_views_actor/src/person_mention_view.rs b/crates/db_views_actor/src/person_mention_view.rs index b391345ff..421c60a40 100644 --- a/crates/db_views_actor/src/person_mention_view.rs +++ b/crates/db_views_actor/src/person_mention_view.rs @@ -19,6 +19,7 @@ use lemmy_db_schema::{ community_person_ban, person, person_alias_1, + person_block, person_mention, post, }, @@ -26,6 +27,7 @@ use lemmy_db_schema::{ comment::{Comment, CommentSaved}, community::{Community, CommunityFollower, CommunityPersonBan, CommunitySafe}, person::{Person, PersonAlias1, PersonSafe, PersonSafeAlias1}, + person_block::PersonBlock, person_mention::PersonMention, post::Post, }, @@ -46,6 +48,7 @@ pub struct PersonMentionView { pub creator_banned_from_community: bool, // Left Join to CommunityPersonBan pub subscribed: bool, // Left join to CommunityFollower pub saved: bool, // Left join to CommentSaved + pub creator_blocked: bool, // Left join to PersonBlock pub my_vote: Option, // Left join to CommentLike } @@ -60,6 +63,7 @@ type PersonMentionViewTuple = ( Option, Option, Option, + Option, Option, ); @@ -83,6 +87,7 @@ impl PersonMentionView { creator_banned_from_community, subscribed, saved, + creator_blocked, my_vote, ) = person_mention::table .find(person_mention_id) @@ -113,6 +118,13 @@ impl PersonMentionView { .and(comment_saved::person_id.eq(person_id_join)), ), ) + .left_join( + person_block::table.on( + comment::creator_id + .eq(person_block::target_id) + .and(person_block::person_id.eq(person_id_join)), + ), + ) .left_join( comment_like::table.on( comment::id @@ -131,6 +143,7 @@ impl PersonMentionView { community_person_ban::all_columns.nullable(), community_follower::all_columns.nullable(), comment_saved::all_columns.nullable(), + person_block::all_columns.nullable(), comment_like::score.nullable(), )) .first::(conn)?; @@ -146,6 +159,7 @@ impl PersonMentionView { creator_banned_from_community: creator_banned_from_community.is_some(), subscribed: subscribed.is_some(), saved: saved.is_some(), + creator_blocked: creator_blocked.is_some(), my_vote, }) } @@ -238,6 +252,13 @@ impl<'a> PersonMentionQueryBuilder<'a> { .and(comment_saved::person_id.eq(person_id_join)), ), ) + .left_join( + person_block::table.on( + comment::creator_id + .eq(person_block::target_id) + .and(person_block::person_id.eq(person_id_join)), + ), + ) .left_join( comment_like::table.on( comment::id @@ -256,6 +277,7 @@ impl<'a> PersonMentionQueryBuilder<'a> { community_person_ban::all_columns.nullable(), community_follower::all_columns.nullable(), comment_saved::all_columns.nullable(), + person_block::all_columns.nullable(), comment_like::score.nullable(), )) .into_boxed(); @@ -317,7 +339,8 @@ impl ViewToVec for PersonMentionView { creator_banned_from_community: a.7.is_some(), subscribed: a.8.is_some(), saved: a.9.is_some(), - my_vote: a.10, + creator_blocked: a.10.is_some(), + my_vote: a.11, }) .collect::>() } diff --git a/crates/websocket/src/lib.rs b/crates/websocket/src/lib.rs index e01514030..fbb2bb512 100644 --- a/crates/websocket/src/lib.rs +++ b/crates/websocket/src/lib.rs @@ -101,7 +101,6 @@ pub enum UserOperation { ListPostReports, GetReportCount, FollowCommunity, - GetFollowedCommunities, GetReplies, GetPersonMentions, MarkPersonMentionAsRead, @@ -126,6 +125,8 @@ pub enum UserOperation { ModJoin, ChangePassword, GetSiteMetadata, + BlockCommunity, + BlockPerson, } #[derive(EnumString, ToString, Debug, Clone)] diff --git a/migrations/2021-08-04-223559_create_user_community_block/down.sql b/migrations/2021-08-04-223559_create_user_community_block/down.sql new file mode 100644 index 000000000..eb5a35e5a --- /dev/null +++ b/migrations/2021-08-04-223559_create_user_community_block/down.sql @@ -0,0 +1,2 @@ +drop table person_block; +drop table community_block; diff --git a/migrations/2021-08-04-223559_create_user_community_block/up.sql b/migrations/2021-08-04-223559_create_user_community_block/up.sql new file mode 100644 index 000000000..cbcadc284 --- /dev/null +++ b/migrations/2021-08-04-223559_create_user_community_block/up.sql @@ -0,0 +1,15 @@ +create table person_block ( + id serial primary key, + person_id int references person on update cascade on delete cascade not null, + target_id int references person on update cascade on delete cascade not null, + published timestamp not null default now(), + unique(person_id, target_id) +); + +create table community_block ( + id serial primary key, + person_id int references person on update cascade on delete cascade not null, + community_id int references community on update cascade on delete cascade not null, + published timestamp not null default now(), + unique(person_id, community_id) +); diff --git a/src/api_routes.rs b/src/api_routes.rs index a3bf39fe1..feaf0e235 100644 --- a/src/api_routes.rs +++ b/src/api_routes.rs @@ -47,6 +47,7 @@ pub fn config(cfg: &mut web::ServiceConfig, rate_limit: &RateLimit) { .route("", web::put().to(route_post_crud::)) .route("/list", web::get().to(route_get_crud::)) .route("/follow", web::post().to(route_post::)) + .route("/block", web::post().to(route_post::)) .route( "/delete", web::post().to(route_post_crud::), @@ -155,13 +156,10 @@ pub fn config(cfg: &mut web::ServiceConfig, rate_limit: &RateLimit) { web::post().to(route_post::), ) .route("/replies", web::get().to(route_get::)) - .route( - "/followed_communities", - web::get().to(route_get::), - ) .route("/join", web::post().to(route_post::)) // Admin action. I don't like that it's in /user .route("/ban", web::post().to(route_post::)) + .route("/block", web::post().to(route_post::)) // Account actions. I don't like that they're in /user maybe /accounts .route("/login", web::post().to(route_post::)) .route("/get_captcha", web::get().to(route_get::))