From ad90cd77f9602205671ae4211f230e4fd415420d Mon Sep 17 00:00:00 2001 From: Nutomic Date: Thu, 7 Nov 2024 11:49:05 +0100 Subject: [PATCH] Implement private communities (#5076) * add private visibility * filter private communities in post_view.rs * also filter in comment_view * community follower state * remove unused method * sql fmt * add CommunityFollower.approved_by * implement api endpoints * api changes * only admins can create private community for now * add local api tests * fix api tests * follow remote private community * use authorized fetch for content in private community * federate community visibility * dont mark content in private community as public * expose ApprovalRequired in api * also check content fetchable for outbox/featured * address private community content to followers * implement reject activity * fix tests * add files * remove local api tests * dont use delay * is_new_instance * single query for is_new_instance * return subscribed type for pending follow * working * need to catch errors in waitUntil * clippy * fix query * lint for unused async * diesel.toml comment * add comment * avoid db reads * rename approved_by to approver_id * add helper * form init * list pending follows should return items for all communities * clippy * ci * fix down migration * fix api tests * references * rename * run git diff * ci * fix schema check * fix joins * ci * ci * skip_serializing_none * fix test --------- Co-authored-by: Dessalines --- .woodpecker.yml | 2 +- Cargo.toml | 1 + api_tests/package.json | 5 +- api_tests/pnpm-lock.yaml | 10 +- api_tests/src/private_community.spec.ts | 214 +++++++++++++++ api_tests/src/shared.ts | 54 +++- crates/api/src/comment/distinguish.rs | 4 +- crates/api/src/comment/like.rs | 5 +- crates/api/src/comment_report/create.rs | 5 +- crates/api/src/comment_report/resolve.rs | 2 +- crates/api/src/community/add_mod.rs | 12 +- crates/api/src/community/ban.rs | 14 +- crates/api/src/community/block.rs | 10 +- crates/api/src/community/follow.rs | 57 ++-- crates/api/src/community/hide.rs | 3 +- crates/api/src/community/mod.rs | 1 + .../src/community/pending_follows/approve.rs | 46 ++++ .../src/community/pending_follows/count.rs | 25 ++ .../api/src/community/pending_follows/list.rs | 29 +++ .../api/src/community/pending_follows/mod.rs | 3 + crates/api/src/community/transfer.rs | 8 +- crates/api/src/lib.rs | 9 +- crates/api/src/local_user/ban_person.rs | 3 +- crates/api/src/post/feature.rs | 7 +- crates/api/src/post/like.rs | 22 +- crates/api/src/post/lock.rs | 11 +- crates/api/src/post_report/create.rs | 5 +- crates/api/src/post_report/resolve.rs | 2 +- crates/api/src/site/purge/comment.rs | 3 +- crates/api/src/site/purge/community.rs | 3 +- crates/api/src/site/purge/person.rs | 3 +- crates/api/src/site/purge/post.rs | 3 +- crates/api/src/sitemap.rs | 8 +- crates/api_common/src/claims.rs | 2 +- crates/api_common/src/community.rs | 48 ++++ crates/api_common/src/context.rs | 2 +- crates/api_common/src/request.rs | 2 +- crates/api_common/src/send_activity.rs | 7 +- crates/api_common/src/utils.rs | 22 +- crates/api_crud/src/comment/create.rs | 10 +- crates/api_crud/src/comment/delete.rs | 5 +- crates/api_crud/src/comment/remove.rs | 5 +- crates/api_crud/src/comment/update.rs | 5 +- crates/api_crud/src/community/create.rs | 11 +- crates/api_crud/src/community/delete.rs | 9 +- crates/api_crud/src/community/mod.rs | 17 ++ crates/api_crud/src/community/remove.rs | 6 +- crates/api_crud/src/community/update.rs | 7 +- crates/api_crud/src/post/create.rs | 15 +- crates/api_crud/src/post/delete.rs | 16 +- crates/api_crud/src/post/remove.rs | 11 +- crates/api_crud/src/post/update.rs | 18 +- crates/api_crud/src/private_message/create.rs | 3 +- crates/api_crud/src/private_message/delete.rs | 3 +- crates/api_crud/src/private_message/update.rs | 3 +- crates/api_crud/src/user/delete.rs | 3 +- .../apub/src/activities/block/block_user.rs | 21 +- crates/apub/src/activities/block/mod.rs | 12 + .../src/activities/block/undo_block_user.rs | 15 +- .../apub/src/activities/community/announce.rs | 9 +- .../activities/community/collection_add.rs | 11 +- .../activities/community/collection_remove.rs | 11 +- .../src/activities/community/lock_page.rs | 13 +- .../apub/src/activities/community/update.rs | 9 +- .../activities/create_or_update/comment.rs | 8 +- .../src/activities/create_or_update/post.rs | 8 +- crates/apub/src/activities/deletion/delete.rs | 4 +- crates/apub/src/activities/deletion/mod.rs | 26 +- .../src/activities/deletion/undo_delete.rs | 4 +- .../apub/src/activities/following/follow.rs | 24 +- crates/apub/src/activities/following/mod.rs | 49 +++- .../apub/src/activities/following/reject.rs | 79 ++++++ .../src/activities/following/undo_follow.rs | 6 +- crates/apub/src/activities/mod.rs | 30 +++ crates/apub/src/activity_lists.rs | 9 +- crates/apub/src/api/user_settings_backup.rs | 21 +- .../src/fetcher/site_or_community_or_user.rs | 11 + crates/apub/src/http/comment.rs | 13 +- crates/apub/src/http/community.rs | 53 ++-- crates/apub/src/http/mod.rs | 49 +++- crates/apub/src/http/post.rs | 14 +- crates/apub/src/objects/comment.rs | 8 +- crates/apub/src/objects/community.rs | 9 +- crates/apub/src/objects/post.rs | 7 +- .../src/protocol/activities/following/mod.rs | 1 + .../protocol/activities/following/reject.rs | 24 ++ crates/apub/src/protocol/objects/group.rs | 2 + crates/db_perf/src/main.rs | 2 +- .../src/aggregates/comment_aggregates.rs | 2 +- .../src/aggregates/community_aggregates.rs | 19 +- .../src/aggregates/person_aggregates.rs | 2 +- .../src/aggregates/post_aggregates.rs | 4 +- .../src/aggregates/site_aggregates.rs | 4 +- crates/db_schema/src/impls/activity.rs | 4 +- crates/db_schema/src/impls/actor_language.rs | 12 +- crates/db_schema/src/impls/captcha_answer.rs | 4 +- crates/db_schema/src/impls/comment.rs | 2 +- crates/db_schema/src/impls/community.rs | 58 +++-- .../src/impls/federation_allowlist.rs | 2 +- crates/db_schema/src/impls/language.rs | 2 +- crates/db_schema/src/impls/local_user.rs | 5 +- crates/db_schema/src/impls/moderator.rs | 2 +- .../src/impls/password_reset_request.rs | 2 +- crates/db_schema/src/impls/person.rs | 4 +- crates/db_schema/src/impls/post.rs | 2 +- crates/db_schema/src/impls/post_report.rs | 4 +- crates/db_schema/src/impls/private_message.rs | 2 +- crates/db_schema/src/lib.rs | 5 +- crates/db_schema/src/schema.rs | 29 ++- crates/db_schema/src/source/community.rs | 25 +- crates/db_schema/src/utils.rs | 6 +- crates/db_views/src/comment_report_view.rs | 5 +- crates/db_views/src/comment_view.rs | 174 +++++++++++-- crates/db_views/src/post_report_view.rs | 5 +- crates/db_views/src/post_view.rs | 195 +++++++++++--- .../src/private_message_report_view.rs | 2 +- crates/db_views/src/private_message_view.rs | 6 +- .../src/registration_application_view.rs | 2 +- crates/db_views/src/vote_view.rs | 2 +- crates/db_views_actor/Cargo.toml | 3 +- .../db_views_actor/src/comment_reply_view.rs | 17 +- .../src/community_follower_view.rs | 243 +++++++++++++++++- crates/db_views_actor/src/community_view.rs | 130 +++++++--- .../db_views_actor/src/person_mention_view.rs | 17 +- crates/db_views_actor/src/person_view.rs | 8 +- crates/db_views_actor/src/structs.rs | 11 + crates/utils/src/error.rs | 1 + diesel.toml | 2 + .../down.sql | 52 ++++ .../up.sql | 47 ++++ scripts/test.sh | 4 +- src/api_routes_http.rs | 14 +- src/lib.rs | 2 +- src/prometheus_metrics.rs | 4 +- src/scheduled_tasks.rs | 1 - src/session_middleware.rs | 2 +- 136 files changed, 1980 insertions(+), 551 deletions(-) create mode 100644 api_tests/src/private_community.spec.ts create mode 100644 crates/api/src/community/pending_follows/approve.rs create mode 100644 crates/api/src/community/pending_follows/count.rs create mode 100644 crates/api/src/community/pending_follows/list.rs create mode 100644 crates/api/src/community/pending_follows/mod.rs create mode 100644 crates/apub/src/activities/following/reject.rs create mode 100644 crates/apub/src/protocol/activities/following/reject.rs create mode 100644 migrations/2024-10-29-090055_private-community/down.sql create mode 100644 migrations/2024-10-29-090055_private-community/up.sql diff --git a/.woodpecker.yml b/.woodpecker.yml index 8930c21fc..060cc3e26 100644 --- a/.woodpecker.yml +++ b/.woodpecker.yml @@ -133,8 +133,8 @@ steps: DATABASE_URL: postgres://lemmy:password@database:5432/lemmy commands: - <<: *install_diesel_cli + - cp crates/db_schema/src/schema.rs tmp.schema - diesel migration run - - diesel print-schema --config-file=diesel.toml > tmp.schema - diff tmp.schema crates/db_schema/src/schema.rs when: *slow_check_paths diff --git a/Cargo.toml b/Cargo.toml index 0373c4e51..8db7b8b8d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -78,6 +78,7 @@ uninlined_format_args = "allow" unused_self = "deny" unwrap_used = "deny" unimplemented = "deny" +unused_async = "deny" [workspace.dependencies] lemmy_api = { version = "=0.19.6-beta.7", path = "./crates/api" } diff --git a/api_tests/package.json b/api_tests/package.json index 81e518ea4..9a5057c00 100644 --- a/api_tests/package.json +++ b/api_tests/package.json @@ -10,12 +10,13 @@ "scripts": { "lint": "tsc --noEmit && eslint --report-unused-disable-directives && prettier --check 'src/**/*.ts'", "fix": "prettier --write src && eslint --fix src", - "api-test": "jest -i follow.spec.ts && jest -i image.spec.ts && jest -i user.spec.ts && jest -i private_message.spec.ts && jest -i community.spec.ts && jest -i post.spec.ts && jest -i comment.spec.ts ", + "api-test": "jest -i follow.spec.ts && jest -i image.spec.ts && jest -i user.spec.ts && jest -i private_message.spec.ts && jest -i community.spec.ts && jest -i private_community.spec.ts && jest -i post.spec.ts && jest -i comment.spec.ts ", "api-test-follow": "jest -i follow.spec.ts", "api-test-comment": "jest -i comment.spec.ts", "api-test-post": "jest -i post.spec.ts", "api-test-user": "jest -i user.spec.ts", "api-test-community": "jest -i community.spec.ts", + "api-test-private-community": "jest -i private_community.spec.ts", "api-test-private-message": "jest -i private_message.spec.ts", "api-test-image": "jest -i image.spec.ts" }, @@ -27,7 +28,7 @@ "eslint": "^9.9.0", "eslint-plugin-prettier": "^5.1.3", "jest": "^29.5.0", - "lemmy-js-client": "0.20.0-alpha.11", + "lemmy-js-client": "0.20.0-private-community.9", "prettier": "^3.2.5", "ts-jest": "^29.1.0", "typescript": "^5.5.4", diff --git a/api_tests/pnpm-lock.yaml b/api_tests/pnpm-lock.yaml index dd357d248..b1f18622e 100644 --- a/api_tests/pnpm-lock.yaml +++ b/api_tests/pnpm-lock.yaml @@ -30,8 +30,8 @@ importers: specifier: ^29.5.0 version: 29.7.0(@types/node@22.8.6) lemmy-js-client: - specifier: 0.20.0-alpha.11 - version: 0.20.0-alpha.11 + specifier: 0.20.0-private-community.9 + version: 0.20.0-private-community.9 prettier: specifier: ^3.2.5 version: 3.3.3 @@ -1159,8 +1159,8 @@ packages: resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} engines: {node: '>=6'} - lemmy-js-client@0.20.0-alpha.11: - resolution: {integrity: sha512-iRSG4xHMjPDIreQqVIoJ5JrMY71uk07G0Zbgyf068xKbib22J3+i1x/XgCTs6tiHlqTnw1Ig/KRq7p7qJoA4uw==} + lemmy-js-client@0.20.0-private-community.9: + resolution: {integrity: sha512-iuFezswCzIco5U5Q4Eo8HAWVE65pDW2zeO+fYLEyFl30SLw9a3gqJkip2vbDfVvoAjDXyUskZKddf1Nnj8mVcg==} leven@3.1.0: resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==} @@ -3061,7 +3061,7 @@ snapshots: kleur@3.0.3: {} - lemmy-js-client@0.20.0-alpha.11: {} + lemmy-js-client@0.20.0-private-community.9: {} leven@3.1.0: {} diff --git a/api_tests/src/private_community.spec.ts b/api_tests/src/private_community.spec.ts new file mode 100644 index 000000000..76faf800f --- /dev/null +++ b/api_tests/src/private_community.spec.ts @@ -0,0 +1,214 @@ +jest.setTimeout(120000); + +import { FollowCommunity } from "lemmy-js-client"; +import { + alpha, + setupLogins, + createCommunity, + unfollows, + registerUser, + listCommunityPendingFollows, + getCommunity, + getCommunityPendingFollowsCount, + approveCommunityPendingFollow, + randomString, + createPost, + createComment, + beta, + resolveCommunity, + betaUrl, + resolvePost, + resolveComment, + likeComment, + waitUntil, +} from "./shared"; + +beforeAll(setupLogins); +afterAll(unfollows); + +test("Follow a private community", async () => { + // create private community + const community = await createCommunity(alpha, randomString(10), "Private"); + expect(community.community_view.community.visibility).toBe("Private"); + const alphaCommunityId = community.community_view.community.id; + + // No pending follows yet + const pendingFollows0 = await listCommunityPendingFollows(alpha); + expect(pendingFollows0.items.length).toBe(0); + const pendingFollowsCount0 = await getCommunityPendingFollowsCount( + alpha, + alphaCommunityId, + ); + expect(pendingFollowsCount0.count).toBe(0); + + // follow as new user + const user = await registerUser(beta, betaUrl); + const betaCommunity = ( + await resolveCommunity(user, community.community_view.community.actor_id) + ).community; + expect(betaCommunity).toBeDefined(); + const betaCommunityId = betaCommunity!.community.id; + const follow_form: FollowCommunity = { + community_id: betaCommunityId, + follow: true, + }; + await user.followCommunity(follow_form); + + // Follow listed as pending + const follow1 = await getCommunity(user, betaCommunityId); + expect(follow1.community_view.subscribed).toBe("ApprovalRequired"); + + // Wait for follow to federate, shown as pending + let pendingFollows1 = await waitUntil( + () => listCommunityPendingFollows(alpha), + f => f.items.length == 1, + ); + expect(pendingFollows1.items[0].is_new_instance).toBe(true); + const pendingFollowsCount1 = await getCommunityPendingFollowsCount( + alpha, + alphaCommunityId, + ); + expect(pendingFollowsCount1.count).toBe(1); + + // user still sees approval required at this point + const betaCommunity2 = await getCommunity(user, betaCommunityId); + expect(betaCommunity2.community_view.subscribed).toBe("ApprovalRequired"); + + // Approve the follow + const approve = await approveCommunityPendingFollow( + alpha, + alphaCommunityId, + pendingFollows1.items[0].person.id, + ); + expect(approve.success).toBe(true); + + // Follow is confirmed + await waitUntil( + () => getCommunity(user, betaCommunityId), + c => c.community_view.subscribed == "Subscribed", + ); + const pendingFollows2 = await listCommunityPendingFollows(alpha); + expect(pendingFollows2.items.length).toBe(0); + const pendingFollowsCount2 = await getCommunityPendingFollowsCount( + alpha, + alphaCommunityId, + ); + expect(pendingFollowsCount2.count).toBe(0); + + // follow with another user from that instance, is_new_instance should be false now + const user2 = await registerUser(beta, betaUrl); + await user2.followCommunity(follow_form); + let pendingFollows3 = await waitUntil( + () => listCommunityPendingFollows(alpha), + f => f.items.length == 1, + ); + expect(pendingFollows3.items[0].is_new_instance).toBe(false); + + // cleanup pending follow + const approve2 = await approveCommunityPendingFollow( + alpha, + alphaCommunityId, + pendingFollows3.items[0].person.id, + ); + expect(approve2.success).toBe(true); +}); + +test("Only followers can view and interact with private community content", async () => { + // create private community + const community = await createCommunity(alpha, randomString(10), "Private"); + expect(community.community_view.community.visibility).toBe("Private"); + const alphaCommunityId = community.community_view.community.id; + + // create post and comment + const post0 = await createPost(alpha, alphaCommunityId); + const post_id = post0.post_view.post.id; + expect(post_id).toBeDefined(); + const comment = await createComment(alpha, post_id); + const comment_id = comment.comment_view.comment.id; + expect(comment_id).toBeDefined(); + + // user is not following the community and cannot view nor create posts + const user = await registerUser(beta, betaUrl); + const betaCommunity = ( + await resolveCommunity(user, community.community_view.community.actor_id) + ).community!.community; + await expect(resolvePost(user, post0.post_view.post)).rejects.toStrictEqual( + Error("not_found"), + ); + await expect( + resolveComment(user, comment.comment_view.comment), + ).rejects.toStrictEqual(Error("not_found")); + await expect(createPost(user, betaCommunity.id)).rejects.toStrictEqual( + Error("not_found"), + ); + + // follow the community and approve + const follow_form: FollowCommunity = { + community_id: betaCommunity.id, + follow: true, + }; + await user.followCommunity(follow_form); + const pendingFollows1 = await waitUntil( + () => listCommunityPendingFollows(alpha), + f => f.items.length == 1, + ); + const approve = await approveCommunityPendingFollow( + alpha, + alphaCommunityId, + pendingFollows1.items[0].person.id, + ); + expect(approve.success).toBe(true); + + // now user can fetch posts and comments in community (using signed fetch), and create posts + await waitUntil( + () => resolvePost(user, post0.post_view.post), + p => p?.post?.post.id != undefined, + ); + const resolvedComment = ( + await resolveComment(user, comment.comment_view.comment) + ).comment; + expect(resolvedComment?.comment.id).toBeDefined(); + + const post1 = await createPost(user, betaCommunity.id); + expect(post1.post_view).toBeDefined(); + const like = await likeComment(user, 1, resolvedComment!.comment); + expect(like.comment_view.my_vote).toBe(1); +}); + +test("Reject follower", async () => { + // create private community + const community = await createCommunity(alpha, randomString(10), "Private"); + expect(community.community_view.community.visibility).toBe("Private"); + const alphaCommunityId = community.community_view.community.id; + + // user is not following the community and cannot view nor create posts + const user = await registerUser(beta, betaUrl); + const betaCommunity1 = ( + await resolveCommunity(user, community.community_view.community.actor_id) + ).community!.community; + + // follow the community and reject + const follow_form: FollowCommunity = { + community_id: betaCommunity1.id, + follow: true, + }; + const follow = await user.followCommunity(follow_form); + expect(follow.community_view.subscribed).toBe("ApprovalRequired"); + + const pendingFollows1 = await waitUntil( + () => listCommunityPendingFollows(alpha), + f => f.items.length == 1, + ); + const approve = await approveCommunityPendingFollow( + alpha, + alphaCommunityId, + pendingFollows1.items[0].person.id, + false, + ); + expect(approve.success).toBe(true); + + await waitUntil( + () => getCommunity(user, betaCommunity1.id), + c => c.community_view.subscribed == "NotSubscribed", + ); +}); diff --git a/api_tests/src/shared.ts b/api_tests/src/shared.ts index 20cb171c5..95e916ef2 100644 --- a/api_tests/src/shared.ts +++ b/api_tests/src/shared.ts @@ -1,17 +1,24 @@ import { + ApproveCommunityPendingFollower, BlockCommunity, BlockCommunityResponse, BlockInstance, BlockInstanceResponse, CommunityId, + CommunityVisibility, CreatePrivateMessageReport, DeleteImage, EditCommunity, + GetCommunityPendingFollowsCount, + GetCommunityPendingFollowsCountResponse, GetReplies, GetRepliesResponse, GetUnreadCountResponse, InstanceId, LemmyHttp, + ListCommunityPendingFollows, + ListCommunityPendingFollowsResponse, + PersonId, PostView, PrivateMessageReportResponse, SuccessResponse, @@ -198,7 +205,7 @@ export async function setupLogins() { // only needed the first time so do in this try await delay(10_000); } catch { - console.log("Communities already exist"); + //console.log("Communities already exist"); } } @@ -554,12 +561,14 @@ export async function likeComment( export async function createCommunity( api: LemmyHttp, name_: string = randomString(10), + visibility: CommunityVisibility = "Public", ): Promise { let description = "a sample description"; let form: CreateCommunity = { name: name_, title: name_, description, + visibility, }; return api.createCommunity(form); } @@ -688,7 +697,6 @@ export async function saveUserSettingsBio( let form: SaveUserSettings = { show_nsfw: true, blur_nsfw: false, - auto_expand: true, theme: "darkly", default_post_sort_type: "Active", default_listing_type: "All", @@ -709,7 +717,6 @@ export async function saveUserSettingsFederated( let form: SaveUserSettings = { show_nsfw: false, blur_nsfw: true, - auto_expand: false, default_post_sort_type: "Hot", default_listing_type: "All", interface_language: "", @@ -872,6 +879,39 @@ export function blockCommunity( return api.blockCommunity(form); } +export function listCommunityPendingFollows( + api: LemmyHttp, +): Promise { + let form: ListCommunityPendingFollows = { + pending_only: true, + all_communities: false, + page: 1, + limit: 50, + }; + return api.listCommunityPendingFollows(form); +} + +export function getCommunityPendingFollowsCount( + api: LemmyHttp, + community_id: CommunityId, +): Promise { + return api.getCommunityPendingFollowsCount(community_id); +} + +export function approveCommunityPendingFollow( + api: LemmyHttp, + community_id: CommunityId, + follower_id: PersonId, + approve: boolean = true, +): Promise { + let form: ApproveCommunityPendingFollower = { + community_id, + follower_id, + approve, + }; + return api.approveCommunityPendingFollow(form); +} + export function delay(millis = 500) { return new Promise(resolve => setTimeout(resolve, millis)); } @@ -962,8 +1002,12 @@ export async function waitUntil( let retry = 0; let result; while (retry++ < retries) { - result = await fetcher(); - if (checker(result)) return result; + try { + result = await fetcher(); + if (checker(result)) return result; + } catch (error) { + //console.error(error); + } await delay( delaySeconds[Math.min(retry - 1, delaySeconds.length - 1)] * 1000, ); diff --git a/crates/api/src/comment/distinguish.rs b/crates/api/src/comment/distinguish.rs index a1b25ea44..17608a230 100644 --- a/crates/api/src/comment/distinguish.rs +++ b/crates/api/src/comment/distinguish.rs @@ -26,7 +26,7 @@ pub async fn distinguish_comment( check_community_user_action( &local_user_view.person, - orig_comment.community.id, + &orig_comment.community, &mut context.pool(), ) .await?; @@ -39,7 +39,7 @@ pub async fn distinguish_comment( // Verify that only a mod or admin can distinguish a comment check_community_mod_action( &local_user_view.person, - orig_comment.community.id, + &orig_comment.community, false, &mut context.pool(), ) diff --git a/crates/api/src/comment/like.rs b/crates/api/src/comment/like.rs index e93b8513f..fbc720102 100644 --- a/crates/api/src/comment/like.rs +++ b/crates/api/src/comment/like.rs @@ -50,7 +50,7 @@ pub async fn like_comment( check_community_user_action( &local_user_view.person, - orig_comment.community.id, + &orig_comment.community, &mut context.pool(), ) .await?; @@ -92,8 +92,7 @@ pub async fn like_comment( score: data.score, }, &context, - ) - .await?; + )?; Ok(Json( build_comment_response( diff --git a/crates/api/src/comment_report/create.rs b/crates/api/src/comment_report/create.rs index a0ff4be77..48066cfe6 100644 --- a/crates/api/src/comment_report/create.rs +++ b/crates/api/src/comment_report/create.rs @@ -44,7 +44,7 @@ pub async fn create_comment_report( check_community_user_action( &local_user_view.person, - comment_view.community.id, + &comment_view.community, &mut context.pool(), ) .await?; @@ -85,8 +85,7 @@ pub async fn create_comment_report( reason: data.reason.clone(), }, &context, - ) - .await?; + )?; Ok(Json(CommentReportResponse { comment_report_view, diff --git a/crates/api/src/comment_report/resolve.rs b/crates/api/src/comment_report/resolve.rs index a663fdf74..58d5041dc 100644 --- a/crates/api/src/comment_report/resolve.rs +++ b/crates/api/src/comment_report/resolve.rs @@ -22,7 +22,7 @@ pub async fn resolve_comment_report( let person_id = local_user_view.person.id; check_community_mod_action( &local_user_view.person, - report.community.id, + &report.community, true, &mut context.pool(), ) diff --git a/crates/api/src/community/add_mod.rs b/crates/api/src/community/add_mod.rs index 7d04f6bb0..9e85788ea 100644 --- a/crates/api/src/community/add_mod.rs +++ b/crates/api/src/community/add_mod.rs @@ -24,12 +24,11 @@ pub async fn add_mod_to_community( context: Data, local_user_view: LocalUserView, ) -> LemmyResult> { - let community_id = data.community_id; - + let community = Community::read(&mut context.pool(), data.community_id).await?; // Verify that only mods or admins can add mod check_community_mod_action( &local_user_view.person, - community_id, + &community, false, &mut context.pool(), ) @@ -39,15 +38,13 @@ pub async fn add_mod_to_community( if !data.added { LocalUser::is_higher_mod_or_admin_check( &mut context.pool(), - community_id, + community.id, local_user_view.person.id, vec![data.person_id], ) .await?; } - let community = Community::read(&mut context.pool(), community_id).await?; - // If user is admin and community is remote, explicitly check that he is a // moderator. This is necessary because otherwise the action would be rejected // by the community's home instance. @@ -98,8 +95,7 @@ pub async fn add_mod_to_community( added: data.added, }, &context, - ) - .await?; + )?; Ok(Json(AddModToCommunityResponse { moderators })) } diff --git a/crates/api/src/community/ban.rs b/crates/api/src/community/ban.rs index 64b1c7196..a0e57061b 100644 --- a/crates/api/src/community/ban.rs +++ b/crates/api/src/community/ban.rs @@ -13,6 +13,7 @@ use lemmy_api_common::{ use lemmy_db_schema::{ source::{ community::{ + Community, CommunityFollower, CommunityFollowerForm, CommunityPersonBan, @@ -38,11 +39,12 @@ pub async fn ban_from_community( ) -> LemmyResult> { let banned_person_id = data.person_id; let expires = check_expire_time(data.expires)?; + let community = Community::read(&mut context.pool(), data.community_id).await?; // Verify that only mods or admins can ban check_community_mod_action( &local_user_view.person, - data.community_id, + &community, false, &mut context.pool(), ) @@ -72,12 +74,7 @@ pub async fn ban_from_community( .with_lemmy_type(LemmyErrorType::CommunityUserAlreadyBanned)?; // Also unsubscribe them from the community, if they are subscribed - let community_follower_form = CommunityFollowerForm { - community_id: data.community_id, - person_id: banned_person_id, - pending: false, - }; - + let community_follower_form = CommunityFollowerForm::new(data.community_id, banned_person_id); CommunityFollower::unfollow(&mut context.pool(), &community_follower_form) .await .ok(); @@ -123,8 +120,7 @@ pub async fn ban_from_community( data: data.0.clone(), }, &context, - ) - .await?; + )?; Ok(Json(BanFromCommunityResponse { person_view, diff --git a/crates/api/src/community/block.rs b/crates/api/src/community/block.rs index 90931c762..a6a48e2e7 100644 --- a/crates/api/src/community/block.rs +++ b/crates/api/src/community/block.rs @@ -35,12 +35,7 @@ pub async fn block_community( .with_lemmy_type(LemmyErrorType::CommunityBlockAlreadyExists)?; // Also, unfollow the community, and send a federated unfollow - let community_follower_form = CommunityFollowerForm { - community_id: data.community_id, - person_id, - pending: false, - }; - + let community_follower_form = CommunityFollowerForm::new(data.community_id, person_id); CommunityFollower::unfollow(&mut context.pool(), &community_follower_form) .await .ok(); @@ -65,8 +60,7 @@ pub async fn block_community( false, ), &context, - ) - .await?; + )?; Ok(Json(BlockCommunityResponse { blocked: data.block, diff --git a/crates/api/src/community/follow.rs b/crates/api/src/community/follow.rs index d0f5bbf0d..d5cd3e5b1 100644 --- a/crates/api/src/community/follow.rs +++ b/crates/api/src/community/follow.rs @@ -4,17 +4,18 @@ use lemmy_api_common::{ community::{CommunityResponse, FollowCommunity}, context::LemmyContext, send_activity::{ActivityChannel, SendActivityData}, - utils::check_community_user_action, + utils::{check_community_deleted_removed, check_user_valid}, }; use lemmy_db_schema::{ source::{ actor_language::CommunityLanguage, - community::{Community, CommunityFollower, CommunityFollowerForm}, + community::{Community, CommunityFollower, CommunityFollowerForm, CommunityFollowerState}, }, traits::{Crud, Followable}, + CommunityVisibility, }; use lemmy_db_views::structs::LocalUserView; -use lemmy_db_views_actor::structs::CommunityView; +use lemmy_db_views_actor::structs::{CommunityPersonBanView, CommunityView}; use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult}; #[tracing::instrument(skip(context))] @@ -23,40 +24,52 @@ pub async fn follow_community( context: Data, local_user_view: LocalUserView, ) -> LemmyResult> { + check_user_valid(&local_user_view.person)?; let community = Community::read(&mut context.pool(), data.community_id).await?; - let mut community_follower_form = CommunityFollowerForm { - community_id: community.id, - person_id: local_user_view.person.id, - pending: false, - }; + let form = CommunityFollowerForm::new(community.id, local_user_view.person.id); if data.follow { + // Only run these checks for local community, in case of remote community the local + // state may be outdated. Can't use check_community_user_action() here as it only allows + // actions from existing followers for private community (so following would be impossible). if community.local { - check_community_user_action(&local_user_view.person, community.id, &mut context.pool()) + check_community_deleted_removed(&community)?; + CommunityPersonBanView::check(&mut context.pool(), local_user_view.person.id, community.id) .await?; - - CommunityFollower::follow(&mut context.pool(), &community_follower_form) - .await - .with_lemmy_type(LemmyErrorType::CommunityFollowerAlreadyExists)?; - } else { - // Mark as pending, the actual federation activity is sent via `SendActivity` handler - community_follower_form.pending = true; - CommunityFollower::follow(&mut context.pool(), &community_follower_form) - .await - .with_lemmy_type(LemmyErrorType::CommunityFollowerAlreadyExists)?; } + + let state = if community.local { + // Local follow is accepted immediately + Some(CommunityFollowerState::Accepted) + } else if community.visibility == CommunityVisibility::Private { + // Private communities require manual approval + Some(CommunityFollowerState::ApprovalRequired) + } else { + // remote follow needs to be federated first + Some(CommunityFollowerState::Pending) + }; + + let form = CommunityFollowerForm { + state, + ..CommunityFollowerForm::new(community.id, local_user_view.person.id) + }; + + // Write to db + CommunityFollower::follow(&mut context.pool(), &form) + .await + .with_lemmy_type(LemmyErrorType::CommunityFollowerAlreadyExists)?; } else { - CommunityFollower::unfollow(&mut context.pool(), &community_follower_form) + CommunityFollower::unfollow(&mut context.pool(), &form) .await .with_lemmy_type(LemmyErrorType::CommunityFollowerAlreadyExists)?; } + // Send the federated follow if !community.local { ActivityChannel::submit_activity( SendActivityData::FollowCommunity(community, local_user_view.person.clone(), data.follow), &context, - ) - .await?; + )?; } let community_id = data.community_id; diff --git a/crates/api/src/community/hide.rs b/crates/api/src/community/hide.rs index 997d88de3..077ed1c5e 100644 --- a/crates/api/src/community/hide.rs +++ b/crates/api/src/community/hide.rs @@ -48,8 +48,7 @@ pub async fn hide_community( ActivityChannel::submit_activity( SendActivityData::UpdateCommunity(local_user_view.person.clone(), community), &context, - ) - .await?; + )?; Ok(Json(SuccessResponse::default())) } diff --git a/crates/api/src/community/mod.rs b/crates/api/src/community/mod.rs index 54bdbef28..121e181c6 100644 --- a/crates/api/src/community/mod.rs +++ b/crates/api/src/community/mod.rs @@ -3,5 +3,6 @@ pub mod ban; pub mod block; pub mod follow; pub mod hide; +pub mod pending_follows; pub mod random; pub mod transfer; diff --git a/crates/api/src/community/pending_follows/approve.rs b/crates/api/src/community/pending_follows/approve.rs new file mode 100644 index 000000000..468e9d9d0 --- /dev/null +++ b/crates/api/src/community/pending_follows/approve.rs @@ -0,0 +1,46 @@ +use activitypub_federation::config::Data; +use actix_web::web::Json; +use lemmy_api_common::{ + community::ApproveCommunityPendingFollower, + context::LemmyContext, + send_activity::{ActivityChannel, SendActivityData}, + utils::is_mod_or_admin, + SuccessResponse, +}; +use lemmy_db_schema::{ + source::community::{CommunityFollower, CommunityFollowerForm}, + traits::Followable, +}; +use lemmy_db_views::structs::LocalUserView; +use lemmy_utils::error::LemmyResult; + +pub async fn post_pending_follows_approve( + data: Json, + context: Data, + local_user_view: LocalUserView, +) -> LemmyResult> { + is_mod_or_admin( + &mut context.pool(), + &local_user_view.person, + data.community_id, + ) + .await?; + + let activity_data = if data.approve { + CommunityFollower::approve( + &mut context.pool(), + data.community_id, + data.follower_id, + local_user_view.person.id, + ) + .await?; + SendActivityData::AcceptFollower(data.community_id, data.follower_id) + } else { + let form = CommunityFollowerForm::new(data.community_id, data.follower_id); + CommunityFollower::unfollow(&mut context.pool(), &form).await?; + SendActivityData::RejectFollower(data.community_id, data.follower_id) + }; + ActivityChannel::submit_activity(activity_data, &context)?; + + Ok(Json(SuccessResponse::default())) +} diff --git a/crates/api/src/community/pending_follows/count.rs b/crates/api/src/community/pending_follows/count.rs new file mode 100644 index 000000000..e8e333c84 --- /dev/null +++ b/crates/api/src/community/pending_follows/count.rs @@ -0,0 +1,25 @@ +use actix_web::web::{Data, Json, Query}; +use lemmy_api_common::{ + community::{GetCommunityPendingFollowsCount, GetCommunityPendingFollowsCountResponse}, + context::LemmyContext, + utils::is_mod_or_admin, +}; +use lemmy_db_views::structs::LocalUserView; +use lemmy_db_views_actor::structs::CommunityFollowerView; +use lemmy_utils::error::LemmyResult; + +pub async fn get_pending_follows_count( + data: Query, + context: Data, + local_user_view: LocalUserView, +) -> LemmyResult> { + is_mod_or_admin( + &mut context.pool(), + &local_user_view.person, + data.community_id, + ) + .await?; + let count = + CommunityFollowerView::count_approval_required(&mut context.pool(), data.community_id).await?; + Ok(Json(GetCommunityPendingFollowsCountResponse { count })) +} diff --git a/crates/api/src/community/pending_follows/list.rs b/crates/api/src/community/pending_follows/list.rs new file mode 100644 index 000000000..9f300a74f --- /dev/null +++ b/crates/api/src/community/pending_follows/list.rs @@ -0,0 +1,29 @@ +use actix_web::web::{Data, Json, Query}; +use lemmy_api_common::{ + community::{ListCommunityPendingFollows, ListCommunityPendingFollowsResponse}, + context::LemmyContext, + utils::check_community_mod_of_any_or_admin_action, +}; +use lemmy_db_views::structs::LocalUserView; +use lemmy_db_views_actor::structs::CommunityFollowerView; +use lemmy_utils::error::LemmyResult; + +pub async fn get_pending_follows_list( + data: Query, + context: Data, + local_user_view: LocalUserView, +) -> LemmyResult> { + check_community_mod_of_any_or_admin_action(&local_user_view, &mut context.pool()).await?; + let all_communities = + data.all_communities.unwrap_or_default() && local_user_view.local_user.admin; + let items = CommunityFollowerView::list_approval_required( + &mut context.pool(), + local_user_view.person.id, + all_communities, + data.pending_only.unwrap_or_default(), + data.page, + data.limit, + ) + .await?; + Ok(Json(ListCommunityPendingFollowsResponse { items })) +} diff --git a/crates/api/src/community/pending_follows/mod.rs b/crates/api/src/community/pending_follows/mod.rs new file mode 100644 index 000000000..dcc82e250 --- /dev/null +++ b/crates/api/src/community/pending_follows/mod.rs @@ -0,0 +1,3 @@ +pub mod approve; +pub mod count; +pub mod list; diff --git a/crates/api/src/community/transfer.rs b/crates/api/src/community/transfer.rs index 195adbd8d..a5255e5e1 100644 --- a/crates/api/src/community/transfer.rs +++ b/crates/api/src/community/transfer.rs @@ -7,7 +7,7 @@ use lemmy_api_common::{ }; use lemmy_db_schema::{ source::{ - community::{CommunityModerator, CommunityModeratorForm}, + community::{Community, CommunityModerator, CommunityModeratorForm}, moderator::{ModTransferCommunity, ModTransferCommunityForm}, }, traits::{Crud, Joinable}, @@ -27,11 +27,11 @@ pub async fn transfer_community( context: Data, local_user_view: LocalUserView, ) -> LemmyResult> { - let community_id = data.community_id; + let community = Community::read(&mut context.pool(), data.community_id).await?; let mut community_mods = - CommunityModeratorView::for_community(&mut context.pool(), community_id).await?; + CommunityModeratorView::for_community(&mut context.pool(), community.id).await?; - check_community_user_action(&local_user_view.person, community_id, &mut context.pool()).await?; + check_community_user_action(&local_user_view.person, &community, &mut context.pool()).await?; // Make sure transferrer is either the top community mod, or an admin if !(is_top_mod(&local_user_view, &community_mods).is_ok() || is_admin(&local_user_view).is_ok()) diff --git a/crates/api/src/lib.rs b/crates/api/src/lib.rs index 6ffa52f77..3ab2ba277 100644 --- a/crates/api/src/lib.rs +++ b/crates/api/src/lib.rs @@ -197,11 +197,7 @@ pub(crate) async fn ban_nonlocal_user_from_local_communities( .ok(); // Also unsubscribe them from the community, if they are subscribed - let community_follower_form = CommunityFollowerForm { - community_id, - person_id: target.id, - pending: false, - }; + let community_follower_form = CommunityFollowerForm::new(community_id, target.id); CommunityFollower::unfollow(&mut context.pool(), &community_follower_form) .await @@ -242,8 +238,7 @@ pub(crate) async fn ban_nonlocal_user_from_local_communities( data: ban_from_community, }, context, - ) - .await?; + )?; } } diff --git a/crates/api/src/local_user/ban_person.rs b/crates/api/src/local_user/ban_person.rs index 2ace7f031..9349cc632 100644 --- a/crates/api/src/local_user/ban_person.rs +++ b/crates/api/src/local_user/ban_person.rs @@ -111,8 +111,7 @@ pub async fn ban_from_site( expires: data.expires, }, &context, - ) - .await?; + )?; Ok(Json(BanPersonResponse { person_view, diff --git a/crates/api/src/post/feature.rs b/crates/api/src/post/feature.rs index cb6e6c144..6fc2f443c 100644 --- a/crates/api/src/post/feature.rs +++ b/crates/api/src/post/feature.rs @@ -9,6 +9,7 @@ use lemmy_api_common::{ }; use lemmy_db_schema::{ source::{ + community::Community, moderator::{ModFeaturePost, ModFeaturePostForm}, post::{Post, PostUpdateForm}, }, @@ -27,9 +28,10 @@ pub async fn feature_post( let post_id = data.post_id; let orig_post = Post::read(&mut context.pool(), post_id).await?; + let community = Community::read(&mut context.pool(), orig_post.community_id).await?; check_community_mod_action( &local_user_view.person, - orig_post.community_id, + &community, false, &mut context.pool(), ) @@ -67,8 +69,7 @@ pub async fn feature_post( ActivityChannel::submit_activity( SendActivityData::FeaturePost(post, local_user_view.person.clone(), data.featured), &context, - ) - .await?; + )?; build_post_response(&context, orig_post.community_id, local_user_view, post_id).await } diff --git a/crates/api/src/post/like.rs b/crates/api/src/post/like.rs index c81d9630a..ec01e3e8c 100644 --- a/crates/api/src/post/like.rs +++ b/crates/api/src/post/like.rs @@ -15,13 +15,12 @@ use lemmy_api_common::{ }; use lemmy_db_schema::{ source::{ - community::Community, local_site::LocalSite, - post::{Post, PostLike, PostLikeForm}, + post::{PostLike, PostLikeForm}, }, - traits::{Crud, Likeable}, + traits::Likeable, }; -use lemmy_db_views::structs::LocalUserView; +use lemmy_db_views::structs::{LocalUserView, PostView}; use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult}; use std::ops::Deref; @@ -45,11 +44,11 @@ pub async fn like_post( check_bot_account(&local_user_view.person)?; // Check for a community ban - let post = Post::read(&mut context.pool(), post_id).await?; + let post = PostView::read(&mut context.pool(), post_id, None, false).await?; check_community_user_action( &local_user_view.person, - post.community_id, + &post.community, &mut context.pool(), ) .await?; @@ -75,18 +74,15 @@ pub async fn like_post( mark_post_as_read(person_id, post_id, &mut context.pool()).await?; - let community = Community::read(&mut context.pool(), post.community_id).await?; - ActivityChannel::submit_activity( SendActivityData::LikePostOrComment { - object_id: post.ap_id, + object_id: post.post.ap_id, actor: local_user_view.person.clone(), - community, + community: post.community.clone(), score: data.score, }, &context, - ) - .await?; + )?; - build_post_response(context.deref(), post.community_id, local_user_view, post_id).await + build_post_response(context.deref(), post.community.id, local_user_view, post_id).await } diff --git a/crates/api/src/post/lock.rs b/crates/api/src/post/lock.rs index 548947b78..011770c2e 100644 --- a/crates/api/src/post/lock.rs +++ b/crates/api/src/post/lock.rs @@ -14,7 +14,7 @@ use lemmy_db_schema::{ }, traits::Crud, }; -use lemmy_db_views::structs::LocalUserView; +use lemmy_db_views::structs::{LocalUserView, PostView}; use lemmy_utils::error::LemmyResult; #[tracing::instrument(skip(context))] @@ -24,11 +24,11 @@ pub async fn lock_post( local_user_view: LocalUserView, ) -> LemmyResult> { let post_id = data.post_id; - let orig_post = Post::read(&mut context.pool(), post_id).await?; + let orig_post = PostView::read(&mut context.pool(), post_id, None, false).await?; check_community_mod_action( &local_user_view.person, - orig_post.community_id, + &orig_post.community, false, &mut context.pool(), ) @@ -58,8 +58,7 @@ pub async fn lock_post( ActivityChannel::submit_activity( SendActivityData::LockPost(post, local_user_view.person.clone(), data.locked), &context, - ) - .await?; + )?; - build_post_response(&context, orig_post.community_id, local_user_view, post_id).await + build_post_response(&context, orig_post.community.id, local_user_view, post_id).await } diff --git a/crates/api/src/post_report/create.rs b/crates/api/src/post_report/create.rs index 590c9af40..b9edf35c5 100644 --- a/crates/api/src/post_report/create.rs +++ b/crates/api/src/post_report/create.rs @@ -39,7 +39,7 @@ pub async fn create_post_report( check_community_user_action( &local_user_view.person, - post_view.community.id, + &post_view.community, &mut context.pool(), ) .await?; @@ -80,8 +80,7 @@ pub async fn create_post_report( reason: data.reason.clone(), }, &context, - ) - .await?; + )?; Ok(Json(PostReportResponse { post_report_view })) } diff --git a/crates/api/src/post_report/resolve.rs b/crates/api/src/post_report/resolve.rs index a3cb85c6c..652327513 100644 --- a/crates/api/src/post_report/resolve.rs +++ b/crates/api/src/post_report/resolve.rs @@ -22,7 +22,7 @@ pub async fn resolve_post_report( let person_id = local_user_view.person.id; check_community_mod_action( &local_user_view.person, - report.community.id, + &report.community, true, &mut context.pool(), ) diff --git a/crates/api/src/site/purge/comment.rs b/crates/api/src/site/purge/comment.rs index b21ffbc80..ae79a835a 100644 --- a/crates/api/src/site/purge/comment.rs +++ b/crates/api/src/site/purge/comment.rs @@ -67,8 +67,7 @@ pub async fn purge_comment( reason: data.reason.clone(), }, &context, - ) - .await?; + )?; Ok(Json(SuccessResponse::default())) } diff --git a/crates/api/src/site/purge/community.rs b/crates/api/src/site/purge/community.rs index bf06bd529..f0252e303 100644 --- a/crates/api/src/site/purge/community.rs +++ b/crates/api/src/site/purge/community.rs @@ -75,8 +75,7 @@ pub async fn purge_community( removed: true, }, &context, - ) - .await?; + )?; Ok(Json(SuccessResponse::default())) } diff --git a/crates/api/src/site/purge/person.rs b/crates/api/src/site/purge/person.rs index 7ab573cbc..6dad4ce65 100644 --- a/crates/api/src/site/purge/person.rs +++ b/crates/api/src/site/purge/person.rs @@ -80,8 +80,7 @@ pub async fn purge_person( expires: None, }, &context, - ) - .await?; + )?; Ok(Json(SuccessResponse::default())) } diff --git a/crates/api/src/site/purge/post.rs b/crates/api/src/site/purge/post.rs index d2cacdae1..f808269e7 100644 --- a/crates/api/src/site/purge/post.rs +++ b/crates/api/src/site/purge/post.rs @@ -66,8 +66,7 @@ pub async fn purge_post( removed: true, }, &context, - ) - .await?; + )?; Ok(Json(SuccessResponse::default())) } diff --git a/crates/api/src/sitemap.rs b/crates/api/src/sitemap.rs index 57b39a5b3..4d3799b1b 100644 --- a/crates/api/src/sitemap.rs +++ b/crates/api/src/sitemap.rs @@ -9,9 +9,7 @@ use lemmy_utils::error::LemmyResult; use sitemap_rs::{url::Url, url_set::UrlSet}; use tracing::info; -async fn generate_urlset( - posts: Vec<(DbUrl, chrono::DateTime)>, -) -> LemmyResult { +fn generate_urlset(posts: Vec<(DbUrl, chrono::DateTime)>) -> LemmyResult { let urls = posts .into_iter() .map_while(|(url, date_time)| { @@ -31,7 +29,7 @@ pub async fn get_sitemap(context: Data) -> LemmyResult::new(); - generate_urlset(posts).await?.write(&mut buf)?; + generate_urlset(posts)?.write(&mut buf)?; Ok( HttpResponse::Ok() @@ -74,7 +72,7 @@ pub(crate) mod tests { ]; let mut buf = Vec::::new(); - generate_urlset(posts).await?.write(&mut buf)?; + generate_urlset(posts)?.write(&mut buf)?; let root = Element::from_reader(buf.as_slice())?; assert_eq!(root.tag().name(), "urlset"); diff --git a/crates/api_common/src/claims.rs b/crates/api_common/src/claims.rs index 6476f855a..759673f4b 100644 --- a/crates/api_common/src/claims.rs +++ b/crates/api_common/src/claims.rs @@ -92,7 +92,7 @@ mod tests { #[tokio::test] #[serial] async fn test_should_not_validate_user_token_after_password_change() -> LemmyResult<()> { - let pool_ = build_db_pool_for_tests().await; + let pool_ = build_db_pool_for_tests(); let pool = &mut (&pool_).into(); let secret = Secret::init(pool).await?; let context = LemmyContext::create( diff --git a/crates/api_common/src/community.rs b/crates/api_common/src/community.rs index 2fab2d05c..898767b34 100644 --- a/crates/api_common/src/community.rs +++ b/crates/api_common/src/community.rs @@ -8,6 +8,7 @@ use lemmy_db_views_actor::structs::{ CommunityModeratorView, CommunitySortType, CommunityView, + PendingFollow, PersonView, }; use serde::{Deserialize, Serialize}; @@ -274,3 +275,50 @@ pub struct GetRandomCommunity { #[cfg_attr(feature = "full", ts(optional))] pub type_: Option, } + +#[skip_serializing_none] +#[derive(Debug, Serialize, Deserialize, Clone)] +#[cfg_attr(feature = "full", derive(TS))] +#[cfg_attr(feature = "full", ts(export))] +pub struct ListCommunityPendingFollows { + /// Only shows the unapproved applications + #[cfg_attr(feature = "full", ts(optional))] + pub pending_only: Option, + // Only for admins, show pending follows for communities which you dont moderate + #[cfg_attr(feature = "full", ts(optional))] + pub all_communities: Option, + #[cfg_attr(feature = "full", ts(optional))] + pub page: Option, + #[cfg_attr(feature = "full", ts(optional))] + pub limit: Option, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +#[cfg_attr(feature = "full", derive(TS))] +#[cfg_attr(feature = "full", ts(export))] +pub struct GetCommunityPendingFollowsCount { + pub community_id: CommunityId, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +#[cfg_attr(feature = "full", derive(TS))] +#[cfg_attr(feature = "full", ts(export))] +pub struct GetCommunityPendingFollowsCountResponse { + pub count: i64, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +#[cfg_attr(feature = "full", derive(TS))] +#[cfg_attr(feature = "full", ts(export))] +pub struct ListCommunityPendingFollowsResponse { + pub items: Vec, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +#[cfg_attr(feature = "full", derive(TS))] +#[cfg_attr(feature = "full", ts(export))] +pub struct ApproveCommunityPendingFollower { + pub community_id: CommunityId, + pub follower_id: PersonId, + pub approve: bool, +} diff --git a/crates/api_common/src/context.rs b/crates/api_common/src/context.rs index 334983b20..c6ab23bfc 100644 --- a/crates/api_common/src/context.rs +++ b/crates/api_common/src/context.rs @@ -57,7 +57,7 @@ impl LemmyContext { /// Do not use this in production code. pub async fn init_test_federation_config() -> FederationConfig { // call this to run migrations - let pool = build_db_pool_for_tests().await; + let pool = build_db_pool_for_tests(); let client = client_builder(&SETTINGS).build().expect("build client"); diff --git a/crates/api_common/src/request.rs b/crates/api_common/src/request.rs index b0da6cf4d..96d64d0e5 100644 --- a/crates/api_common/src/request.rs +++ b/crates/api_common/src/request.rs @@ -175,7 +175,7 @@ pub async fn generate_post_link_metadata( }; let updated_post = Post::update(&mut context.pool(), post.id, &form).await?; if let Some(send_activity) = send_activity(updated_post) { - ActivityChannel::submit_activity(send_activity, &context).await?; + ActivityChannel::submit_activity(send_activity, &context)?; } Ok(()) } diff --git a/crates/api_common/src/send_activity.rs b/crates/api_common/src/send_activity.rs index 465e074f4..b606c9a90 100644 --- a/crates/api_common/src/send_activity.rs +++ b/crates/api_common/src/send_activity.rs @@ -59,6 +59,8 @@ pub enum SendActivityData { score: i16, }, FollowCommunity(Community, Person, bool), + AcceptFollower(CommunityId, PersonId), + RejectFollower(CommunityId, PersonId), UpdateCommunity(Person, Community), DeleteCommunity(Person, Community, bool), RemoveCommunity { @@ -123,10 +125,7 @@ impl ActivityChannel { lock.recv().await } - pub async fn submit_activity( - data: SendActivityData, - _context: &Data, - ) -> LemmyResult<()> { + pub fn submit_activity(data: SendActivityData, _context: &Data) -> LemmyResult<()> { // could do `ACTIVITY_CHANNEL.keepalive_sender.lock()` instead and get rid of weak_sender, // not sure which way is more efficient if let Some(sender) = ACTIVITY_CHANNEL.weak_sender.upgrade() { diff --git a/crates/api_common/src/utils.rs b/crates/api_common/src/utils.rs index ddddd35e9..6b79ce6ba 100644 --- a/crates/api_common/src/utils.rs +++ b/crates/api_common/src/utils.rs @@ -42,6 +42,7 @@ use lemmy_db_views::{ structs::{LocalImageView, LocalUserView, SiteView}, }; use lemmy_db_views_actor::structs::{ + CommunityFollowerView, CommunityModeratorView, CommunityPersonBanView, CommunityView, @@ -232,20 +233,17 @@ pub async fn check_registration_application( /// the user isn't banned. pub async fn check_community_user_action( person: &Person, - community_id: CommunityId, + community: &Community, pool: &mut DbPool<'_>, ) -> LemmyResult<()> { check_user_valid(person)?; - check_community_deleted_removed(community_id, pool).await?; - CommunityPersonBanView::check(pool, person.id, community_id).await?; + check_community_deleted_removed(community)?; + CommunityPersonBanView::check(pool, person.id, community.id).await?; + CommunityFollowerView::check_private_community_action(pool, person.id, community).await?; Ok(()) } -async fn check_community_deleted_removed( - community_id: CommunityId, - pool: &mut DbPool<'_>, -) -> LemmyResult<()> { - let community = Community::read(pool, community_id).await?; +pub fn check_community_deleted_removed(community: &Community) -> LemmyResult<()> { if community.deleted || community.removed { Err(LemmyErrorType::Deleted)? } @@ -258,16 +256,16 @@ async fn check_community_deleted_removed( /// removed/deleted. pub async fn check_community_mod_action( person: &Person, - community_id: CommunityId, + community: &Community, allow_deleted: bool, pool: &mut DbPool<'_>, ) -> LemmyResult<()> { - is_mod_or_admin(pool, person, community_id).await?; - CommunityPersonBanView::check(pool, person.id, community_id).await?; + is_mod_or_admin(pool, person, community.id).await?; + CommunityPersonBanView::check(pool, person.id, community.id).await?; // it must be possible to restore deleted community if !allow_deleted { - check_community_deleted_removed(community_id, pool).await?; + check_community_deleted_removed(community)?; } Ok(()) } diff --git a/crates/api_crud/src/comment/create.rs b/crates/api_crud/src/comment/create.rs index 2f67fa7e7..65aa1f612 100644 --- a/crates/api_crud/src/comment/create.rs +++ b/crates/api_crud/src/comment/create.rs @@ -61,7 +61,12 @@ pub async fn create_comment( let post = post_view.post; let community_id = post_view.community.id; - check_community_user_action(&local_user_view.person, community_id, &mut context.pool()).await?; + check_community_user_action( + &local_user_view.person, + &post_view.community, + &mut context.pool(), + ) + .await?; check_post_deleted_or_removed(&post)?; // Check if post is locked, no new comments @@ -143,8 +148,7 @@ pub async fn create_comment( ActivityChannel::submit_activity( SendActivityData::CreateComment(inserted_comment.clone()), &context, - ) - .await?; + )?; // Update the read comments, so your own new comment doesn't appear as a +1 unread update_read_comments( diff --git a/crates/api_crud/src/comment/delete.rs b/crates/api_crud/src/comment/delete.rs index 2b5f35827..60a22a2ef 100644 --- a/crates/api_crud/src/comment/delete.rs +++ b/crates/api_crud/src/comment/delete.rs @@ -35,7 +35,7 @@ pub async fn delete_comment( check_community_user_action( &local_user_view.person, - orig_comment.community.id, + &orig_comment.community, &mut context.pool(), ) .await?; @@ -76,8 +76,7 @@ pub async fn delete_comment( orig_comment.community, ), &context, - ) - .await?; + )?; Ok(Json( build_comment_response( diff --git a/crates/api_crud/src/comment/remove.rs b/crates/api_crud/src/comment/remove.rs index 3c137a984..4e8a1871a 100644 --- a/crates/api_crud/src/comment/remove.rs +++ b/crates/api_crud/src/comment/remove.rs @@ -35,7 +35,7 @@ pub async fn remove_comment( check_community_mod_action( &local_user_view.person, - orig_comment.community.id, + &orig_comment.community, false, &mut context.pool(), ) @@ -99,8 +99,7 @@ pub async fn remove_comment( reason: data.reason.clone(), }, &context, - ) - .await?; + )?; Ok(Json( build_comment_response( diff --git a/crates/api_crud/src/comment/update.rs b/crates/api_crud/src/comment/update.rs index 51f65aa67..95cc85fe4 100644 --- a/crates/api_crud/src/comment/update.rs +++ b/crates/api_crud/src/comment/update.rs @@ -45,7 +45,7 @@ pub async fn update_comment( check_community_user_action( &local_user_view.person, - orig_comment.community.id, + &orig_comment.community, &mut context.pool(), ) .await?; @@ -98,8 +98,7 @@ pub async fn update_comment( ActivityChannel::submit_activity( SendActivityData::UpdateComment(updated_comment.clone()), &context, - ) - .await?; + )?; Ok(Json( build_comment_response( diff --git a/crates/api_crud/src/community/create.rs b/crates/api_crud/src/community/create.rs index cd0fc985e..c81157950 100644 --- a/crates/api_crud/src/community/create.rs +++ b/crates/api_crud/src/community/create.rs @@ -1,3 +1,4 @@ +use super::check_community_visibility_allowed; use activitypub_federation::{config::Data, http_signatures::generate_actor_keypair}; use actix_web::web::Json; use lemmy_api_common::{ @@ -23,6 +24,7 @@ use lemmy_db_schema::{ Community, CommunityFollower, CommunityFollowerForm, + CommunityFollowerState, CommunityInsertForm, CommunityModerator, CommunityModeratorForm, @@ -82,6 +84,12 @@ pub async fn create_community( is_valid_actor_name(&data.name, local_site.actor_name_max_length as usize)?; + if let Some(desc) = &data.description { + is_valid_body_field(desc, false)?; + } + + check_community_visibility_allowed(data.visibility, &local_user_view)?; + // Double check for duplicate community actor_ids let community_actor_id = generate_local_apub_endpoint( EndpointType::Community, @@ -135,7 +143,8 @@ pub async fn create_community( let community_follower_form = CommunityFollowerForm { community_id: inserted_community.id, person_id: local_user_view.person.id, - pending: false, + state: Some(CommunityFollowerState::Accepted), + approver_id: None, }; CommunityFollower::follow(&mut context.pool(), &community_follower_form) diff --git a/crates/api_crud/src/community/delete.rs b/crates/api_crud/src/community/delete.rs index a2ceaff50..7f9d04933 100644 --- a/crates/api_crud/src/community/delete.rs +++ b/crates/api_crud/src/community/delete.rs @@ -22,13 +22,13 @@ pub async fn delete_community( local_user_view: LocalUserView, ) -> LemmyResult> { // Fetch the community mods - let community_id = data.community_id; let community_mods = - CommunityModeratorView::for_community(&mut context.pool(), community_id).await?; + CommunityModeratorView::for_community(&mut context.pool(), data.community_id).await?; + let community = Community::read(&mut context.pool(), data.community_id).await?; check_community_mod_action( &local_user_view.person, - community_id, + &community, true, &mut context.pool(), ) @@ -54,8 +54,7 @@ pub async fn delete_community( ActivityChannel::submit_activity( SendActivityData::DeleteCommunity(local_user_view.person.clone(), community, data.deleted), &context, - ) - .await?; + )?; build_community_response(&context, local_user_view, community_id).await } diff --git a/crates/api_crud/src/community/mod.rs b/crates/api_crud/src/community/mod.rs index 4bd028482..0c9a507f1 100644 --- a/crates/api_crud/src/community/mod.rs +++ b/crates/api_crud/src/community/mod.rs @@ -1,5 +1,22 @@ +use lemmy_api_common::utils::is_admin; +use lemmy_db_schema::CommunityVisibility; +use lemmy_db_views::structs::LocalUserView; +use lemmy_utils::error::LemmyResult; + pub mod create; pub mod delete; pub mod list; pub mod remove; pub mod update; + +/// For now only admins can make communities private, in order to prevent abuse. +/// Need to implement admin approval for new communities to get rid of this. +fn check_community_visibility_allowed( + visibility: Option, + local_user_view: &LocalUserView, +) -> LemmyResult<()> { + if visibility == Some(lemmy_db_schema::CommunityVisibility::Private) { + is_admin(local_user_view)?; + } + Ok(()) +} diff --git a/crates/api_crud/src/community/remove.rs b/crates/api_crud/src/community/remove.rs index f4271565d..c506bde1b 100644 --- a/crates/api_crud/src/community/remove.rs +++ b/crates/api_crud/src/community/remove.rs @@ -23,9 +23,10 @@ pub async fn remove_community( context: Data, local_user_view: LocalUserView, ) -> LemmyResult> { + let community = Community::read(&mut context.pool(), data.community_id).await?; check_community_mod_action( &local_user_view.person, - data.community_id, + &community, true, &mut context.pool(), ) @@ -65,8 +66,7 @@ pub async fn remove_community( removed: data.removed, }, &context, - ) - .await?; + )?; build_community_response(&context, local_user_view, community_id).await } diff --git a/crates/api_crud/src/community/update.rs b/crates/api_crud/src/community/update.rs index cde8058ee..3dca7d892 100644 --- a/crates/api_crud/src/community/update.rs +++ b/crates/api_crud/src/community/update.rs @@ -1,3 +1,4 @@ +use super::check_community_visibility_allowed; use activitypub_federation::config::Data; use actix_web::web::Json; use lemmy_api_common::{ @@ -51,6 +52,7 @@ pub async fn update_community( is_valid_body_field(sidebar, false)?; } + check_community_visibility_allowed(data.visibility, &local_user_view)?; let description = diesel_string_update(data.description.as_deref()); let old_community = Community::read(&mut context.pool(), data.community_id).await?; @@ -66,7 +68,7 @@ pub async fn update_community( // Verify its a mod (only mods can edit it) check_community_mod_action( &local_user_view.person, - data.community_id, + &old_community, false, &mut context.pool(), ) @@ -105,8 +107,7 @@ pub async fn update_community( ActivityChannel::submit_activity( SendActivityData::UpdateCommunity(local_user_view.person.clone(), community), &context, - ) - .await?; + )?; build_community_response(&context, local_user_view, community_id).await } diff --git a/crates/api_crud/src/post/create.rs b/crates/api_crud/src/post/create.rs index 90c68bdbd..16932cacb 100644 --- a/crates/api_crud/src/post/create.rs +++ b/crates/api_crud/src/post/create.rs @@ -85,15 +85,9 @@ pub async fn create_post( is_valid_body_field(body, true)?; } - check_community_user_action( - &local_user_view.person, - data.community_id, - &mut context.pool(), - ) - .await?; + let community = Community::read(&mut context.pool(), data.community_id).await?; + check_community_user_action(&local_user_view.person, &community, &mut context.pool()).await?; - let community_id = data.community_id; - let community = Community::read(&mut context.pool(), community_id).await?; if community.posting_restricted_to_mods { let community_id = data.community_id; CommunityModeratorView::check_is_community_moderator( @@ -110,7 +104,7 @@ pub async fn create_post( None => { default_post_language( &mut context.pool(), - community_id, + community.id, local_user_view.local_user.id, ) .await? @@ -119,7 +113,7 @@ pub async fn create_post( // Only need to check if language is allowed in case user set it explicitly. When using default // language, it already only returns allowed languages. - CommunityLanguage::is_allowed_community_language(&mut context.pool(), language_id, community_id) + CommunityLanguage::is_allowed_community_language(&mut context.pool(), language_id, community.id) .await?; let scheduled_publish_time = @@ -142,6 +136,7 @@ pub async fn create_post( .await .with_lemmy_type(LemmyErrorType::CouldntCreatePost)?; + let community_id = community.id; let federate_post = if scheduled_publish_time.is_none() { send_webmention(inserted_post.clone(), community); |post| Some(SendActivityData::CreatePost(post)) diff --git a/crates/api_crud/src/post/delete.rs b/crates/api_crud/src/post/delete.rs index be31759d5..e54086911 100644 --- a/crates/api_crud/src/post/delete.rs +++ b/crates/api_crud/src/post/delete.rs @@ -8,7 +8,10 @@ use lemmy_api_common::{ utils::check_community_user_action, }; use lemmy_db_schema::{ - source::post::{Post, PostUpdateForm}, + source::{ + community::Community, + post::{Post, PostUpdateForm}, + }, traits::Crud, }; use lemmy_db_views::structs::LocalUserView; @@ -28,12 +31,8 @@ pub async fn delete_post( Err(LemmyErrorType::CouldntUpdatePost)? } - check_community_user_action( - &local_user_view.person, - orig_post.community_id, - &mut context.pool(), - ) - .await?; + let community = Community::read(&mut context.pool(), orig_post.community_id).await?; + check_community_user_action(&local_user_view.person, &community, &mut context.pool()).await?; // Verify that only the creator can delete if !Post::is_post_creator(local_user_view.person.id, orig_post.creator_id) { @@ -54,8 +53,7 @@ pub async fn delete_post( ActivityChannel::submit_activity( SendActivityData::DeletePost(post, local_user_view.person.clone(), data.0), &context, - ) - .await?; + )?; build_post_response( &context, diff --git a/crates/api_crud/src/post/remove.rs b/crates/api_crud/src/post/remove.rs index c53a4552c..7e3261e6f 100644 --- a/crates/api_crud/src/post/remove.rs +++ b/crates/api_crud/src/post/remove.rs @@ -9,6 +9,7 @@ use lemmy_api_common::{ }; use lemmy_db_schema::{ source::{ + community::Community, local_user::LocalUser, moderator::{ModRemovePost, ModRemovePostForm}, post::{Post, PostUpdateForm}, @@ -26,11 +27,16 @@ pub async fn remove_post( local_user_view: LocalUserView, ) -> LemmyResult> { let post_id = data.post_id; + + // We cannot use PostView to avoid a database read here, as it doesn't return removed items + // by default. So we would have to pass in `is_mod_or_admin`, but that is impossible without + // knowing which community the post belongs to. let orig_post = Post::read(&mut context.pool(), post_id).await?; + let community = Community::read(&mut context.pool(), orig_post.community_id).await?; check_community_mod_action( &local_user_view.person, - orig_post.community_id, + &community, false, &mut context.pool(), ) @@ -77,8 +83,7 @@ pub async fn remove_post( removed: data.removed, }, &context, - ) - .await?; + )?; build_post_response(&context, orig_post.community_id, local_user_view, post_id).await } diff --git a/crates/api_crud/src/post/update.rs b/crates/api_crud/src/post/update.rs index cef8bfea8..fc23e7d9e 100644 --- a/crates/api_crud/src/post/update.rs +++ b/crates/api_crud/src/post/update.rs @@ -24,7 +24,7 @@ use lemmy_db_schema::{ traits::Crud, utils::{diesel_string_update, diesel_url_update, naive_now}, }; -use lemmy_db_views::structs::LocalUserView; +use lemmy_db_views::structs::{LocalUserView, PostView}; use lemmy_utils::{ error::{LemmyErrorExt, LemmyErrorType, LemmyResult}, utils::{ @@ -87,17 +87,17 @@ pub async fn update_post( } let post_id = data.post_id; - let orig_post = Post::read(&mut context.pool(), post_id).await?; + let orig_post = PostView::read(&mut context.pool(), post_id, None, false).await?; check_community_user_action( &local_user_view.person, - orig_post.community_id, + &orig_post.community, &mut context.pool(), ) .await?; // Verify that only the creator can edit - if !Post::is_post_creator(local_user_view.person.id, orig_post.creator_id) { + if !Post::is_post_creator(local_user_view.person.id, orig_post.post.creator_id) { Err(LemmyErrorType::NoPostEditAllowed)? } @@ -105,14 +105,14 @@ pub async fn update_post( CommunityLanguage::is_allowed_community_language( &mut context.pool(), language_id, - orig_post.community_id, + orig_post.community.id, ) .await?; } // handle changes to scheduled_publish_time let scheduled_publish_time = match ( - orig_post.scheduled_publish_time, + orig_post.post.scheduled_publish_time, data.scheduled_publish_time, ) { // schedule time can be changed if post is still scheduled (and not published yet) @@ -144,12 +144,12 @@ pub async fn update_post( // send out federation/webmention if necessary match ( - orig_post.scheduled_publish_time, + orig_post.post.scheduled_publish_time, data.scheduled_publish_time, ) { // schedule was removed, send create activity and webmention (Some(_), None) => { - let community = Community::read(&mut context.pool(), orig_post.community_id).await?; + let community = Community::read(&mut context.pool(), orig_post.community.id).await?; send_webmention(updated_post.clone(), community); generate_post_link_metadata( updated_post.clone(), @@ -175,7 +175,7 @@ pub async fn update_post( build_post_response( context.deref(), - orig_post.community_id, + orig_post.community.id, local_user_view, post_id, ) diff --git a/crates/api_crud/src/private_message/create.rs b/crates/api_crud/src/private_message/create.rs index 2a49e4ac0..5f3c7b639 100644 --- a/crates/api_crud/src/private_message/create.rs +++ b/crates/api_crud/src/private_message/create.rs @@ -78,8 +78,7 @@ pub async fn create_private_message( ActivityChannel::submit_activity( SendActivityData::CreatePrivateMessage(view.clone()), &context, - ) - .await?; + )?; Ok(Json(PrivateMessageResponse { private_message_view: view, diff --git a/crates/api_crud/src/private_message/delete.rs b/crates/api_crud/src/private_message/delete.rs index 936ff57b8..30efc020c 100644 --- a/crates/api_crud/src/private_message/delete.rs +++ b/crates/api_crud/src/private_message/delete.rs @@ -42,8 +42,7 @@ pub async fn delete_private_message( ActivityChannel::submit_activity( SendActivityData::DeletePrivateMessage(local_user_view.person, private_message, data.deleted), &context, - ) - .await?; + )?; let view = PrivateMessageView::read(&mut context.pool(), private_message_id).await?; Ok(Json(PrivateMessageResponse { diff --git a/crates/api_crud/src/private_message/update.rs b/crates/api_crud/src/private_message/update.rs index 20eaadb36..aa562c626 100644 --- a/crates/api_crud/src/private_message/update.rs +++ b/crates/api_crud/src/private_message/update.rs @@ -59,8 +59,7 @@ pub async fn update_private_message( ActivityChannel::submit_activity( SendActivityData::UpdatePrivateMessage(view.clone()), &context, - ) - .await?; + )?; Ok(Json(PrivateMessageResponse { private_message_view: view, diff --git a/crates/api_crud/src/user/delete.rs b/crates/api_crud/src/user/delete.rs index d1825425c..39598265a 100644 --- a/crates/api_crud/src/user/delete.rs +++ b/crates/api_crud/src/user/delete.rs @@ -45,8 +45,7 @@ pub async fn delete_account( ActivityChannel::submit_activity( SendActivityData::DeleteUser(local_user_view.person, data.delete_content), &context, - ) - .await?; + )?; Ok(Json(SuccessResponse::default())) } diff --git a/crates/apub/src/activities/block/block_user.rs b/crates/apub/src/activities/block/block_user.rs index 64d5e7816..866e1cc6c 100644 --- a/crates/apub/src/activities/block/block_user.rs +++ b/crates/apub/src/activities/block/block_user.rs @@ -1,3 +1,4 @@ +use super::to_and_audience; use crate::{ activities::{ block::{generate_cc, SiteOrCommunity}, @@ -7,6 +8,7 @@ use crate::{ verify_is_public, verify_mod_action, verify_person_in_community, + verify_visibility, }, activity_lists::AnnouncableActivities, insert_received_activity, @@ -15,7 +17,7 @@ use crate::{ }; use activitypub_federation::{ config::Data, - kinds::{activity::BlockType, public}, + kinds::activity::BlockType, protocol::verification::verify_domains_match, traits::{ActivityHandler, Actor}, }; @@ -52,14 +54,10 @@ impl BlockUser { expires: Option>, context: &Data, ) -> LemmyResult { - let audience = if let SiteOrCommunity::Community(c) = target { - Some(c.id().into()) - } else { - None - }; + let (to, audience) = to_and_audience(target)?; Ok(BlockUser { actor: mod_.id().into(), - to: vec![public()], + to, object: user.id().into(), cc: generate_cc(target, &mut context.pool()).await?, target: target.id(), @@ -125,9 +123,9 @@ impl ActivityHandler for BlockUser { #[tracing::instrument(skip_all)] async fn verify(&self, context: &Data) -> LemmyResult<()> { - verify_is_public(&self.to, &self.cc)?; match self.target.dereference(context).await? { SiteOrCommunity::Site(site) => { + verify_is_public(&self.to, &self.cc)?; let domain = self .object .inner() @@ -143,6 +141,7 @@ impl ActivityHandler for BlockUser { verify_domains_match(&site.id(), self.object.inner())?; } SiteOrCommunity::Community(community) => { + verify_visibility(&self.to, &self.cc, &community)?; verify_person_in_community(&self.actor, &community, context).await?; verify_mod_action(&self.actor, &community, context).await?; } @@ -194,11 +193,7 @@ impl ActivityHandler for BlockUser { CommunityPersonBan::ban(&mut context.pool(), &community_user_ban_form).await?; // Also unsubscribe them from the community, if they are subscribed - let community_follower_form = CommunityFollowerForm { - community_id: community.id, - person_id: blocked_person.id, - pending: false, - }; + let community_follower_form = CommunityFollowerForm::new(community.id, blocked_person.id); CommunityFollower::unfollow(&mut context.pool(), &community_follower_form) .await .ok(); diff --git a/crates/apub/src/activities/block/mod.rs b/crates/apub/src/activities/block/mod.rs index c8323fcb4..550d98183 100644 --- a/crates/apub/src/activities/block/mod.rs +++ b/crates/apub/src/activities/block/mod.rs @@ -1,3 +1,4 @@ +use super::generate_to; use crate::{ objects::{community::ApubCommunity, instance::ApubSite}, protocol::{ @@ -8,6 +9,7 @@ use crate::{ use activitypub_federation::{ config::Data, fetch::object_id::ObjectId, + kinds::public, traits::{Actor, Object}, }; use chrono::{DateTime, Utc}; @@ -205,3 +207,13 @@ pub(crate) async fn send_ban_from_community( .await } } + +fn to_and_audience( + target: &SiteOrCommunity, +) -> LemmyResult<(Vec, Option>)> { + Ok(if let SiteOrCommunity::Community(c) = target { + (vec![generate_to(c)?], Some(c.id().into())) + } else { + (vec![public()], None) + }) +} diff --git a/crates/apub/src/activities/block/undo_block_user.rs b/crates/apub/src/activities/block/undo_block_user.rs index f9f6890b6..29fc22f0c 100644 --- a/crates/apub/src/activities/block/undo_block_user.rs +++ b/crates/apub/src/activities/block/undo_block_user.rs @@ -1,3 +1,4 @@ +use super::to_and_audience; use crate::{ activities::{ block::{generate_cc, SiteOrCommunity}, @@ -5,6 +6,7 @@ use crate::{ generate_activity_id, send_lemmy_activity, verify_is_public, + verify_visibility, }, activity_lists::AnnouncableActivities, insert_received_activity, @@ -13,7 +15,7 @@ use crate::{ }; use activitypub_federation::{ config::Data, - kinds::{activity::UndoType, public}, + kinds::activity::UndoType, protocol::verification::verify_domains_match, traits::{ActivityHandler, Actor}, }; @@ -44,11 +46,7 @@ impl UndoBlockUser { context: &Data, ) -> LemmyResult<()> { let block = BlockUser::new(target, user, mod_, None, reason, None, context).await?; - let audience = if let SiteOrCommunity::Community(c) = target { - Some(c.id().into()) - } else { - None - }; + let (to, audience) = to_and_audience(target)?; let id = generate_activity_id( UndoType::Undo, @@ -56,7 +54,7 @@ impl UndoBlockUser { )?; let undo = UndoBlockUser { actor: mod_.id().into(), - to: vec![public()], + to, object: block, cc: generate_cc(target, &mut context.pool()).await?, kind: UndoType::Undo, @@ -94,7 +92,6 @@ impl ActivityHandler for UndoBlockUser { #[tracing::instrument(skip_all)] async fn verify(&self, context: &Data) -> LemmyResult<()> { - verify_is_public(&self.to, &self.cc)?; verify_domains_match(self.actor.inner(), self.object.actor.inner())?; self.object.verify(context).await?; Ok(()) @@ -108,6 +105,7 @@ impl ActivityHandler for UndoBlockUser { let blocked_person = self.object.object.dereference(context).await?; match self.object.target.dereference(context).await? { SiteOrCommunity::Site(_site) => { + verify_is_public(&self.to, &self.cc)?; let blocked_person = Person::update( &mut context.pool(), blocked_person.id, @@ -135,6 +133,7 @@ impl ActivityHandler for UndoBlockUser { ModBan::create(&mut context.pool(), &form).await?; } SiteOrCommunity::Community(community) => { + verify_visibility(&self.to, &self.cc, &community)?; let community_user_ban_form = CommunityPersonBanForm { community_id: community.id, person_id: blocked_person.id, diff --git a/crates/apub/src/activities/community/announce.rs b/crates/apub/src/activities/community/announce.rs index e374d2874..d32b9d76e 100644 --- a/crates/apub/src/activities/community/announce.rs +++ b/crates/apub/src/activities/community/announce.rs @@ -2,9 +2,10 @@ use crate::{ activities::{ generate_activity_id, generate_announce_activity_id, + generate_to, send_lemmy_activity, - verify_is_public, verify_person_in_community, + verify_visibility, }, activity_lists::AnnouncableActivities, insert_received_activity, @@ -18,7 +19,7 @@ use crate::{ }; use activitypub_federation::{ config::Data, - kinds::{activity::AnnounceType, public}, + kinds::activity::AnnounceType, traits::{ActivityHandler, Actor}, }; use lemmy_api_common::context::LemmyContext; @@ -92,7 +93,7 @@ impl AnnounceActivity { generate_announce_activity_id(inner_kind, &context.settings().get_protocol_and_hostname())?; Ok(AnnounceActivity { actor: community.id().into(), - to: vec![public()], + to: vec![generate_to(community)?], object: IdOrNestedObject::NestedObject(object), cc: community .followers_url @@ -154,7 +155,6 @@ impl ActivityHandler for AnnounceActivity { #[tracing::instrument(skip_all)] async fn verify(&self, _context: &Data) -> LemmyResult<()> { - verify_is_public(&self.to, &self.cc)?; Ok(()) } @@ -169,6 +169,7 @@ impl ActivityHandler for AnnounceActivity { } let community = object.community(context).await?; + verify_visibility(&self.to, &self.cc, &community)?; can_accept_activity_in_community(&Some(community), context).await?; // verify here in order to avoid fetching the object twice over http diff --git a/crates/apub/src/activities/community/collection_add.rs b/crates/apub/src/activities/community/collection_add.rs index 5ab754d35..ae508c2c5 100644 --- a/crates/apub/src/activities/community/collection_add.rs +++ b/crates/apub/src/activities/community/collection_add.rs @@ -2,9 +2,10 @@ use crate::{ activities::{ community::send_activity_in_community, generate_activity_id, - verify_is_public, + generate_to, verify_mod_action, verify_person_in_community, + verify_visibility, }, activity_lists::AnnouncableActivities, insert_received_activity, @@ -17,7 +18,7 @@ use crate::{ use activitypub_federation::{ config::Data, fetch::object_id::ObjectId, - kinds::{activity::AddType, public}, + kinds::activity::AddType, traits::{ActivityHandler, Actor}, }; use lemmy_api_common::{ @@ -53,7 +54,7 @@ impl CollectionAdd { )?; let add = CollectionAdd { actor: actor.id().into(), - to: vec![public()], + to: vec![generate_to(community)?], object: added_mod.id(), target: generate_moderators_url(&community.actor_id)?.into(), cc: vec![community.id()], @@ -79,7 +80,7 @@ impl CollectionAdd { )?; let add = CollectionAdd { actor: actor.id().into(), - to: vec![public()], + to: vec![generate_to(community)?], object: featured_post.ap_id.clone().into(), target: generate_featured_url(&community.actor_id)?.into(), cc: vec![community.id()], @@ -115,8 +116,8 @@ impl ActivityHandler for CollectionAdd { #[tracing::instrument(skip_all)] async fn verify(&self, context: &Data) -> LemmyResult<()> { - verify_is_public(&self.to, &self.cc)?; let community = self.community(context).await?; + verify_visibility(&self.to, &self.cc, &community)?; verify_person_in_community(&self.actor, &community, context).await?; verify_mod_action(&self.actor, &community, context).await?; Ok(()) diff --git a/crates/apub/src/activities/community/collection_remove.rs b/crates/apub/src/activities/community/collection_remove.rs index 90df1fd14..6c08735ed 100644 --- a/crates/apub/src/activities/community/collection_remove.rs +++ b/crates/apub/src/activities/community/collection_remove.rs @@ -2,9 +2,10 @@ use crate::{ activities::{ community::send_activity_in_community, generate_activity_id, - verify_is_public, + generate_to, verify_mod_action, verify_person_in_community, + verify_visibility, }, activity_lists::AnnouncableActivities, insert_received_activity, @@ -14,7 +15,7 @@ use crate::{ use activitypub_federation::{ config::Data, fetch::object_id::ObjectId, - kinds::{activity::RemoveType, public}, + kinds::activity::RemoveType, traits::{ActivityHandler, Actor}, }; use lemmy_api_common::{ @@ -48,7 +49,7 @@ impl CollectionRemove { )?; let remove = CollectionRemove { actor: actor.id().into(), - to: vec![public()], + to: vec![generate_to(community)?], object: removed_mod.id(), target: generate_moderators_url(&community.actor_id)?.into(), id: id.clone(), @@ -74,7 +75,7 @@ impl CollectionRemove { )?; let remove = CollectionRemove { actor: actor.id().into(), - to: vec![public()], + to: vec![generate_to(community)?], object: featured_post.ap_id.clone().into(), target: generate_featured_url(&community.actor_id)?.into(), cc: vec![community.id()], @@ -110,8 +111,8 @@ impl ActivityHandler for CollectionRemove { #[tracing::instrument(skip_all)] async fn verify(&self, context: &Data) -> LemmyResult<()> { - verify_is_public(&self.to, &self.cc)?; let community = self.community(context).await?; + verify_visibility(&self.to, &self.cc, &community)?; verify_person_in_community(&self.actor, &community, context).await?; verify_mod_action(&self.actor, &community, context).await?; Ok(()) diff --git a/crates/apub/src/activities/community/lock_page.rs b/crates/apub/src/activities/community/lock_page.rs index 0d90b5bb0..a9bacea8a 100644 --- a/crates/apub/src/activities/community/lock_page.rs +++ b/crates/apub/src/activities/community/lock_page.rs @@ -3,9 +3,10 @@ use crate::{ check_community_deleted_or_removed, community::send_activity_in_community, generate_activity_id, - verify_is_public, + generate_to, verify_mod_action, verify_person_in_community, + verify_visibility, }, activity_lists::AnnouncableActivities, insert_received_activity, @@ -18,7 +19,7 @@ use crate::{ use activitypub_federation::{ config::Data, fetch::object_id::ObjectId, - kinds::{activity::UndoType, public}, + kinds::activity::UndoType, traits::ActivityHandler, }; use lemmy_api_common::context::LemmyContext; @@ -49,8 +50,8 @@ impl ActivityHandler for LockPage { } async fn verify(&self, context: &Data) -> Result<(), Self::Error> { - verify_is_public(&self.to, &self.cc)?; let community = self.community(context).await?; + verify_visibility(&self.to, &self.cc, &community)?; verify_person_in_community(&self.actor, &community, context).await?; check_community_deleted_or_removed(&community)?; verify_mod_action(&self.actor, &community, context).await?; @@ -92,8 +93,8 @@ impl ActivityHandler for UndoLockPage { } async fn verify(&self, context: &Data) -> Result<(), Self::Error> { - verify_is_public(&self.to, &self.cc)?; let community = self.community(context).await?; + verify_visibility(&self.to, &self.cc, &community)?; verify_person_in_community(&self.actor, &community, context).await?; check_community_deleted_or_removed(&community)?; verify_mod_action(&self.actor, &community, context).await?; @@ -137,7 +138,7 @@ pub(crate) async fn send_lock_post( let community_id = community.actor_id.inner().clone(); let lock = LockPage { actor: actor.actor_id.clone().into(), - to: vec![public()], + to: vec![generate_to(&community)?], object: ObjectId::from(post.ap_id), cc: vec![community_id.clone()], kind: LockType::Lock, @@ -153,7 +154,7 @@ pub(crate) async fn send_lock_post( )?; let undo = UndoLockPage { actor: lock.actor.clone(), - to: vec![public()], + to: vec![generate_to(&community)?], cc: lock.cc.clone(), kind: UndoType::Undo, id, diff --git a/crates/apub/src/activities/community/update.rs b/crates/apub/src/activities/community/update.rs index 48a64bd9d..85be94246 100644 --- a/crates/apub/src/activities/community/update.rs +++ b/crates/apub/src/activities/community/update.rs @@ -2,9 +2,10 @@ use crate::{ activities::{ community::send_activity_in_community, generate_activity_id, - verify_is_public, + generate_to, verify_mod_action, verify_person_in_community, + verify_visibility, }, activity_lists::AnnouncableActivities, insert_received_activity, @@ -13,7 +14,7 @@ use crate::{ }; use activitypub_federation::{ config::Data, - kinds::{activity::UpdateType, public}, + kinds::activity::UpdateType, traits::{ActivityHandler, Actor, Object}, }; use lemmy_api_common::context::LemmyContext; @@ -42,7 +43,7 @@ pub(crate) async fn send_update_community( )?; let update = UpdateCommunity { actor: actor.id().into(), - to: vec![public()], + to: vec![generate_to(&community)?], object: Box::new(community.clone().into_json(&context).await?), cc: vec![community.id()], kind: UpdateType::Update, @@ -77,8 +78,8 @@ impl ActivityHandler for UpdateCommunity { #[tracing::instrument(skip_all)] async fn verify(&self, context: &Data) -> LemmyResult<()> { - verify_is_public(&self.to, &self.cc)?; let community = self.community(context).await?; + verify_visibility(&self.to, &self.cc, &community)?; verify_person_in_community(&self.actor, &community, context).await?; verify_mod_action(&self.actor, &community, context).await?; ApubCommunity::verify(&self.object, &community.actor_id.clone().into(), context).await?; diff --git a/crates/apub/src/activities/create_or_update/comment.rs b/crates/apub/src/activities/create_or_update/comment.rs index 0a0737151..90ab0153f 100644 --- a/crates/apub/src/activities/create_or_update/comment.rs +++ b/crates/apub/src/activities/create_or_update/comment.rs @@ -3,8 +3,9 @@ use crate::{ check_community_deleted_or_removed, community::send_activity_in_community, generate_activity_id, - verify_is_public, + generate_to, verify_person_in_community, + verify_visibility, }, activity_lists::AnnouncableActivities, insert_received_activity, @@ -18,7 +19,6 @@ use crate::{ use activitypub_federation::{ config::Data, fetch::object_id::ObjectId, - kinds::public, protocol::verification::{verify_domains_match, verify_urls_match}, traits::{ActivityHandler, Actor, Object}, }; @@ -70,7 +70,7 @@ impl CreateOrUpdateNote { let create_or_update = CreateOrUpdateNote { actor: person.id().into(), - to: vec![public()], + to: vec![generate_to(&community)?], cc: note.cc.clone(), tag: note.tag.clone(), object: note, @@ -118,9 +118,9 @@ impl ActivityHandler for CreateOrUpdateNote { #[tracing::instrument(skip_all)] async fn verify(&self, context: &Data) -> LemmyResult<()> { - verify_is_public(&self.to, &self.cc)?; let post = self.object.get_parents(context).await?.0; let community = self.community(context).await?; + verify_visibility(&self.to, &self.cc, &community)?; verify_person_in_community(&self.actor, &community, context).await?; verify_domains_match(self.actor.inner(), self.object.id.inner())?; diff --git a/crates/apub/src/activities/create_or_update/post.rs b/crates/apub/src/activities/create_or_update/post.rs index fb53100f6..d0cf17a51 100644 --- a/crates/apub/src/activities/create_or_update/post.rs +++ b/crates/apub/src/activities/create_or_update/post.rs @@ -3,8 +3,9 @@ use crate::{ check_community_deleted_or_removed, community::send_activity_in_community, generate_activity_id, - verify_is_public, + generate_to, verify_person_in_community, + verify_visibility, }, activity_lists::AnnouncableActivities, insert_received_activity, @@ -16,7 +17,6 @@ use crate::{ }; use activitypub_federation::{ config::Data, - kinds::public, protocol::verification::{verify_domains_match, verify_urls_match}, traits::{ActivityHandler, Actor, Object}, }; @@ -49,7 +49,7 @@ impl CreateOrUpdatePage { )?; Ok(CreateOrUpdatePage { actor: actor.id().into(), - to: vec![public()], + to: vec![generate_to(community)?], object: post.into_json(context).await?, cc: vec![community.id()], kind, @@ -102,8 +102,8 @@ impl ActivityHandler for CreateOrUpdatePage { #[tracing::instrument(skip_all)] async fn verify(&self, context: &Data) -> LemmyResult<()> { - verify_is_public(&self.to, &self.cc)?; let community = self.community(context).await?; + verify_visibility(&self.to, &self.cc, &community)?; verify_person_in_community(&self.actor, &community, context).await?; check_community_deleted_or_removed(&community)?; verify_domains_match(self.actor.inner(), self.object.id.inner())?; diff --git a/crates/apub/src/activities/deletion/delete.rs b/crates/apub/src/activities/deletion/delete.rs index 1ddf642b9..064f0bc82 100644 --- a/crates/apub/src/activities/deletion/delete.rs +++ b/crates/apub/src/activities/deletion/delete.rs @@ -84,7 +84,7 @@ impl Delete { pub(in crate::activities::deletion) fn new( actor: &ApubPerson, object: DeletableObjects, - to: Url, + to: Vec, community: Option<&Community>, summary: Option, context: &Data, @@ -96,7 +96,7 @@ impl Delete { let cc: Option = community.map(|c| c.actor_id.clone().into()); Ok(Delete { actor: actor.actor_id.clone().into(), - to: vec![to], + to, object: IdOrNestedObject::Id(object.id()), cc: cc.into_iter().collect(), kind: DeleteType::Delete, diff --git a/crates/apub/src/activities/deletion/mod.rs b/crates/apub/src/activities/deletion/mod.rs index b12532087..15118a476 100644 --- a/crates/apub/src/activities/deletion/mod.rs +++ b/crates/apub/src/activities/deletion/mod.rs @@ -1,11 +1,12 @@ +use super::{generate_to, verify_is_public}; use crate::{ activities::{ community::send_activity_in_community, send_lemmy_activity, - verify_is_public, verify_mod_action, verify_person, verify_person_in_community, + verify_visibility, }, activity_lists::AnnouncableActivities, objects::{ @@ -59,11 +60,12 @@ pub(crate) async fn send_apub_delete_in_community( ) -> LemmyResult<()> { let actor = ApubPerson::from(actor); let is_mod_action = reason.is_some(); + let to = vec![generate_to(&community)?]; let activity = if deleted { - let delete = Delete::new(&actor, object, public(), Some(&community), reason, context)?; + let delete = Delete::new(&actor, object, to, Some(&community), reason, context)?; AnnouncableActivities::Delete(delete) } else { - let undo = UndoDelete::new(&actor, object, public(), Some(&community), reason, context)?; + let undo = UndoDelete::new(&actor, object, to, Some(&community), reason, context)?; AnnouncableActivities::UndoDelete(undo) }; send_activity_in_community( @@ -92,10 +94,10 @@ pub(crate) async fn send_apub_delete_private_message( let deletable = DeletableObjects::PrivateMessage(pm.into()); let inbox = ActivitySendTargets::to_inbox(recipient.shared_inbox_or_inbox()); if deleted { - let delete: Delete = Delete::new(actor, deletable, recipient.id(), None, None, &context)?; + let delete: Delete = Delete::new(actor, deletable, vec![recipient.id()], None, None, &context)?; send_lemmy_activity(&context, delete, actor, inbox, true).await?; } else { - let undo = UndoDelete::new(actor, deletable, recipient.id(), None, None, &context)?; + let undo = UndoDelete::new(actor, deletable, vec![recipient.id()], None, None, &context)?; send_lemmy_activity(&context, undo, actor, inbox, true).await?; }; Ok(()) @@ -109,7 +111,7 @@ pub async fn send_apub_delete_user( let person: ApubPerson = person.into(); let deletable = DeletableObjects::Person(person.clone()); - let mut delete: Delete = Delete::new(&person, deletable, public(), None, None, &context)?; + let mut delete: Delete = Delete::new(&person, deletable, vec![public()], None, None, &context)?; delete.remove_data = Some(remove_data); let inboxes = ActivitySendTargets::to_all_instances(); @@ -170,7 +172,7 @@ pub(in crate::activities) async fn verify_delete_activity( let object = DeletableObjects::read_from_db(activity.object.id(), context).await?; match object { DeletableObjects::Community(community) => { - verify_is_public(&activity.to, &[])?; + verify_visibility(&activity.to, &[], &community)?; if community.local { // can only do this check for local community, in remote case it would try to fetch the // deleted community (which fails) @@ -185,22 +187,24 @@ pub(in crate::activities) async fn verify_delete_activity( verify_urls_match(person.actor_id.inner(), activity.object.id())?; } DeletableObjects::Post(p) => { - verify_is_public(&activity.to, &[])?; + let community = activity.community(context).await?; + verify_visibility(&activity.to, &[], &community)?; verify_delete_post_or_comment( &activity.actor, &p.ap_id.clone().into(), - &activity.community(context).await?, + &community, is_mod_action, context, ) .await?; } DeletableObjects::Comment(c) => { - verify_is_public(&activity.to, &[])?; + let community = activity.community(context).await?; + verify_visibility(&activity.to, &[], &community)?; verify_delete_post_or_comment( &activity.actor, &c.ap_id.clone().into(), - &activity.community(context).await?, + &community, is_mod_action, context, ) diff --git a/crates/apub/src/activities/deletion/undo_delete.rs b/crates/apub/src/activities/deletion/undo_delete.rs index 6328bb427..f4a7bb9b9 100644 --- a/crates/apub/src/activities/deletion/undo_delete.rs +++ b/crates/apub/src/activities/deletion/undo_delete.rs @@ -68,7 +68,7 @@ impl UndoDelete { pub(in crate::activities::deletion) fn new( actor: &ApubPerson, object: DeletableObjects, - to: Url, + to: Vec, community: Option<&Community>, summary: Option, context: &Data, @@ -82,7 +82,7 @@ impl UndoDelete { let cc: Option = community.map(|c| c.actor_id.clone().into()); Ok(UndoDelete { actor: actor.actor_id.clone().into(), - to: vec![to], + to, object, cc: cc.into_iter().collect(), kind: UndoType::Undo, diff --git a/crates/apub/src/activities/following/follow.rs b/crates/apub/src/activities/following/follow.rs index 02f29a1a9..befa2e00c 100644 --- a/crates/apub/src/activities/following/follow.rs +++ b/crates/apub/src/activities/following/follow.rs @@ -20,7 +20,7 @@ use lemmy_api_common::context::LemmyContext; use lemmy_db_schema::{ source::{ activity::ActivitySendTargets, - community::{CommunityFollower, CommunityFollowerForm}, + community::{CommunityFollower, CommunityFollowerForm, CommunityFollowerState}, person::{PersonFollower, PersonFollowerForm}, }, traits::Followable, @@ -102,21 +102,25 @@ impl ActivityHandler for Follow { pending: false, }; PersonFollower::follow(&mut context.pool(), &form).await?; + AcceptFollow::send(self, context).await?; } UserOrCommunity::Community(c) => { - // Dont allow following local-only community via federation. - if c.visibility != CommunityVisibility::Public { - return Err(LemmyErrorType::NotFound.into()); - } + let state = Some(match c.visibility { + CommunityVisibility::Public => CommunityFollowerState::Accepted, + CommunityVisibility::Private => CommunityFollowerState::ApprovalRequired, + // Dont allow following local-only community via federation. + CommunityVisibility::LocalOnly => return Err(LemmyErrorType::NotFound.into()), + }); let form = CommunityFollowerForm { - community_id: c.id, - person_id: actor.id, - pending: false, + state, + ..CommunityFollowerForm::new(c.id, actor.id) }; CommunityFollower::follow(&mut context.pool(), &form).await?; + if c.visibility == CommunityVisibility::Public { + AcceptFollow::send(self, context).await?; + } } } - - AcceptFollow::send(self, context).await + Ok(()) } } diff --git a/crates/apub/src/activities/following/mod.rs b/crates/apub/src/activities/following/mod.rs index 7c7163f12..83cdc841c 100644 --- a/crates/apub/src/activities/following/mod.rs +++ b/crates/apub/src/activities/following/mod.rs @@ -1,15 +1,26 @@ +use super::generate_activity_id; use crate::{ objects::{community::ApubCommunity, person::ApubPerson}, - protocol::activities::following::{follow::Follow, undo_follow::UndoFollow}, + protocol::activities::following::{ + accept::AcceptFollow, + follow::Follow, + reject::RejectFollow, + undo_follow::UndoFollow, + }, }; -use activitypub_federation::config::Data; +use activitypub_federation::{config::Data, kinds::activity::FollowType}; use lemmy_api_common::context::LemmyContext; -use lemmy_db_schema::source::{community::Community, person::Person}; +use lemmy_db_schema::{ + newtypes::{CommunityId, PersonId}, + source::{community::Community, person::Person}, + traits::Crud, +}; use lemmy_utils::error::LemmyResult; -pub mod accept; -pub mod follow; -pub mod undo_follow; +pub(crate) mod accept; +pub(crate) mod follow; +pub(crate) mod reject; +pub(crate) mod undo_follow; pub async fn send_follow_community( community: Community, @@ -25,3 +36,29 @@ pub async fn send_follow_community( UndoFollow::send(&actor, &community, context).await } } + +pub async fn send_accept_or_reject_follow( + community_id: CommunityId, + person_id: PersonId, + accepted: bool, + context: &Data, +) -> LemmyResult<()> { + let community = Community::read(&mut context.pool(), community_id).await?; + let person = Person::read(&mut context.pool(), person_id).await?; + + let follow = Follow { + actor: person.actor_id.into(), + to: Some([community.actor_id.clone().into()]), + object: community.actor_id.into(), + kind: FollowType::Follow, + id: generate_activity_id( + FollowType::Follow, + &context.settings().get_protocol_and_hostname(), + )?, + }; + if accepted { + AcceptFollow::send(follow, context).await + } else { + RejectFollow::send(follow, context).await + } +} diff --git a/crates/apub/src/activities/following/reject.rs b/crates/apub/src/activities/following/reject.rs new file mode 100644 index 000000000..8f1623d20 --- /dev/null +++ b/crates/apub/src/activities/following/reject.rs @@ -0,0 +1,79 @@ +use crate::{ + activities::{generate_activity_id, send_lemmy_activity}, + insert_received_activity, + protocol::activities::following::{follow::Follow, reject::RejectFollow}, +}; +use activitypub_federation::{ + config::Data, + kinds::activity::RejectType, + protocol::verification::verify_urls_match, + traits::{ActivityHandler, Actor}, +}; +use lemmy_api_common::context::LemmyContext; +use lemmy_db_schema::{ + source::{ + activity::ActivitySendTargets, + community::{CommunityFollower, CommunityFollowerForm}, + }, + traits::Followable, +}; +use lemmy_utils::error::{LemmyError, LemmyResult}; +use url::Url; + +impl RejectFollow { + #[tracing::instrument(skip_all)] + pub async fn send(follow: Follow, context: &Data) -> LemmyResult<()> { + let user_or_community = follow.object.dereference_local(context).await?; + let person = follow.actor.clone().dereference(context).await?; + let reject = RejectFollow { + actor: user_or_community.id().into(), + to: Some([person.id().into()]), + object: follow, + kind: RejectType::Reject, + id: generate_activity_id( + RejectType::Reject, + &context.settings().get_protocol_and_hostname(), + )?, + }; + let inbox = ActivitySendTargets::to_inbox(person.shared_inbox_or_inbox()); + send_lemmy_activity(context, reject, &user_or_community, inbox, true).await + } +} + +/// Handle rejected follows +#[async_trait::async_trait] +impl ActivityHandler for RejectFollow { + type DataType = LemmyContext; + type Error = LemmyError; + + fn id(&self) -> &Url { + &self.id + } + + fn actor(&self) -> &Url { + self.actor.inner() + } + + #[tracing::instrument(skip_all)] + async fn verify(&self, context: &Data) -> LemmyResult<()> { + verify_urls_match(self.actor.inner(), self.object.object.inner())?; + self.object.verify(context).await?; + if let Some(to) = &self.to { + verify_urls_match(to[0].inner(), self.object.actor.inner())?; + } + Ok(()) + } + + #[tracing::instrument(skip_all)] + async fn receive(self, context: &Data) -> LemmyResult<()> { + insert_received_activity(&self.id, context).await?; + let community = self.actor.dereference(context).await?; + let person = self.object.actor.dereference(context).await?; + + // remove the follow + let form = CommunityFollowerForm::new(community.id, person.id); + CommunityFollower::unfollow(&mut context.pool(), &form).await?; + + Ok(()) + } +} diff --git a/crates/apub/src/activities/following/undo_follow.rs b/crates/apub/src/activities/following/undo_follow.rs index ba6253946..1aa6bb7fc 100644 --- a/crates/apub/src/activities/following/undo_follow.rs +++ b/crates/apub/src/activities/following/undo_follow.rs @@ -90,11 +90,7 @@ impl ActivityHandler for UndoFollow { PersonFollower::unfollow(&mut context.pool(), &form).await?; } UserOrCommunity::Community(c) => { - let form = CommunityFollowerForm { - community_id: c.id, - person_id: person.id, - pending: false, - }; + let form = CommunityFollowerForm::new(c.id, person.id); CommunityFollower::unfollow(&mut context.pool(), &form).await?; } } diff --git a/crates/apub/src/activities/mod.rs b/crates/apub/src/activities/mod.rs index 21723c390..ffb6a662e 100644 --- a/crates/apub/src/activities/mod.rs +++ b/crates/apub/src/activities/mod.rs @@ -30,6 +30,7 @@ use activitypub_federation::{ traits::{ActivityHandler, Actor}, }; use anyhow::anyhow; +use following::send_accept_or_reject_follow; use lemmy_api_common::{ context::LemmyContext, send_activity::{ActivityChannel, SendActivityData}, @@ -40,6 +41,7 @@ use lemmy_db_schema::{ community::Community, }, traits::Crud, + CommunityVisibility, }; use lemmy_db_views_actor::structs::{CommunityPersonBanView, CommunityView}; use lemmy_utils::error::{FederationError, LemmyError, LemmyErrorExt, LemmyErrorType, LemmyResult}; @@ -120,6 +122,28 @@ pub(crate) fn verify_is_public(to: &[Url], cc: &[Url]) -> LemmyResult<()> { } } +/// Returns an error if object visibility doesnt match community visibility +/// (ie content in private community must also be private). +pub(crate) fn verify_visibility(to: &[Url], cc: &[Url], community: &Community) -> LemmyResult<()> { + use CommunityVisibility::*; + let object_is_public = [to, cc].iter().any(|set| set.contains(&public())); + match community.visibility { + Public if !object_is_public => Err(FederationError::ObjectIsNotPublic)?, + Private if object_is_public => Err(FederationError::ObjectIsNotPrivate)?, + LocalOnly => Err(LemmyErrorType::NotFound.into()), + _ => Ok(()), + } +} + +/// Marks object as public only if the community is public +pub(crate) fn generate_to(community: &Community) -> LemmyResult { + if community.visibility == CommunityVisibility::Public { + Ok(public()) + } else { + Ok(Url::parse(&format!("{}/followers", community.actor_id))?) + } +} + pub(crate) fn verify_community_matches(a: &ObjectId, b: T) -> LemmyResult<()> where T: Into>, @@ -367,6 +391,12 @@ pub async fn match_outgoing_activities( community, reason, } => Report::send(ObjectId::from(object_id), actor, community, reason, context).await, + AcceptFollower(community_id, person_id) => { + send_accept_or_reject_follow(community_id, person_id, true, &context).await + } + RejectFollower(community_id, person_id) => { + send_accept_or_reject_follow(community_id, person_id, false, &context).await + } } }; fed_task.await?; diff --git a/crates/apub/src/activity_lists.rs b/crates/apub/src/activity_lists.rs index 1ba31b9b4..7ed1d8baf 100644 --- a/crates/apub/src/activity_lists.rs +++ b/crates/apub/src/activity_lists.rs @@ -17,7 +17,12 @@ use crate::{ page::CreateOrUpdatePage, }, deletion::{delete::Delete, undo_delete::UndoDelete}, - following::{accept::AcceptFollow, follow::Follow, undo_follow::UndoFollow}, + following::{ + accept::AcceptFollow, + follow::Follow, + reject::RejectFollow, + undo_follow::UndoFollow, + }, voting::{undo_vote::UndoVote, vote::Vote}, }, objects::page::Page, @@ -41,6 +46,7 @@ use url::Url; pub enum SharedInboxActivities { Follow(Follow), AcceptFollow(AcceptFollow), + RejectFollow(RejectFollow), UndoFollow(UndoFollow), CreateOrUpdatePrivateMessage(CreateOrUpdateChatMessage), Report(Report), @@ -68,6 +74,7 @@ pub enum GroupInboxActivities { pub enum PersonInboxActivities { Follow(Follow), AcceptFollow(AcceptFollow), + RejectFollow(RejectFollow), UndoFollow(UndoFollow), CreateOrUpdatePrivateMessage(CreateOrUpdateChatMessage), Delete(Delete), diff --git a/crates/apub/src/api/user_settings_backup.rs b/crates/apub/src/api/user_settings_backup.rs index 2e075c202..601ba8664 100644 --- a/crates/apub/src/api/user_settings_backup.rs +++ b/crates/apub/src/api/user_settings_backup.rs @@ -13,7 +13,7 @@ use lemmy_db_schema::{ newtypes::DbUrl, source::{ comment::{CommentSaved, CommentSavedForm}, - community::{CommunityFollower, CommunityFollowerForm}, + community::{CommunityFollower, CommunityFollowerForm, CommunityFollowerState}, community_block::{CommunityBlock, CommunityBlockForm}, instance::Instance, instance_block::{InstanceBlock, InstanceBlockForm}, @@ -186,9 +186,8 @@ pub async fn import_settings( |(followed, context)| async move { let community = followed.dereference(&context).await?; let form = CommunityFollowerForm { - person_id, - community_id: community.id, - pending: true, + state: Some(CommunityFollowerState::Pending), + ..CommunityFollowerForm::new(community.id, person_id) }; CommunityFollower::follow(&mut context.pool(), &form).await?; LemmyResult::Ok(()) @@ -319,7 +318,13 @@ pub(crate) mod tests { use lemmy_api_common::context::LemmyContext; use lemmy_db_schema::{ source::{ - community::{Community, CommunityFollower, CommunityFollowerForm, CommunityInsertForm}, + community::{ + Community, + CommunityFollower, + CommunityFollowerForm, + CommunityFollowerState, + CommunityInsertForm, + }, local_user::LocalUser, }, traits::{Crud, Followable}, @@ -327,7 +332,6 @@ pub(crate) mod tests { use lemmy_db_views::structs::LocalUserView; use lemmy_db_views_actor::structs::CommunityFollowerView; use lemmy_utils::error::{LemmyErrorType, LemmyResult}; - use pretty_assertions::assert_eq; use serial_test::serial; use std::time::Duration; use tokio::time::sleep; @@ -348,9 +352,8 @@ pub(crate) mod tests { ); let community = Community::create(pool, &community_form).await?; let follower_form = CommunityFollowerForm { - community_id: community.id, - person_id: export_user.person.id, - pending: false, + state: Some(CommunityFollowerState::Accepted), + ..CommunityFollowerForm::new(community.id, export_user.person.id) }; CommunityFollower::follow(pool, &follower_form).await?; diff --git a/crates/apub/src/fetcher/site_or_community_or_user.rs b/crates/apub/src/fetcher/site_or_community_or_user.rs index c6a1bb17e..79d7978ae 100644 --- a/crates/apub/src/fetcher/site_or_community_or_user.rs +++ b/crates/apub/src/fetcher/site_or_community_or_user.rs @@ -9,6 +9,7 @@ use activitypub_federation::{ }; use chrono::{DateTime, Utc}; use lemmy_api_common::context::LemmyContext; +use lemmy_db_schema::newtypes::InstanceId; use lemmy_utils::error::{LemmyError, LemmyResult}; use reqwest::Url; use serde::{Deserialize, Serialize}; @@ -127,3 +128,13 @@ impl Actor for SiteOrCommunityOrUser { } } } + +impl SiteOrCommunityOrUser { + pub fn instance_id(&self) -> InstanceId { + match self { + SiteOrCommunityOrUser::Site(s) => s.instance_id, + SiteOrCommunityOrUser::UserOrCommunity(UserOrCommunity::User(u)) => u.instance_id, + SiteOrCommunityOrUser::UserOrCommunity(UserOrCommunity::Community(c)) => c.instance_id, + } + } +} diff --git a/crates/apub/src/http/comment.rs b/crates/apub/src/http/comment.rs index d6b3c818d..41160234f 100644 --- a/crates/apub/src/http/comment.rs +++ b/crates/apub/src/http/comment.rs @@ -1,14 +1,10 @@ +use super::check_community_content_fetchable; use crate::{ - http::{ - check_community_public, - create_apub_response, - create_apub_tombstone_response, - redirect_remote_object, - }, + http::{create_apub_response, create_apub_tombstone_response, redirect_remote_object}, objects::comment::ApubComment, }; use activitypub_federation::{config::Data, traits::Object}; -use actix_web::{web::Path, HttpResponse}; +use actix_web::{web::Path, HttpRequest, HttpResponse}; use lemmy_api_common::context::LemmyContext; use lemmy_db_schema::{ newtypes::CommentId, @@ -28,13 +24,14 @@ pub(crate) struct CommentQuery { pub(crate) async fn get_apub_comment( info: Path, context: Data, + request: HttpRequest, ) -> LemmyResult { let id = CommentId(info.comment_id.parse::()?); // Can't use CommentView here because it excludes deleted/removed/local-only items let comment: ApubComment = Comment::read(&mut context.pool(), id).await?.into(); let post = Post::read(&mut context.pool(), comment.post_id).await?; let community = Community::read(&mut context.pool(), post.community_id).await?; - check_community_public(&community)?; + check_community_content_fetchable(&community, &request, &context).await?; if !comment.local { Ok(redirect_remote_object(&comment.ap_id)) diff --git a/crates/apub/src/http/community.rs b/crates/apub/src/http/community.rs index 2516020d3..96a917d91 100644 --- a/crates/apub/src/http/community.rs +++ b/crates/apub/src/http/community.rs @@ -1,3 +1,4 @@ +use super::check_community_content_fetchable; use crate::{ collections::{ community_featured::ApubCommunityFeatured, @@ -5,14 +6,14 @@ use crate::{ community_moderators::ApubCommunityModerators, community_outbox::ApubCommunityOutbox, }, - http::{check_community_public, create_apub_response, create_apub_tombstone_response}, + http::{check_community_fetchable, create_apub_response, create_apub_tombstone_response}, objects::community::ApubCommunity, }; use activitypub_federation::{ config::Data, traits::{Collection, Object}, }; -use actix_web::{web, HttpResponse}; +use actix_web::{web, HttpRequest, HttpResponse}; use lemmy_api_common::context::LemmyContext; use lemmy_db_schema::{source::community::Community, traits::ApubActor}; use lemmy_utils::error::{LemmyErrorType, LemmyResult}; @@ -38,7 +39,7 @@ pub(crate) async fn get_apub_community_http( if community.deleted || community.removed { return create_apub_tombstone_response(community.actor_id.clone()); } - check_community_public(&community)?; + check_community_fetchable(&community)?; let apub = community.into_json(&context).await?; create_apub_response(&apub) @@ -52,7 +53,7 @@ pub(crate) async fn get_apub_community_followers( let community = Community::read_from_name(&mut context.pool(), &info.community_name, false) .await? .ok_or(LemmyErrorType::NotFound)?; - check_community_public(&community)?; + check_community_fetchable(&community)?; let followers = ApubCommunityFollower::read_local(&community.into(), &context).await?; create_apub_response(&followers) } @@ -62,13 +63,14 @@ pub(crate) async fn get_apub_community_followers( pub(crate) async fn get_apub_community_outbox( info: web::Path, context: Data, + request: HttpRequest, ) -> LemmyResult { let community: ApubCommunity = Community::read_from_name(&mut context.pool(), &info.community_name, false) .await? .ok_or(LemmyErrorType::NotFound)? .into(); - check_community_public(&community)?; + check_community_content_fetchable(&community, &request, &context).await?; let outbox = ApubCommunityOutbox::read_local(&community, &context).await?; create_apub_response(&outbox) } @@ -83,7 +85,7 @@ pub(crate) async fn get_apub_community_moderators( .await? .ok_or(LemmyErrorType::NotFound)? .into(); - check_community_public(&community)?; + check_community_fetchable(&community)?; let moderators = ApubCommunityModerators::read_local(&community, &context).await?; create_apub_response(&moderators) } @@ -92,13 +94,14 @@ pub(crate) async fn get_apub_community_moderators( pub(crate) async fn get_apub_community_featured( info: web::Path, context: Data, + request: HttpRequest, ) -> LemmyResult { let community: ApubCommunity = Community::read_from_name(&mut context.pool(), &info.community_name, false) .await? .ok_or(LemmyErrorType::NotFound)? .into(); - check_community_public(&community)?; + check_community_content_fetchable(&community, &request, &context).await?; let featured = ApubCommunityFeatured::read_local(&community, &context).await?; create_apub_response(&featured) } @@ -108,7 +111,7 @@ pub(crate) mod tests { use super::*; use crate::protocol::objects::{group::Group, tombstone::Tombstone}; - use actix_web::body::to_bytes; + use actix_web::{body::to_bytes, test::TestRequest}; use lemmy_db_schema::{ newtypes::InstanceId, source::{ @@ -175,6 +178,7 @@ pub(crate) mod tests { async fn test_get_community() -> LemmyResult<()> { let context = LemmyContext::init_test_context().await; let (instance, community) = init(false, CommunityVisibility::Public, &context).await?; + let request = TestRequest::default().to_http_request(); // fetch invalid community let query = CommunityQuery { @@ -194,8 +198,12 @@ pub(crate) mod tests { let group = community.clone().into_json(&context).await?; assert_eq!(group, res_group); - let res = - get_apub_community_featured(query.clone().into(), context.reset_request_count()).await?; + let res = get_apub_community_featured( + query.clone().into(), + context.reset_request_count(), + request.clone(), + ) + .await?; assert_eq!(200, res.status()); let res = get_apub_community_followers(query.clone().into(), context.reset_request_count()).await?; @@ -203,7 +211,8 @@ pub(crate) mod tests { let res = get_apub_community_moderators(query.clone().into(), context.reset_request_count()).await?; assert_eq!(200, res.status()); - let res = get_apub_community_outbox(query.into(), context.reset_request_count()).await?; + let res = + get_apub_community_outbox(query.into(), context.reset_request_count(), request).await?; assert_eq!(200, res.status()); Instance::delete(&mut context.pool(), instance.id).await?; @@ -215,6 +224,7 @@ pub(crate) mod tests { async fn test_get_deleted_community() -> LemmyResult<()> { let context = LemmyContext::init_test_context().await; let (instance, community) = init(true, CommunityVisibility::LocalOnly, &context).await?; + let request = TestRequest::default().to_http_request(); // should return tombstone let query = CommunityQuery { @@ -225,8 +235,12 @@ pub(crate) mod tests { let res_tombstone = decode_response::(res).await; assert!(res_tombstone.is_ok()); - let res = - get_apub_community_featured(query.clone().into(), context.reset_request_count()).await; + let res = get_apub_community_featured( + query.clone().into(), + context.reset_request_count(), + request.clone(), + ) + .await; assert!(res.is_err()); let res = get_apub_community_followers(query.clone().into(), context.reset_request_count()).await; @@ -234,7 +248,7 @@ pub(crate) mod tests { let res = get_apub_community_moderators(query.clone().into(), context.reset_request_count()).await; assert!(res.is_err()); - let res = get_apub_community_outbox(query.into(), context.reset_request_count()).await; + let res = get_apub_community_outbox(query.into(), context.reset_request_count(), request).await; assert!(res.is_err()); //Community::delete(&mut context.pool(), community.id).await?; @@ -247,14 +261,19 @@ pub(crate) mod tests { async fn test_get_local_only_community() -> LemmyResult<()> { let context = LemmyContext::init_test_context().await; let (instance, community) = init(false, CommunityVisibility::LocalOnly, &context).await?; + let request = TestRequest::default().to_http_request(); let query = CommunityQuery { community_name: community.name.clone(), }; let res = get_apub_community_http(query.clone().into(), context.reset_request_count()).await; assert!(res.is_err()); - let res = - get_apub_community_featured(query.clone().into(), context.reset_request_count()).await; + let res = get_apub_community_featured( + query.clone().into(), + context.reset_request_count(), + request.clone(), + ) + .await; assert!(res.is_err()); let res = get_apub_community_followers(query.clone().into(), context.reset_request_count()).await; @@ -262,7 +281,7 @@ pub(crate) mod tests { let res = get_apub_community_moderators(query.clone().into(), context.reset_request_count()).await; assert!(res.is_err()); - let res = get_apub_community_outbox(query.into(), context.reset_request_count()).await; + let res = get_apub_community_outbox(query.into(), context.reset_request_count(), request).await; assert!(res.is_err()); Instance::delete(&mut context.pool(), instance.id).await?; diff --git a/crates/apub/src/http/mod.rs b/crates/apub/src/http/mod.rs index bc148eb9c..d79cd3d55 100644 --- a/crates/apub/src/http/mod.rs +++ b/crates/apub/src/http/mod.rs @@ -1,11 +1,11 @@ use crate::{ activity_lists::SharedInboxActivities, - fetcher::user_or_community::UserOrCommunity, + fetcher::{site_or_community_or_user::SiteOrCommunityOrUser, user_or_community::UserOrCommunity}, protocol::objects::tombstone::Tombstone, FEDERATION_CONTEXT, }; use activitypub_federation::{ - actix_web::inbox::receive_activity, + actix_web::{inbox::receive_activity, signing_actor}, config::Data, protocol::context::WithContext, FEDERATION_CONTENT_TYPE, @@ -17,6 +17,7 @@ use lemmy_db_schema::{ source::{activity::SentActivity, community::Community}, CommunityVisibility, }; +use lemmy_db_views_actor::structs::CommunityFollowerView; use lemmy_utils::error::{FederationError, LemmyErrorType, LemmyResult}; use serde::{Deserialize, Serialize}; use std::{ops::Deref, time::Duration}; @@ -119,12 +120,46 @@ pub(crate) async fn get_activity( } /// Ensure that the community is public and not removed/deleted. -fn check_community_public(community: &Community) -> LemmyResult<()> { - if community.deleted || community.removed { - Err(LemmyErrorType::Deleted)? - } - if community.visibility != CommunityVisibility::Public { +fn check_community_fetchable(community: &Community) -> LemmyResult<()> { + check_community_removed_or_deleted(community)?; + if community.visibility == CommunityVisibility::LocalOnly { return Err(LemmyErrorType::NotFound.into()); } Ok(()) } + +/// Check if posts or comments in the community are allowed to be fetched +async fn check_community_content_fetchable( + community: &Community, + request: &HttpRequest, + context: &Data, +) -> LemmyResult<()> { + use CommunityVisibility::*; + check_community_removed_or_deleted(community)?; + match community.visibility { + // content in public community can always be fetched + Public => Ok(()), + // no federation for local only community + LocalOnly => Err(LemmyErrorType::NotFound.into()), + // for private community check http signature of request, if there is any approved follower + // from the fetching instance then fetching is allowed + Private => { + let signing_actor = signing_actor::(request, None, context).await?; + Ok( + CommunityFollowerView::check_has_followers_from_instance( + community.id, + signing_actor.instance_id(), + &mut context.pool(), + ) + .await?, + ) + } + } +} + +fn check_community_removed_or_deleted(community: &Community) -> LemmyResult<()> { + if community.deleted || community.removed { + Err(LemmyErrorType::Deleted)? + } + Ok(()) +} diff --git a/crates/apub/src/http/post.rs b/crates/apub/src/http/post.rs index ce6612826..6afb9fc3e 100644 --- a/crates/apub/src/http/post.rs +++ b/crates/apub/src/http/post.rs @@ -1,14 +1,10 @@ +use super::check_community_content_fetchable; use crate::{ - http::{ - check_community_public, - create_apub_response, - create_apub_tombstone_response, - redirect_remote_object, - }, + http::{create_apub_response, create_apub_tombstone_response, redirect_remote_object}, objects::post::ApubPost, }; use activitypub_federation::{config::Data, traits::Object}; -use actix_web::{web, HttpResponse}; +use actix_web::{web, HttpRequest, HttpResponse}; use lemmy_api_common::context::LemmyContext; use lemmy_db_schema::{ newtypes::PostId, @@ -28,12 +24,14 @@ pub(crate) struct PostQuery { pub(crate) async fn get_apub_post( info: web::Path, context: Data, + request: HttpRequest, ) -> LemmyResult { let id = PostId(info.post_id.parse::()?); // Can't use PostView here because it excludes deleted/removed/local-only items let post: ApubPost = Post::read(&mut context.pool(), id).await?.into(); let community = Community::read(&mut context.pool(), post.community_id).await?; - check_community_public(&community)?; + + check_community_content_fetchable(&community, &request, &context).await?; if !post.local { Ok(redirect_remote_object(&post.ap_id)) diff --git a/crates/apub/src/objects/comment.rs b/crates/apub/src/objects/comment.rs index 6e13afc91..b7c6a5f51 100644 --- a/crates/apub/src/objects/comment.rs +++ b/crates/apub/src/objects/comment.rs @@ -1,5 +1,5 @@ use crate::{ - activities::{verify_is_public, verify_person_in_community}, + activities::{generate_to, verify_person_in_community, verify_visibility}, check_apub_id_valid_with_strictness, fetcher::markdown_links::markdown_rewrite_remote_links, mentions::collect_non_local_mentions, @@ -12,7 +12,7 @@ use crate::{ }; use activitypub_federation::{ config::Data, - kinds::{object::NoteType, public}, + kinds::object::NoteType, protocol::{values::MediaTypeMarkdownOrHtml, verification::verify_domains_match}, traits::Object, }; @@ -112,7 +112,7 @@ impl Object for ApubComment { r#type: NoteType::Note, id: self.ap_id.clone().into(), attributed_to: creator.actor_id.into(), - to: vec![public()], + to: vec![generate_to(&community)?], cc: maa.ccs, content: markdown_to_html(&self.content), media_type: Some(MediaTypeMarkdownOrHtml::Html), @@ -140,8 +140,8 @@ impl Object for ApubComment { ) -> LemmyResult<()> { verify_domains_match(note.id.inner(), expected_domain)?; verify_domains_match(note.attributed_to.inner(), note.id.inner())?; - verify_is_public(¬e.to, ¬e.cc)?; let community = Box::pin(note.community(context)).await?; + verify_visibility(¬e.to, ¬e.cc, &community)?; Box::pin(check_apub_id_valid_with_strictness( note.id.inner(), diff --git a/crates/apub/src/objects/community.rs b/crates/apub/src/objects/community.rs index 7ee204ac9..efa2c5247 100644 --- a/crates/apub/src/objects/community.rs +++ b/crates/apub/src/objects/community.rs @@ -39,6 +39,7 @@ use lemmy_db_schema::{ }, traits::{ApubActor, Crud}, utils::naive_now, + CommunityVisibility, }; use lemmy_db_views_actor::structs::CommunityFollowerView; use lemmy_utils::{ @@ -126,6 +127,7 @@ impl Object for ApubCommunity { updated: self.updated, posting_restricted_to_mods: Some(self.posting_restricted_to_mods), attributed_to: Some(generate_moderators_url(&self.actor_id)?.into()), + manually_approves_followers: Some(self.visibility == CommunityVisibility::Private), }; Ok(group) } @@ -152,7 +154,11 @@ impl Object for ApubCommunity { let sidebar = markdown_rewrite_remote_links_opt(sidebar, context).await; let icon = proxy_image_link_opt_apub(group.icon.map(|i| i.url), context).await?; let banner = proxy_image_link_opt_apub(group.image.map(|i| i.url), context).await?; - + let visibility = Some(if group.manually_approves_followers.unwrap_or_default() { + CommunityVisibility::Private + } else { + CommunityVisibility::Public + }); let form = CommunityInsertForm { published: group.published, updated: group.updated, @@ -176,6 +182,7 @@ impl Object for ApubCommunity { moderators_url: group.attributed_to.clone().map(Into::into), posting_restricted_to_mods: group.posting_restricted_to_mods, featured_url: group.featured.clone().map(Into::into), + visibility, ..CommunityInsertForm::new( instance_id, group.preferred_username.clone(), diff --git a/crates/apub/src/objects/post.rs b/crates/apub/src/objects/post.rs index ee88cf3ec..b72fa1728 100644 --- a/crates/apub/src/objects/post.rs +++ b/crates/apub/src/objects/post.rs @@ -1,5 +1,5 @@ use crate::{ - activities::{verify_is_public, verify_person_in_community}, + activities::{generate_to, verify_person_in_community, verify_visibility}, check_apub_id_valid_with_strictness, fetcher::markdown_links::{markdown_rewrite_remote_links_opt, to_local_url}, local_site_data_cached, @@ -16,7 +16,6 @@ use crate::{ }; use activitypub_federation::{ config::Data, - kinds::public, protocol::{values::MediaTypeMarkdownOrHtml, verification::verify_domains_match}, traits::Object, }; @@ -135,7 +134,7 @@ impl Object for ApubPost { kind: PageType::Page, id: self.ap_id.clone().into(), attributed_to: AttributedTo::Lemmy(creator.actor_id.into()), - to: vec![community.actor_id.clone().into(), public()], + to: vec![generate_to(&community)?], cc: vec![], name: Some(self.name.clone()), content: self.body.as_ref().map(|b| markdown_to_html(b)), @@ -172,7 +171,7 @@ impl Object for ApubPost { check_slurs_opt(&page.name, slur_regex)?; verify_domains_match(page.creator()?.inner(), page.id.inner())?; - verify_is_public(&page.to, &page.cc)?; + verify_visibility(&page.to, &page.cc, &community)?; Ok(()) } diff --git a/crates/apub/src/protocol/activities/following/mod.rs b/crates/apub/src/protocol/activities/following/mod.rs index ec263adae..1bb805608 100644 --- a/crates/apub/src/protocol/activities/following/mod.rs +++ b/crates/apub/src/protocol/activities/following/mod.rs @@ -1,5 +1,6 @@ pub(crate) mod accept; pub mod follow; +pub(crate) mod reject; pub mod undo_follow; #[cfg(test)] diff --git a/crates/apub/src/protocol/activities/following/reject.rs b/crates/apub/src/protocol/activities/following/reject.rs new file mode 100644 index 000000000..1584dfb11 --- /dev/null +++ b/crates/apub/src/protocol/activities/following/reject.rs @@ -0,0 +1,24 @@ +use crate::{ + objects::{community::ApubCommunity, person::ApubPerson}, + protocol::activities::following::follow::Follow, +}; +use activitypub_federation::{ + fetch::object_id::ObjectId, + kinds::activity::RejectType, + protocol::helpers::deserialize_skip_error, +}; +use serde::{Deserialize, Serialize}; +use url::Url; + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct RejectFollow { + pub(crate) actor: ObjectId, + /// Optional, for compatibility with platforms that always expect recipient field + #[serde(deserialize_with = "deserialize_skip_error", default)] + pub(crate) to: Option<[ObjectId; 1]>, + pub(crate) object: Follow, + #[serde(rename = "type")] + pub(crate) kind: RejectType, + pub(crate) id: Url, +} diff --git a/crates/apub/src/protocol/objects/group.rs b/crates/apub/src/protocol/objects/group.rs index affafe269..dbf4af892 100644 --- a/crates/apub/src/protocol/objects/group.rs +++ b/crates/apub/src/protocol/objects/group.rs @@ -73,6 +73,8 @@ pub struct Group { pub(crate) featured: Option>, #[serde(default)] pub(crate) language: Vec, + /// True if this is a private community + pub(crate) manually_approves_followers: Option, pub(crate) published: Option>, pub(crate) updated: Option>, } diff --git a/crates/db_perf/src/main.rs b/crates/db_perf/src/main.rs index 0fa5c0549..02796a906 100644 --- a/crates/db_perf/src/main.rs +++ b/crates/db_perf/src/main.rs @@ -54,7 +54,7 @@ async fn main() -> anyhow::Result<()> { async fn try_main() -> LemmyResult<()> { let args = CmdArgs::parse(); - let pool = &build_db_pool().await?; + let pool = &build_db_pool()?; let pool = &mut pool.into(); let conn = &mut get_conn(pool).await?; diff --git a/crates/db_schema/src/aggregates/comment_aggregates.rs b/crates/db_schema/src/aggregates/comment_aggregates.rs index a97bb565b..b26d27736 100644 --- a/crates/db_schema/src/aggregates/comment_aggregates.rs +++ b/crates/db_schema/src/aggregates/comment_aggregates.rs @@ -51,7 +51,7 @@ mod tests { #[tokio::test] #[serial] async fn test_crud() -> Result<(), Error> { - let pool = &build_db_pool_for_tests().await; + let pool = &build_db_pool_for_tests(); let pool = &mut pool.into(); let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?; diff --git a/crates/db_schema/src/aggregates/community_aggregates.rs b/crates/db_schema/src/aggregates/community_aggregates.rs index 0359d8632..3ec56d73d 100644 --- a/crates/db_schema/src/aggregates/community_aggregates.rs +++ b/crates/db_schema/src/aggregates/community_aggregates.rs @@ -37,7 +37,13 @@ mod tests { aggregates::community_aggregates::CommunityAggregates, source::{ comment::{Comment, CommentInsertForm}, - community::{Community, CommunityFollower, CommunityFollowerForm, CommunityInsertForm}, + community::{ + Community, + CommunityFollower, + CommunityFollowerForm, + CommunityFollowerState, + CommunityInsertForm, + }, instance::Instance, person::{Person, PersonInsertForm}, post::{Post, PostInsertForm}, @@ -52,7 +58,7 @@ mod tests { #[tokio::test] #[serial] async fn test_crud() -> Result<(), Error> { - let pool = &build_db_pool_for_tests().await; + let pool = &build_db_pool_for_tests(); let pool = &mut pool.into(); let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?; @@ -84,7 +90,8 @@ mod tests { let first_person_follow = CommunityFollowerForm { community_id: inserted_community.id, person_id: inserted_person.id, - pending: false, + state: Some(CommunityFollowerState::Accepted), + approver_id: None, }; CommunityFollower::follow(pool, &first_person_follow).await?; @@ -92,7 +99,8 @@ mod tests { let second_person_follow = CommunityFollowerForm { community_id: inserted_community.id, person_id: another_inserted_person.id, - pending: false, + state: Some(CommunityFollowerState::Accepted), + approver_id: None, }; CommunityFollower::follow(pool, &second_person_follow).await?; @@ -100,7 +108,8 @@ mod tests { let another_community_follow = CommunityFollowerForm { community_id: another_inserted_community.id, person_id: inserted_person.id, - pending: false, + state: Some(CommunityFollowerState::Accepted), + approver_id: None, }; CommunityFollower::follow(pool, &another_community_follow).await?; diff --git a/crates/db_schema/src/aggregates/person_aggregates.rs b/crates/db_schema/src/aggregates/person_aggregates.rs index 6e0eacc07..62aa9b609 100644 --- a/crates/db_schema/src/aggregates/person_aggregates.rs +++ b/crates/db_schema/src/aggregates/person_aggregates.rs @@ -36,7 +36,7 @@ mod tests { #[tokio::test] #[serial] async fn test_crud() -> Result<(), Error> { - let pool = &build_db_pool_for_tests().await; + let pool = &build_db_pool_for_tests(); let pool = &mut pool.into(); let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?; diff --git a/crates/db_schema/src/aggregates/post_aggregates.rs b/crates/db_schema/src/aggregates/post_aggregates.rs index b63017317..46747b076 100644 --- a/crates/db_schema/src/aggregates/post_aggregates.rs +++ b/crates/db_schema/src/aggregates/post_aggregates.rs @@ -70,7 +70,7 @@ mod tests { #[tokio::test] #[serial] async fn test_crud() -> Result<(), Error> { - let pool = &build_db_pool_for_tests().await; + let pool = &build_db_pool_for_tests(); let pool = &mut pool.into(); let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?; @@ -182,7 +182,7 @@ mod tests { #[tokio::test] #[serial] async fn test_soft_delete() -> Result<(), Error> { - let pool = &build_db_pool_for_tests().await; + let pool = &build_db_pool_for_tests(); let pool = &mut pool.into(); let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?; diff --git a/crates/db_schema/src/aggregates/site_aggregates.rs b/crates/db_schema/src/aggregates/site_aggregates.rs index 379ddd2d9..2df566290 100644 --- a/crates/db_schema/src/aggregates/site_aggregates.rs +++ b/crates/db_schema/src/aggregates/site_aggregates.rs @@ -65,7 +65,7 @@ mod tests { #[tokio::test] #[serial] async fn test_crud() -> Result<(), Error> { - let pool = &build_db_pool_for_tests().await; + let pool = &build_db_pool_for_tests(); let pool = &mut pool.into(); let (inserted_instance, inserted_person, inserted_site, inserted_community) = @@ -136,7 +136,7 @@ mod tests { #[tokio::test] #[serial] async fn test_soft_delete() -> Result<(), Error> { - let pool = &build_db_pool_for_tests().await; + let pool = &build_db_pool_for_tests(); let pool = &mut pool.into(); let (inserted_instance, inserted_person, inserted_site, inserted_community) = diff --git a/crates/db_schema/src/impls/activity.rs b/crates/db_schema/src/impls/activity.rs index fff0c2f0c..d2cc6dcef 100644 --- a/crates/db_schema/src/impls/activity.rs +++ b/crates/db_schema/src/impls/activity.rs @@ -71,7 +71,7 @@ mod tests { #[tokio::test] #[serial] async fn receive_activity_duplicate() -> LemmyResult<()> { - let pool = &build_db_pool_for_tests().await; + let pool = &build_db_pool_for_tests(); let pool = &mut pool.into(); let ap_id: DbUrl = Url::parse("http://example.com/activity/531")?.into(); @@ -86,7 +86,7 @@ mod tests { #[tokio::test] #[serial] async fn sent_activity_write_read() -> LemmyResult<()> { - let pool = &build_db_pool_for_tests().await; + let pool = &build_db_pool_for_tests(); let pool = &mut pool.into(); let ap_id: DbUrl = Url::parse("http://example.com/activity/412")?.into(); let data = json!({ diff --git a/crates/db_schema/src/impls/actor_language.rs b/crates/db_schema/src/impls/actor_language.rs index bff729f41..b4ad0d347 100644 --- a/crates/db_schema/src/impls/actor_language.rs +++ b/crates/db_schema/src/impls/actor_language.rs @@ -438,7 +438,7 @@ mod tests { #[tokio::test] #[serial] async fn test_convert_update_languages() -> Result<(), Error> { - let pool = &build_db_pool_for_tests().await; + let pool = &build_db_pool_for_tests(); let pool = &mut pool.into(); // call with empty vec, returns all languages @@ -457,7 +457,7 @@ mod tests { #[serial] async fn test_convert_read_languages() -> Result<(), Error> { use crate::schema::language::dsl::{id, language}; - let pool = &build_db_pool_for_tests().await; + let pool = &build_db_pool_for_tests(); let pool = &mut pool.into(); // call with all languages, returns empty vec @@ -477,7 +477,7 @@ mod tests { #[tokio::test] #[serial] async fn test_site_languages() -> Result<(), Error> { - let pool = &build_db_pool_for_tests().await; + let pool = &build_db_pool_for_tests(); let pool = &mut pool.into(); let (site, instance) = create_test_site(pool).await?; @@ -502,7 +502,7 @@ mod tests { #[tokio::test] #[serial] async fn test_user_languages() -> Result<(), Error> { - let pool = &build_db_pool_for_tests().await; + let pool = &build_db_pool_for_tests(); let pool = &mut pool.into(); let (site, instance) = create_test_site(pool).await?; @@ -535,7 +535,7 @@ mod tests { #[tokio::test] #[serial] async fn test_community_languages() -> Result<(), Error> { - let pool = &build_db_pool_for_tests().await; + let pool = &build_db_pool_for_tests(); let pool = &mut pool.into(); let (site, instance) = create_test_site(pool).await?; let test_langs = test_langs1(pool).await?; @@ -591,7 +591,7 @@ mod tests { #[tokio::test] #[serial] async fn test_default_post_language() -> Result<(), Error> { - let pool = &build_db_pool_for_tests().await; + let pool = &build_db_pool_for_tests(); let pool = &mut pool.into(); let (site, instance) = create_test_site(pool).await?; let test_langs = test_langs1(pool).await?; diff --git a/crates/db_schema/src/impls/captcha_answer.rs b/crates/db_schema/src/impls/captcha_answer.rs index e7ba86d39..8be8fc5de 100644 --- a/crates/db_schema/src/impls/captcha_answer.rs +++ b/crates/db_schema/src/impls/captcha_answer.rs @@ -62,7 +62,7 @@ mod tests { #[tokio::test] #[serial] async fn test_captcha_happy_path() { - let pool = &build_db_pool_for_tests().await; + let pool = &build_db_pool_for_tests(); let pool = &mut pool.into(); let inserted = CaptchaAnswer::insert( @@ -89,7 +89,7 @@ mod tests { #[tokio::test] #[serial] async fn test_captcha_repeat_answer_fails() { - let pool = &build_db_pool_for_tests().await; + let pool = &build_db_pool_for_tests(); let pool = &mut pool.into(); let inserted = CaptchaAnswer::insert( diff --git a/crates/db_schema/src/impls/comment.rs b/crates/db_schema/src/impls/comment.rs index 30d18465f..d261dbf2c 100644 --- a/crates/db_schema/src/impls/comment.rs +++ b/crates/db_schema/src/impls/comment.rs @@ -227,7 +227,7 @@ mod tests { #[tokio::test] #[serial] async fn test_crud() -> LemmyResult<()> { - let pool = &build_db_pool_for_tests().await; + let pool = &build_db_pool_for_tests(); let pool = &mut pool.into(); let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?; diff --git a/crates/db_schema/src/impls/community.rs b/crates/db_schema/src/impls/community.rs index 8efc579e9..5375bcc3c 100644 --- a/crates/db_schema/src/impls/community.rs +++ b/crates/db_schema/src/impls/community.rs @@ -15,6 +15,7 @@ use crate::{ Community, CommunityFollower, CommunityFollowerForm, + CommunityFollowerState, CommunityInsertForm, CommunityModerator, CommunityModeratorForm, @@ -324,22 +325,8 @@ impl Bannable for CommunityPersonBan { } impl CommunityFollower { - pub fn to_subscribed_type(follower: &Option) -> SubscribedType { - match follower { - Some(f) => { - if f.pending { - SubscribedType::Pending - } else { - SubscribedType::Subscribed - } - } - // If the row doesn't exist, the person isn't a follower. - None => SubscribedType::NotSubscribed, - } - } - - pub fn select_subscribed_type() -> dsl::Nullable { - community_follower::pending.nullable() + pub fn select_subscribed_type() -> dsl::Nullable { + community_follower::state.nullable() } /// Check if a remote instance has any followers on local instance. For this it is enough to check @@ -357,14 +344,34 @@ impl CommunityFollower { .then_some(()) .ok_or(LemmyErrorType::CommunityHasNoFollowers.into()) } + + pub async fn approve( + pool: &mut DbPool<'_>, + community_id: CommunityId, + follower_id: PersonId, + approver_id: PersonId, + ) -> LemmyResult<()> { + let conn = &mut get_conn(pool).await?; + diesel::update(community_follower::table.find((follower_id, community_id))) + .set(( + community_follower::state.eq(CommunityFollowerState::Accepted), + community_follower::approver_id.eq(approver_id), + )) + .get_result::(conn) + .await?; + Ok(()) + } } -impl Queryable, Pg> for SubscribedType { - type Row = Option; +impl Queryable, Pg> + for SubscribedType +{ + type Row = Option; fn build(row: Self::Row) -> deserialize::Result { Ok(match row { - Some(true) => SubscribedType::Pending, - Some(false) => SubscribedType::Subscribed, + Some(CommunityFollowerState::Pending) => SubscribedType::Pending, + Some(CommunityFollowerState::Accepted) => SubscribedType::Subscribed, + Some(CommunityFollowerState::ApprovalRequired) => SubscribedType::ApprovalRequired, None => SubscribedType::NotSubscribed, }) } @@ -393,7 +400,7 @@ impl Followable for CommunityFollower { ) -> Result { let conn = &mut get_conn(pool).await?; diesel::update(community_follower::table.find((person_id, community_id))) - .set(community_follower::pending.eq(false)) + .set(community_follower::state.eq(CommunityFollowerState::Accepted)) .get_result::(conn) .await } @@ -463,6 +470,7 @@ mod tests { Community, CommunityFollower, CommunityFollowerForm, + CommunityFollowerState, CommunityInsertForm, CommunityModerator, CommunityModeratorForm, @@ -485,7 +493,7 @@ mod tests { #[tokio::test] #[serial] async fn test_crud() -> LemmyResult<()> { - let pool = &build_db_pool_for_tests().await; + let pool = &build_db_pool_for_tests(); let pool = &mut pool.into(); let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?; @@ -535,7 +543,8 @@ mod tests { let community_follower_form = CommunityFollowerForm { community_id: inserted_community.id, person_id: inserted_bobby.id, - pending: false, + state: Some(CommunityFollowerState::Accepted), + approver_id: None, }; let inserted_community_follower = @@ -544,8 +553,9 @@ mod tests { let expected_community_follower = CommunityFollower { community_id: inserted_community.id, person_id: inserted_bobby.id, - pending: false, + state: CommunityFollowerState::Accepted, published: inserted_community_follower.published, + approver_id: None, }; let bobby_moderator_form = CommunityModeratorForm { diff --git a/crates/db_schema/src/impls/federation_allowlist.rs b/crates/db_schema/src/impls/federation_allowlist.rs index cbfd14b03..099e0b231 100644 --- a/crates/db_schema/src/impls/federation_allowlist.rs +++ b/crates/db_schema/src/impls/federation_allowlist.rs @@ -61,7 +61,7 @@ mod tests { #[tokio::test] #[serial] async fn test_allowlist_insert_and_clear() -> Result<(), Error> { - let pool = &build_db_pool_for_tests().await; + let pool = &build_db_pool_for_tests(); let pool = &mut pool.into(); let domains = vec![ "tld1.xyz".to_string(), diff --git a/crates/db_schema/src/impls/language.rs b/crates/db_schema/src/impls/language.rs index 57420fcd4..3b8bc1d20 100644 --- a/crates/db_schema/src/impls/language.rs +++ b/crates/db_schema/src/impls/language.rs @@ -46,7 +46,7 @@ mod tests { #[tokio::test] #[serial] async fn test_languages() -> Result<(), Error> { - let pool = &build_db_pool_for_tests().await; + let pool = &build_db_pool_for_tests(); let pool = &mut pool.into(); let all = Language::read_all(pool).await?; diff --git a/crates/db_schema/src/impls/local_user.rs b/crates/db_schema/src/impls/local_user.rs index 235f053c1..69a5ef314 100644 --- a/crates/db_schema/src/impls/local_user.rs +++ b/crates/db_schema/src/impls/local_user.rs @@ -331,6 +331,7 @@ impl LocalUserOptionHelper for Option<&LocalUser> { .unwrap_or(site.content_warning.is_some()) } + // TODO: use this function for private community checks, but the generics get extremely confusing fn visible_communities_only(&self, query: Q) -> Q where Q: diesel::query_dsl::methods::FilterDsl< @@ -385,7 +386,7 @@ mod tests { #[tokio::test] #[serial] async fn test_admin_higher_check() -> LemmyResult<()> { - let pool = &build_db_pool_for_tests().await; + let pool = &build_db_pool_for_tests(); let pool = &mut pool.into(); let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?; @@ -424,7 +425,7 @@ mod tests { #[tokio::test] #[serial] async fn test_email_taken() -> LemmyResult<()> { - let pool = &build_db_pool_for_tests().await; + let pool = &build_db_pool_for_tests(); let pool = &mut pool.into(); let darwin_email = "charles.darwin@gmail.com"; diff --git a/crates/db_schema/src/impls/moderator.rs b/crates/db_schema/src/impls/moderator.rs index b2ef26e69..8deb56258 100644 --- a/crates/db_schema/src/impls/moderator.rs +++ b/crates/db_schema/src/impls/moderator.rs @@ -533,7 +533,7 @@ mod tests { #[tokio::test] #[serial] async fn test_crud() -> Result<(), Error> { - let pool = &build_db_pool_for_tests().await; + let pool = &build_db_pool_for_tests(); let pool = &mut pool.into(); let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?; diff --git a/crates/db_schema/src/impls/password_reset_request.rs b/crates/db_schema/src/impls/password_reset_request.rs index 015db5581..a9ac3a9c2 100644 --- a/crates/db_schema/src/impls/password_reset_request.rs +++ b/crates/db_schema/src/impls/password_reset_request.rs @@ -61,7 +61,7 @@ mod tests { #[tokio::test] #[serial] async fn test_password_reset() -> LemmyResult<()> { - let pool = &build_db_pool_for_tests().await; + let pool = &build_db_pool_for_tests(); let pool = &mut pool.into(); // Setup diff --git a/crates/db_schema/src/impls/person.rs b/crates/db_schema/src/impls/person.rs index fb287ef77..85ab20d6a 100644 --- a/crates/db_schema/src/impls/person.rs +++ b/crates/db_schema/src/impls/person.rs @@ -252,7 +252,7 @@ mod tests { #[tokio::test] #[serial] async fn test_crud() -> LemmyResult<()> { - let pool = &build_db_pool_for_tests().await; + let pool = &build_db_pool_for_tests(); let pool = &mut pool.into(); let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?; @@ -306,7 +306,7 @@ mod tests { #[tokio::test] #[serial] async fn follow() -> LemmyResult<()> { - let pool = &build_db_pool_for_tests().await; + let pool = &build_db_pool_for_tests(); let pool = &mut pool.into(); let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?; diff --git a/crates/db_schema/src/impls/post.rs b/crates/db_schema/src/impls/post.rs index fb6245585..bd99344b9 100644 --- a/crates/db_schema/src/impls/post.rs +++ b/crates/db_schema/src/impls/post.rs @@ -423,7 +423,7 @@ mod tests { #[tokio::test] #[serial] async fn test_crud() -> LemmyResult<()> { - let pool = &build_db_pool_for_tests().await; + let pool = &build_db_pool_for_tests(); let pool = &mut pool.into(); let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?; diff --git a/crates/db_schema/src/impls/post_report.rs b/crates/db_schema/src/impls/post_report.rs index 5507423e1..e7d27aee9 100644 --- a/crates/db_schema/src/impls/post_report.rs +++ b/crates/db_schema/src/impls/post_report.rs @@ -126,7 +126,7 @@ mod tests { #[tokio::test] #[serial] async fn test_resolve_post_report() -> Result<(), Error> { - let pool = &build_db_pool_for_tests().await; + let pool = &build_db_pool_for_tests(); let pool = &mut pool.into(); let (person, report) = init(pool).await?; @@ -146,7 +146,7 @@ mod tests { #[tokio::test] #[serial] async fn test_resolve_all_post_reports() -> Result<(), Error> { - let pool = &build_db_pool_for_tests().await; + let pool = &build_db_pool_for_tests(); let pool = &mut pool.into(); let (person, report) = init(pool).await?; diff --git a/crates/db_schema/src/impls/private_message.rs b/crates/db_schema/src/impls/private_message.rs index 264175fe2..e08b4cf7f 100644 --- a/crates/db_schema/src/impls/private_message.rs +++ b/crates/db_schema/src/impls/private_message.rs @@ -104,7 +104,7 @@ mod tests { #[tokio::test] #[serial] async fn test_crud() -> LemmyResult<()> { - let pool = &build_db_pool_for_tests().await; + let pool = &build_db_pool_for_tests(); let pool = &mut pool.into(); let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?; diff --git a/crates/db_schema/src/lib.rs b/crates/db_schema/src/lib.rs index dbadaaf95..0397c939a 100644 --- a/crates/db_schema/src/lib.rs +++ b/crates/db_schema/src/lib.rs @@ -191,6 +191,7 @@ pub enum SubscribedType { Subscribed, NotSubscribed, Pending, + ApprovalRequired, } #[derive(EnumString, Display, Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Hash)] @@ -241,14 +242,14 @@ pub enum PostFeatureType { #[cfg_attr(feature = "full", DbValueStyle = "verbatim")] #[cfg_attr(feature = "full", ts(export))] /// Defines who can browse and interact with content in a community. -/// -/// TODO: Also use this to define private communities pub enum CommunityVisibility { /// Public community, any local or federated user can interact. #[default] Public, /// Unfederated community, only local users can interact. LocalOnly, + /// Users need to be approved by mods before they are able to browse or post. + Private, } #[derive( diff --git a/crates/db_schema/src/schema.rs b/crates/db_schema/src/schema.rs index 9f1d00568..a986f55d1 100644 --- a/crates/db_schema/src/schema.rs +++ b/crates/db_schema/src/schema.rs @@ -1,39 +1,43 @@ // @generated automatically by Diesel CLI. pub mod sql_types { - #[derive(diesel::sql_types::SqlType)] + #[derive(diesel::query_builder::QueryId, diesel::sql_types::SqlType)] #[diesel(postgres_type(name = "actor_type_enum"))] pub struct ActorTypeEnum; - #[derive(diesel::sql_types::SqlType)] + #[derive(diesel::query_builder::QueryId, diesel::sql_types::SqlType)] #[diesel(postgres_type(name = "comment_sort_type_enum"))] pub struct CommentSortTypeEnum; - #[derive(diesel::sql_types::SqlType)] + #[derive(diesel::query_builder::QueryId, diesel::sql_types::SqlType)] + #[diesel(postgres_type(name = "community_follower_state"))] + pub struct CommunityFollowerState; + + #[derive(diesel::query_builder::QueryId, diesel::sql_types::SqlType)] #[diesel(postgres_type(name = "community_visibility"))] pub struct CommunityVisibility; - #[derive(diesel::sql_types::SqlType)] + #[derive(diesel::query_builder::QueryId, diesel::sql_types::SqlType)] #[diesel(postgres_type(name = "federation_mode_enum"))] pub struct FederationModeEnum; - #[derive(diesel::sql_types::SqlType)] + #[derive(diesel::query_builder::QueryId, diesel::sql_types::SqlType)] #[diesel(postgres_type(name = "listing_type_enum"))] pub struct ListingTypeEnum; - #[derive(diesel::sql_types::SqlType)] + #[derive(diesel::query_builder::QueryId, diesel::sql_types::SqlType)] #[diesel(postgres_type(name = "ltree"))] pub struct Ltree; - #[derive(diesel::sql_types::SqlType)] + #[derive(diesel::query_builder::QueryId, diesel::sql_types::SqlType)] #[diesel(postgres_type(name = "post_listing_mode_enum"))] pub struct PostListingModeEnum; - #[derive(diesel::sql_types::SqlType)] + #[derive(diesel::query_builder::QueryId, diesel::sql_types::SqlType)] #[diesel(postgres_type(name = "post_sort_type_enum"))] pub struct PostSortTypeEnum; - #[derive(diesel::sql_types::SqlType)] + #[derive(diesel::query_builder::QueryId, diesel::sql_types::SqlType)] #[diesel(postgres_type(name = "registration_mode_enum"))] pub struct RegistrationModeEnum; } @@ -226,11 +230,15 @@ diesel::table! { } diesel::table! { + use diesel::sql_types::*; + use super::sql_types::CommunityFollowerState; + community_follower (person_id, community_id) { community_id -> Int4, person_id -> Int4, published -> Timestamptz, - pending -> Bool, + state -> CommunityFollowerState, + approver_id -> Nullable, } } @@ -1006,7 +1014,6 @@ diesel::joinable!(community_aggregates -> community (community_id)); diesel::joinable!(community_block -> community (community_id)); diesel::joinable!(community_block -> person (person_id)); diesel::joinable!(community_follower -> community (community_id)); -diesel::joinable!(community_follower -> person (person_id)); diesel::joinable!(community_language -> community (community_id)); diesel::joinable!(community_language -> language (language_id)); diesel::joinable!(community_moderator -> community (community_id)); diff --git a/crates/db_schema/src/source/community.rs b/crates/db_schema/src/source/community.rs index 870a132f2..95d2a67c3 100644 --- a/crates/db_schema/src/source/community.rs +++ b/crates/db_schema/src/source/community.rs @@ -9,6 +9,7 @@ use crate::{ use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use serde_with::skip_serializing_none; +use strum::{Display, EnumString}; #[cfg(feature = "full")] use ts_rs::TS; @@ -207,6 +208,20 @@ pub struct CommunityPersonBanForm { pub expires: Option>>, } +#[derive(EnumString, Display, Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "full", derive(DbEnum, TS))] +#[cfg_attr( + feature = "full", + ExistingTypePath = "crate::schema::sql_types::CommunityFollowerState" +)] +#[cfg_attr(feature = "full", DbValueStyle = "verbatim")] +#[cfg_attr(feature = "full", ts(export))] +pub enum CommunityFollowerState { + Accepted, + Pending, + ApprovalRequired, +} + #[derive(PartialEq, Eq, Debug)] #[cfg_attr( feature = "full", @@ -223,14 +238,18 @@ pub struct CommunityFollower { pub community_id: CommunityId, pub person_id: PersonId, pub published: DateTime, - pub pending: bool, + pub state: CommunityFollowerState, + pub approver_id: Option, } -#[derive(Clone)] +#[derive(Clone, derive_new::new)] #[cfg_attr(feature = "full", derive(Insertable, AsChangeset))] #[cfg_attr(feature = "full", diesel(table_name = community_follower))] pub struct CommunityFollowerForm { pub community_id: CommunityId, pub person_id: PersonId, - pub pending: bool, + #[new(default)] + pub state: Option, + #[new(default)] + pub approver_id: Option, } diff --git a/crates/db_schema/src/utils.rs b/crates/db_schema/src/utils.rs index 6c5b792eb..03f1bb8ca 100644 --- a/crates/db_schema/src/utils.rs +++ b/crates/db_schema/src/utils.rs @@ -453,7 +453,7 @@ impl ServerCertVerifier for NoCertVerifier { } } -pub async fn build_db_pool() -> LemmyResult { +pub fn build_db_pool() -> LemmyResult { let db_url = SETTINGS.get_database_url(); // diesel-async does not support any TLS connections out of the box, so we need to manually // provide a setup function which handles creating the connection @@ -482,8 +482,8 @@ pub async fn build_db_pool() -> LemmyResult { Ok(pool) } -pub async fn build_db_pool_for_tests() -> ActualDbPool { - build_db_pool().await.expect("db pool missing") +pub fn build_db_pool_for_tests() -> ActualDbPool { + build_db_pool().expect("db pool missing") } pub fn naive_now() -> DateTime { diff --git a/crates/db_views/src/comment_report_view.rs b/crates/db_views/src/comment_report_view.rs index be5e76562..06c05639d 100644 --- a/crates/db_views/src/comment_report_view.rs +++ b/crates/db_views/src/comment_report_view.rs @@ -28,6 +28,7 @@ use lemmy_db_schema::{ person_block, post, }, + source::community::CommunityFollower, utils::{get_conn, limit_and_offset, DbConn, DbPool, ListFn, Queries, ReadFn}, }; @@ -122,7 +123,7 @@ fn queries<'a>() -> Queries< .is_not_null(), local_user::admin.nullable().is_not_null(), person_block::target_id.nullable().is_not_null(), - community_follower::pending.nullable(), + CommunityFollower::select_subscribed_type(), comment_saved::published.nullable().is_not_null(), comment_like::score.nullable(), aliases::person2.fields(person::all_columns).nullable(), @@ -290,7 +291,7 @@ mod tests { #[tokio::test] #[serial] async fn test_crud() -> LemmyResult<()> { - let pool = &build_db_pool_for_tests().await; + let pool = &build_db_pool_for_tests(); let pool = &mut pool.into(); let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?; diff --git a/crates/db_views/src/comment_view.rs b/crates/db_views/src/comment_view.rs index ff1405508..0521e401c 100644 --- a/crates/db_views/src/comment_view.rs +++ b/crates/db_views/src/comment_view.rs @@ -35,9 +35,14 @@ use lemmy_db_schema::{ person_block, post, }, - source::{local_user::LocalUser, site::Site}, + source::{ + community::{CommunityFollower, CommunityFollowerState}, + local_user::LocalUser, + site::Site, + }, utils::{fuzzy_search, limit_and_offset, DbConn, DbPool, ListFn, Queries, ReadFn}, CommentSortType, + CommunityVisibility, ListingType, }; @@ -70,7 +75,7 @@ fn queries<'a>() -> Queries< .eq(community_follower::community_id) .and(community_follower::person_id.eq(person_id)), ) - .select(community_follower::pending.nullable()) + .select(CommunityFollower::select_subscribed_type()) .single_value() }; @@ -129,11 +134,15 @@ fn queries<'a>() -> Queries< }; let subscribed_type_selection: Box< - dyn BoxableExpression<_, Pg, SqlType = sql_types::Nullable>, + dyn BoxableExpression< + _, + Pg, + SqlType = sql_types::Nullable, + >, > = if let Some(person_id) = my_person_id { Box::new(is_community_followed(person_id)) } else { - Box::new(None::.into_sql::>()) + Box::new(None::.into_sql::>()) }; let is_creator_blocked_selection: Box> = @@ -179,6 +188,26 @@ fn queries<'a>() -> Queries< my_local_user.person_id(), ); query = my_local_user.visible_communities_only(query); + + // Check permissions to view private community content. + // Specifically, if the community is private then only accepted followers may view its + // content, otherwise it is filtered out. Admins can view private community content + // without restriction. + if !my_local_user.is_admin() { + query = query.filter( + community::visibility + .ne(CommunityVisibility::Private) + .or(exists( + community_follower::table.filter( + post::community_id.eq(community_follower::community_id).and( + community_follower::person_id + .eq(my_local_user.map(|l| l.person_id).unwrap_or_default()) + .and(community_follower::state.eq(CommunityFollowerState::Accepted)), + ), + ), + )), + ); + } query.first(&mut conn).await }; @@ -301,6 +330,22 @@ fn queries<'a>() -> Queries< query = options.local_user.visible_communities_only(query); + if !options.local_user.is_admin() { + query = query.filter( + community::visibility + .ne(CommunityVisibility::Private) + .or(exists( + community_follower::table.filter( + post::community_id.eq(community_follower::community_id).and( + community_follower::person_id + .eq(person_id_join) + .and(community_follower::state.eq(CommunityFollowerState::Accepted)), + ), + ), + )), + ); + } + // A Max depth given means its a tree fetch let (limit, offset) = if let Some(max_depth) = options.max_depth { let depth_limit = if let Some(parent_path) = options.parent_path.as_ref() { @@ -445,6 +490,9 @@ mod tests { }, community::{ Community, + CommunityFollower, + CommunityFollowerForm, + CommunityFollowerState, CommunityInsertForm, CommunityModerator, CommunityModeratorForm, @@ -461,7 +509,7 @@ mod tests { post::{Post, PostInsertForm, PostUpdateForm}, site::{Site, SiteInsertForm}, }, - traits::{Bannable, Blockable, Crud, Joinable, Likeable, Saveable}, + traits::{Bannable, Blockable, Crud, Followable, Joinable, Likeable, Saveable}, utils::{build_db_pool_for_tests, RANK_DEFAULT}, CommunityVisibility, SubscribedType, @@ -631,7 +679,7 @@ mod tests { #[tokio::test] #[serial] async fn test_crud() -> LemmyResult<()> { - let pool = &build_db_pool_for_tests().await; + let pool = &build_db_pool_for_tests(); let pool = &mut pool.into(); let data = init_data(pool).await?; @@ -686,7 +734,7 @@ mod tests { #[tokio::test] #[serial] async fn test_liked_only() -> LemmyResult<()> { - let pool = &build_db_pool_for_tests().await; + let pool = &build_db_pool_for_tests(); let pool = &mut pool.into(); let data = init_data(pool).await?; @@ -737,7 +785,7 @@ mod tests { #[tokio::test] #[serial] async fn test_comment_tree() -> LemmyResult<()> { - let pool = &build_db_pool_for_tests().await; + let pool = &build_db_pool_for_tests(); let pool = &mut pool.into(); let data = init_data(pool).await?; @@ -810,7 +858,7 @@ mod tests { #[tokio::test] #[serial] async fn test_languages() -> LemmyResult<()> { - let pool = &build_db_pool_for_tests().await; + let pool = &build_db_pool_for_tests(); let pool = &mut pool.into(); let data = init_data(pool).await?; @@ -869,7 +917,7 @@ mod tests { #[tokio::test] #[serial] async fn test_distinguished_first() -> LemmyResult<()> { - let pool = &build_db_pool_for_tests().await; + let pool = &build_db_pool_for_tests(); let pool = &mut pool.into(); let data = init_data(pool).await?; @@ -894,7 +942,7 @@ mod tests { #[tokio::test] #[serial] async fn test_creator_is_moderator() -> LemmyResult<()> { - let pool = &build_db_pool_for_tests().await; + let pool = &build_db_pool_for_tests(); let pool = &mut pool.into(); let data = init_data(pool).await?; @@ -925,7 +973,7 @@ mod tests { #[tokio::test] #[serial] async fn test_creator_is_admin() -> LemmyResult<()> { - let pool = &build_db_pool_for_tests().await; + let pool = &build_db_pool_for_tests(); let pool = &mut pool.into(); let data = init_data(pool).await?; @@ -950,7 +998,7 @@ mod tests { #[tokio::test] #[serial] async fn test_saved_order() -> LemmyResult<()> { - let pool = &build_db_pool_for_tests().await; + let pool = &build_db_pool_for_tests(); let pool = &mut pool.into(); let data = init_data(pool).await?; @@ -1125,7 +1173,7 @@ mod tests { #[tokio::test] #[serial] async fn local_only_instance() -> LemmyResult<()> { - let pool = &build_db_pool_for_tests().await; + let pool = &build_db_pool_for_tests(); let pool = &mut pool.into(); let data = init_data(pool).await?; @@ -1171,7 +1219,7 @@ mod tests { #[tokio::test] #[serial] async fn comment_listing_local_user_banned_from_community() -> LemmyResult<()> { - let pool = &build_db_pool_for_tests().await; + let pool = &build_db_pool_for_tests(); let pool = &mut pool.into(); let data = init_data(pool).await?; @@ -1213,7 +1261,7 @@ mod tests { #[tokio::test] #[serial] async fn comment_listing_local_user_not_banned_from_community() -> LemmyResult<()> { - let pool = &build_db_pool_for_tests().await; + let pool = &build_db_pool_for_tests(); let pool = &mut pool.into(); let data = init_data(pool).await?; @@ -1232,7 +1280,7 @@ mod tests { #[tokio::test] #[serial] async fn comment_listings_hide_nsfw() -> LemmyResult<()> { - let pool = &build_db_pool_for_tests().await; + let pool = &build_db_pool_for_tests(); let pool = &mut pool.into(); let data = init_data(pool).await?; @@ -1257,4 +1305,96 @@ mod tests { cleanup(data, pool).await } + + #[tokio::test] + #[serial] + async fn comment_listing_private_community() -> LemmyResult<()> { + let pool = &build_db_pool_for_tests(); + let pool = &mut pool.into(); + let mut data = init_data(pool).await?; + + // Mark community as private + Community::update( + pool, + data.inserted_community.id, + &CommunityUpdateForm { + visibility: Some(CommunityVisibility::Private), + ..Default::default() + }, + ) + .await?; + + // No comments returned without auth + let read_comment_listing = CommentQuery::default().list(&data.site, pool).await?; + assert_eq!(0, read_comment_listing.len()); + let comment_view = CommentView::read(pool, data.inserted_comment_0.id, None).await; + assert!(comment_view.is_err()); + + // No comments returned for non-follower who is not admin + data.timmy_local_user_view.local_user.admin = false; + let read_comment_listing = CommentQuery { + community_id: Some(data.inserted_community.id), + local_user: Some(&data.timmy_local_user_view.local_user), + ..Default::default() + } + .list(&data.site, pool) + .await?; + assert_eq!(0, read_comment_listing.len()); + let comment_view = CommentView::read( + pool, + data.inserted_comment_0.id, + Some(&data.timmy_local_user_view.local_user), + ) + .await; + assert!(comment_view.is_err()); + + // Admin can view content without following + data.timmy_local_user_view.local_user.admin = true; + let read_comment_listing = CommentQuery { + community_id: Some(data.inserted_community.id), + local_user: Some(&data.timmy_local_user_view.local_user), + ..Default::default() + } + .list(&data.site, pool) + .await?; + assert_eq!(5, read_comment_listing.len()); + let comment_view = CommentView::read( + pool, + data.inserted_comment_0.id, + Some(&data.timmy_local_user_view.local_user), + ) + .await; + assert!(comment_view.is_ok()); + data.timmy_local_user_view.local_user.admin = false; + + // User can view after following + CommunityFollower::follow( + pool, + &CommunityFollowerForm { + state: Some(CommunityFollowerState::Accepted), + ..CommunityFollowerForm::new( + data.inserted_community.id, + data.timmy_local_user_view.person.id, + ) + }, + ) + .await?; + let read_comment_listing = CommentQuery { + community_id: Some(data.inserted_community.id), + local_user: Some(&data.timmy_local_user_view.local_user), + ..Default::default() + } + .list(&data.site, pool) + .await?; + assert_eq!(5, read_comment_listing.len()); + let comment_view = CommentView::read( + pool, + data.inserted_comment_0.id, + Some(&data.timmy_local_user_view.local_user), + ) + .await; + assert!(comment_view.is_ok()); + + cleanup(data, pool).await + } } diff --git a/crates/db_views/src/post_report_view.rs b/crates/db_views/src/post_report_view.rs index 82e4c5d5b..d6577af38 100644 --- a/crates/db_views/src/post_report_view.rs +++ b/crates/db_views/src/post_report_view.rs @@ -29,6 +29,7 @@ use lemmy_db_schema::{ post_report, post_saved, }, + source::community::CommunityFollower, utils::{ functions::coalesce, get_conn, @@ -143,7 +144,7 @@ fn queries<'a>() -> Queries< .nullable() .is_not_null(), local_user::admin.nullable().is_not_null(), - community_follower::pending.nullable(), + CommunityFollower::select_subscribed_type(), post_saved::post_id.nullable().is_not_null(), post_read::post_id.nullable().is_not_null(), post_hide::post_id.nullable().is_not_null(), @@ -312,7 +313,7 @@ mod tests { #[tokio::test] #[serial] async fn test_crud() -> LemmyResult<()> { - let pool = &build_db_pool_for_tests().await; + let pool = &build_db_pool_for_tests(); let pool = &mut pool.into(); let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?; diff --git a/crates/db_views/src/post_view.rs b/crates/db_views/src/post_view.rs index dc00b0438..78b10cc3f 100644 --- a/crates/db_views/src/post_view.rs +++ b/crates/db_views/src/post_view.rs @@ -42,7 +42,11 @@ use lemmy_db_schema::{ post_read, post_saved, }, - source::{local_user::LocalUser, site::Site}, + source::{ + community::{CommunityFollower, CommunityFollowerState}, + local_user::LocalUser, + site::Site, + }, utils::{ functions::coalesce, fuzzy_search, @@ -57,6 +61,7 @@ use lemmy_db_schema::{ ReadFn, ReverseTimestampKey, }, + CommunityVisibility, ListingType, PostSortType, }; @@ -175,7 +180,11 @@ fn queries<'a>() -> Queries< }; let subscribed_type_selection: Box< - dyn BoxableExpression<_, Pg, SqlType = sql_types::Nullable>, + dyn BoxableExpression< + _, + Pg, + SqlType = sql_types::Nullable, + >, > = if let Some(person_id) = my_person_id { Box::new( community_follower::table @@ -184,11 +193,11 @@ fn queries<'a>() -> Queries< .eq(community_follower::community_id) .and(community_follower::person_id.eq(person_id)), ) - .select(community_follower::pending.nullable()) + .select(CommunityFollower::select_subscribed_type()) .single_value(), ) } else { - Box::new(None::.into_sql::>()) + Box::new(None::.into_sql::>()) }; let score_selection: Box< @@ -291,6 +300,22 @@ fn queries<'a>() -> Queries< post::deleted .eq(false) .or(post::creator_id.eq(person_id_join)), + ) + // private communities can only by browsed by accepted followers + .filter( + community::visibility + .ne(CommunityVisibility::Private) + .or(exists( + community_follower::table.filter( + post_aggregates::community_id + .eq(community_follower::community_id) + .and( + community_follower::person_id + .eq(my_local_user.map(|l| l.person_id).unwrap_or_default()) + .and(community_follower::state.eq(CommunityFollowerState::Accepted)), + ), + ), + )), ); } @@ -443,6 +468,21 @@ fn queries<'a>() -> Queries< query = options.local_user.visible_communities_only(query); + if !options.local_user.is_admin() { + query = query.filter( + community::visibility + .ne(CommunityVisibility::Private) + .or(exists( + community_follower::table.filter( + post_aggregates::community_id + .eq(community_follower::community_id) + .and(community_follower::person_id.eq(person_id_join)) + .and(community_follower::state.eq(CommunityFollowerState::Accepted)), + ), + )), + ); + } + // Dont filter blocks or missing languages for moderator view type if let (Some(person_id), false) = ( options.local_user.person_id(), @@ -746,6 +786,7 @@ mod tests { Community, CommunityFollower, CommunityFollowerForm, + CommunityFollowerState, CommunityInsertForm, CommunityModerator, CommunityModeratorForm, @@ -937,7 +978,7 @@ mod tests { #[tokio::test] #[serial] async fn post_listing_with_person() -> LemmyResult<()> { - let pool = &build_db_pool().await?; + let pool = &build_db_pool()?; let pool = &mut pool.into(); let mut data = init_data(pool).await?; @@ -997,7 +1038,7 @@ mod tests { #[tokio::test] #[serial] async fn post_listing_no_person() -> LemmyResult<()> { - let pool = &build_db_pool().await?; + let pool = &build_db_pool()?; let pool = &mut pool.into(); let data = init_data(pool).await?; @@ -1035,7 +1076,7 @@ mod tests { #[tokio::test] #[serial] async fn post_listing_title_only() -> LemmyResult<()> { - let pool = &build_db_pool().await?; + let pool = &build_db_pool()?; let pool = &mut pool.into(); let data = init_data(pool).await?; @@ -1094,7 +1135,7 @@ mod tests { #[tokio::test] #[serial] async fn post_listing_block_community() -> LemmyResult<()> { - let pool = &build_db_pool().await?; + let pool = &build_db_pool()?; let pool = &mut pool.into(); let data = init_data(pool).await?; @@ -1120,7 +1161,7 @@ mod tests { #[tokio::test] #[serial] async fn post_listing_like() -> LemmyResult<()> { - let pool = &build_db_pool().await?; + let pool = &build_db_pool()?; let pool = &mut pool.into(); let mut data = init_data(pool).await?; @@ -1178,7 +1219,7 @@ mod tests { #[tokio::test] #[serial] async fn post_listing_liked_only() -> LemmyResult<()> { - let pool = &build_db_pool().await?; + let pool = &build_db_pool()?; let pool = &mut pool.into(); let data = init_data(pool).await?; @@ -1227,7 +1268,7 @@ mod tests { #[tokio::test] #[serial] async fn post_listing_saved_only() -> LemmyResult<()> { - let pool = &build_db_pool().await?; + let pool = &build_db_pool()?; let pool = &mut pool.into(); let data = init_data(pool).await?; @@ -1257,7 +1298,7 @@ mod tests { #[tokio::test] #[serial] async fn creator_info() -> LemmyResult<()> { - let pool = &build_db_pool().await?; + let pool = &build_db_pool()?; let pool = &mut pool.into(); let data = init_data(pool).await?; @@ -1295,7 +1336,7 @@ mod tests { async fn post_listing_person_language() -> LemmyResult<()> { const EL_POSTO: &str = "el posto"; - let pool = &build_db_pool().await?; + let pool = &build_db_pool()?; let pool = &mut pool.into(); let data = init_data(pool).await?; @@ -1356,7 +1397,7 @@ mod tests { #[tokio::test] #[serial] async fn post_listings_removed() -> LemmyResult<()> { - let pool = &build_db_pool().await?; + let pool = &build_db_pool()?; let pool = &mut pool.into(); let mut data = init_data(pool).await?; @@ -1391,7 +1432,7 @@ mod tests { #[tokio::test] #[serial] async fn post_listings_deleted() -> LemmyResult<()> { - let pool = &build_db_pool().await?; + let pool = &build_db_pool()?; let pool = &mut pool.into(); let data = init_data(pool).await?; @@ -1430,7 +1471,7 @@ mod tests { #[tokio::test] #[serial] async fn post_listings_hidden_community() -> LemmyResult<()> { - let pool = &build_db_pool().await?; + let pool = &build_db_pool()?; let pool = &mut pool.into(); let data = init_data(pool).await?; @@ -1452,9 +1493,8 @@ mod tests { // Follow the community let form = CommunityFollowerForm { - community_id: data.inserted_community.id, - person_id: data.local_user_view.person.id, - pending: false, + state: Some(CommunityFollowerState::Accepted), + ..CommunityFollowerForm::new(data.inserted_community.id, data.local_user_view.person.id) }; CommunityFollower::follow(pool, &form).await?; @@ -1469,7 +1509,7 @@ mod tests { async fn post_listing_instance_block() -> LemmyResult<()> { const POST_FROM_BLOCKED_INSTANCE: &str = "post on blocked instance"; - let pool = &build_db_pool().await?; + let pool = &build_db_pool()?; let pool = &mut pool.into(); let data = init_data(pool).await?; @@ -1529,7 +1569,7 @@ mod tests { #[tokio::test] #[serial] async fn pagination_includes_each_post_once() -> LemmyResult<()> { - let pool = &build_db_pool().await?; + let pool = &build_db_pool()?; let pool = &mut pool.into(); let data = init_data(pool).await?; @@ -1639,7 +1679,7 @@ mod tests { #[tokio::test] #[serial] async fn post_listings_hide_read() -> LemmyResult<()> { - let pool = &build_db_pool().await?; + let pool = &build_db_pool()?; let pool = &mut pool.into(); let mut data = init_data(pool).await?; @@ -1689,7 +1729,7 @@ mod tests { #[tokio::test] #[serial] async fn post_listings_hide_hidden() -> LemmyResult<()> { - let pool = &build_db_pool().await?; + let pool = &build_db_pool()?; let pool = &mut pool.into(); let data = init_data(pool).await?; @@ -1725,7 +1765,7 @@ mod tests { #[tokio::test] #[serial] async fn post_listings_hide_nsfw() -> LemmyResult<()> { - let pool = &build_db_pool().await?; + let pool = &build_db_pool()?; let pool = &mut pool.into(); let data = init_data(pool).await?; @@ -1891,7 +1931,7 @@ mod tests { #[tokio::test] #[serial] async fn local_only_instance() -> LemmyResult<()> { - let pool = &build_db_pool_for_tests().await; + let pool = &build_db_pool_for_tests(); let pool = &mut pool.into(); let data = init_data(pool).await?; @@ -1939,7 +1979,7 @@ mod tests { #[tokio::test] #[serial] async fn post_listing_local_user_banned_from_community() -> LemmyResult<()> { - let pool = &build_db_pool().await?; + let pool = &build_db_pool()?; let pool = &mut pool.into(); let data = init_data(pool).await?; @@ -1982,7 +2022,7 @@ mod tests { #[tokio::test] #[serial] async fn post_listing_local_user_not_banned_from_community() -> LemmyResult<()> { - let pool = &build_db_pool().await?; + let pool = &build_db_pool()?; let pool = &mut pool.into(); let data = init_data(pool).await?; @@ -2002,7 +2042,7 @@ mod tests { #[tokio::test] #[serial] async fn speed_check() -> LemmyResult<()> { - let pool = &build_db_pool().await?; + let pool = &build_db_pool()?; let pool = &mut pool.into(); let data = init_data(pool).await?; @@ -2058,7 +2098,7 @@ mod tests { #[tokio::test] #[serial] async fn post_listings_no_comments_only() -> LemmyResult<()> { - let pool = &build_db_pool().await?; + let pool = &build_db_pool()?; let pool = &mut pool.into(); let data = init_data(pool).await?; @@ -2084,4 +2124,101 @@ mod tests { cleanup(data, pool).await } + + #[tokio::test] + #[serial] + async fn post_listing_private_community() -> LemmyResult<()> { + let pool = &build_db_pool()?; + let pool = &mut pool.into(); + let mut data = init_data(pool).await?; + + // Mark community as private + Community::update( + pool, + data.inserted_community.id, + &CommunityUpdateForm { + visibility: Some(CommunityVisibility::Private), + ..Default::default() + }, + ) + .await?; + + // No posts returned without auth + let read_post_listing = PostQuery { + community_id: Some(data.inserted_community.id), + ..Default::default() + } + .list(&data.site, pool) + .await?; + assert_eq!(0, read_post_listing.len()); + let post_view = PostView::read(pool, data.inserted_post.id, None, false).await; + assert!(post_view.is_err()); + + // No posts returned for non-follower who is not admin + data.local_user_view.local_user.admin = false; + let read_post_listing = PostQuery { + community_id: Some(data.inserted_community.id), + local_user: Some(&data.local_user_view.local_user), + ..Default::default() + } + .list(&data.site, pool) + .await?; + assert_eq!(0, read_post_listing.len()); + let post_view = PostView::read( + pool, + data.inserted_post.id, + Some(&data.local_user_view.local_user), + false, + ) + .await; + assert!(post_view.is_err()); + + // Admin can view content without following + data.local_user_view.local_user.admin = true; + let read_post_listing = PostQuery { + community_id: Some(data.inserted_community.id), + local_user: Some(&data.local_user_view.local_user), + ..Default::default() + } + .list(&data.site, pool) + .await?; + assert_eq!(2, read_post_listing.len()); + let post_view = PostView::read( + pool, + data.inserted_post.id, + Some(&data.local_user_view.local_user), + true, + ) + .await; + assert!(post_view.is_ok()); + data.local_user_view.local_user.admin = false; + + // User can view after following + CommunityFollower::follow( + pool, + &CommunityFollowerForm { + state: Some(CommunityFollowerState::Accepted), + ..CommunityFollowerForm::new(data.inserted_community.id, data.local_user_view.person.id) + }, + ) + .await?; + let read_post_listing = PostQuery { + community_id: Some(data.inserted_community.id), + local_user: Some(&data.local_user_view.local_user), + ..Default::default() + } + .list(&data.site, pool) + .await?; + assert_eq!(2, read_post_listing.len()); + let post_view = PostView::read( + pool, + data.inserted_post.id, + Some(&data.local_user_view.local_user), + true, + ) + .await; + assert!(post_view.is_ok()); + + cleanup(data, pool).await + } } diff --git a/crates/db_views/src/private_message_report_view.rs b/crates/db_views/src/private_message_report_view.rs index 56d0d6e7b..e59d99608 100644 --- a/crates/db_views/src/private_message_report_view.rs +++ b/crates/db_views/src/private_message_report_view.rs @@ -133,7 +133,7 @@ mod tests { #[tokio::test] #[serial] async fn test_crud() -> LemmyResult<()> { - let pool = &build_db_pool_for_tests().await; + let pool = &build_db_pool_for_tests(); let pool = &mut pool.into(); let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?; diff --git a/crates/db_views/src/private_message_view.rs b/crates/db_views/src/private_message_view.rs index 0fbc0ee16..0b9a2708a 100644 --- a/crates/db_views/src/private_message_view.rs +++ b/crates/db_views/src/private_message_view.rs @@ -251,7 +251,7 @@ mod tests { #[tokio::test] #[serial] async fn read_private_messages() -> LemmyResult<()> { - let pool = &build_db_pool_for_tests().await; + let pool = &build_db_pool_for_tests(); let pool = &mut pool.into(); let Data { timmy, @@ -322,7 +322,7 @@ mod tests { #[tokio::test] #[serial] async fn ensure_person_block() -> LemmyResult<()> { - let pool = &build_db_pool_for_tests().await; + let pool = &build_db_pool_for_tests(); let pool = &mut pool.into(); let Data { timmy, @@ -365,7 +365,7 @@ mod tests { #[tokio::test] #[serial] async fn ensure_instance_block() -> LemmyResult<()> { - let pool = &build_db_pool_for_tests().await; + let pool = &build_db_pool_for_tests(); let pool = &mut pool.into(); let Data { timmy, diff --git a/crates/db_views/src/registration_application_view.rs b/crates/db_views/src/registration_application_view.rs index a0a40789b..e0a1ea953 100644 --- a/crates/db_views/src/registration_application_view.rs +++ b/crates/db_views/src/registration_application_view.rs @@ -162,7 +162,7 @@ mod tests { #[tokio::test] #[serial] async fn test_crud() -> LemmyResult<()> { - let pool = &build_db_pool_for_tests().await; + let pool = &build_db_pool_for_tests(); let pool = &mut pool.into(); let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?; diff --git a/crates/db_views/src/vote_view.rs b/crates/db_views/src/vote_view.rs index 0fd64deca..79ba7f72a 100644 --- a/crates/db_views/src/vote_view.rs +++ b/crates/db_views/src/vote_view.rs @@ -105,7 +105,7 @@ mod tests { #[tokio::test] #[serial] async fn post_and_comment_vote_views() -> LemmyResult<()> { - let pool = &build_db_pool_for_tests().await; + let pool = &build_db_pool_for_tests(); let pool = &mut pool.into(); let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?; diff --git a/crates/db_views_actor/Cargo.toml b/crates/db_views_actor/Cargo.toml index d623959d5..18a79826b 100644 --- a/crates/db_views_actor/Cargo.toml +++ b/crates/db_views_actor/Cargo.toml @@ -46,5 +46,4 @@ serial_test = { workspace = true } tokio = { workspace = true } pretty_assertions = { workspace = true } url.workspace = true -lemmy_db_views.workspace = true -lemmy_utils.workspace = true +lemmy_db_views = { workspace = true, features = ["full"] } diff --git a/crates/db_views_actor/src/comment_reply_view.rs b/crates/db_views_actor/src/comment_reply_view.rs index 1b657866a..8694298e0 100644 --- a/crates/db_views_actor/src/comment_reply_view.rs +++ b/crates/db_views_actor/src/comment_reply_view.rs @@ -31,7 +31,10 @@ use lemmy_db_schema::{ person_block, post, }, - source::local_user::LocalUser, + source::{ + community::{CommunityFollower, CommunityFollowerState}, + local_user::LocalUser, + }, utils::{get_conn, limit_and_offset, DbConn, DbPool, ListFn, Queries, ReadFn}, CommentSortType, }; @@ -75,7 +78,7 @@ fn queries<'a>() -> Queries< .eq(community_follower::community_id) .and(community_follower::person_id.eq(person_id)), ) - .select(community_follower::pending.nullable()) + .select(CommunityFollower::select_subscribed_type()) .single_value() }; @@ -135,11 +138,15 @@ fn queries<'a>() -> Queries< }; let subscribed_type_selection: Box< - dyn BoxableExpression<_, Pg, SqlType = sql_types::Nullable>, + dyn BoxableExpression< + _, + Pg, + SqlType = sql_types::Nullable, + >, > = if let Some(person_id) = my_person_id { Box::new(is_community_followed(person_id)) } else { - Box::new(None::.into_sql::>()) + Box::new(None::.into_sql::>()) }; let is_saved_selection: Box> = @@ -328,7 +335,7 @@ mod tests { #[tokio::test] #[serial] async fn test_crud() -> LemmyResult<()> { - let pool = &build_db_pool_for_tests().await; + let pool = &build_db_pool_for_tests(); let pool = &mut pool.into(); let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?; diff --git a/crates/db_views_actor/src/community_follower_view.rs b/crates/db_views_actor/src/community_follower_view.rs index 92889d12d..f9413a078 100644 --- a/crates/db_views_actor/src/community_follower_view.rs +++ b/crates/db_views_actor/src/community_follower_view.rs @@ -1,17 +1,27 @@ -use crate::structs::CommunityFollowerView; +use crate::structs::{CommunityFollowerView, PendingFollow}; use chrono::Utc; use diesel::{ - dsl::{count_star, not}, + dsl::{count, count_star, exists, not}, result::Error, + select, + BoolExpressionMethods, ExpressionMethods, + JoinOnDsl, QueryDsl, }; use diesel_async::RunQueryDsl; use lemmy_db_schema::{ newtypes::{CommunityId, DbUrl, InstanceId, PersonId}, - schema::{community, community_follower, person}, - utils::{get_conn, DbPool}, + schema::{community, community_follower, community_moderator, person}, + source::{ + community::{Community, CommunityFollower, CommunityFollowerState}, + person::Person, + }, + utils::{get_conn, limit_and_offset, DbPool}, + CommunityVisibility, + SubscribedType, }; +use lemmy_utils::error::{LemmyErrorType, LemmyResult}; impl CommunityFollowerView { /// return a list of local community ids and remote inboxes that at least one user of the given @@ -31,7 +41,7 @@ impl CommunityFollowerView { community_follower::table .inner_join(community::table) - .inner_join(person::table) + .inner_join(person::table.on(community_follower::person_id.eq(person::id))) .filter(person::instance_id.eq(instance_id)) .filter(community::local) // this should be a no-op since community_followers table only has // local-person+remote-community or remote-person+local-community @@ -50,7 +60,7 @@ impl CommunityFollowerView { let res = community_follower::table .filter(community_follower::community_id.eq(community_id)) .filter(not(person::local)) - .inner_join(person::table) + .inner_join(person::table.on(community_follower::person_id.eq(person::id))) .select(person::inbox_url) .distinct() .load::(conn) @@ -76,7 +86,7 @@ impl CommunityFollowerView { let conn = &mut get_conn(pool).await?; community_follower::table .inner_join(community::table) - .inner_join(person::table) + .inner_join(person::table.on(community_follower::person_id.eq(person::id))) .select((community::all_columns, person::all_columns)) .filter(community_follower::person_id.eq(person_id)) .filter(community::deleted.eq(false)) @@ -85,4 +95,223 @@ impl CommunityFollowerView { .load::(conn) .await } + + pub async fn list_approval_required( + pool: &mut DbPool<'_>, + person_id: PersonId, + // TODO: if this is true dont check for community mod, but only check for local community + // also need to check is_admin() + all_communities: bool, + pending_only: bool, + page: Option, + limit: Option, + ) -> Result, Error> { + let conn = &mut get_conn(pool).await?; + let (limit, offset) = limit_and_offset(page, limit)?; + let (person_alias, community_follower_alias) = diesel::alias!( + person as person_alias, + community_follower as community_follower_alias + ); + + // check if the community already has an accepted follower from the same instance + let is_new_instance = not(exists( + person_alias + .inner_join( + community_follower_alias.on( + person_alias + .field(person::id) + .eq(community_follower_alias.field(community_follower::person_id)), + ), + ) + .filter( + person::instance_id + .eq(person_alias.field(person::instance_id)) + .and( + community_follower_alias + .field(community_follower::community_id) + .eq(community_follower::community_id), + ) + .and( + community_follower_alias + .field(community_follower::state) + .eq(CommunityFollowerState::Accepted), + ), + ), + )); + + let mut query = community_follower::table + .inner_join(person::table.on(community_follower::person_id.eq(person::id))) + .inner_join(community::table) + .into_boxed(); + if all_communities { + // if param is false, only return items for communities where user is a mod + query = query.filter(exists( + community_moderator::table.filter( + community_follower::community_id + .eq(community_moderator::community_id) + .and(community_moderator::person_id.eq(person_id)), + ), + )); + } + if pending_only { + query = query.filter(community_follower::state.eq(CommunityFollowerState::ApprovalRequired)); + } + let res = query + .order_by(community_follower::published.asc()) + .limit(limit) + .offset(offset) + .select(( + person::all_columns, + community::all_columns, + is_new_instance, + CommunityFollower::select_subscribed_type(), + )) + .load::<(Person, Community, bool, SubscribedType)>(conn) + .await?; + Ok( + res + .into_iter() + .map( + |(person, community, is_new_instance, subscribed)| PendingFollow { + person, + community, + is_new_instance, + subscribed, + }, + ) + .collect(), + ) + } + + pub async fn count_approval_required( + pool: &mut DbPool<'_>, + community_id: CommunityId, + ) -> Result { + let conn = &mut get_conn(pool).await?; + community_follower::table + .inner_join(person::table.on(community_follower::person_id.eq(person::id))) + .filter(community_follower::community_id.eq(community_id)) + .filter(community_follower::state.eq(CommunityFollowerState::ApprovalRequired)) + .select(count(community_follower::community_id)) + .first::(conn) + .await + } + pub async fn check_private_community_action( + pool: &mut DbPool<'_>, + from_person_id: PersonId, + community: &Community, + ) -> LemmyResult<()> { + if community.visibility != CommunityVisibility::Private { + return Ok(()); + } + let conn = &mut get_conn(pool).await?; + select(exists( + community_follower::table + .filter(community_follower::community_id.eq(community.id)) + .filter(community_follower::person_id.eq(from_person_id)) + .filter(community_follower::state.eq(CommunityFollowerState::Accepted)), + )) + .get_result::(conn) + .await? + .then_some(()) + .ok_or(LemmyErrorType::NotFound.into()) + } + pub async fn check_has_followers_from_instance( + community_id: CommunityId, + instance_id: InstanceId, + pool: &mut DbPool<'_>, + ) -> Result<(), Error> { + let conn = &mut get_conn(pool).await?; + select(exists( + community_follower::table + .inner_join(person::table.on(community_follower::person_id.eq(person::id))) + .filter(community_follower::community_id.eq(community_id)) + .filter(person::instance_id.eq(instance_id)) + .filter(community_follower::state.eq(CommunityFollowerState::Accepted)), + )) + .get_result::(conn) + .await? + .then_some(()) + .ok_or(diesel::NotFound) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use lemmy_db_schema::{ + source::{ + community::{CommunityFollower, CommunityFollowerForm, CommunityInsertForm}, + instance::Instance, + person::PersonInsertForm, + }, + traits::{Crud, Followable}, + utils::build_db_pool_for_tests, + }; + use serial_test::serial; + + #[tokio::test] + #[serial] + async fn test_has_followers_from_instance() -> LemmyResult<()> { + let pool = &build_db_pool_for_tests(); + let pool = &mut pool.into(); + + // insert local community + let local_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?; + let community_form = CommunityInsertForm::new( + local_instance.id, + "test_community_3".to_string(), + "nada".to_owned(), + "pubkey".to_string(), + ); + let community = Community::create(pool, &community_form).await?; + + // insert remote user + let remote_instance = Instance::read_or_create(pool, "other_domain.tld".to_string()).await?; + let person_form = + PersonInsertForm::new("name".to_string(), "pubkey".to_string(), remote_instance.id); + let person = Person::create(pool, &person_form).await?; + + // community has no follower from remote instance, returns error + let has_followers = CommunityFollowerView::check_has_followers_from_instance( + community.id, + remote_instance.id, + pool, + ) + .await; + assert!(has_followers.is_err()); + + // insert unapproved follower + let mut follower_form = CommunityFollowerForm { + state: Some(CommunityFollowerState::ApprovalRequired), + ..CommunityFollowerForm::new(community.id, person.id) + }; + CommunityFollower::follow(pool, &follower_form).await?; + + // still returns error + let has_followers = CommunityFollowerView::check_has_followers_from_instance( + community.id, + remote_instance.id, + pool, + ) + .await; + assert!(has_followers.is_err()); + + // mark follower as accepted + follower_form.state = Some(CommunityFollowerState::Accepted); + CommunityFollower::follow(pool, &follower_form).await?; + + // now returns ok + let has_followers = CommunityFollowerView::check_has_followers_from_instance( + community.id, + remote_instance.id, + pool, + ) + .await; + assert!(has_followers.is_ok()); + + Instance::delete(pool, local_instance.id).await?; + Instance::delete(pool, remote_instance.id).await?; + Ok(()) + } } diff --git a/crates/db_views_actor/src/community_view.rs b/crates/db_views_actor/src/community_view.rs index de749fff3..999ec23f0 100644 --- a/crates/db_views_actor/src/community_view.rs +++ b/crates/db_views_actor/src/community_view.rs @@ -21,7 +21,11 @@ use lemmy_db_schema::{ community_person_ban, instance_block, }, - source::{community::CommunityFollower, local_user::LocalUser, site::Site}, + source::{ + community::{CommunityFollower, CommunityFollowerState}, + local_user::LocalUser, + site::Site, + }, utils::{ functions::lower, fuzzy_search, @@ -163,7 +167,9 @@ fn queries<'a>() -> Queries< if let Some(listing_type) = options.listing_type { query = match listing_type { - ListingType::Subscribed => query.filter(community_follower::pending.is_not_null()), /* TODO could be this: and(community_follower::person_id.eq(person_id_join)), */ + ListingType::Subscribed => { + query.filter(community_follower::state.eq(CommunityFollowerState::Accepted)) + } ListingType::Local => query.filter(community::local.eq(true)), _ => query, }; @@ -293,44 +299,52 @@ mod tests { }; use lemmy_db_schema::{ source::{ - community::{Community, CommunityInsertForm, CommunityUpdateForm}, + community::{ + Community, + CommunityFollower, + CommunityFollowerForm, + CommunityFollowerState, + CommunityInsertForm, + CommunityUpdateForm, + }, instance::Instance, local_user::{LocalUser, LocalUserInsertForm}, person::{Person, PersonInsertForm}, site::Site, }, - traits::Crud, + traits::{Crud, Followable}, utils::{build_db_pool_for_tests, DbPool}, CommunityVisibility, + SubscribedType, }; use lemmy_utils::error::LemmyResult; use serial_test::serial; use url::Url; struct Data { - inserted_instance: Instance, + instance: Instance, local_user: LocalUser, - inserted_communities: [Community; 3], + communities: [Community; 3], site: Site, } async fn init_data(pool: &mut DbPool<'_>) -> LemmyResult { - let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?; + let instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?; let person_name = "tegan".to_string(); - let new_person = PersonInsertForm::test_form(inserted_instance.id, &person_name); + let new_person = PersonInsertForm::test_form(instance.id, &person_name); let inserted_person = Person::create(pool, &new_person).await?; let local_user_form = LocalUserInsertForm::test_form(inserted_person.id); let local_user = LocalUser::create(pool, &local_user_form, vec![]).await?; - let inserted_communities = [ + let communities = [ Community::create( pool, &CommunityInsertForm::new( - inserted_instance.id, + instance.id, "test_community_1".to_string(), "nada1".to_owned(), "pubkey".to_string(), @@ -340,7 +354,7 @@ mod tests { Community::create( pool, &CommunityInsertForm::new( - inserted_instance.id, + instance.id, "test_community_2".to_string(), "nada2".to_owned(), "pubkey".to_string(), @@ -350,7 +364,7 @@ mod tests { Community::create( pool, &CommunityInsertForm::new( - inserted_instance.id, + instance.id, "test_community_3".to_string(), "nada3".to_owned(), "pubkey".to_string(), @@ -379,33 +393,93 @@ mod tests { }; Ok(Data { - inserted_instance, + instance, local_user, - inserted_communities, + communities, site, }) } async fn cleanup(data: Data, pool: &mut DbPool<'_>) -> LemmyResult<()> { - for Community { id, .. } in data.inserted_communities { + for Community { id, .. } in data.communities { Community::delete(pool, id).await?; } Person::delete(pool, data.local_user.person_id).await?; - Instance::delete(pool, data.inserted_instance.id).await?; + Instance::delete(pool, data.instance.id).await?; Ok(()) } + #[tokio::test] + #[serial] + async fn subscribe_state() -> LemmyResult<()> { + let pool = &build_db_pool_for_tests(); + let pool = &mut pool.into(); + let data = init_data(pool).await?; + let community = &data.communities[0]; + + let unauthenticated = CommunityView::read(pool, community.id, None, false).await?; + assert_eq!(SubscribedType::NotSubscribed, unauthenticated.subscribed); + + let authenticated = + CommunityView::read(pool, community.id, Some(&data.local_user), false).await?; + assert_eq!(SubscribedType::NotSubscribed, authenticated.subscribed); + + let form = CommunityFollowerForm { + state: Some(CommunityFollowerState::Pending), + ..CommunityFollowerForm::new(community.id, data.local_user.person_id) + }; + CommunityFollower::follow(pool, &form).await?; + + let with_pending_follow = + CommunityView::read(pool, community.id, Some(&data.local_user), false).await?; + assert_eq!(SubscribedType::Pending, with_pending_follow.subscribed); + + // mark community private and set follow as approval required + Community::update( + pool, + community.id, + &CommunityUpdateForm { + visibility: Some(CommunityVisibility::Private), + ..Default::default() + }, + ) + .await?; + let form = CommunityFollowerForm { + state: Some(CommunityFollowerState::ApprovalRequired), + ..CommunityFollowerForm::new(community.id, data.local_user.person_id) + }; + CommunityFollower::follow(pool, &form).await?; + + let with_approval_required_follow = + CommunityView::read(pool, community.id, Some(&data.local_user), false).await?; + assert_eq!( + SubscribedType::ApprovalRequired, + with_approval_required_follow.subscribed + ); + + let form = CommunityFollowerForm { + state: Some(CommunityFollowerState::Accepted), + ..CommunityFollowerForm::new(community.id, data.local_user.person_id) + }; + CommunityFollower::follow(pool, &form).await?; + let with_accepted_follow = + CommunityView::read(pool, community.id, Some(&data.local_user), false).await?; + assert_eq!(SubscribedType::Subscribed, with_accepted_follow.subscribed); + + cleanup(data, pool).await + } + #[tokio::test] #[serial] async fn local_only_community() -> LemmyResult<()> { - let pool = &build_db_pool_for_tests().await; + let pool = &build_db_pool_for_tests(); let pool = &mut pool.into(); let data = init_data(pool).await?; Community::update( pool, - data.inserted_communities[0].id, + data.communities[0].id, &CommunityUpdateForm { visibility: Some(CommunityVisibility::LocalOnly), ..Default::default() @@ -418,10 +492,7 @@ mod tests { } .list(&data.site, pool) .await?; - assert_eq!( - data.inserted_communities.len() - 1, - unauthenticated_query.len() - ); + assert_eq!(data.communities.len() - 1, unauthenticated_query.len()); let authenticated_query = CommunityQuery { local_user: Some(&data.local_user), @@ -429,19 +500,14 @@ mod tests { } .list(&data.site, pool) .await?; - assert_eq!(data.inserted_communities.len(), authenticated_query.len()); + assert_eq!(data.communities.len(), authenticated_query.len()); let unauthenticated_community = - CommunityView::read(pool, data.inserted_communities[0].id, None, false).await; + CommunityView::read(pool, data.communities[0].id, None, false).await; assert!(unauthenticated_community.is_err()); - let authenticated_community = CommunityView::read( - pool, - data.inserted_communities[0].id, - Some(&data.local_user), - false, - ) - .await; + let authenticated_community = + CommunityView::read(pool, data.communities[0].id, Some(&data.local_user), false).await; assert!(authenticated_community.is_ok()); cleanup(data, pool).await @@ -450,7 +516,7 @@ mod tests { #[tokio::test] #[serial] async fn community_sort_name() -> LemmyResult<()> { - let pool = &build_db_pool_for_tests().await; + let pool = &build_db_pool_for_tests(); let pool = &mut pool.into(); let data = init_data(pool).await?; diff --git a/crates/db_views_actor/src/person_mention_view.rs b/crates/db_views_actor/src/person_mention_view.rs index 2478c0183..2bc701805 100644 --- a/crates/db_views_actor/src/person_mention_view.rs +++ b/crates/db_views_actor/src/person_mention_view.rs @@ -31,7 +31,10 @@ use lemmy_db_schema::{ person_mention, post, }, - source::local_user::LocalUser, + source::{ + community::{CommunityFollower, CommunityFollowerState}, + local_user::LocalUser, + }, utils::{get_conn, limit_and_offset, DbConn, DbPool, ListFn, Queries, ReadFn}, CommentSortType, }; @@ -75,7 +78,7 @@ fn queries<'a>() -> Queries< .eq(community_follower::community_id) .and(community_follower::person_id.eq(person_id)), ) - .select(community_follower::pending.nullable()) + .select(CommunityFollower::select_subscribed_type()) .single_value() }; @@ -134,11 +137,15 @@ fn queries<'a>() -> Queries< }; let subscribed_type_selection: Box< - dyn BoxableExpression<_, Pg, SqlType = sql_types::Nullable>, + dyn BoxableExpression< + _, + Pg, + SqlType = sql_types::Nullable, + >, > = if let Some(person_id) = my_person_id { Box::new(is_community_followed(person_id)) } else { - Box::new(None::.into_sql::>()) + Box::new(None::.into_sql::>()) }; let is_saved_selection: Box> = @@ -328,7 +335,7 @@ mod tests { #[tokio::test] #[serial] async fn test_crud() -> LemmyResult<()> { - let pool = &build_db_pool_for_tests().await; + let pool = &build_db_pool_for_tests(); let pool = &mut pool.into(); let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?; diff --git a/crates/db_views_actor/src/person_view.rs b/crates/db_views_actor/src/person_view.rs index 724a700ad..39d1ac27c 100644 --- a/crates/db_views_actor/src/person_view.rs +++ b/crates/db_views_actor/src/person_view.rs @@ -229,7 +229,7 @@ mod tests { #[tokio::test] #[serial] async fn exclude_deleted() -> LemmyResult<()> { - let pool = &build_db_pool_for_tests().await; + let pool = &build_db_pool_for_tests(); let pool = &mut pool.into(); let data = init_data(pool).await?; @@ -261,7 +261,7 @@ mod tests { #[tokio::test] #[serial] async fn list_banned() -> LemmyResult<()> { - let pool = &build_db_pool_for_tests().await; + let pool = &build_db_pool_for_tests(); let pool = &mut pool.into(); let data = init_data(pool).await?; @@ -285,7 +285,7 @@ mod tests { #[tokio::test] #[serial] async fn list_admins() -> LemmyResult<()> { - let pool = &build_db_pool_for_tests().await; + let pool = &build_db_pool_for_tests(); let pool = &mut pool.into(); let data = init_data(pool).await?; @@ -315,7 +315,7 @@ mod tests { #[tokio::test] #[serial] async fn listing_type() -> LemmyResult<()> { - let pool = &build_db_pool_for_tests().await; + let pool = &build_db_pool_for_tests(); let pool = &mut pool.into(); let data = init_data(pool).await?; diff --git a/crates/db_views_actor/src/structs.rs b/crates/db_views_actor/src/structs.rs index db5cb1899..6b609a753 100644 --- a/crates/db_views_actor/src/structs.rs +++ b/crates/db_views_actor/src/structs.rs @@ -148,3 +148,14 @@ pub struct PersonView { pub counts: PersonAggregates, pub is_admin: bool, } + +#[derive(Debug, Serialize, Deserialize, Clone)] +#[cfg_attr(feature = "full", derive(TS, Queryable))] +#[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] +#[cfg_attr(feature = "full", ts(export))] +pub struct PendingFollow { + pub person: Person, + pub community: Community, + pub is_new_instance: bool, + pub subscribed: SubscribedType, +} diff --git a/crates/utils/src/error.rs b/crates/utils/src/error.rs index 75cecc41f..d52df2f72 100644 --- a/crates/utils/src/error.rs +++ b/crates/utils/src/error.rs @@ -184,6 +184,7 @@ pub enum FederationError { InboxTimeout, CantDeleteSite, ObjectIsNotPublic, + ObjectIsNotPrivate, } cfg_if! { diff --git a/diesel.toml b/diesel.toml index ce23d470e..0da2434d4 100644 --- a/diesel.toml +++ b/diesel.toml @@ -1,3 +1,5 @@ [print_schema] file = "crates/db_schema/src/schema.rs" patch_file = "crates/db_schema/src/diesel_ltree.patch" +# Required for https://github.com/adwhit/diesel-derive-enum +custom_type_derives = ["diesel::query_builder::QueryId"] diff --git a/migrations/2024-10-29-090055_private-community/down.sql b/migrations/2024-10-29-090055_private-community/down.sql new file mode 100644 index 000000000..3a5c516c2 --- /dev/null +++ b/migrations/2024-10-29-090055_private-community/down.sql @@ -0,0 +1,52 @@ +-- Remove private visibility +ALTER TYPE community_visibility RENAME TO community_visibility__; + +CREATE TYPE community_visibility AS enum ( + 'Public', + 'LocalOnly' +); + +ALTER TABLE community + ALTER COLUMN visibility DROP DEFAULT; + +ALTER TABLE community + ALTER COLUMN visibility TYPE community_visibility + USING visibility::text::community_visibility; + +ALTER TABLE community + ALTER COLUMN visibility SET DEFAULT 'Public'; + +DROP TYPE community_visibility__; + +-- Revert community follower changes +CREATE OR REPLACE FUNCTION convert_follower_state (s community_follower_state) + RETURNS bool + LANGUAGE sql + AS $$ + SELECT + CASE WHEN s = 'Pending' THEN + TRUE + ELSE + FALSE + END +$$; + +ALTER TABLE community_follower + ALTER COLUMN state TYPE bool + USING convert_follower_state (state); + +DROP FUNCTION convert_follower_state; + +ALTER TABLE community_follower + ALTER COLUMN state SET DEFAULT FALSE; + +ALTER TABLE community_follower RENAME COLUMN state TO pending; + +DROP TYPE community_follower_state; + +ALTER TABLE community_follower + DROP COLUMN approver_id; + +ALTER TABLE ONLY local_site + ALTER COLUMN federation_signed_fetch SET DEFAULT FALSE; + diff --git a/migrations/2024-10-29-090055_private-community/up.sql b/migrations/2024-10-29-090055_private-community/up.sql new file mode 100644 index 000000000..d1c0585ae --- /dev/null +++ b/migrations/2024-10-29-090055_private-community/up.sql @@ -0,0 +1,47 @@ +ALTER TYPE community_visibility + ADD value 'Private'; + +-- Change `community_follower.pending` to `state` enum +CREATE TYPE community_follower_state AS enum ( + 'Accepted', + 'Pending', + 'ApprovalRequired' +); + +ALTER TABLE community_follower + ALTER COLUMN pending DROP DEFAULT; + +CREATE OR REPLACE FUNCTION convert_follower_state (b bool) + RETURNS community_follower_state + LANGUAGE sql + AS $$ + SELECT + CASE WHEN b = TRUE THEN + 'Pending'::community_follower_state + ELSE + 'Accepted'::community_follower_state + END +$$; + +ALTER TABLE community_follower + ALTER COLUMN pending TYPE community_follower_state + USING convert_follower_state (pending); + +DROP FUNCTION convert_follower_state; + +ALTER TABLE community_follower RENAME COLUMN pending TO state; + +-- Add column for mod who approved the private community follower +-- Dont use foreign key here, otherwise joining to person table doesnt work easily +ALTER TABLE community_follower + ADD COLUMN approver_id int REFERENCES person ON UPDATE CASCADE ON DELETE CASCADE; + +-- Enable signed fetch, necessary to fetch content in private communities +ALTER TABLE ONLY local_site + ALTER COLUMN federation_signed_fetch SET DEFAULT TRUE; + +UPDATE + local_site +SET + federation_signed_fetch = TRUE; + diff --git a/scripts/test.sh b/scripts/test.sh index e08148db0..d3e95886f 100755 --- a/scripts/test.sh +++ b/scripts/test.sh @@ -20,9 +20,7 @@ if [ -n "$PACKAGE" ]; then cargo test -p $PACKAGE --all-features --no-fail-fast $TEST else - cargo test --workspace --no-fail-fast - # Testing lemmy utils all features in particular (for ts-rs bindings) - cargo test -p lemmy_utils --all-features --no-fail-fast + cargo test --workspace --all-features --no-fail-fast fi # Add this to do printlns: -- --nocapture diff --git a/src/api_routes_http.rs b/src/api_routes_http.rs index df1aebf84..fd65e0671 100644 --- a/src/api_routes_http.rs +++ b/src/api_routes_http.rs @@ -17,6 +17,11 @@ use lemmy_api::{ block::block_community, follow::follow_community, hide::hide_community, + pending_follows::{ + approve::post_pending_follows_approve, + count::get_pending_follows_count, + list::get_pending_follows_list, + }, random::get_random_community, transfer::transfer_community, }, @@ -204,7 +209,14 @@ pub fn config(cfg: &mut web::ServiceConfig, rate_limit: &RateLimitCell) { .route("/remove", web::post().to(remove_community)) .route("/transfer", web::post().to(transfer_community)) .route("/ban_user", web::post().to(ban_from_community)) - .route("/mod", web::post().to(add_mod_to_community)), + .route("/mod", web::post().to(add_mod_to_community)) + .service( + web::scope("/pending_follows") + .wrap(rate_limit.message()) + .route("/count", web::get().to(get_pending_follows_count)) + .route("/list", web::get().to(get_pending_follows_list)) + .route("/approve", web::post().to(post_pending_follows_approve)), + ), ) .service( web::scope("/federated_instances") diff --git a/src/lib.rs b/src/lib.rs index 4aee4be69..9da09f65b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -116,7 +116,7 @@ pub async fn start_lemmy_server(args: CmdArgs) -> LemmyResult<()> { } // Set up the connection pool - let pool = build_db_pool().await?; + let pool = build_db_pool()?; // Run the Code-required migrations run_advanced_migrations(&mut (&pool).into(), &SETTINGS).await?; diff --git a/src/prometheus_metrics.rs b/src/prometheus_metrics.rs index c4ab204e7..512d63f38 100644 --- a/src/prometheus_metrics.rs +++ b/src/prometheus_metrics.rs @@ -47,7 +47,7 @@ pub fn serve_prometheus(config: PrometheusConfig, lemmy_context: LemmyContext) - // handler for the /metrics path async fn metrics(context: web::Data>) -> LemmyResult { // collect metrics - collect_db_pool_metrics(&context).await; + collect_db_pool_metrics(&context); let mut buffer = Vec::new(); let encoder = TextEncoder::new(); @@ -84,7 +84,7 @@ fn create_db_pool_metrics() -> LemmyResult { Ok(metrics) } -async fn collect_db_pool_metrics(context: &PromContext) { +fn collect_db_pool_metrics(context: &PromContext) { let pool_status = context.lemmy.inner_pool().status(); context .db_pool_metrics diff --git a/src/scheduled_tasks.rs b/src/scheduled_tasks.rs index e7c8a676c..37f2ff809 100644 --- a/src/scheduled_tasks.rs +++ b/src/scheduled_tasks.rs @@ -496,7 +496,6 @@ async fn publish_scheduled_posts(context: &Data) { // send out post via federation and webmention let send_activity = SendActivityData::CreatePost(post.clone()); ActivityChannel::submit_activity(send_activity, context) - .await .inspect_err(|e| error!("Failed federate scheduled post: {e}")) .ok(); send_webmention(post, community); diff --git a/src/session_middleware.rs b/src/session_middleware.rs index ec8f4399c..b495bdbb9 100644 --- a/src/session_middleware.rs +++ b/src/session_middleware.rs @@ -125,7 +125,7 @@ mod tests { // hack, necessary so that config file can be loaded from hardcoded, relative path set_current_dir("crates/utils")?; - let pool_ = build_db_pool_for_tests().await; + let pool_ = build_db_pool_for_tests(); let pool = &mut (&pool_).into(); let secret = Secret::init(pool).await?;