diff --git a/.woodpecker.yml b/.woodpecker.yml index 82da8ebac..1f8f45186 100644 --- a/.woodpecker.yml +++ b/.woodpecker.yml @@ -2,6 +2,10 @@ # See https://github.com/woodpecker-ci/woodpecker/issues/1677 variables: + # When updating the rust version here, be sure to update versions in `docker/Dockerfile` + # as well. Otherwise release builds can fail if Lemmy or dependencies rely on new Rust + # features. In particular the ARM builder image needs to be updated manually in the repo below: + # https://github.com/raskyld/lemmy-cross-toolchains - &rust_image "rust:1.81" - &rust_nightly_image "rustlang/rust:nightly" - &install_pnpm "corepack enable pnpm" @@ -268,13 +272,15 @@ steps: # using https://github.com/pksunkara/cargo-workspaces publish_to_crates_io: image: *rust_image + environment: + CARGO_API_TOKEN: + from_secret: cargo_api_token commands: - *install_binstall # Install cargo-workspaces - cargo binstall -y cargo-workspaces - cp -r migrations crates/db_schema/ - cargo workspaces publish --token "$CARGO_API_TOKEN" --from-git --allow-dirty --no-verify --allow-branch "${CI_COMMIT_TAG}" --yes custom "${CI_COMMIT_TAG}" - secrets: [cargo_api_token] when: - event: tag diff --git a/Cargo.lock b/Cargo.lock index f9a32d3de..cba93d7cd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -779,6 +779,17 @@ dependencies = [ "nom", ] +[[package]] +name = "cfb" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38f2da7a0a2c4ccf0065be06397cc26a81f4e528be095826eee9d4adbb8c60f" +dependencies = [ + "byteorder", + "fnv", + "uuid", +] + [[package]] name = "cfg-if" version = "1.0.0" @@ -1239,9 +1250,9 @@ dependencies = [ [[package]] name = "diesel" -version = "2.2.4" +version = "2.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "158fe8e2e68695bd615d7e4f3227c0727b151330d3e253b525086c348d055d5e" +checksum = "cbf9649c05e0a9dbd6d0b0b8301db5182b972d0fd02f0a7c6736cf632d7c0fd5" dependencies = [ "bitflags 2.6.0", "byteorder", @@ -1255,9 +1266,9 @@ dependencies = [ [[package]] name = "diesel-async" -version = "0.5.1" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c5c6ec8d5c7b8444d19a47161797cbe361e0fb1ee40c6a8124ec915b64a4125" +checksum = "51a307ac00f7c23f526a04a77761a0519b9f0eb2838ebf5b905a58580095bdcb" dependencies = [ "async-trait", "deadpool", @@ -2360,6 +2371,15 @@ dependencies = [ "serde", ] +[[package]] +name = "infer" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc150e5ce2330295b8616ce0e3f53250e53af31759a9dbedad1621ba29151847" +dependencies = [ + "cfb", +] + [[package]] name = "inout" version = "0.1.3" @@ -2520,6 +2540,7 @@ dependencies = [ "encoding_rs", "enum-map", "futures", + "infer", "jsonwebtoken", "lemmy_db_schema", "lemmy_db_views", @@ -2562,6 +2583,7 @@ dependencies = [ "lemmy_db_views", "lemmy_db_views_actor", "lemmy_utils", + "regex", "serde", "serde_json", "serde_with", @@ -2677,8 +2699,10 @@ dependencies = [ "lemmy_utils", "pretty_assertions", "serde", + "serde_json", "serde_with", "serial_test", + "test-context", "tokio", "tracing", "ts-rs", diff --git a/api_tests/package.json b/api_tests/package.json index 57173595a..7ea21d0ba 100644 --- a/api_tests/package.json +++ b/api_tests/package.json @@ -28,7 +28,7 @@ "eslint": "^9.14.0", "eslint-plugin-prettier": "^5.1.3", "jest": "^29.5.0", - "lemmy-js-client": "0.20.0-alpha.18", + "lemmy-js-client": "0.20.0-api-v4.16", "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 01d4a8e74..496606e6c 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.9.0) lemmy-js-client: - specifier: 0.20.0-alpha.18 - version: 0.20.0-alpha.18 + specifier: 0.20.0-api-v4.16 + version: 0.20.0-api-v4.16 prettier: specifier: ^3.2.5 version: 3.3.3 @@ -1167,8 +1167,8 @@ packages: resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} engines: {node: '>=6'} - lemmy-js-client@0.20.0-alpha.18: - resolution: {integrity: sha512-oZy8DboTWfUar4mPWpi7SYrOEjTBJxkvd1e6QaVwoA5UhqQV1WhxEYbzrpi/gXnEokaVQ0i5sjtL/Y2PHMO3MQ==} + lemmy-js-client@0.20.0-api-v4.16: + resolution: {integrity: sha512-9Wn7b8YT2KnEA286+RV1B3mLmecAynvAERoC0ZZiccfSgkEvd3rG9A5X9ejiPqp+JzDZJeisO57+Ut4QHr5oTw==} leven@3.1.0: resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==} @@ -3077,7 +3077,7 @@ snapshots: kleur@3.0.3: {} - lemmy-js-client@0.20.0-alpha.18: {} + lemmy-js-client@0.20.0-api-v4.16: {} leven@3.1.0: {} diff --git a/api_tests/prepare-drone-federation-test.sh b/api_tests/prepare-drone-federation-test.sh index 65c4827d9..e5a4bc604 100755 --- a/api_tests/prepare-drone-federation-test.sh +++ b/api_tests/prepare-drone-federation-test.sh @@ -82,13 +82,13 @@ LEMMY_CONFIG_LOCATION=./docker/federation/lemmy_epsilon.hjson \ target/lemmy_server >$LOG_DIR/lemmy_epsilon.out 2>&1 & echo "wait for all instances to start" -while [[ "$(curl -s -o /dev/null -w '%{http_code}' 'lemmy-alpha:8541/api/v3/site')" != "200" ]]; do sleep 1; done +while [[ "$(curl -s -o /dev/null -w '%{http_code}' 'lemmy-alpha:8541/api/v4/site')" != "200" ]]; do sleep 1; done echo "alpha started" -while [[ "$(curl -s -o /dev/null -w '%{http_code}' 'lemmy-beta:8551/api/v3/site')" != "200" ]]; do sleep 1; done +while [[ "$(curl -s -o /dev/null -w '%{http_code}' 'lemmy-beta:8551/api/v4/site')" != "200" ]]; do sleep 1; done echo "beta started" -while [[ "$(curl -s -o /dev/null -w '%{http_code}' 'lemmy-gamma:8561/api/v3/site')" != "200" ]]; do sleep 1; done +while [[ "$(curl -s -o /dev/null -w '%{http_code}' 'lemmy-gamma:8561/api/v4/site')" != "200" ]]; do sleep 1; done echo "gamma started" -while [[ "$(curl -s -o /dev/null -w '%{http_code}' 'lemmy-delta:8571/api/v3/site')" != "200" ]]; do sleep 1; done +while [[ "$(curl -s -o /dev/null -w '%{http_code}' 'lemmy-delta:8571/api/v4/site')" != "200" ]]; do sleep 1; done echo "delta started" -while [[ "$(curl -s -o /dev/null -w '%{http_code}' 'lemmy-epsilon:8581/api/v3/site')" != "200" ]]; do sleep 1; done +while [[ "$(curl -s -o /dev/null -w '%{http_code}' 'lemmy-epsilon:8581/api/v4/site')" != "200" ]]; do sleep 1; done echo "epsilon started. All started" diff --git a/api_tests/src/comment.spec.ts b/api_tests/src/comment.spec.ts index c3f4b3efe..5cf94aa03 100644 --- a/api_tests/src/comment.spec.ts +++ b/api_tests/src/comment.spec.ts @@ -156,7 +156,6 @@ test("Delete a comment", async () => { commentRes.comment_view.comment.id, ); expect(deleteCommentRes.comment_view.comment.deleted).toBe(true); - expect(deleteCommentRes.comment_view.comment.content).toBe(""); // Make sure that comment is deleted on beta await waitUntil( @@ -254,7 +253,6 @@ test("Remove a comment from admin and community on different instance", async () betaComment.comment.id, ); expect(removeCommentRes.comment_view.comment.removed).toBe(true); - expect(removeCommentRes.comment_view.comment.content).toBe(""); // Comment text is also hidden from list let listComments = await getComments( @@ -263,7 +261,6 @@ test("Remove a comment from admin and community on different instance", async () ); expect(listComments.comments.length).toBe(1); expect(listComments.comments[0].comment.removed).toBe(true); - expect(listComments.comments[0].comment.content).toBe(""); // Make sure its not removed on alpha let refetchedPostComments = await getComments( @@ -702,10 +699,10 @@ test("Check that activity from another instance is sent to third instance", asyn test("Fetch in_reply_tos: A is unsubbed from B, B makes a post, and some embedded comments, A subs to B, B updates the lowest level comment, A fetches both the post and all the inreplyto comments for that post.", async () => { // Unfollow all remote communities - let site = await unfollowRemotes(alpha); - expect( - site.my_user?.follows.filter(c => c.community.local == false).length, - ).toBe(0); + let my_user = await unfollowRemotes(alpha); + expect(my_user.follows.filter(c => c.community.local == false).length).toBe( + 0, + ); // B creates a post, and two comments, should be invisible to A let postOnBetaRes = await createPost(beta, 2); diff --git a/api_tests/src/community.spec.ts b/api_tests/src/community.spec.ts index 77b68e2fc..2bb092088 100644 --- a/api_tests/src/community.spec.ts +++ b/api_tests/src/community.spec.ts @@ -25,17 +25,18 @@ import { getComments, createComment, getCommunityByName, - blockInstance, waitUntil, alphaUrl, delta, - betaAllowedInstances, searchPostLocal, longDelay, editCommunity, unfollows, + getMyUser, + userBlockInstance, } from "./shared"; -import { EditCommunity, EditSite } from "lemmy-js-client"; +import { AdminAllowInstanceParams } from "lemmy-js-client/dist/types/AdminAllowInstanceParams"; +import { EditCommunity, EditSite, GetPosts } from "lemmy-js-client"; beforeAll(setupLogins); afterAll(unfollows); @@ -226,7 +227,7 @@ test("Admin actions in remote community are not federated to origin", async () = if (!betaCommunity) { throw "Missing beta community"; } - let bannedUserInfo1 = (await getSite(gamma)).my_user?.local_user_view.person; + let bannedUserInfo1 = (await getMyUser(gamma)).local_user_view.person; if (!bannedUserInfo1) { throw "Missing banned user 1"; } @@ -363,7 +364,7 @@ test("User blocks instance, communities are hidden", async () => { expect(listing_ids).toContain(postRes.post_view.post.ap_id); // block the beta instance - await blockInstance(alpha, alphaPost.community.instance_id, true); + await userBlockInstance(alpha, alphaPost.community.instance_id, true); // after blocking, post should not be in listing let listing2 = await getPosts(alpha, "All"); @@ -371,7 +372,7 @@ test("User blocks instance, communities are hidden", async () => { expect(listing_ids2.indexOf(postRes.post_view.post.ap_id)).toBe(-1); // unblock instance again - await blockInstance(alpha, alphaPost.community.instance_id, false); + await userBlockInstance(alpha, alphaPost.community.instance_id, false); // post should be included in listing let listing3 = await getPosts(alpha, "All"); @@ -455,9 +456,12 @@ test("Dont receive community activities after unsubscribe", async () => { expect(communityRes1.community_view.counts.subscribers).toBe(2); // temporarily block alpha, so that it doesn't know about unfollow - let editSiteForm: EditSite = {}; - editSiteForm.allowed_instances = ["lemmy-epsilon"]; - await beta.editSite(editSiteForm); + var allow_instance_params: AdminAllowInstanceParams = { + instance: "lemmy-alpha", + allow: false, + reason: undefined, + }; + await beta.adminAllowInstance(allow_instance_params); await longDelay(); // unfollow @@ -471,8 +475,8 @@ test("Dont receive community activities after unsubscribe", async () => { expect(communityRes2.community_view.counts.subscribers).toBe(2); // unblock alpha - editSiteForm.allowed_instances = betaAllowedInstances; - await beta.editSite(editSiteForm); + allow_instance_params.allow = true; + await beta.adminAllowInstance(allow_instance_params); await longDelay(); // create a post, it shouldnt reach beta @@ -573,3 +577,29 @@ test("Remote mods can edit communities", async () => { "Example description", ); }); + +test("Community name with non-ascii chars", async () => { + const name = "това_ме_ядосва" + Math.random().toString().slice(2, 6); + let communityRes = await createCommunity(alpha, name); + + let betaCommunity1 = await resolveCommunity( + beta, + communityRes.community_view.community.actor_id, + ); + expect(betaCommunity1.community!.community.name).toBe(name); + + let alphaCommunity2 = await getCommunityByName(alpha, name); + expect(alphaCommunity2.community_view.community.name).toBe(name); + + let fediName = `${communityRes.community_view.community.name}@LEMMY-ALPHA:8541`; + let betaCommunity2 = await getCommunityByName(beta, fediName); + expect(betaCommunity2.community_view.community.name).toBe(name); + + let postRes = await createPost(beta, betaCommunity1.community!.community.id); + + let form: GetPosts = { + community_name: fediName, + }; + let posts = await beta.getPosts(form); + expect(posts.posts[0].post.name).toBe(postRes.post_view.post.name); +}); diff --git a/api_tests/src/follow.spec.ts b/api_tests/src/follow.spec.ts index 22fdfa305..936ce2606 100644 --- a/api_tests/src/follow.spec.ts +++ b/api_tests/src/follow.spec.ts @@ -12,6 +12,7 @@ import { registerUser, unfollows, delay, + getMyUser, } from "./shared"; beforeAll(setupLogins); @@ -85,8 +86,8 @@ test("Follow federated community", async () => { ); // Check it from local - let site = await getSite(alpha); - let remoteCommunityId = site.my_user?.follows.find( + let my_user = await getMyUser(alpha); + let remoteCommunityId = my_user?.follows.find( c => c.community.local == false && c.community.id === betaCommunityInitial.community.id, @@ -102,9 +103,9 @@ test("Follow federated community", async () => { expect(unfollow.community_view.subscribed).toBe("NotSubscribed"); // Make sure you are unsubbed locally - let siteUnfollowCheck = await getSite(alpha); + let siteUnfollowCheck = await getMyUser(alpha); expect( - siteUnfollowCheck.my_user?.follows.find( + siteUnfollowCheck.follows.find( c => c.community.id === betaCommunityInitial.community.id, ), ).toBe(undefined); diff --git a/api_tests/src/image.spec.ts b/api_tests/src/image.spec.ts index 7ac6e7221..a3478081a 100644 --- a/api_tests/src/image.spec.ts +++ b/api_tests/src/image.spec.ts @@ -32,6 +32,7 @@ import { createPostWithThumbnail, sampleImage, sampleSite, + getMyUser, } from "./shared"; beforeAll(setupLogins); @@ -129,9 +130,9 @@ test("Purge user, uploaded image removed", async () => { expect(content.length).toBeGreaterThan(0); // purge user - let site = await getSite(user); + let my_user = await getMyUser(user); const purgeForm: PurgePerson = { - person_id: site.my_user!.local_user_view.person.id, + person_id: my_user.local_user_view.person.id, }; const delete_ = await alphaImage.purgePerson(purgeForm); expect(delete_.success).toBe(true); @@ -199,11 +200,11 @@ test("Images in remote image post are proxied if setting enabled", async () => { // remote image gets proxied after upload expect( post.thumbnail_url?.startsWith( - "http://lemmy-gamma:8561/api/v3/image_proxy?url", + "http://lemmy-gamma:8561/api/v4/image_proxy?url", ), ).toBeTruthy(); expect( - post.body?.startsWith("![](http://lemmy-gamma:8561/api/v3/image_proxy?url"), + post.body?.startsWith("![](http://lemmy-gamma:8561/api/v4/image_proxy?url"), ).toBeTruthy(); // Make sure that it ends with jpg, to be sure its an image @@ -222,12 +223,12 @@ test("Images in remote image post are proxied if setting enabled", async () => { expect( epsilonPost.thumbnail_url?.startsWith( - "http://lemmy-epsilon:8581/api/v3/image_proxy?url", + "http://lemmy-epsilon:8581/api/v4/image_proxy?url", ), ).toBeTruthy(); expect( epsilonPost.body?.startsWith( - "![](http://lemmy-epsilon:8581/api/v3/image_proxy?url", + "![](http://lemmy-epsilon:8581/api/v4/image_proxy?url", ), ).toBeTruthy(); @@ -249,7 +250,7 @@ test("Thumbnail of remote image link is proxied if setting enabled", async () => // remote image gets proxied after upload expect( post.thumbnail_url?.startsWith( - "http://lemmy-gamma:8561/api/v3/image_proxy?url", + "http://lemmy-gamma:8561/api/v4/image_proxy?url", ), ).toBeTruthy(); @@ -267,7 +268,7 @@ test("Thumbnail of remote image link is proxied if setting enabled", async () => expect( epsilonPost.thumbnail_url?.startsWith( - "http://lemmy-epsilon:8581/api/v3/image_proxy?url", + "http://lemmy-epsilon:8581/api/v4/image_proxy?url", ), ).toBeTruthy(); diff --git a/api_tests/src/post.spec.ts b/api_tests/src/post.spec.ts index 59e3d774e..4158bbdc7 100644 --- a/api_tests/src/post.spec.ts +++ b/api_tests/src/post.spec.ts @@ -38,8 +38,10 @@ import { alphaUrl, loginUser, createCommunity, + getMyUser, } from "./shared"; import { PostView } from "lemmy-js-client/dist/types/PostView"; +import { AdminBlockInstanceParams } from "lemmy-js-client/dist/types/AdminBlockInstanceParams"; import { EditSite, ResolveObject } from "lemmy-js-client"; let betaCommunity: CommunityView | undefined; @@ -87,12 +89,12 @@ async function assertPostFederation( } test("Create a post", async () => { - // Setup some allowlists and blocklists - const editSiteForm: EditSite = {}; - - editSiteForm.allowed_instances = []; - editSiteForm.blocked_instances = ["lemmy-alpha"]; - await epsilon.editSite(editSiteForm); + // Block alpha + var block_instance_params: AdminBlockInstanceParams = { + instance: "lemmy-alpha", + block: true, + }; + await epsilon.adminBlockInstance(block_instance_params); if (!betaCommunity) { throw "Missing beta community"; @@ -132,11 +134,9 @@ test("Create a post", async () => { resolvePost(epsilon, postRes.post_view.post), ).rejects.toStrictEqual(Error("not_found")); - // remove added allow/blocklists - editSiteForm.allowed_instances = []; - editSiteForm.blocked_instances = []; - await delta.editSite(editSiteForm); - await epsilon.editSite(editSiteForm); + // remove blocked instance + block_instance_params.block = false; + await epsilon.adminBlockInstance(block_instance_params); }); test("Create a post in a non-existent community", async () => { @@ -452,8 +452,7 @@ test("Enforce site ban federation for local user", async () => { // create a test user let alphaUserHttp = await registerUser(alpha, alphaUrl); - let alphaUserPerson = (await getSite(alphaUserHttp)).my_user?.local_user_view - .person; + let alphaUserPerson = (await getMyUser(alphaUserHttp)).local_user_view.person; let alphaUserActorId = alphaUserPerson?.actor_id; if (!alphaUserActorId) { throw "Missing alpha user actor id"; @@ -533,8 +532,7 @@ test("Enforce site ban federation for federated user", async () => { // create a test user let alphaUserHttp = await registerUser(alpha, alphaUrl); - let alphaUserPerson = (await getSite(alphaUserHttp)).my_user?.local_user_view - .person; + let alphaUserPerson = (await getMyUser(alphaUserHttp)).local_user_view.person; let alphaUserActorId = alphaUserPerson?.actor_id; if (!alphaUserActorId) { throw "Missing alpha user actor id"; @@ -564,8 +562,7 @@ test("Enforce site ban federation for federated user", async () => { expect(banAlphaOnBeta.banned).toBe(true); // The beta site ban should NOT be federated to alpha - let alphaPerson2 = (await getSite(alphaUserHttp)).my_user!.local_user_view - .person; + let alphaPerson2 = (await getMyUser(alphaUserHttp)).local_user_view.person; expect(alphaPerson2.banned).toBe(false); // existing alpha post should be removed on beta diff --git a/api_tests/src/shared.ts b/api_tests/src/shared.ts index 0b0a9862c..1ed13d9cf 100644 --- a/api_tests/src/shared.ts +++ b/api_tests/src/shared.ts @@ -1,9 +1,8 @@ import { + AdminBlockInstanceParams, ApproveCommunityPendingFollower, BlockCommunity, BlockCommunityResponse, - BlockInstance, - BlockInstanceResponse, CommunityId, CommunityVisibility, CreatePrivateMessageReport, @@ -17,15 +16,18 @@ import { LemmyHttp, ListCommunityPendingFollows, ListCommunityPendingFollowsResponse, + MyUserInfo, PersonId, PostView, PrivateMessageReportResponse, SuccessResponse, + UserBlockInstanceParams, } from "lemmy-js-client"; import { CreatePost } from "lemmy-js-client/dist/types/CreatePost"; import { DeletePost } from "lemmy-js-client/dist/types/DeletePost"; import { EditPost } from "lemmy-js-client/dist/types/EditPost"; import { EditSite } from "lemmy-js-client/dist/types/EditSite"; +import { AdminAllowInstanceParams } from "lemmy-js-client/dist/types/AdminAllowInstanceParams"; import { FeaturePost } from "lemmy-js-client/dist/types/FeaturePost"; import { GetComments } from "lemmy-js-client/dist/types/GetComments"; import { GetCommentsResponse } from "lemmy-js-client/dist/types/GetCommentsResponse"; @@ -104,13 +106,6 @@ export const gamma = new LemmyHttp(gammaUrl, { fetchFunction }); export const delta = new LemmyHttp(deltaUrl, { fetchFunction }); export const epsilon = new LemmyHttp(epsilonUrl, { fetchFunction }); -export const betaAllowedInstances = [ - "lemmy-alpha", - "lemmy-gamma", - "lemmy-delta", - "lemmy-epsilon", -]; - const password = "lemmylemmy"; export async function setupLogins() { @@ -168,30 +163,29 @@ export async function setupLogins() { rate_limit_comment: 999, rate_limit_search: 999, }; - - // Set the blocks and auths for each - editSiteForm.allowed_instances = [ - "lemmy-beta", - "lemmy-gamma", - "lemmy-delta", - "lemmy-epsilon", - ]; await alpha.editSite(editSiteForm); - - editSiteForm.allowed_instances = betaAllowedInstances; await beta.editSite(editSiteForm); - - editSiteForm.allowed_instances = [ - "lemmy-alpha", - "lemmy-beta", - "lemmy-delta", - "lemmy-epsilon", - ]; await gamma.editSite(editSiteForm); - - // Setup delta allowed instance - editSiteForm.allowed_instances = ["lemmy-beta"]; await delta.editSite(editSiteForm); + await epsilon.editSite(editSiteForm); + + // Set the blocks for each + await allowInstance(alpha, "lemmy-beta"); + await allowInstance(alpha, "lemmy-gamma"); + await allowInstance(alpha, "lemmy-delta"); + await allowInstance(alpha, "lemmy-epsilon"); + + await allowInstance(beta, "lemmy-alpha"); + await allowInstance(beta, "lemmy-gamma"); + await allowInstance(beta, "lemmy-delta"); + await allowInstance(beta, "lemmy-epsilon"); + + await allowInstance(gamma, "lemmy-alpha"); + await allowInstance(gamma, "lemmy-beta"); + await allowInstance(gamma, "lemmy-delta"); + await allowInstance(gamma, "lemmy-epsilon"); + + await allowInstance(delta, "lemmy-beta"); // Create the main alpha/beta communities // Ignore thrown errors of duplicates @@ -208,6 +202,17 @@ export async function setupLogins() { } } +async function allowInstance(api: LemmyHttp, instance: string) { + const params: AdminAllowInstanceParams = { + instance, + allow: true, + }; + // Ignore errors from duplicate allows (because setup gets called for each test file) + try { + await api.adminAllowInstance(params); + } catch {} +} + export async function createPost( api: LemmyHttp, community_id: number, @@ -757,6 +762,10 @@ export async function getSite(api: LemmyHttp): Promise { return api.getSite(); } +export async function getMyUser(api: LemmyHttp): Promise { + return api.getMyUser(); +} + export async function listPrivateMessages( api: LemmyHttp, ): Promise { @@ -766,19 +775,16 @@ export async function listPrivateMessages( return api.getPrivateMessages(form); } -export async function unfollowRemotes( - api: LemmyHttp, -): Promise { +export async function unfollowRemotes(api: LemmyHttp): Promise { // Unfollow all remote communities - let site = await getSite(api); + let my_user = await getMyUser(api); let remoteFollowed = - site.my_user?.follows.filter(c => c.community.local == false) ?? []; + my_user.follows.filter(c => c.community.local == false) ?? []; await Promise.all( remoteFollowed.map(cu => followCommunity(api, false, cu.community.id)), ); - let siteRes = await getSite(api); - return siteRes; + return await getMyUser(api); } export async function followBeta(api: LemmyHttp): Promise { @@ -854,16 +860,16 @@ export function getPosts( return api.getPosts(form); } -export function blockInstance( +export function userBlockInstance( api: LemmyHttp, instance_id: InstanceId, block: boolean, -): Promise { - let form: BlockInstance = { +): Promise { + let form: UserBlockInstanceParams = { instance_id, block, }; - return api.blockInstance(form); + return api.userBlockInstance(form); } export function blockCommunity( diff --git a/api_tests/src/user.spec.ts b/api_tests/src/user.spec.ts index 2edcf54ea..d1d6144f5 100644 --- a/api_tests/src/user.spec.ts +++ b/api_tests/src/user.spec.ts @@ -22,8 +22,15 @@ import { alphaImage, unfollows, saveUserSettingsBio, + getMyUser, + getPersonDetails, } from "./shared"; -import { LemmyHttp, SaveUserSettings, UploadImage } from "lemmy-js-client"; +import { + EditSite, + LemmyHttp, + SaveUserSettings, + UploadImage, +} from "lemmy-js-client"; import { GetPosts } from "lemmy-js-client/dist/types/GetPosts"; beforeAll(setupLogins); @@ -44,12 +51,9 @@ function assertUserFederation(userOne?: PersonView, userTwo?: PersonView) { test("Create user", async () => { let user = await registerUser(alpha, alphaUrl); - let site = await getSite(user); - expect(site.my_user).toBeDefined(); - if (!site.my_user) { - throw "Missing site user"; - } - apShortname = `${site.my_user.local_user_view.person.name}@lemmy-alpha:8541`; + let my_user = await getMyUser(user); + expect(my_user).toBeDefined(); + apShortname = `${my_user.local_user_view.person.name}@lemmy-alpha:8541`; }); test("Set some user settings, check that they are federated", async () => { @@ -64,12 +68,15 @@ test("Set some user settings, check that they are federated", async () => { }; await saveUserSettings(beta, form); - let site = await getSite(beta); - expect(site.my_user?.local_user_view.local_user.theme).toBe("test"); + let my_user = await getMyUser(beta); + expect(my_user.local_user_view.local_user.theme).toBe("test"); }); test("Delete user", async () => { let user = await registerUser(alpha, alphaUrl); + let user_profile = await getMyUser(user); + let person_id = user_profile.local_user_view.person.id; + let actor_id = user_profile.local_user_view.person.actor_id; // make a local post and comment let alphaCommunity = (await resolveCommunity(user, "main@lemmy-alpha:8541")) @@ -97,6 +104,10 @@ test("Delete user", async () => { expect(remoteComment).toBeDefined(); await deleteUser(user); + await expect(getMyUser(user)).rejects.toStrictEqual(Error("incorrect_login")); + await expect(getPersonDetails(user, person_id)).rejects.toStrictEqual( + Error("not_found"), + ); // check that posts and comments are marked as deleted on other instances. // use get methods to avoid refetching from origin instance @@ -114,6 +125,9 @@ test("Delete user", async () => { (await getComments(alpha, remoteComment.post_id)).comments[0].comment .deleted, ).toBe(true); + await expect( + getPersonDetails(user, remoteComment.creator_id), + ).rejects.toStrictEqual(Error("not_found")); }); test("Requests with invalid auth should be treated as unauthenticated", async () => { @@ -121,8 +135,10 @@ test("Requests with invalid auth should be treated as unauthenticated", async () headers: { Authorization: "Bearer foobar" }, fetchFunction, }); + await expect(getMyUser(invalid_auth)).rejects.toStrictEqual( + Error("incorrect_login"), + ); let site = await getSite(invalid_auth); - expect(site.my_user).toBeUndefined(); expect(site.site_view).toBeDefined(); let form: GetPosts = {}; @@ -131,37 +147,39 @@ test("Requests with invalid auth should be treated as unauthenticated", async () }); test("Create user with Arabic name", async () => { - let user = await registerUser( - alpha, - alphaUrl, - "تجريب" + Math.random().toString().slice(2, 10), // less than actor_name_max_length - ); + // less than actor_name_max_length + const name = "تجريب" + Math.random().toString().slice(2, 10); + let user = await registerUser(alpha, alphaUrl, name); - let site = await getSite(user); - expect(site.my_user).toBeDefined(); - if (!site.my_user) { - throw "Missing site user"; - } - apShortname = `${site.my_user.local_user_view.person.name}@lemmy-alpha:8541`; + let my_user = await getMyUser(user); + expect(my_user).toBeDefined(); + apShortname = `${my_user.local_user_view.person.name}@lemmy-alpha:8541`; - let alphaPerson = (await resolvePerson(alpha, apShortname)).person; - expect(alphaPerson).toBeDefined(); + let betaPerson1 = (await resolvePerson(beta, apShortname)).person; + expect(betaPerson1!.person.name).toBe(name); + + let betaPerson2 = await getPersonDetails(beta, betaPerson1!.person.id); + expect(betaPerson2!.person_view.person.name).toBe(name); }); test("Create user with accept-language", async () => { + const edit: EditSite = { + discussion_languages: [32], + }; + await alpha.editSite(edit); + let lemmy_http = new LemmyHttp(alphaUrl, { // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Language#syntax - headers: { "Accept-Language": "fr-CH, en;q=0.8, de;q=0.7, *;q=0.5" }, + headers: { "Accept-Language": "fr-CH, en;q=0.8, *;q=0.5" }, }); let user = await registerUser(lemmy_http, alphaUrl); + let my_user = await getMyUser(user); + expect(my_user).toBeDefined(); + expect(my_user?.local_user_view.local_user.interface_language).toBe("fr"); let site = await getSite(user); - expect(site.my_user).toBeDefined(); - expect(site.my_user?.local_user_view.local_user.interface_language).toBe( - "fr", - ); let langs = site.all_languages - .filter(a => site.my_user?.discussion_languages.includes(a.id)) + .filter(a => my_user.discussion_languages.includes(a.id)) .map(l => l.code); // should have languages from accept header, as well as "undetermined" // which is automatically enabled by backend @@ -207,8 +225,8 @@ test("Set a new avatar, old avatar is deleted", async () => { // Now try to save a user settings, with the icon missing, // and make sure it doesn't clear the data, or delete the image await saveUserSettingsBio(alpha); - let site = await getSite(alpha); - expect(site.my_user?.local_user_view.person.avatar).toBe(upload2.url); + let my_user = await getMyUser(alpha); + expect(my_user.local_user_view.person.avatar).toBe(upload2.url); // make sure only the new avatar is kept const listMediaRes4 = await alphaImage.listMedia(); diff --git a/config/defaults.hjson b/config/defaults.hjson index c12f879c7..9e24407cd 100644 --- a/config/defaults.hjson +++ b/config/defaults.hjson @@ -1,11 +1,7 @@ { # settings related to the postgresql database database: { - # Configure the database by specifying a URI - # - # This is the preferred method to specify database connection details since - # it is the most flexible. - # Connection URI pointing to a postgres instance + # Configure the database by specifying URI pointing to a postgres instance # # This example uses peer authentication to obviate the need for creating, # configuring, and managing passwords. @@ -14,25 +10,7 @@ # PostgreSQL's documentation. # # [0]: https://www.postgresql.org/docs/current/libpq-connect.html#id-1.7.3.8.3.6 - uri: "postgresql:///lemmy?user=lemmy&host=/var/run/postgresql" - - # or - - # Configure the database by specifying parts of a URI - # - # Note that specifying the `uri` field should be preferred since it provides - # greater control over how the connection is made. This merely exists for - # backwards-compatibility. - # Username to connect to postgres - user: "string" - # Password to connect to postgres - password: "string" - # Host where postgres is running - host: "string" - # Port where postgres can be accessed - port: 123 - # Name of the postgres database for lemmy - database: "string" + connection: "postgres://lemmy:password@localhost:5432/lemmy" # Maximum number of active sql connections pool_size: 30 } @@ -66,13 +44,22 @@ # or # If enabled, all images from remote domains are rewritten to pass through - # `/api/v3/image_proxy`, including embedded images in markdown. Images are stored temporarily + # `/api/v4/image_proxy`, including embedded images in markdown. Images are stored temporarily # in pict-rs for caching. This improves privacy as users don't expose their IP to untrusted # servers, and decreases load on other servers. However it increases bandwidth use for the # local server. # # Requires pict-rs 0.5 "ProxyAllImages" + # Allows bypassing proxy for specific image hosts when using ProxyAllImages. + # + # imgur.com is bypassed by default to avoid rate limit errors. When specifying any bypass + # in the config, this default is ignored and you need to list imgur explicitly. To proxy imgur + # requests, specify a noop bypass list, eg `proxy_bypass_domains ["example.org"]`. + proxy_bypass_domains: [ + "i.imgur.com" + /* ... */ + ] # Timeout for uploading images to pictrs (in seconds) upload_timeout: 30 # Resize post thumbnails to this maximum width/height. diff --git a/crates/api/src/community/add_mod.rs b/crates/api/src/community/add_mod.rs index 9e85788ea..4c5b4eae5 100644 --- a/crates/api/src/community/add_mod.rs +++ b/crates/api/src/community/add_mod.rs @@ -10,7 +10,7 @@ use lemmy_db_schema::{ source::{ community::{Community, CommunityModerator, CommunityModeratorForm}, local_user::LocalUser, - moderator::{ModAddCommunity, ModAddCommunityForm}, + mod_log::moderator::{ModAddCommunity, ModAddCommunityForm}, }, traits::{Crud, Joinable}, }; diff --git a/crates/api/src/community/ban.rs b/crates/api/src/community/ban.rs index a0e57061b..547838fa7 100644 --- a/crates/api/src/community/ban.rs +++ b/crates/api/src/community/ban.rs @@ -20,7 +20,7 @@ use lemmy_db_schema::{ CommunityPersonBanForm, }, local_user::LocalUser, - moderator::{ModBanFromCommunity, ModBanFromCommunityForm}, + mod_log::moderator::{ModBanFromCommunity, ModBanFromCommunityForm}, }, traits::{Bannable, Crud, Followable}, }; @@ -110,7 +110,7 @@ pub async fn ban_from_community( ModBanFromCommunity::create(&mut context.pool(), &form).await?; - let person_view = PersonView::read(&mut context.pool(), data.person_id).await?; + let person_view = PersonView::read(&mut context.pool(), data.person_id, false).await?; ActivityChannel::submit_activity( SendActivityData::BanFromCommunity { diff --git a/crates/api/src/community/block.rs b/crates/api/src/community/block.rs index a6a48e2e7..d49872493 100644 --- a/crates/api/src/community/block.rs +++ b/crates/api/src/community/block.rs @@ -17,7 +17,7 @@ use lemmy_db_views_actor::structs::CommunityView; use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult}; #[tracing::instrument(skip(context))] -pub async fn block_community( +pub async fn user_block_community( data: Json, context: Data, local_user_view: LocalUserView, diff --git a/crates/api/src/community/hide.rs b/crates/api/src/community/hide.rs index 077ed1c5e..f494ad732 100644 --- a/crates/api/src/community/hide.rs +++ b/crates/api/src/community/hide.rs @@ -10,7 +10,7 @@ use lemmy_api_common::{ use lemmy_db_schema::{ source::{ community::{Community, CommunityUpdateForm}, - moderator::{ModHideCommunity, ModHideCommunityForm}, + mod_log::moderator::{ModHideCommunity, ModHideCommunityForm}, }, traits::Crud, }; diff --git a/crates/api/src/community/transfer.rs b/crates/api/src/community/transfer.rs index a5255e5e1..e60b50aa2 100644 --- a/crates/api/src/community/transfer.rs +++ b/crates/api/src/community/transfer.rs @@ -8,7 +8,7 @@ use lemmy_api_common::{ use lemmy_db_schema::{ source::{ community::{Community, CommunityModerator, CommunityModeratorForm}, - moderator::{ModTransferCommunity, ModTransferCommunityForm}, + mod_log::moderator::{ModTransferCommunity, ModTransferCommunityForm}, }, traits::{Crud, Joinable}, }; diff --git a/crates/api/src/lib.rs b/crates/api/src/lib.rs index 83979212d..6a2c94332 100644 --- a/crates/api/src/lib.rs +++ b/crates/api/src/lib.rs @@ -19,7 +19,7 @@ use lemmy_db_schema::{ CommunityPersonBanForm, }, local_site::LocalSite, - moderator::{ModBanFromCommunity, ModBanFromCommunityForm}, + mod_log::moderator::{ModBanFromCommunity, ModBanFromCommunityForm}, person::Person, }, traits::{Bannable, Crud, Followable}, diff --git a/crates/api/src/local_user/add_admin.rs b/crates/api/src/local_user/add_admin.rs index 1e515952e..1e821bf3e 100644 --- a/crates/api/src/local_user/add_admin.rs +++ b/crates/api/src/local_user/add_admin.rs @@ -7,7 +7,7 @@ use lemmy_api_common::{ use lemmy_db_schema::{ source::{ local_user::{LocalUser, LocalUserUpdateForm}, - moderator::{ModAdd, ModAddForm}, + mod_log::moderator::{ModAdd, ModAddForm}, }, traits::Crud, }; diff --git a/crates/api/src/local_user/ban_person.rs b/crates/api/src/local_user/ban_person.rs index 9349cc632..715bd206d 100644 --- a/crates/api/src/local_user/ban_person.rs +++ b/crates/api/src/local_user/ban_person.rs @@ -11,7 +11,7 @@ use lemmy_db_schema::{ source::{ local_user::LocalUser, login_token::LoginToken, - moderator::{ModBan, ModBanForm}, + mod_log::moderator::{ModBan, ModBanForm}, person::{Person, PersonUpdateForm}, }, traits::Crud, @@ -88,7 +88,7 @@ pub async fn ban_from_site( ModBan::create(&mut context.pool(), &form).await?; - let person_view = PersonView::read(&mut context.pool(), person.id).await?; + let person_view = PersonView::read(&mut context.pool(), person.id, false).await?; ban_nonlocal_user_from_local_communities( &local_user_view, diff --git a/crates/api/src/local_user/block.rs b/crates/api/src/local_user/block.rs index 250277be3..3aee554d4 100644 --- a/crates/api/src/local_user/block.rs +++ b/crates/api/src/local_user/block.rs @@ -12,7 +12,7 @@ use lemmy_db_views_actor::structs::PersonView; use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult}; #[tracing::instrument(skip(context))] -pub async fn block_person( +pub async fn user_block_person( data: Json, context: Data, local_user_view: LocalUserView, @@ -48,7 +48,7 @@ pub async fn block_person( .with_lemmy_type(LemmyErrorType::PersonBlockAlreadyExists)?; } - let person_view = PersonView::read(&mut context.pool(), target_id).await?; + let person_view = PersonView::read(&mut context.pool(), target_id, false).await?; Ok(Json(BlockPersonResponse { person_view, blocked: data.block, diff --git a/crates/api/src/local_user/mod.rs b/crates/api/src/local_user/mod.rs index b1ee7c0b6..d3fc37a73 100644 --- a/crates/api/src/local_user/mod.rs +++ b/crates/api/src/local_user/mod.rs @@ -15,5 +15,6 @@ pub mod report_count; pub mod reset_password; pub mod save_settings; pub mod update_totp; +pub mod user_block_instance; pub mod validate_auth; pub mod verify_email; diff --git a/crates/api/src/site/block.rs b/crates/api/src/local_user/user_block_instance.rs similarity index 79% rename from crates/api/src/site/block.rs rename to crates/api/src/local_user/user_block_instance.rs index 823dda612..940538833 100644 --- a/crates/api/src/site/block.rs +++ b/crates/api/src/local_user/user_block_instance.rs @@ -1,9 +1,6 @@ use activitypub_federation::config::Data; use actix_web::web::Json; -use lemmy_api_common::{ - context::LemmyContext, - site::{BlockInstance, BlockInstanceResponse}, -}; +use lemmy_api_common::{context::LemmyContext, site::UserBlockInstanceParams, SuccessResponse}; use lemmy_db_schema::{ source::instance_block::{InstanceBlock, InstanceBlockForm}, traits::Blockable, @@ -12,11 +9,11 @@ use lemmy_db_views::structs::LocalUserView; use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult}; #[tracing::instrument(skip(context))] -pub async fn block_instance( - data: Json, +pub async fn user_block_instance( + data: Json, local_user_view: LocalUserView, context: Data, -) -> LemmyResult> { +) -> LemmyResult> { let instance_id = data.instance_id; let person_id = local_user_view.person.id; if local_user_view.person.instance_id == instance_id { @@ -38,7 +35,5 @@ pub async fn block_instance( .with_lemmy_type(LemmyErrorType::InstanceBlockAlreadyExists)?; } - Ok(Json(BlockInstanceResponse { - blocked: data.block, - })) + Ok(Json(SuccessResponse::default())) } diff --git a/crates/api/src/post/feature.rs b/crates/api/src/post/feature.rs index 6fc2f443c..8ede8c31c 100644 --- a/crates/api/src/post/feature.rs +++ b/crates/api/src/post/feature.rs @@ -10,7 +10,7 @@ use lemmy_api_common::{ use lemmy_db_schema::{ source::{ community::Community, - moderator::{ModFeaturePost, ModFeaturePostForm}, + mod_log::moderator::{ModFeaturePost, ModFeaturePostForm}, post::{Post, PostUpdateForm}, }, traits::Crud, diff --git a/crates/api/src/post/lock.rs b/crates/api/src/post/lock.rs index 011770c2e..ad7fa7264 100644 --- a/crates/api/src/post/lock.rs +++ b/crates/api/src/post/lock.rs @@ -9,7 +9,7 @@ use lemmy_api_common::{ }; use lemmy_db_schema::{ source::{ - moderator::{ModLockPost, ModLockPostForm}, + mod_log::moderator::{ModLockPost, ModLockPostForm}, post::{Post, PostUpdateForm}, }, traits::Crud, diff --git a/crates/api/src/site/admin_allow_instance.rs b/crates/api/src/site/admin_allow_instance.rs new file mode 100644 index 000000000..81879ecae --- /dev/null +++ b/crates/api/src/site/admin_allow_instance.rs @@ -0,0 +1,53 @@ +use activitypub_federation::config::Data; +use actix_web::web::Json; +use lemmy_api_common::{ + context::LemmyContext, + site::AdminAllowInstanceParams, + utils::is_admin, + LemmyErrorType, + SuccessResponse, +}; +use lemmy_db_schema::source::{ + federation_allowlist::{FederationAllowList, FederationAllowListForm}, + instance::Instance, + mod_log::admin::{AdminAllowInstance, AdminAllowInstanceForm}, +}; +use lemmy_db_views::structs::LocalUserView; +use lemmy_utils::error::LemmyResult; + +#[tracing::instrument(skip(context))] +pub async fn admin_allow_instance( + data: Json, + local_user_view: LocalUserView, + context: Data, +) -> LemmyResult> { + is_admin(&local_user_view)?; + + let blocklist = Instance::blocklist(&mut context.pool()).await?; + if !blocklist.is_empty() { + Err(LemmyErrorType::CannotCombineFederationBlocklistAndAllowlist)?; + } + + let instance_id = Instance::read_or_create(&mut context.pool(), data.instance.clone()) + .await? + .id; + let form = FederationAllowListForm { + instance_id, + updated: None, + }; + if data.allow { + FederationAllowList::allow(&mut context.pool(), &form).await?; + } else { + FederationAllowList::unallow(&mut context.pool(), instance_id).await?; + } + + let mod_log_form = AdminAllowInstanceForm { + instance_id, + admin_person_id: local_user_view.person.id, + reason: data.reason.clone(), + allowed: data.allow, + }; + AdminAllowInstance::insert(&mut context.pool(), &mod_log_form).await?; + + Ok(Json(SuccessResponse::default())) +} diff --git a/crates/api/src/site/admin_block_instance.rs b/crates/api/src/site/admin_block_instance.rs new file mode 100644 index 000000000..54962ccf3 --- /dev/null +++ b/crates/api/src/site/admin_block_instance.rs @@ -0,0 +1,56 @@ +use activitypub_federation::config::Data; +use actix_web::web::Json; +use lemmy_api_common::{ + context::LemmyContext, + site::AdminBlockInstanceParams, + utils::is_admin, + LemmyErrorType, + SuccessResponse, +}; +use lemmy_db_schema::source::{ + federation_blocklist::{FederationBlockList, FederationBlockListForm}, + instance::Instance, + mod_log::admin::{AdminBlockInstance, AdminBlockInstanceForm}, +}; +use lemmy_db_views::structs::LocalUserView; +use lemmy_utils::error::LemmyResult; + +#[tracing::instrument(skip(context))] +pub async fn admin_block_instance( + data: Json, + local_user_view: LocalUserView, + context: Data, +) -> LemmyResult> { + is_admin(&local_user_view)?; + + let allowlist = Instance::allowlist(&mut context.pool()).await?; + if !allowlist.is_empty() { + Err(LemmyErrorType::CannotCombineFederationBlocklistAndAllowlist)?; + } + + let instance_id = Instance::read_or_create(&mut context.pool(), data.instance.clone()) + .await? + .id; + let form = FederationBlockListForm { + instance_id, + expires: data.expires, + updated: None, + }; + + if data.block { + FederationBlockList::block(&mut context.pool(), &form).await?; + } else { + FederationBlockList::unblock(&mut context.pool(), instance_id).await?; + } + + let mod_log_form = AdminBlockInstanceForm { + instance_id, + admin_person_id: local_user_view.person.id, + blocked: data.block, + reason: data.reason.clone(), + when_: data.expires, + }; + AdminBlockInstance::insert(&mut context.pool(), &mod_log_form).await?; + + Ok(Json(SuccessResponse::default())) +} diff --git a/crates/api/src/site/leave_admin.rs b/crates/api/src/site/leave_admin.rs index 97ad7e2e5..fde258dd2 100644 --- a/crates/api/src/site/leave_admin.rs +++ b/crates/api/src/site/leave_admin.rs @@ -6,7 +6,7 @@ use lemmy_db_schema::{ language::Language, local_site_url_blocklist::LocalSiteUrlBlocklist, local_user::{LocalUser, LocalUserUpdateForm}, - moderator::{ModAdd, ModAddForm}, + mod_log::moderator::{ModAdd, ModAddForm}, oauth_provider::OAuthProvider, tagline::Tagline, }, @@ -69,14 +69,12 @@ pub async fn leave_admin( site_view, admins, version: VERSION.to_string(), - my_user: None, all_languages, discussion_languages, oauth_providers: Some(oauth_providers), admin_oauth_providers: None, blocked_urls, tagline, - taglines: vec![], - custom_emojis: vec![], + my_user: None, })) } diff --git a/crates/api/src/site/mod.rs b/crates/api/src/site/mod.rs index f18dea3d0..bab66f33b 100644 --- a/crates/api/src/site/mod.rs +++ b/crates/api/src/site/mod.rs @@ -1,4 +1,5 @@ -pub mod block; +pub mod admin_allow_instance; +pub mod admin_block_instance; pub mod federated_instances; pub mod leave_admin; pub mod list_all_media; diff --git a/crates/api/src/site/mod_log.rs b/crates/api/src/site/mod_log.rs index 8f5538566..bbf147666 100644 --- a/crates/api/src/site/mod_log.rs +++ b/crates/api/src/site/mod_log.rs @@ -7,6 +7,8 @@ use lemmy_api_common::{ use lemmy_db_schema::{source::local_site::LocalSite, ModlogActionType}; use lemmy_db_views::structs::LocalUserView; use lemmy_db_views_moderator::structs::{ + AdminAllowInstanceView, + AdminBlockInstanceView, AdminPurgeCommentView, AdminPurgeCommunityView, AdminPurgePersonView, @@ -121,6 +123,8 @@ pub async fn get_mod_log( admin_purged_communities, admin_purged_posts, admin_purged_comments, + admin_block_instance, + admin_allow_instance, ) = if data.community_id.is_none() { ( match type_ { @@ -161,6 +165,18 @@ pub async fn get_mod_log( } _ => Default::default(), }, + match type_ { + All | AdminBlockInstance if other_person_id.is_none() => { + AdminBlockInstanceView::list(&mut context.pool(), params).await? + } + _ => Default::default(), + }, + match type_ { + All | AdminAllowInstance if other_person_id.is_none() => { + AdminAllowInstanceView::list(&mut context.pool(), params).await? + } + _ => Default::default(), + }, ) } else { Default::default() @@ -183,5 +199,7 @@ pub async fn get_mod_log( admin_purged_posts, admin_purged_comments, hidden_communities, + admin_block_instance, + admin_allow_instance, })) } diff --git a/crates/api/src/site/purge/comment.rs b/crates/api/src/site/purge/comment.rs index ae79a835a..5208cc397 100644 --- a/crates/api/src/site/purge/comment.rs +++ b/crates/api/src/site/purge/comment.rs @@ -11,7 +11,7 @@ use lemmy_db_schema::{ source::{ comment::Comment, local_user::LocalUser, - moderator::{AdminPurgeComment, AdminPurgeCommentForm}, + mod_log::admin::{AdminPurgeComment, AdminPurgeCommentForm}, }, traits::Crud, }; diff --git a/crates/api/src/site/purge/community.rs b/crates/api/src/site/purge/community.rs index f0252e303..c55f753dc 100644 --- a/crates/api/src/site/purge/community.rs +++ b/crates/api/src/site/purge/community.rs @@ -13,7 +13,7 @@ use lemmy_db_schema::{ source::{ community::Community, local_user::LocalUser, - moderator::{AdminPurgeCommunity, AdminPurgeCommunityForm}, + mod_log::admin::{AdminPurgeCommunity, AdminPurgeCommunityForm}, }, traits::Crud, }; diff --git a/crates/api/src/site/purge/person.rs b/crates/api/src/site/purge/person.rs index 6dad4ce65..0f15e7726 100644 --- a/crates/api/src/site/purge/person.rs +++ b/crates/api/src/site/purge/person.rs @@ -11,7 +11,7 @@ use lemmy_api_common::{ use lemmy_db_schema::{ source::{ local_user::LocalUser, - moderator::{AdminPurgePerson, AdminPurgePersonForm}, + mod_log::admin::{AdminPurgePerson, AdminPurgePersonForm}, person::{Person, PersonUpdateForm}, }, traits::Crud, diff --git a/crates/api/src/site/purge/post.rs b/crates/api/src/site/purge/post.rs index f808269e7..e726945f5 100644 --- a/crates/api/src/site/purge/post.rs +++ b/crates/api/src/site/purge/post.rs @@ -11,7 +11,7 @@ use lemmy_api_common::{ use lemmy_db_schema::{ source::{ local_user::LocalUser, - moderator::{AdminPurgePost, AdminPurgePostForm}, + mod_log::admin::{AdminPurgePost, AdminPurgePostForm}, post::Post, }, traits::Crud, diff --git a/crates/api_common/Cargo.toml b/crates/api_common/Cargo.toml index 3ae14717d..74a0390ca 100644 --- a/crates/api_common/Cargo.toml +++ b/crates/api_common/Cargo.toml @@ -66,6 +66,7 @@ enum-map = { workspace = true } urlencoding = { workspace = true } mime = { version = "0.3.17", optional = true } mime_guess = "2.0.5" +infer = "0.16.0" webpage = { version = "2.0", default-features = false, features = [ "serde", ], optional = true } diff --git a/crates/api_common/README.md b/crates/api_common/README.md index b4e7ad63b..ded59d34a 100644 --- a/crates/api_common/README.md +++ b/crates/api_common/README.md @@ -11,7 +11,7 @@ Here is an example using [reqwest](https://crates.io/crates/reqwest): }; let client = Client::new(); let response = client - .get("https://lemmy.ml/api/v3/post/list") + .get("https://lemmy.ml/api/v4/post/list") .query(¶ms) .send() .await?; diff --git a/crates/api_common/src/oauth_provider.rs b/crates/api_common/src/oauth_provider.rs index 36fef3b18..2f3344802 100644 --- a/crates/api_common/src/oauth_provider.rs +++ b/crates/api_common/src/oauth_provider.rs @@ -25,6 +25,8 @@ pub struct CreateOAuthProvider { #[cfg_attr(feature = "full", ts(optional))] pub account_linking_enabled: Option, #[cfg_attr(feature = "full", ts(optional))] + pub use_pkce: Option, + #[cfg_attr(feature = "full", ts(optional))] pub enabled: Option, } @@ -54,6 +56,8 @@ pub struct EditOAuthProvider { #[cfg_attr(feature = "full", ts(optional))] pub account_linking_enabled: Option, #[cfg_attr(feature = "full", ts(optional))] + pub use_pkce: Option, + #[cfg_attr(feature = "full", ts(optional))] pub enabled: Option, } @@ -82,4 +86,6 @@ pub struct AuthenticateWithOauth { /// An answer is mandatory if require application is enabled on the server #[cfg_attr(feature = "full", ts(optional))] pub answer: Option, + #[cfg_attr(feature = "full", ts(optional))] + pub pkce_code_verifier: Option, } diff --git a/crates/api_common/src/post.rs b/crates/api_common/src/post.rs index 405de3a92..fb16c8aa8 100644 --- a/crates/api_common/src/post.rs +++ b/crates/api_common/src/post.rs @@ -1,5 +1,5 @@ use lemmy_db_schema::{ - newtypes::{CommentId, CommunityId, DbUrl, LanguageId, PostId, PostReportId}, + newtypes::{CommentId, CommunityId, DbUrl, LanguageId, PostId, PostReportId, TagId}, ListingType, PostFeatureType, PostSortType, @@ -37,6 +37,8 @@ pub struct CreatePost { /// Instead of fetching a thumbnail, use a custom one. #[cfg_attr(feature = "full", ts(optional))] pub custom_thumbnail: Option, + #[cfg_attr(feature = "full", ts(optional))] + pub tags: Option>, /// Time when this post should be scheduled. Null means publish immediately. #[cfg_attr(feature = "full", ts(optional))] pub scheduled_publish_time: Option, @@ -164,6 +166,8 @@ pub struct EditPost { /// Instead of fetching a thumbnail, use a custom one. #[cfg_attr(feature = "full", ts(optional))] pub custom_thumbnail: Option, + #[cfg_attr(feature = "full", ts(optional))] + pub tags: Option>, /// Time when this post should be scheduled. Null means publish immediately. #[cfg_attr(feature = "full", ts(optional))] pub scheduled_publish_time: Option, diff --git a/crates/api_common/src/request.rs b/crates/api_common/src/request.rs index cc506b896..02e889872 100644 --- a/crates/api_common/src/request.rs +++ b/crates/api_common/src/request.rs @@ -23,6 +23,7 @@ use lemmy_utils::{ REQWEST_TIMEOUT, VERSION, }; +use mime::{Mime, TEXT_HTML}; use reqwest::{ header::{CONTENT_TYPE, RANGE}, Client, @@ -50,9 +51,11 @@ pub fn client_builder(settings: &Settings) -> ClientBuilder { #[tracing::instrument(skip_all)] pub async fn fetch_link_metadata(url: &Url, context: &LemmyContext) -> LemmyResult { info!("Fetching site metadata for url: {}", url); - // We only fetch the first 64kB of data in order to not waste bandwidth especially for large - // binary files - let bytes_to_fetch = 64 * 1024; + // We only fetch the first MB of data in order to not waste bandwidth especially for large + // binary files. This high limit is particularly needed for youtube, which includes a lot of + // javascript code before the opengraph tags. Mastodon also uses a 1 MB limit: + // https://github.com/mastodon/mastodon/blob/295ad6f19a016b3f16e1201ffcbb1b3ad6b455a2/app/lib/request.rb#L213 + let bytes_to_fetch = 1024 * 1024; let response = context .client() .get(url.as_str()) @@ -63,47 +66,54 @@ pub async fn fetch_link_metadata(url: &Url, context: &LemmyContext) -> LemmyResu .await? .error_for_status()?; - // In some cases servers send a wrong mime type for images, which prevents thumbnail - // generation. To avoid this we also try to guess the mime type from file extension. - let content_type = mime_guess::from_path(url.path()) - .first() - // If you can guess that its an image type, then return that first. - .filter(|guess| guess.type_() == mime::IMAGE) - // Otherwise, get the content type from the headers - .or( - response - .headers() - .get(CONTENT_TYPE) - .and_then(|h| h.to_str().ok()) - .and_then(|h| h.parse().ok()), - ); + let mut content_type: Option = response + .headers() + .get(CONTENT_TYPE) + .and_then(|h| h.to_str().ok()) + .and_then(|h| h.parse().ok()) + // If we don't get a content_type from the response (e.g. if the server is down), + // then try to infer the content_type from the file extension. + .or(mime_guess::from_path(url.path()).first()); let opengraph_data = { - // if the content type is not text/html, we don't need to parse it let is_html = content_type .as_ref() .map(|c| { - (c.type_() == mime::TEXT && c.subtype() == mime::HTML) - || - // application/xhtml+xml is a subset of HTML - (c.type_() == mime::APPLICATION && c.subtype() == "xhtml") + // application/xhtml+xml is a subset of HTML + let application_xhtml: Mime = "application/xhtml+xml".parse::().unwrap_or(TEXT_HTML); + let allowed_mime_types = [TEXT_HTML.essence_str(), application_xhtml.essence_str()]; + allowed_mime_types.contains(&c.essence_str()) }) - .unwrap_or(false); - if !is_html { - Default::default() - } else { + .unwrap_or_default(); + + if is_html { // Can't use .text() here, because it only checks the content header, not the actual bytes // https://github.com/LemmyNet/lemmy/issues/1964 - // So we want to do deep inspection of the actually returned bytes but need to be careful not - // spend too much time parsing binary data as HTML - + // So we want to do deep inspection of the actually returned bytes but need to be careful + // not spend too much time parsing binary data as HTML // only take first bytes regardless of how many bytes the server returns let html_bytes = collect_bytes_until_limit(response, bytes_to_fetch).await?; extract_opengraph_data(&html_bytes, url) .map_err(|e| info!("{e}")) .unwrap_or_default() + } else { + let is_octet_type = content_type + .as_ref() + .map(|c| c.subtype() == "octet-stream") + .unwrap_or_default(); + + // Overwrite the content type if its an octet type + if is_octet_type { + // Don't need to fetch as much data for this as we do with opengraph + let octet_bytes = collect_bytes_until_limit(response, 512).await?; + content_type = + infer::get(&octet_bytes).map_or(content_type, |t| t.mime_type().parse().ok()); + } + + Default::default() } }; + Ok(LinkMetadata { opengraph_data, content_type: content_type.map(|c| c.to_string()), diff --git a/crates/api_common/src/site.rs b/crates/api_common/src/site.rs index 91c6151d7..7f1000b14 100644 --- a/crates/api_common/src/site.rs +++ b/crates/api_common/src/site.rs @@ -43,6 +43,8 @@ use lemmy_db_views_actor::structs::{ PersonView, }; use lemmy_db_views_moderator::structs::{ + AdminAllowInstanceView, + AdminBlockInstanceView, AdminPurgeCommentView, AdminPurgeCommunityView, AdminPurgePersonView, @@ -183,6 +185,8 @@ pub struct GetModlogResponse { pub admin_purged_posts: Vec, pub admin_purged_comments: Vec, pub hidden_communities: Vec, + pub admin_block_instance: Vec, + pub admin_allow_instance: Vec, } #[skip_serializing_none] @@ -265,10 +269,6 @@ pub struct CreateSite { #[cfg_attr(feature = "full", ts(optional))] pub captcha_difficulty: Option, #[cfg_attr(feature = "full", ts(optional))] - pub allowed_instances: Option>, - #[cfg_attr(feature = "full", ts(optional))] - pub blocked_instances: Option>, - #[cfg_attr(feature = "full", ts(optional))] pub registration_mode: Option, #[cfg_attr(feature = "full", ts(optional))] pub oauth_registration: Option, @@ -394,12 +394,6 @@ pub struct EditSite { /// The captcha difficulty. Can be easy, medium, or hard #[cfg_attr(feature = "full", ts(optional))] pub captcha_difficulty: Option, - /// A list of allowed instances. If none are set, federation is open. - #[cfg_attr(feature = "full", ts(optional))] - pub allowed_instances: Option>, - /// A list of blocked instances. - #[cfg_attr(feature = "full", ts(optional))] - pub blocked_instances: Option>, /// A list of blocked URLs #[cfg_attr(feature = "full", ts(optional))] pub blocked_urls: Option>, @@ -435,7 +429,7 @@ pub struct EditSite { /// The response for a site. pub struct SiteResponse { pub site_view: SiteView, - /// deprecated, use field `tagline` or /api/v3/tagline/list + /// deprecated, use field `tagline` or /api/v4/tagline/list pub taglines: Vec<()>, } @@ -448,14 +442,10 @@ pub struct GetSiteResponse { pub site_view: SiteView, pub admins: Vec, pub version: String, - #[cfg_attr(feature = "full", ts(optional))] + #[cfg_attr(feature = "full", ts(skip))] pub my_user: Option, pub all_languages: Vec, pub discussion_languages: Vec, - /// deprecated, use field `tagline` or /api/v3/tagline/list - pub taglines: Vec<()>, - /// deprecated, use /api/v3/custom_emoji/list - pub custom_emojis: Vec<()>, /// If the site has any taglines, a random one is included here for displaying #[cfg_attr(feature = "full", ts(optional))] pub tagline: Option, @@ -648,15 +638,29 @@ pub struct GetUnreadRegistrationApplicationCountResponse { #[cfg_attr(feature = "full", derive(TS))] #[cfg_attr(feature = "full", ts(export))] /// Block an instance as user -pub struct BlockInstance { +pub struct UserBlockInstanceParams { pub instance_id: InstanceId, pub block: bool, } -#[skip_serializing_none] -#[derive(Debug, Serialize, Deserialize, Clone)] +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Hash)] #[cfg_attr(feature = "full", derive(TS))] #[cfg_attr(feature = "full", ts(export))] -pub struct BlockInstanceResponse { - pub blocked: bool, +pub struct AdminBlockInstanceParams { + pub instance: String, + pub block: bool, + #[cfg_attr(feature = "full", ts(optional))] + pub reason: Option, + #[cfg_attr(feature = "full", ts(optional))] + pub expires: Option>, +} + +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "full", derive(TS))] +#[cfg_attr(feature = "full", ts(export))] +pub struct AdminAllowInstanceParams { + pub instance: String, + pub allow: bool, + #[cfg_attr(feature = "full", ts(optional))] + pub reason: Option, } diff --git a/crates/api_common/src/utils.rs b/crates/api_common/src/utils.rs index d46e57749..80f559edb 100644 --- a/crates/api_common/src/utils.rs +++ b/crates/api_common/src/utils.rs @@ -23,7 +23,12 @@ use lemmy_db_schema::{ local_site::LocalSite, local_site_rate_limit::LocalSiteRateLimit, local_site_url_blocklist::LocalSiteUrlBlocklist, - moderator::{ModRemoveComment, ModRemoveCommentForm, ModRemovePost, ModRemovePostForm}, + mod_log::moderator::{ + ModRemoveComment, + ModRemoveCommentForm, + ModRemovePost, + ModRemovePostForm, + }, oauth_account::OAuthAccount, password_reset_request::PasswordResetRequest, person::{Person, PersonUpdateForm}, @@ -71,7 +76,7 @@ use tracing::warn; use url::{ParseError, Url}; use urlencoding::encode; -pub static AUTH_COOKIE_NAME: &str = "jwt"; +pub const AUTH_COOKIE_NAME: &str = "jwt"; #[tracing::instrument(skip_all)] pub async fn is_mod_or_admin( @@ -118,8 +123,6 @@ pub fn is_admin(local_user_view: &LocalUserView) -> LemmyResult<()> { check_user_valid(&local_user_view.person)?; if !local_user_view.local_user.admin { Err(LemmyErrorType::NotAnAdmin)? - } else if local_user_view.person.banned { - Err(LemmyErrorType::Banned)? } else { Ok(()) } @@ -1120,7 +1123,7 @@ async fn proxy_image_link_internal( } } -/// Rewrite a link to go through `/api/v3/image_proxy` endpoint. This is only for remote urls and +/// Rewrite a link to go through `/api/v4/image_proxy` endpoint. This is only for remote urls and /// if image_proxy setting is enabled. pub async fn proxy_image_link(link: Url, context: &LemmyContext) -> LemmyResult { proxy_image_link_internal( @@ -1172,7 +1175,7 @@ fn build_proxied_image_url( protocol_and_hostname: &str, ) -> Result { Url::parse(&format!( - "{}/api/v3/image_proxy?url={}", + "{}/api/v4/image_proxy?url={}", protocol_and_hostname, encode(link.as_str()) )) @@ -1251,7 +1254,7 @@ mod tests { ) .await?; assert_eq!( - "https://lemmy-alpha/api/v3/image_proxy?url=http%3A%2F%2Flemmy-beta%2Fimage.png", + "https://lemmy-alpha/api/v4/image_proxy?url=http%3A%2F%2Flemmy-beta%2Fimage.png", proxied.as_str() ); diff --git a/crates/api_crud/Cargo.toml b/crates/api_crud/Cargo.toml index 3f1a00ccd..a05a4deed 100644 --- a/crates/api_crud/Cargo.toml +++ b/crates/api_crud/Cargo.toml @@ -29,6 +29,7 @@ anyhow.workspace = true chrono.workspace = true webmention = "0.6.0" accept-language = "3.1.0" +regex = { workspace = true } serde_json = { workspace = true } serde = { workspace = true } serde_with = { workspace = true } diff --git a/crates/api_crud/src/comment/remove.rs b/crates/api_crud/src/comment/remove.rs index 4e8a1871a..1ac6201e8 100644 --- a/crates/api_crud/src/comment/remove.rs +++ b/crates/api_crud/src/comment/remove.rs @@ -12,7 +12,7 @@ use lemmy_db_schema::{ comment::{Comment, CommentUpdateForm}, comment_report::CommentReport, local_user::LocalUser, - moderator::{ModRemoveComment, ModRemoveCommentForm}, + mod_log::moderator::{ModRemoveComment, ModRemoveCommentForm}, }, traits::{Crud, Reportable}, }; diff --git a/crates/api_crud/src/community/remove.rs b/crates/api_crud/src/community/remove.rs index c506bde1b..7dc78a37a 100644 --- a/crates/api_crud/src/community/remove.rs +++ b/crates/api_crud/src/community/remove.rs @@ -10,7 +10,7 @@ use lemmy_api_common::{ use lemmy_db_schema::{ source::{ community::{Community, CommunityUpdateForm}, - moderator::{ModRemoveCommunity, ModRemoveCommunityForm}, + mod_log::moderator::{ModRemoveCommunity, ModRemoveCommunityForm}, }, traits::Crud, }; diff --git a/crates/api_crud/src/oauth_provider/create.rs b/crates/api_crud/src/oauth_provider/create.rs index fe44ae56e..c1e30066a 100644 --- a/crates/api_crud/src/oauth_provider/create.rs +++ b/crates/api_crud/src/oauth_provider/create.rs @@ -35,6 +35,7 @@ pub async fn create_oauth_provider( scopes: data.scopes.to_string(), auto_verify_email: data.auto_verify_email, account_linking_enabled: data.account_linking_enabled, + use_pkce: data.use_pkce, enabled: data.enabled, }; let oauth_provider = OAuthProvider::create(&mut context.pool(), &oauth_provider_form).await?; diff --git a/crates/api_crud/src/oauth_provider/update.rs b/crates/api_crud/src/oauth_provider/update.rs index 29ba19b49..f8631a487 100644 --- a/crates/api_crud/src/oauth_provider/update.rs +++ b/crates/api_crud/src/oauth_provider/update.rs @@ -33,6 +33,7 @@ pub async fn update_oauth_provider( auto_verify_email: data.auto_verify_email, account_linking_enabled: data.account_linking_enabled, enabled: data.enabled, + use_pkce: data.use_pkce, updated: Some(Some(Utc::now())), }; diff --git a/crates/api_crud/src/post/remove.rs b/crates/api_crud/src/post/remove.rs index 7e3261e6f..95aa5fc56 100644 --- a/crates/api_crud/src/post/remove.rs +++ b/crates/api_crud/src/post/remove.rs @@ -11,7 +11,7 @@ use lemmy_db_schema::{ source::{ community::Community, local_user::LocalUser, - moderator::{ModRemovePost, ModRemovePostForm}, + mod_log::moderator::{ModRemovePost, ModRemovePostForm}, post::{Post, PostUpdateForm}, post_report::PostReport, }, diff --git a/crates/api_crud/src/site/read.rs b/crates/api_crud/src/site/read.rs index 6bee0fda6..220fe1bd5 100644 --- a/crates/api_crud/src/site/read.rs +++ b/crates/api_crud/src/site/read.rs @@ -1,30 +1,32 @@ +use crate::user::my_user::get_my_user; use actix_web::web::{Data, Json}; -use lemmy_api_common::{ - context::LemmyContext, - site::{GetSiteResponse, MyUserInfo}, -}; +use lemmy_api_common::{context::LemmyContext, site::GetSiteResponse}; use lemmy_db_schema::source::{ - actor_language::{LocalUserLanguage, SiteLanguage}, - community_block::CommunityBlock, - instance_block::InstanceBlock, + actor_language::SiteLanguage, language::Language, local_site_url_blocklist::LocalSiteUrlBlocklist, oauth_provider::OAuthProvider, - person_block::PersonBlock, tagline::Tagline, }; use lemmy_db_views::structs::{LocalUserView, SiteView}; -use lemmy_db_views_actor::structs::{CommunityFollowerView, CommunityModeratorView, PersonView}; -use lemmy_utils::{ - build_cache, - error::{LemmyErrorExt, LemmyErrorType, LemmyResult}, - CacheLock, - VERSION, -}; +use lemmy_db_views_actor::structs::PersonView; +use lemmy_utils::{build_cache, error::LemmyResult, CacheLock, VERSION}; use std::sync::LazyLock; #[tracing::instrument(skip(context))] -pub async fn get_site( +pub async fn get_site_v3( + local_user_view: Option, + context: Data, +) -> LemmyResult> { + let mut site = get_site_v4(local_user_view.clone(), context.clone()).await?; + if let Some(local_user_view) = local_user_view { + site.my_user = Some(get_my_user(local_user_view, context).await?.0); + } + Ok(site) +} + +#[tracing::instrument(skip(context))] +pub async fn get_site_v4( local_user_view: Option, context: Data, ) -> LemmyResult> { @@ -35,42 +37,6 @@ pub async fn get_site( .await .map_err(|e| anyhow::anyhow!("Failed to construct site response: {e}"))?; - // Build the local user with parallel queries and add it to site response - site_response.my_user = if let Some(ref local_user_view) = local_user_view { - let person_id = local_user_view.person.id; - let local_user_id = local_user_view.local_user.id; - let pool = &mut context.pool(); - - let ( - follows, - community_blocks, - instance_blocks, - person_blocks, - moderates, - discussion_languages, - ) = lemmy_db_schema::try_join_with_pool!(pool => ( - |pool| CommunityFollowerView::for_person(pool, person_id), - |pool| CommunityBlock::for_person(pool, person_id), - |pool| InstanceBlock::for_person(pool, person_id), - |pool| PersonBlock::for_person(pool, person_id), - |pool| CommunityModeratorView::for_person(pool, person_id, Some(&local_user_view.local_user)), - |pool| LocalUserLanguage::read(pool, local_user_id) - )) - .with_lemmy_type(LemmyErrorType::SystemErrLogin)?; - - Some(MyUserInfo { - local_user_view: local_user_view.clone(), - follows, - moderates, - community_blocks, - instance_blocks, - person_blocks, - discussion_languages, - }) - } else { - None - }; - // filter oauth_providers for public access if !local_user_view .map(|l| l.local_user.admin) @@ -103,7 +69,5 @@ async fn read_site(context: &LemmyContext) -> LemmyResult { tagline, oauth_providers: Some(oauth_providers), admin_oauth_providers: Some(admin_oauth_providers), - taglines: vec![], - custom_emojis: vec![], }) } diff --git a/crates/api_crud/src/site/update.rs b/crates/api_crud/src/site/update.rs index 6c23adfb4..d2585ea43 100644 --- a/crates/api_crud/src/site/update.rs +++ b/crates/api_crud/src/site/update.rs @@ -19,8 +19,6 @@ use lemmy_api_common::{ use lemmy_db_schema::{ source::{ actor_language::SiteLanguage, - federation_allowlist::FederationAllowList, - federation_blocklist::FederationBlockList, local_site::{LocalSite, LocalSiteUpdateForm}, local_site_rate_limit::{LocalSiteRateLimit, LocalSiteRateLimitUpdateForm}, local_site_url_blocklist::LocalSiteUrlBlocklist, @@ -152,12 +150,6 @@ pub async fn update_site( .await .ok(); - // Replace the blocked and allowed instances - let allowed = data.allowed_instances.clone(); - FederationAllowList::replace(&mut context.pool(), allowed).await?; - let blocked = data.blocked_instances.clone(); - FederationBlockList::replace(&mut context.pool(), blocked).await?; - if let Some(url_blocklist) = data.blocked_urls.clone() { let parsed_urls = check_urls_are_valid(&url_blocklist)?; LocalSiteUrlBlocklist::replace(&mut context.pool(), parsed_urls).await?; diff --git a/crates/api_crud/src/user/create.rs b/crates/api_crud/src/user/create.rs index 3bd372937..16156abe4 100644 --- a/crates/api_crud/src/user/create.rs +++ b/crates/api_crud/src/user/create.rs @@ -21,8 +21,9 @@ use lemmy_api_common::{ }; use lemmy_db_schema::{ aggregates::structs::PersonAggregates, - newtypes::{InstanceId, OAuthProviderId}, + newtypes::{InstanceId, OAuthProviderId, SiteId}, source::{ + actor_language::SiteLanguage, captcha_answer::{CaptchaAnswer, CheckCaptchaAnswer}, language::Language, local_site::LocalSite, @@ -44,9 +45,10 @@ use lemmy_utils::{ validation::is_valid_actor_name, }, }; +use regex::Regex; use serde::{Deserialize, Serialize}; use serde_with::skip_serializing_none; -use std::collections::HashSet; +use std::{collections::HashSet, sync::LazyLock}; #[skip_serializing_none] #[derive(Debug, Serialize, Deserialize, Clone, Default)] @@ -145,7 +147,13 @@ pub async fn register( ..LocalUserInsertForm::new(inserted_person.id, Some(data.password.to_string())) }; - let inserted_local_user = create_local_user(&context, language_tags, &local_user_form).await?; + let inserted_local_user = create_local_user( + &context, + language_tags, + &local_user_form, + local_site.site_id, + ) + .await?; if local_site.site_setup && require_registration_application { if let Some(answer) = data.answer.clone() { @@ -218,6 +226,11 @@ pub async fn authenticate_with_oauth( Err(LemmyErrorType::OauthAuthorizationInvalid)? } + // validate the PKCE challenge + if let Some(code_verifier) = &data.pkce_code_verifier { + check_code_verifier(code_verifier)?; + } + // Fetch the OAUTH provider and make sure it's enabled let oauth_provider_id = data.oauth_provider_id; let oauth_provider = OAuthProvider::read(&mut context.pool(), oauth_provider_id) @@ -229,9 +242,14 @@ pub async fn authenticate_with_oauth( return Err(LemmyErrorType::OauthAuthorizationInvalid)?; } - let token_response = - oauth_request_access_token(&context, &oauth_provider, &data.code, redirect_uri.as_str()) - .await?; + let token_response = oauth_request_access_token( + &context, + &oauth_provider, + &data.code, + data.pkce_code_verifier.as_deref(), + redirect_uri.as_str(), + ) + .await?; let user_info = oidc_get_user_info( &context, @@ -358,7 +376,13 @@ pub async fn authenticate_with_oauth( ..LocalUserInsertForm::new(person.id, None) }; - local_user = create_local_user(&context, language_tags, &local_user_form).await?; + local_user = create_local_user( + &context, + language_tags, + &local_user_form, + local_site.site_id, + ) + .await?; // Create the oauth account let oauth_account_form = @@ -449,15 +473,23 @@ async fn create_local_user( context: &Data, language_tags: Vec, local_user_form: &LocalUserInsertForm, + local_site_id: SiteId, ) -> Result { let all_languages = Language::read_all(&mut context.pool()).await?; // use hashset to avoid duplicates let mut language_ids = HashSet::new(); + + // Enable languages from `Accept-Language` header for l in language_tags { if let Some(found) = all_languages.iter().find(|all| all.code == l) { language_ids.insert(found.id); } } + + // Enable site languages. Ignored if all languages are enabled. + let discussion_languages = SiteLanguage::read(&mut context.pool(), local_site_id).await?; + language_ids.extend(discussion_languages); + let language_ids = language_ids.into_iter().collect(); let inserted_local_user = @@ -512,20 +544,27 @@ async fn oauth_request_access_token( context: &Data, oauth_provider: &OAuthProvider, code: &str, + pkce_code_verifier: Option<&str>, redirect_uri: &str, ) -> LemmyResult { + let mut form = vec![ + ("client_id", &*oauth_provider.client_id), + ("client_secret", &*oauth_provider.client_secret), + ("code", code), + ("grant_type", "authorization_code"), + ("redirect_uri", redirect_uri), + ]; + + if let Some(code_verifier) = pkce_code_verifier { + form.push(("code_verifier", code_verifier)); + } + // Request an Access Token from the OAUTH provider let response = context .client() .post(oauth_provider.token_endpoint.as_str()) .header("Accept", "application/json") - .form(&[ - ("grant_type", "authorization_code"), - ("code", code), - ("redirect_uri", redirect_uri), - ("client_id", &oauth_provider.client_id), - ("client_secret", &oauth_provider.client_secret), - ]) + .form(&form[..]) .send() .await .with_lemmy_type(LemmyErrorType::OauthLoginFailed)? @@ -575,3 +614,17 @@ fn read_user_info(user_info: &serde_json::Value, key: &str) -> LemmyResult LemmyResult<()> { + static VALID_CODE_VERIFIER_REGEX: LazyLock = + LazyLock::new(|| Regex::new(r"^[a-zA-Z0-9\-._~]{43,128}$").expect("compile regex")); + + let check = VALID_CODE_VERIFIER_REGEX.is_match(code_verifier); + + if check { + Ok(()) + } else { + Err(LemmyErrorType::InvalidCodeVerifier.into()) + } +} diff --git a/crates/api_crud/src/user/mod.rs b/crates/api_crud/src/user/mod.rs index da1aa3ace..e6e88e6cd 100644 --- a/crates/api_crud/src/user/mod.rs +++ b/crates/api_crud/src/user/mod.rs @@ -1,2 +1,3 @@ pub mod create; pub mod delete; +pub mod my_user; diff --git a/crates/api_crud/src/user/my_user.rs b/crates/api_crud/src/user/my_user.rs new file mode 100644 index 000000000..f7a92eb99 --- /dev/null +++ b/crates/api_crud/src/user/my_user.rs @@ -0,0 +1,45 @@ +use actix_web::web::{Data, Json}; +use lemmy_api_common::{context::LemmyContext, site::MyUserInfo, utils::check_user_valid}; +use lemmy_db_schema::source::{ + actor_language::LocalUserLanguage, + community_block::CommunityBlock, + instance_block::InstanceBlock, + person_block::PersonBlock, +}; +use lemmy_db_views::structs::LocalUserView; +use lemmy_db_views_actor::structs::{CommunityFollowerView, CommunityModeratorView}; +use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult}; + +#[tracing::instrument(skip(context))] +pub async fn get_my_user( + local_user_view: LocalUserView, + context: Data, +) -> LemmyResult> { + check_user_valid(&local_user_view.person)?; + + // Build the local user with parallel queries and add it to site response + let person_id = local_user_view.person.id; + let local_user_id = local_user_view.local_user.id; + let pool = &mut context.pool(); + + let (follows, community_blocks, instance_blocks, person_blocks, moderates, discussion_languages) = + lemmy_db_schema::try_join_with_pool!(pool => ( + |pool| CommunityFollowerView::for_person(pool, person_id), + |pool| CommunityBlock::for_person(pool, person_id), + |pool| InstanceBlock::for_person(pool, person_id), + |pool| PersonBlock::for_person(pool, person_id), + |pool| CommunityModeratorView::for_person(pool, person_id, Some(&local_user_view.local_user)), + |pool| LocalUserLanguage::read(pool, local_user_id) + )) + .with_lemmy_type(LemmyErrorType::SystemErrLogin)?; + + Ok(Json(MyUserInfo { + local_user_view: local_user_view.clone(), + follows, + moderates, + community_blocks, + instance_blocks, + person_blocks, + discussion_languages, + })) +} diff --git a/crates/apub/assets/pleroma/objects/chat_message.json b/crates/apub/assets/pleroma/objects/chat_message.json new file mode 100644 index 000000000..6a2afc82e --- /dev/null +++ b/crates/apub/assets/pleroma/objects/chat_message.json @@ -0,0 +1,15 @@ +{ + "@context": [ + "https://www.w3.org/ns/activitystreams", + "https://queer.hacktivis.me/schemas/litepub-0.1.jsonld", + { + "@language": "und" + } + ], + "attributedTo": "https://queer.hacktivis.me/users/lanodan", + "content": "Hi!", + "id": "https://queer.hacktivis.me/objects/2", + "published": "2020-02-12T14:08:20Z", + "to": ["https://enterprise.lemmy.ml/u/picard"], + "type": "ChatMessage" +} diff --git a/crates/apub/src/activities/block/block_user.rs b/crates/apub/src/activities/block/block_user.rs index 866e1cc6c..64c402482 100644 --- a/crates/apub/src/activities/block/block_user.rs +++ b/crates/apub/src/activities/block/block_user.rs @@ -36,7 +36,7 @@ use lemmy_db_schema::{ CommunityPersonBan, CommunityPersonBanForm, }, - moderator::{ModBan, ModBanForm, ModBanFromCommunity, ModBanFromCommunityForm}, + mod_log::moderator::{ModBan, ModBanForm, ModBanFromCommunity, ModBanFromCommunityForm}, person::{Person, PersonUpdateForm}, }, traits::{Bannable, Crud, Followable}, diff --git a/crates/apub/src/activities/block/undo_block_user.rs b/crates/apub/src/activities/block/undo_block_user.rs index 29fc22f0c..122eae429 100644 --- a/crates/apub/src/activities/block/undo_block_user.rs +++ b/crates/apub/src/activities/block/undo_block_user.rs @@ -27,7 +27,7 @@ use lemmy_db_schema::{ source::{ activity::ActivitySendTargets, community::{CommunityPersonBan, CommunityPersonBanForm}, - moderator::{ModBan, ModBanForm, ModBanFromCommunity, ModBanFromCommunityForm}, + mod_log::moderator::{ModBan, ModBanForm, ModBanFromCommunity, ModBanFromCommunityForm}, person::{Person, PersonUpdateForm}, }, traits::{Bannable, Crud}, diff --git a/crates/apub/src/activities/community/collection_add.rs b/crates/apub/src/activities/community/collection_add.rs index ae508c2c5..1014229c8 100644 --- a/crates/apub/src/activities/community/collection_add.rs +++ b/crates/apub/src/activities/community/collection_add.rs @@ -31,7 +31,7 @@ use lemmy_db_schema::{ source::{ activity::ActivitySendTargets, community::{Community, CommunityModerator, CommunityModeratorForm}, - moderator::{ModAddCommunity, ModAddCommunityForm}, + mod_log::moderator::{ModAddCommunity, ModAddCommunityForm}, person::Person, post::{Post, PostUpdateForm}, }, diff --git a/crates/apub/src/activities/community/collection_remove.rs b/crates/apub/src/activities/community/collection_remove.rs index 6c08735ed..c94286703 100644 --- a/crates/apub/src/activities/community/collection_remove.rs +++ b/crates/apub/src/activities/community/collection_remove.rs @@ -27,7 +27,7 @@ use lemmy_db_schema::{ source::{ activity::ActivitySendTargets, community::{Community, CommunityModerator, CommunityModeratorForm}, - moderator::{ModAddCommunity, ModAddCommunityForm}, + mod_log::moderator::{ModAddCommunity, ModAddCommunityForm}, post::{Post, PostUpdateForm}, }, traits::{Crud, Joinable}, diff --git a/crates/apub/src/activities/community/lock_page.rs b/crates/apub/src/activities/community/lock_page.rs index a9bacea8a..af6a5796f 100644 --- a/crates/apub/src/activities/community/lock_page.rs +++ b/crates/apub/src/activities/community/lock_page.rs @@ -27,7 +27,7 @@ use lemmy_db_schema::{ source::{ activity::ActivitySendTargets, community::Community, - moderator::{ModLockPost, ModLockPostForm}, + mod_log::moderator::{ModLockPost, ModLockPostForm}, person::Person, post::{Post, PostUpdateForm}, }, diff --git a/crates/apub/src/activities/create_or_update/comment.rs b/crates/apub/src/activities/create_or_update/comment.rs index 93cac92ee..72dae48b7 100644 --- a/crates/apub/src/activities/create_or_update/comment.rs +++ b/crates/apub/src/activities/create_or_update/comment.rs @@ -99,8 +99,11 @@ impl CreateOrUpdateNote { inboxes.add_inbox(person.shared_inbox_or_inbox()); } - let activity = - AnnouncableActivities::CreateOrUpdateNoteWrapper(from_value(to_value(create_or_update)?)?); + // AnnouncableActivities doesnt contain Comment activity but only NoteWrapper, + // to be able to handle both comment and private message. So to send this out we need + // to convert this to NoteWrapper, by serializing and then deserializing again. + let converted = from_value(to_value(create_or_update)?)?; + let activity = AnnouncableActivities::CreateOrUpdateNoteWrapper(converted); send_activity_in_community(activity, &person, &community, inboxes, false, &context).await } } diff --git a/crates/apub/src/activities/create_or_update/note_wrapper.rs b/crates/apub/src/activities/create_or_update/note_wrapper.rs index 9206d0c05..ca79b45d2 100644 --- a/crates/apub/src/activities/create_or_update/note_wrapper.rs +++ b/crates/apub/src/activities/create_or_update/note_wrapper.rs @@ -1,5 +1,5 @@ use crate::{ - objects::{community::ApubCommunity, note_wrapper::is_public}, + objects::community::ApubCommunity, protocol::{ activities::create_or_update::{ note::CreateOrUpdateNote, @@ -11,10 +11,13 @@ use crate::{ }; use activitypub_federation::{config::Data, traits::ActivityHandler}; use lemmy_api_common::context::LemmyContext; -use lemmy_utils::error::{FederationError, LemmyError, LemmyResult}; +use lemmy_utils::error::{LemmyError, LemmyResult}; use serde_json::{from_value, to_value}; use url::Url; +/// In Activitypub, both private messages and comments are represented by `type: Note` which +/// makes it difficult to distinguish them. This wrapper handles receiving of both types, and +/// routes them to the correct handler. #[async_trait::async_trait] impl ActivityHandler for CreateOrUpdateNoteWrapper { type DataType = LemmyContext; @@ -29,38 +32,43 @@ impl ActivityHandler for CreateOrUpdateNoteWrapper { } #[tracing::instrument(skip_all)] - async fn verify(&self, context: &Data) -> LemmyResult<()> { - let val = to_value(self)?; - if is_public(&self.to, &self.cc) { - CreateOrUpdateNote::verify(&from_value(val)?, context).await?; - } else { - CreateOrUpdatePrivateMessage::verify(&from_value(val)?, context).await?; - } + async fn verify(&self, _context: &Data) -> LemmyResult<()> { + // Do everything in receive to avoid extra checks. Ok(()) } #[tracing::instrument(skip_all)] async fn receive(self, context: &Data) -> LemmyResult<()> { - let is_public = is_public(&self.to, &self.cc); + // Use serde to convert NoteWrapper either into Comment or PrivateMessage, + // depending on conditions below. This works because NoteWrapper keeps all + // additional data in field `other: Map`. let val = to_value(self)?; - if is_public { - CreateOrUpdateNote::receive(from_value(val)?, context).await?; - } else { - CreateOrUpdatePrivateMessage::receive(from_value(val)?, context).await?; + + // Convert self to a comment and get the community. If the conversion is + // successful and a community is returned, this is a comment. + let comment = from_value::(val.clone()); + if let Ok(comment) = comment { + if comment.community(context).await.is_ok() { + CreateOrUpdateNote::verify(&comment, context).await?; + CreateOrUpdateNote::receive(comment, context).await?; + return Ok(()); + } } + + // If any of the previous checks failed, we are dealing with a private message. + let private_message = from_value(val)?; + CreateOrUpdatePrivateMessage::verify(&private_message, context).await?; + CreateOrUpdatePrivateMessage::receive(private_message, context).await?; Ok(()) } } #[async_trait::async_trait] impl InCommunity for CreateOrUpdateNoteWrapper { - #[tracing::instrument(skip(self, context))] async fn community(&self, context: &Data) -> LemmyResult { - if is_public(&self.to, &self.cc) { - let comment: CreateOrUpdateNote = from_value(to_value(self)?)?; - comment.community(context).await - } else { - Err(FederationError::ObjectIsNotPublic.into()) - } + // Same logic as in receive. In case this is a private message, an error is returned. + let val = to_value(self)?; + let comment: CreateOrUpdateNote = from_value(val.clone())?; + comment.community(context).await } } diff --git a/crates/apub/src/activities/deletion/delete.rs b/crates/apub/src/activities/deletion/delete.rs index 064f0bc82..4ad24d966 100644 --- a/crates/apub/src/activities/deletion/delete.rs +++ b/crates/apub/src/activities/deletion/delete.rs @@ -14,7 +14,7 @@ use lemmy_db_schema::{ comment::{Comment, CommentUpdateForm}, comment_report::CommentReport, community::{Community, CommunityUpdateForm}, - moderator::{ + mod_log::moderator::{ ModRemoveComment, ModRemoveCommentForm, ModRemoveCommunity, diff --git a/crates/apub/src/activities/deletion/undo_delete.rs b/crates/apub/src/activities/deletion/undo_delete.rs index f4a7bb9b9..b30b22fd4 100644 --- a/crates/apub/src/activities/deletion/undo_delete.rs +++ b/crates/apub/src/activities/deletion/undo_delete.rs @@ -13,7 +13,7 @@ use lemmy_db_schema::{ source::{ comment::{Comment, CommentUpdateForm}, community::{Community, CommunityUpdateForm}, - moderator::{ + mod_log::moderator::{ ModRemoveComment, ModRemoveCommentForm, ModRemoveCommunity, diff --git a/crates/apub/src/api/read_person.rs b/crates/apub/src/api/read_person.rs index fac68cd63..72dce8140 100644 --- a/crates/apub/src/api/read_person.rs +++ b/crates/apub/src/api/read_person.rs @@ -4,7 +4,7 @@ use actix_web::web::{Json, Query}; use lemmy_api_common::{ context::LemmyContext, person::{GetPersonDetails, GetPersonDetailsResponse}, - utils::{check_private_instance, read_site_for_actor}, + utils::{check_private_instance, is_admin, read_site_for_actor}, }; use lemmy_db_schema::{source::person::Person, utils::post_to_comment_sort_type}; use lemmy_db_views::{ @@ -45,7 +45,11 @@ pub async fn read_person( // You don't need to return settings for the user, since this comes back with GetSite // `my_user` - let person_view = PersonView::read(&mut context.pool(), person_details_id).await?; + let is_admin = local_user_view + .as_ref() + .map(|l| is_admin(l).is_ok()) + .unwrap_or_default(); + let person_view = PersonView::read(&mut context.pool(), person_details_id, is_admin).await?; let sort = data.sort; let page = data.page; diff --git a/crates/apub/src/api/resolve_object.rs b/crates/apub/src/api/resolve_object.rs index 04d489592..8d2cd384f 100644 --- a/crates/apub/src/api/resolve_object.rs +++ b/crates/apub/src/api/resolve_object.rs @@ -60,7 +60,7 @@ async fn convert_response( } }, SearchableObjects::PersonOrCommunity(pc) => match *pc { - UserOrCommunity::User(u) => res.person = Some(PersonView::read(pool, u.id).await?), + UserOrCommunity::User(u) => res.person = Some(PersonView::read(pool, u.id, is_admin).await?), UserOrCommunity::Community(c) => { res.community = Some(CommunityView::read(pool, c.id, local_user.as_ref(), is_admin).await?) } diff --git a/crates/apub/src/api/user_settings_backup.rs b/crates/apub/src/api/user_settings_backup.rs index d5a864bec..6184df7d3 100644 --- a/crates/apub/src/api/user_settings_backup.rs +++ b/crates/apub/src/api/user_settings_backup.rs @@ -322,7 +322,7 @@ pub(crate) mod tests { CommunityFollowerState, CommunityInsertForm, }, - local_user::LocalUser, + person::Person, }, traits::{Crud, Followable}, }; @@ -376,8 +376,8 @@ pub(crate) mod tests { assert_eq!(follows.len(), 1); assert_eq!(follows[0].community.actor_id, community.actor_id); - LocalUser::delete(pool, export_user.local_user.id).await?; - LocalUser::delete(pool, import_user.local_user.id).await?; + Person::delete(pool, export_user.person.id).await?; + Person::delete(pool, import_user.person.id).await?; Ok(()) } @@ -412,8 +412,8 @@ pub(crate) mod tests { Some(LemmyErrorType::TooManyItems) ); - LocalUser::delete(pool, export_user.local_user.id).await?; - LocalUser::delete(pool, import_user.local_user.id).await?; + Person::delete(pool, export_user.person.id).await?; + Person::delete(pool, import_user.person.id).await?; Ok(()) } diff --git a/crates/apub/src/fetcher/markdown_links.rs b/crates/apub/src/fetcher/markdown_links.rs index d83aae515..a5e51caa7 100644 --- a/crates/apub/src/fetcher/markdown_links.rs +++ b/crates/apub/src/fetcher/markdown_links.rs @@ -42,7 +42,8 @@ pub async fn markdown_rewrite_remote_links( let mut local_url = local_url.to_string(); // restore title if let Some(extra) = extra { - local_url = format!("{local_url} {extra}"); + local_url.push(' '); + local_url.push_str(extra); } src.replace_range(start..end, local_url.as_str()); } diff --git a/crates/apub/src/objects/mod.rs b/crates/apub/src/objects/mod.rs index 58841b29e..f837f7ad3 100644 --- a/crates/apub/src/objects/mod.rs +++ b/crates/apub/src/objects/mod.rs @@ -15,7 +15,6 @@ use std::fmt::Debug; pub mod comment; pub mod community; pub mod instance; -pub mod note_wrapper; pub mod person; pub mod post; pub mod private_message; diff --git a/crates/apub/src/objects/note_wrapper.rs b/crates/apub/src/objects/note_wrapper.rs deleted file mode 100644 index 5d613c7ae..000000000 --- a/crates/apub/src/objects/note_wrapper.rs +++ /dev/null @@ -1,85 +0,0 @@ -use super::comment::ApubComment; -use crate::{ - objects::private_message::ApubPrivateMessage, - protocol::objects::note_wrapper::NoteWrapper, -}; -use activitypub_federation::{config::Data, kinds::public, traits::Object}; -use chrono::{DateTime, Utc}; -use lemmy_api_common::{context::LemmyContext, LemmyErrorType}; -use lemmy_utils::error::{LemmyError, LemmyResult}; -use serde_json::{from_value, to_value}; -use url::Url; - -/// Private messages and public comments are quite awkward in Activitypub, because the json -/// format looks identical. They only way to differentiate them is to check for the presence -/// or absence of `https://www.w3.org/ns/activitystreams#Public` in `to` or `cc` which this -/// wrapper does. -#[derive(Debug)] -pub(crate) struct ApubNote {} - -#[async_trait::async_trait] -impl Object for ApubNote { - type DataType = LemmyContext; - type Kind = NoteWrapper; - type Error = LemmyError; - - fn last_refreshed_at(&self) -> Option> { - None - } - - #[tracing::instrument(skip_all)] - async fn read_from_id( - _object_id: Url, - _context: &Data, - ) -> LemmyResult> { - Err(LemmyErrorType::Unknown("not implemented".to_string()).into()) - } - - #[tracing::instrument(skip_all)] - async fn delete(self, _context: &Data) -> LemmyResult<()> { - Err(LemmyErrorType::Unknown("not implemented".to_string()).into()) - } - - async fn verify( - note: &NoteWrapper, - expected_domain: &Url, - context: &Data, - ) -> LemmyResult<()> { - let val = to_value(note)?; - if is_public(¬e.to, ¬e.cc) { - ApubComment::verify(&from_value(val)?, expected_domain, context).await?; - } else { - ApubPrivateMessage::verify(&from_value(val)?, expected_domain, context).await?; - } - Ok(()) - } - - async fn from_json(note: NoteWrapper, context: &Data) -> LemmyResult { - let is_public = is_public(¬e.to, ¬e.cc); - let val = to_value(note)?; - if is_public { - ApubComment::from_json(from_value(val)?, context).await?; - } else { - ApubPrivateMessage::from_json(from_value(val)?, context).await?; - } - Ok(ApubNote {}) - } - - async fn into_json(self, _context: &Data) -> LemmyResult { - Err(LemmyErrorType::Unknown("not implemented".to_string()).into()) - } -} - -pub(crate) fn is_public(to: &Option>, cc: &Option>) -> bool { - if let Some(to) = to { - if to.contains(&public()) { - return true; - } - } - if let Some(cc) = cc { - if cc.contains(&public()) { - return true; - } - } - false -} diff --git a/crates/apub/src/objects/private_message.rs b/crates/apub/src/objects/private_message.rs index ba64bd593..ec3e16fac 100644 --- a/crates/apub/src/objects/private_message.rs +++ b/crates/apub/src/objects/private_message.rs @@ -242,4 +242,24 @@ mod tests { cleanup(data, &context).await?; Ok(()) } + + #[tokio::test] + #[serial] + async fn test_parse_pleroma_pm() -> LemmyResult<()> { + let context = LemmyContext::init_test_context().await; + let url = Url::parse("https://enterprise.lemmy.ml/private_message/1621")?; + let data = prepare_comment_test(&url, &context).await?; + let pleroma_url = Url::parse("https://queer.hacktivis.me/objects/2")?; + let json = file_to_json_object("assets/pleroma/objects/chat_message.json")?; + ApubPrivateMessage::verify(&json, &pleroma_url, &context).await?; + let pm = ApubPrivateMessage::from_json(json, &context).await?; + + assert_eq!(pm.ap_id, pleroma_url.into()); + assert_eq!(pm.content.len(), 3); + assert_eq!(context.request_count(), 0); + + DbPrivateMessage::delete(&mut context.pool(), pm.id).await?; + cleanup(data, &context).await?; + Ok(()) + } } diff --git a/crates/apub/src/protocol/activities/create_or_update/note_wrapper.rs b/crates/apub/src/protocol/activities/create_or_update/note_wrapper.rs index bc53c80fd..242bfe519 100644 --- a/crates/apub/src/protocol/activities/create_or_update/note_wrapper.rs +++ b/crates/apub/src/protocol/activities/create_or_update/note_wrapper.rs @@ -1,4 +1,4 @@ -use crate::protocol::objects::note_wrapper::NoteWrapper; +use activitypub_federation::kinds::object::NoteType; use serde::{Deserialize, Serialize}; use serde_json::{Map, Value}; use url::Url; @@ -6,11 +6,21 @@ use url::Url; #[derive(Clone, Debug, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct CreateOrUpdateNoteWrapper { - object: NoteWrapper, + pub(crate) object: NoteWrapper, pub(crate) id: Url, + #[serde(default)] + pub(crate) to: Vec, + #[serde(default)] + pub(crate) cc: Vec, pub(crate) actor: Url, - pub(crate) to: Option>, - pub(crate) cc: Option>, + #[serde(flatten)] + other: Map, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct NoteWrapper { + pub(crate) r#type: NoteType, #[serde(flatten)] other: Map, } diff --git a/crates/apub/src/protocol/mod.rs b/crates/apub/src/protocol/mod.rs index 9f218e351..a4774ac1d 100644 --- a/crates/apub/src/protocol/mod.rs +++ b/crates/apub/src/protocol/mod.rs @@ -116,7 +116,6 @@ pub(crate) mod tests { // parse file into hashmap, which ensures that every field is included let raw = file_to_json_object::>(path)?; // assert that all fields are identical, otherwise print diff - //dbg!(&parsed, &raw); assert_json_include!(actual: &parsed, expected: raw); Ok(parsed) } diff --git a/crates/apub/src/protocol/objects/mod.rs b/crates/apub/src/protocol/objects/mod.rs index 757f49ae4..acc8c14dd 100644 --- a/crates/apub/src/protocol/objects/mod.rs +++ b/crates/apub/src/protocol/objects/mod.rs @@ -11,7 +11,6 @@ use url::Url; pub(crate) mod group; pub(crate) mod instance; pub(crate) mod note; -pub(crate) mod note_wrapper; pub(crate) mod page; pub(crate) mod person; pub(crate) mod private_message; @@ -102,8 +101,8 @@ impl LanguageTag { #[cfg(test)] mod tests { - use super::note_wrapper::NoteWrapper; use crate::protocol::{ + activities::create_or_update::note_wrapper::NoteWrapper, objects::{ group::Group, instance::Instance, diff --git a/crates/apub/src/protocol/objects/note_wrapper.rs b/crates/apub/src/protocol/objects/note_wrapper.rs deleted file mode 100644 index f1bcf605b..000000000 --- a/crates/apub/src/protocol/objects/note_wrapper.rs +++ /dev/null @@ -1,14 +0,0 @@ -use activitypub_federation::kinds::object::NoteType; -use serde::{Deserialize, Serialize}; -use serde_json::{Map, Value}; -use url::Url; - -#[derive(Clone, Debug, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -pub(crate) struct NoteWrapper { - pub(crate) r#type: NoteType, - pub(crate) to: Option>, - pub(crate) cc: Option>, - #[serde(flatten)] - other: Map, -} diff --git a/crates/apub/src/protocol/objects/private_message.rs b/crates/apub/src/protocol/objects/private_message.rs index bf7fe90cb..93b9ba39c 100644 --- a/crates/apub/src/protocol/objects/private_message.rs +++ b/crates/apub/src/protocol/objects/private_message.rs @@ -34,7 +34,7 @@ pub struct PrivateMessage { #[derive(Clone, Debug, Deserialize, Serialize)] pub enum PrivateMessageType { - /// For compatibility with Lemmy 0.19 and earlier + /// Deprecated, for compatibility with Lemmy 0.19 and earlier /// https://docs.pleroma.social/backend/development/ap_extensions/#chatmessages ChatMessage, Note, diff --git a/crates/db_schema/src/impls/federation_allowlist.rs b/crates/db_schema/src/impls/federation_allowlist.rs index 099e0b231..41ced26f7 100644 --- a/crates/db_schema/src/impls/federation_allowlist.rs +++ b/crates/db_schema/src/impls/federation_allowlist.rs @@ -1,60 +1,51 @@ use crate::{ - schema::federation_allowlist, + newtypes::InstanceId, + schema::{admin_allow_instance, federation_allowlist}, source::{ federation_allowlist::{FederationAllowList, FederationAllowListForm}, - instance::Instance, + mod_log::admin::{AdminAllowInstance, AdminAllowInstanceForm}, }, utils::{get_conn, DbPool}, }; -use diesel::{dsl::insert_into, result::Error}; -use diesel_async::{AsyncPgConnection, RunQueryDsl}; +use diesel::{delete, dsl::insert_into, result::Error, ExpressionMethods, QueryDsl}; +use diesel_async::RunQueryDsl; -impl FederationAllowList { - pub async fn replace(pool: &mut DbPool<'_>, list_opt: Option>) -> Result<(), Error> { +impl AdminAllowInstance { + pub async fn insert(pool: &mut DbPool<'_>, form: &AdminAllowInstanceForm) -> Result<(), Error> { let conn = &mut get_conn(pool).await?; - conn - .build_transaction() - .run(|conn| { - Box::pin(async move { - if let Some(list) = list_opt { - Self::clear(conn).await?; - - for domain in list { - // Upsert all of these as instances - let instance = Instance::read_or_create(&mut conn.into(), domain).await?; - - let form = FederationAllowListForm { - instance_id: instance.id, - updated: None, - }; - insert_into(federation_allowlist::table) - .values(form) - .get_result::(conn) - .await?; - } - Ok(()) - } else { - Ok(()) - } - }) as _ - }) - .await - } - - async fn clear(conn: &mut AsyncPgConnection) -> Result { - diesel::delete(federation_allowlist::table) + insert_into(admin_allow_instance::table) + .values(form) .execute(conn) - .await + .await?; + + Ok(()) } } + +impl FederationAllowList { + pub async fn allow(pool: &mut DbPool<'_>, form: &FederationAllowListForm) -> Result<(), Error> { + let conn = &mut get_conn(pool).await?; + insert_into(federation_allowlist::table) + .values(form) + .execute(conn) + .await?; + Ok(()) + } + pub async fn unallow(pool: &mut DbPool<'_>, instance_id_: InstanceId) -> Result<(), Error> { + use federation_allowlist::dsl::instance_id; + let conn = &mut get_conn(pool).await?; + delete(federation_allowlist::table.filter(instance_id.eq(instance_id_))) + .execute(conn) + .await?; + Ok(()) + } +} + #[cfg(test)] mod tests { - use crate::{ - source::{federation_allowlist::FederationAllowList, instance::Instance}, - utils::build_db_pool_for_tests, - }; - use diesel::result::Error; + use super::*; + use crate::{source::instance::Instance, utils::build_db_pool_for_tests}; use pretty_assertions::assert_eq; use serial_test::serial; @@ -63,31 +54,33 @@ mod tests { async fn test_allowlist_insert_and_clear() -> Result<(), Error> { let pool = &build_db_pool_for_tests(); let pool = &mut pool.into(); - let domains = vec![ - "tld1.xyz".to_string(), - "tld2.xyz".to_string(), - "tld3.xyz".to_string(), + let instances = vec![ + Instance::read_or_create(pool, "tld1.xyz".to_string()).await?, + Instance::read_or_create(pool, "tld2.xyz".to_string()).await?, + Instance::read_or_create(pool, "tld3.xyz".to_string()).await?, ]; + let forms: Vec<_> = instances + .iter() + .map(|i| FederationAllowListForm { + instance_id: i.id, + updated: None, + }) + .collect(); - let allowed = Some(domains.clone()); - - FederationAllowList::replace(pool, allowed).await?; + for f in &forms { + FederationAllowList::allow(pool, f).await?; + } let allows = Instance::allowlist(pool).await?; - let allows_domains = allows - .iter() - .map(|i| i.domain.clone()) - .collect::>(); assert_eq!(3, allows.len()); - assert_eq!(domains, allows_domains); + assert_eq!(instances, allows); - // Now test clearing them via Some(empty vec) - let clear_allows = Some(Vec::new()); - - FederationAllowList::replace(pool, clear_allows).await?; + // Now test clearing them + for f in forms { + FederationAllowList::unallow(pool, f.instance_id).await?; + } let allows = Instance::allowlist(pool).await?; - assert_eq!(0, allows.len()); Instance::delete_all(pool).await?; diff --git a/crates/db_schema/src/impls/federation_blocklist.rs b/crates/db_schema/src/impls/federation_blocklist.rs index 2a6e0671d..4a42e81b6 100644 --- a/crates/db_schema/src/impls/federation_blocklist.rs +++ b/crates/db_schema/src/impls/federation_blocklist.rs @@ -1,49 +1,42 @@ use crate::{ - schema::federation_blocklist, + newtypes::InstanceId, + schema::{admin_block_instance, federation_blocklist}, source::{ federation_blocklist::{FederationBlockList, FederationBlockListForm}, - instance::Instance, + mod_log::admin::{AdminBlockInstance, AdminBlockInstanceForm}, }, utils::{get_conn, DbPool}, }; -use diesel::{dsl::insert_into, result::Error}; -use diesel_async::{AsyncPgConnection, RunQueryDsl}; +use diesel::{delete, dsl::insert_into, result::Error, ExpressionMethods, QueryDsl}; +use diesel_async::RunQueryDsl; -impl FederationBlockList { - pub async fn replace(pool: &mut DbPool<'_>, list_opt: Option>) -> Result<(), Error> { +impl AdminBlockInstance { + pub async fn insert(pool: &mut DbPool<'_>, form: &AdminBlockInstanceForm) -> Result<(), Error> { let conn = &mut get_conn(pool).await?; - conn - .build_transaction() - .run(|conn| { - Box::pin(async move { - if let Some(list) = list_opt { - Self::clear(conn).await?; - - for domain in list { - // Upsert all of these as instances - let instance = Instance::read_or_create(&mut conn.into(), domain).await?; - - let form = FederationBlockListForm { - instance_id: instance.id, - updated: None, - }; - insert_into(federation_blocklist::table) - .values(form) - .get_result::(conn) - .await?; - } - Ok(()) - } else { - Ok(()) - } - }) as _ - }) - .await - } - - async fn clear(conn: &mut AsyncPgConnection) -> Result { - diesel::delete(federation_blocklist::table) + insert_into(admin_block_instance::table) + .values(form) .execute(conn) - .await + .await?; + + Ok(()) + } +} + +impl FederationBlockList { + pub async fn block(pool: &mut DbPool<'_>, form: &FederationBlockListForm) -> Result<(), Error> { + let conn = &mut get_conn(pool).await?; + insert_into(federation_blocklist::table) + .values(form) + .execute(conn) + .await?; + Ok(()) + } + pub async fn unblock(pool: &mut DbPool<'_>, instance_id_: InstanceId) -> Result<(), Error> { + use federation_blocklist::dsl::instance_id; + let conn = &mut get_conn(pool).await?; + delete(federation_blocklist::table.filter(instance_id.eq(instance_id_))) + .execute(conn) + .await?; + Ok(()) } } diff --git a/crates/db_schema/src/impls/mod.rs b/crates/db_schema/src/impls/mod.rs index f115a101f..2d7a16c2c 100644 --- a/crates/db_schema/src/impls/mod.rs +++ b/crates/db_schema/src/impls/mod.rs @@ -21,7 +21,7 @@ pub mod local_site_url_blocklist; pub mod local_user; pub mod local_user_vote_display_mode; pub mod login_token; -pub mod moderator; +pub mod mod_log; pub mod oauth_account; pub mod oauth_provider; pub mod password_reset_request; @@ -35,4 +35,5 @@ pub mod private_message_report; pub mod registration_application; pub mod secret; pub mod site; +pub mod tag; pub mod tagline; diff --git a/crates/db_schema/src/impls/mod_log/admin.rs b/crates/db_schema/src/impls/mod_log/admin.rs new file mode 100644 index 000000000..c1b2bf69f --- /dev/null +++ b/crates/db_schema/src/impls/mod_log/admin.rs @@ -0,0 +1,132 @@ +use crate::{ + source::mod_log::admin::{ + AdminPurgeComment, + AdminPurgeCommentForm, + AdminPurgeCommunity, + AdminPurgeCommunityForm, + AdminPurgePerson, + AdminPurgePersonForm, + AdminPurgePost, + AdminPurgePostForm, + }, + traits::Crud, + utils::{get_conn, DbPool}, +}; +use diesel::{dsl::insert_into, result::Error, QueryDsl}; +use diesel_async::RunQueryDsl; + +#[async_trait] +impl Crud for AdminPurgePerson { + type InsertForm = AdminPurgePersonForm; + type UpdateForm = AdminPurgePersonForm; + type IdType = i32; + + async fn create(pool: &mut DbPool<'_>, form: &Self::InsertForm) -> Result { + use crate::schema::admin_purge_person::dsl::admin_purge_person; + let conn = &mut get_conn(pool).await?; + insert_into(admin_purge_person) + .values(form) + .get_result::(conn) + .await + } + + async fn update( + pool: &mut DbPool<'_>, + from_id: i32, + form: &Self::InsertForm, + ) -> Result { + use crate::schema::admin_purge_person::dsl::admin_purge_person; + let conn = &mut get_conn(pool).await?; + diesel::update(admin_purge_person.find(from_id)) + .set(form) + .get_result::(conn) + .await + } +} + +#[async_trait] +impl Crud for AdminPurgeCommunity { + type InsertForm = AdminPurgeCommunityForm; + type UpdateForm = AdminPurgeCommunityForm; + type IdType = i32; + + async fn create(pool: &mut DbPool<'_>, form: &Self::InsertForm) -> Result { + use crate::schema::admin_purge_community::dsl::admin_purge_community; + let conn = &mut get_conn(pool).await?; + insert_into(admin_purge_community) + .values(form) + .get_result::(conn) + .await + } + + async fn update( + pool: &mut DbPool<'_>, + from_id: i32, + form: &Self::InsertForm, + ) -> Result { + use crate::schema::admin_purge_community::dsl::admin_purge_community; + let conn = &mut get_conn(pool).await?; + diesel::update(admin_purge_community.find(from_id)) + .set(form) + .get_result::(conn) + .await + } +} + +#[async_trait] +impl Crud for AdminPurgePost { + type InsertForm = AdminPurgePostForm; + type UpdateForm = AdminPurgePostForm; + type IdType = i32; + + async fn create(pool: &mut DbPool<'_>, form: &Self::InsertForm) -> Result { + use crate::schema::admin_purge_post::dsl::admin_purge_post; + let conn = &mut get_conn(pool).await?; + insert_into(admin_purge_post) + .values(form) + .get_result::(conn) + .await + } + + async fn update( + pool: &mut DbPool<'_>, + from_id: i32, + form: &Self::InsertForm, + ) -> Result { + use crate::schema::admin_purge_post::dsl::admin_purge_post; + let conn = &mut get_conn(pool).await?; + diesel::update(admin_purge_post.find(from_id)) + .set(form) + .get_result::(conn) + .await + } +} + +#[async_trait] +impl Crud for AdminPurgeComment { + type InsertForm = AdminPurgeCommentForm; + type UpdateForm = AdminPurgeCommentForm; + type IdType = i32; + + async fn create(pool: &mut DbPool<'_>, form: &Self::InsertForm) -> Result { + use crate::schema::admin_purge_comment::dsl::admin_purge_comment; + let conn = &mut get_conn(pool).await?; + insert_into(admin_purge_comment) + .values(form) + .get_result::(conn) + .await + } + + async fn update( + pool: &mut DbPool<'_>, + from_id: i32, + form: &Self::InsertForm, + ) -> Result { + use crate::schema::admin_purge_comment::dsl::admin_purge_comment; + let conn = &mut get_conn(pool).await?; + diesel::update(admin_purge_comment.find(from_id)) + .set(form) + .get_result::(conn) + .await + } +} diff --git a/crates/db_schema/src/impls/mod_log/mod.rs b/crates/db_schema/src/impls/mod_log/mod.rs new file mode 100644 index 000000000..54341c69a --- /dev/null +++ b/crates/db_schema/src/impls/mod_log/mod.rs @@ -0,0 +1,2 @@ +pub mod admin; +pub mod moderator; diff --git a/crates/db_schema/src/impls/moderator.rs b/crates/db_schema/src/impls/mod_log/moderator.rs similarity index 82% rename from crates/db_schema/src/impls/moderator.rs rename to crates/db_schema/src/impls/mod_log/moderator.rs index 8deb56258..37b66480d 100644 --- a/crates/db_schema/src/impls/moderator.rs +++ b/crates/db_schema/src/impls/mod_log/moderator.rs @@ -1,13 +1,5 @@ use crate::{ - source::moderator::{ - AdminPurgeComment, - AdminPurgeCommentForm, - AdminPurgeCommunity, - AdminPurgeCommunityForm, - AdminPurgePerson, - AdminPurgePersonForm, - AdminPurgePost, - AdminPurgePostForm, + source::mod_log::moderator::{ ModAdd, ModAddCommunity, ModAddCommunityForm, @@ -376,157 +368,20 @@ impl Crud for ModAdd { } } -#[async_trait] -impl Crud for AdminPurgePerson { - type InsertForm = AdminPurgePersonForm; - type UpdateForm = AdminPurgePersonForm; - type IdType = i32; - - async fn create(pool: &mut DbPool<'_>, form: &Self::InsertForm) -> Result { - use crate::schema::admin_purge_person::dsl::admin_purge_person; - let conn = &mut get_conn(pool).await?; - insert_into(admin_purge_person) - .values(form) - .get_result::(conn) - .await - } - - async fn update( - pool: &mut DbPool<'_>, - from_id: i32, - form: &Self::InsertForm, - ) -> Result { - use crate::schema::admin_purge_person::dsl::admin_purge_person; - let conn = &mut get_conn(pool).await?; - diesel::update(admin_purge_person.find(from_id)) - .set(form) - .get_result::(conn) - .await - } -} - -#[async_trait] -impl Crud for AdminPurgeCommunity { - type InsertForm = AdminPurgeCommunityForm; - type UpdateForm = AdminPurgeCommunityForm; - type IdType = i32; - - async fn create(pool: &mut DbPool<'_>, form: &Self::InsertForm) -> Result { - use crate::schema::admin_purge_community::dsl::admin_purge_community; - let conn = &mut get_conn(pool).await?; - insert_into(admin_purge_community) - .values(form) - .get_result::(conn) - .await - } - - async fn update( - pool: &mut DbPool<'_>, - from_id: i32, - form: &Self::InsertForm, - ) -> Result { - use crate::schema::admin_purge_community::dsl::admin_purge_community; - let conn = &mut get_conn(pool).await?; - diesel::update(admin_purge_community.find(from_id)) - .set(form) - .get_result::(conn) - .await - } -} - -#[async_trait] -impl Crud for AdminPurgePost { - type InsertForm = AdminPurgePostForm; - type UpdateForm = AdminPurgePostForm; - type IdType = i32; - - async fn create(pool: &mut DbPool<'_>, form: &Self::InsertForm) -> Result { - use crate::schema::admin_purge_post::dsl::admin_purge_post; - let conn = &mut get_conn(pool).await?; - insert_into(admin_purge_post) - .values(form) - .get_result::(conn) - .await - } - - async fn update( - pool: &mut DbPool<'_>, - from_id: i32, - form: &Self::InsertForm, - ) -> Result { - use crate::schema::admin_purge_post::dsl::admin_purge_post; - let conn = &mut get_conn(pool).await?; - diesel::update(admin_purge_post.find(from_id)) - .set(form) - .get_result::(conn) - .await - } -} - -#[async_trait] -impl Crud for AdminPurgeComment { - type InsertForm = AdminPurgeCommentForm; - type UpdateForm = AdminPurgeCommentForm; - type IdType = i32; - - async fn create(pool: &mut DbPool<'_>, form: &Self::InsertForm) -> Result { - use crate::schema::admin_purge_comment::dsl::admin_purge_comment; - let conn = &mut get_conn(pool).await?; - insert_into(admin_purge_comment) - .values(form) - .get_result::(conn) - .await - } - - async fn update( - pool: &mut DbPool<'_>, - from_id: i32, - form: &Self::InsertForm, - ) -> Result { - use crate::schema::admin_purge_comment::dsl::admin_purge_comment; - let conn = &mut get_conn(pool).await?; - diesel::update(admin_purge_comment.find(from_id)) - .set(form) - .get_result::(conn) - .await - } -} - #[cfg(test)] mod tests { + use super::*; use crate::{ source::{ comment::{Comment, CommentInsertForm}, community::{Community, CommunityInsertForm}, instance::Instance, - moderator::{ - ModAdd, - ModAddCommunity, - ModAddCommunityForm, - ModAddForm, - ModBan, - ModBanForm, - ModBanFromCommunity, - ModBanFromCommunityForm, - ModFeaturePost, - ModFeaturePostForm, - ModLockPost, - ModLockPostForm, - ModRemoveComment, - ModRemoveCommentForm, - ModRemoveCommunity, - ModRemoveCommunityForm, - ModRemovePost, - ModRemovePostForm, - }, person::{Person, PersonInsertForm}, post::{Post, PostInsertForm}, }, - traits::Crud, utils::build_db_pool_for_tests, }; - use diesel::result::Error; use pretty_assertions::assert_eq; use serial_test::serial; diff --git a/crates/db_schema/src/impls/tag.rs b/crates/db_schema/src/impls/tag.rs new file mode 100644 index 000000000..c0171e04c --- /dev/null +++ b/crates/db_schema/src/impls/tag.rs @@ -0,0 +1,53 @@ +use crate::{ + newtypes::TagId, + schema::{post_tag, tag}, + source::tag::{PostTagInsertForm, Tag, TagInsertForm}, + traits::Crud, + utils::{get_conn, DbPool}, +}; +use diesel::{insert_into, result::Error, QueryDsl}; +use diesel_async::RunQueryDsl; +use lemmy_utils::error::LemmyResult; + +#[async_trait] +impl Crud for Tag { + type InsertForm = TagInsertForm; + + type UpdateForm = TagInsertForm; + + type IdType = TagId; + + async fn create(pool: &mut DbPool<'_>, form: &Self::InsertForm) -> Result { + let conn = &mut get_conn(pool).await?; + insert_into(tag::table) + .values(form) + .get_result::(conn) + .await + } + + async fn update( + pool: &mut DbPool<'_>, + pid: TagId, + form: &Self::UpdateForm, + ) -> Result { + let conn = &mut get_conn(pool).await?; + diesel::update(tag::table.find(pid)) + .set(form) + .get_result::(conn) + .await + } +} + +impl PostTagInsertForm { + pub async fn insert_tag_associations( + pool: &mut DbPool<'_>, + tags: &[PostTagInsertForm], + ) -> LemmyResult<()> { + let conn = &mut get_conn(pool).await?; + insert_into(post_tag::table) + .values(tags) + .execute(conn) + .await?; + Ok(()) + } +} diff --git a/crates/db_schema/src/lib.rs b/crates/db_schema/src/lib.rs index 6e1abaf0f..bc45a7737 100644 --- a/crates/db_schema/src/lib.rs +++ b/crates/db_schema/src/lib.rs @@ -210,6 +210,8 @@ pub enum ModlogActionType { AdminPurgeCommunity, AdminPurgePost, AdminPurgeComment, + AdminBlockInstance, + AdminAllowInstance, } #[derive( diff --git a/crates/db_schema/src/newtypes.rs b/crates/db_schema/src/newtypes.rs index c28be8222..963f847a5 100644 --- a/crates/db_schema/src/newtypes.rs +++ b/crates/db_schema/src/newtypes.rs @@ -283,3 +283,9 @@ impl InstanceId { self.0 } } + +#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, Default, Serialize, Deserialize)] +#[cfg_attr(feature = "full", derive(DieselNewType, TS))] +#[cfg_attr(feature = "full", ts(export))] +/// The internal tag id. +pub struct TagId(pub i32); diff --git a/crates/db_schema/src/schema.rs b/crates/db_schema/src/schema.rs index 4635be4ed..99dfeb420 100644 --- a/crates/db_schema/src/schema.rs +++ b/crates/db_schema/src/schema.rs @@ -42,6 +42,29 @@ pub mod sql_types { pub struct RegistrationModeEnum; } +diesel::table! { + admin_allow_instance (id) { + id -> Int4, + instance_id -> Int4, + admin_person_id -> Int4, + allowed -> Bool, + reason -> Nullable, + when_ -> Timestamptz, + } +} + +diesel::table! { + admin_block_instance (id) { + id -> Int4, + instance_id -> Int4, + admin_person_id -> Int4, + blocked -> Bool, + reason -> Nullable, + expires -> Nullable, + when_ -> Timestamptz, + } +} + diesel::table! { admin_purge_comment (id) { id -> Int4, @@ -284,6 +307,7 @@ diesel::table! { instance_id -> Int4, published -> Timestamptz, updated -> Nullable, + expires -> Nullable, } } @@ -636,6 +660,7 @@ diesel::table! { enabled -> Bool, published -> Timestamptz, updated -> Nullable, + use_pkce -> Bool, } } @@ -801,6 +826,14 @@ diesel::table! { } } +diesel::table! { + post_tag (post_id, tag_id) { + post_id -> Int4, + tag_id -> Int4, + published -> Timestamptz, + } +} + diesel::table! { previously_run_sql (id) { id -> Bool, @@ -933,6 +966,18 @@ diesel::table! { } } +diesel::table! { + tag (id) { + id -> Int4, + ap_id -> Text, + name -> Text, + community_id -> Int4, + published -> Timestamptz, + updated -> Nullable, + deleted -> Bool, + } +} + diesel::table! { tagline (id) { id -> Int4, @@ -942,6 +987,10 @@ diesel::table! { } } +diesel::joinable!(admin_allow_instance -> instance (instance_id)); +diesel::joinable!(admin_allow_instance -> person (admin_person_id)); +diesel::joinable!(admin_block_instance -> instance (instance_id)); +diesel::joinable!(admin_block_instance -> person (admin_person_id)); diesel::joinable!(admin_purge_comment -> person (admin_person_id)); diesel::joinable!(admin_purge_comment -> post (post_id)); diesel::joinable!(admin_purge_community -> person (admin_person_id)); @@ -1010,6 +1059,8 @@ diesel::joinable!(post_aggregates -> instance (instance_id)); diesel::joinable!(post_aggregates -> person (creator_id)); diesel::joinable!(post_aggregates -> post (post_id)); diesel::joinable!(post_report -> post (post_id)); +diesel::joinable!(post_tag -> post (post_id)); +diesel::joinable!(post_tag -> tag (tag_id)); diesel::joinable!(private_message_report -> private_message (private_message_id)); diesel::joinable!(registration_application -> local_user (local_user_id)); diesel::joinable!(registration_application -> person (admin_id)); @@ -1017,8 +1068,11 @@ diesel::joinable!(site -> instance (instance_id)); diesel::joinable!(site_aggregates -> site (site_id)); diesel::joinable!(site_language -> language (language_id)); diesel::joinable!(site_language -> site (site_id)); +diesel::joinable!(tag -> community (community_id)); diesel::allow_tables_to_appear_in_same_query!( + admin_allow_instance, + admin_block_instance, admin_purge_comment, admin_purge_community, admin_purge_person, @@ -1074,6 +1128,7 @@ diesel::allow_tables_to_appear_in_same_query!( post_actions, post_aggregates, post_report, + post_tag, previously_run_sql, private_message, private_message_report, @@ -1085,5 +1140,6 @@ diesel::allow_tables_to_appear_in_same_query!( site, site_aggregates, site_language, + tag, tagline, ); diff --git a/crates/db_schema/src/source/federation_blocklist.rs b/crates/db_schema/src/source/federation_blocklist.rs index 2176ce42d..df877facf 100644 --- a/crates/db_schema/src/source/federation_blocklist.rs +++ b/crates/db_schema/src/source/federation_blocklist.rs @@ -1,14 +1,14 @@ use crate::newtypes::InstanceId; -#[cfg(feature = "full")] -use crate::schema::federation_blocklist; use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use std::fmt::Debug; +#[cfg(feature = "full")] +use {crate::schema::federation_blocklist, ts_rs::TS}; #[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)] #[cfg_attr( feature = "full", - derive(Queryable, Selectable, Associations, Identifiable) + derive(TS, Queryable, Selectable, Associations, Identifiable) )] #[cfg_attr( feature = "full", @@ -17,10 +17,14 @@ use std::fmt::Debug; #[cfg_attr(feature = "full", diesel(table_name = federation_blocklist))] #[cfg_attr(feature = "full", diesel(primary_key(instance_id)))] #[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] +#[cfg_attr(feature = "full", ts(export))] pub struct FederationBlockList { pub instance_id: InstanceId, pub published: DateTime, + #[cfg_attr(feature = "full", ts(optional))] pub updated: Option>, + #[cfg_attr(feature = "full", ts(optional))] + pub expires: Option>, } #[derive(Clone, Default)] @@ -29,4 +33,5 @@ pub struct FederationBlockList { pub struct FederationBlockListForm { pub instance_id: InstanceId, pub updated: Option>, + pub expires: Option>, } diff --git a/crates/db_schema/src/source/mod.rs b/crates/db_schema/src/source/mod.rs index 5082ddbd1..6230d004d 100644 --- a/crates/db_schema/src/source/mod.rs +++ b/crates/db_schema/src/source/mod.rs @@ -26,7 +26,7 @@ pub mod local_site_url_blocklist; pub mod local_user; pub mod local_user_vote_display_mode; pub mod login_token; -pub mod moderator; +pub mod mod_log; pub mod oauth_account; pub mod oauth_provider; pub mod password_reset_request; @@ -40,6 +40,7 @@ pub mod private_message_report; pub mod registration_application; pub mod secret; pub mod site; +pub mod tag; pub mod tagline; /// Default value for columns like [community::Community.inbox_url] which are marked as serde(skip). diff --git a/crates/db_schema/src/source/mod_log/admin.rs b/crates/db_schema/src/source/mod_log/admin.rs new file mode 100644 index 000000000..d6e48b8ee --- /dev/null +++ b/crates/db_schema/src/source/mod_log/admin.rs @@ -0,0 +1,176 @@ +use crate::newtypes::{CommunityId, InstanceId, PersonId, PostId}; +#[cfg(feature = "full")] +use crate::schema::{ + admin_allow_instance, + admin_block_instance, + admin_purge_comment, + admin_purge_community, + admin_purge_person, + admin_purge_post, +}; +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; +use serde_with::skip_serializing_none; +#[cfg(feature = "full")] +use ts_rs::TS; + +#[skip_serializing_none] +#[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)] +#[cfg_attr(feature = "full", derive(Queryable, Selectable, Identifiable, TS))] +#[cfg_attr(feature = "full", diesel(table_name = admin_purge_person))] +#[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] +#[cfg_attr(feature = "full", ts(export))] +/// When an admin purges a person. +pub struct AdminPurgePerson { + pub id: i32, + pub admin_person_id: PersonId, + #[cfg_attr(feature = "full", ts(optional))] + pub reason: Option, + pub when_: DateTime, +} + +#[cfg_attr(feature = "full", derive(Insertable, AsChangeset))] +#[cfg_attr(feature = "full", diesel(table_name = admin_purge_person))] +pub struct AdminPurgePersonForm { + pub admin_person_id: PersonId, + pub reason: Option, +} + +#[skip_serializing_none] +#[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)] +#[cfg_attr(feature = "full", derive(Queryable, Selectable, Identifiable, TS))] +#[cfg_attr(feature = "full", diesel(table_name = admin_purge_community))] +#[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] +#[cfg_attr(feature = "full", ts(export))] +/// When an admin purges a community. +pub struct AdminPurgeCommunity { + pub id: i32, + pub admin_person_id: PersonId, + #[cfg_attr(feature = "full", ts(optional))] + pub reason: Option, + pub when_: DateTime, +} + +#[cfg_attr(feature = "full", derive(Insertable, AsChangeset))] +#[cfg_attr(feature = "full", diesel(table_name = admin_purge_community))] +pub struct AdminPurgeCommunityForm { + pub admin_person_id: PersonId, + pub reason: Option, +} + +#[skip_serializing_none] +#[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)] +#[cfg_attr(feature = "full", derive(Queryable, Selectable, Identifiable, TS))] +#[cfg_attr(feature = "full", diesel(table_name = admin_purge_post))] +#[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] +#[cfg_attr(feature = "full", ts(export))] +/// When an admin purges a post. +pub struct AdminPurgePost { + pub id: i32, + pub admin_person_id: PersonId, + pub community_id: CommunityId, + #[cfg_attr(feature = "full", ts(optional))] + pub reason: Option, + pub when_: DateTime, +} + +#[cfg_attr(feature = "full", derive(Insertable, AsChangeset))] +#[cfg_attr(feature = "full", diesel(table_name = admin_purge_post))] +pub struct AdminPurgePostForm { + pub admin_person_id: PersonId, + pub community_id: CommunityId, + pub reason: Option, +} + +#[skip_serializing_none] +#[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)] +#[cfg_attr(feature = "full", derive(Queryable, Selectable, Identifiable, TS))] +#[cfg_attr(feature = "full", diesel(table_name = admin_purge_comment))] +#[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] +#[cfg_attr(feature = "full", ts(export))] +/// When an admin purges a comment. +pub struct AdminPurgeComment { + pub id: i32, + pub admin_person_id: PersonId, + pub post_id: PostId, + #[cfg_attr(feature = "full", ts(optional))] + pub reason: Option, + pub when_: DateTime, +} + +#[cfg_attr(feature = "full", derive(Insertable, AsChangeset))] +#[cfg_attr(feature = "full", diesel(table_name = admin_purge_comment))] +pub struct AdminPurgeCommentForm { + pub admin_person_id: PersonId, + pub post_id: PostId, + pub reason: Option, +} + +#[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)] +#[cfg_attr( + feature = "full", + derive(TS, Queryable, Selectable, Associations, Identifiable) +)] +#[cfg_attr( + feature = "full", + diesel(belongs_to(crate::source::instance::Instance)) +)] +#[cfg_attr(feature = "full", diesel(table_name = admin_allow_instance))] +#[cfg_attr(feature = "full", diesel(primary_key(instance_id)))] +#[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] +#[cfg_attr(feature = "full", ts(export))] +pub struct AdminAllowInstance { + pub id: i32, + pub instance_id: InstanceId, + pub admin_person_id: PersonId, + pub allowed: bool, + #[cfg_attr(feature = "full", ts(optional))] + pub reason: Option, + pub when_: DateTime, +} + +#[derive(Clone, Default)] +#[cfg_attr(feature = "full", derive(Insertable, AsChangeset))] +#[cfg_attr(feature = "full", diesel(table_name = admin_allow_instance))] +pub struct AdminAllowInstanceForm { + pub instance_id: InstanceId, + pub admin_person_id: PersonId, + pub allowed: bool, + pub reason: Option, +} + +#[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)] +#[cfg_attr( + feature = "full", + derive(TS, Queryable, Selectable, Associations, Identifiable) +)] +#[cfg_attr( + feature = "full", + diesel(belongs_to(crate::source::instance::Instance)) +)] +#[cfg_attr(feature = "full", diesel(table_name = admin_block_instance))] +#[cfg_attr(feature = "full", diesel(primary_key(instance_id)))] +#[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] +#[cfg_attr(feature = "full", ts(export))] +pub struct AdminBlockInstance { + pub id: i32, + pub instance_id: InstanceId, + pub admin_person_id: PersonId, + pub blocked: bool, + #[cfg_attr(feature = "full", ts(optional))] + pub reason: Option, + #[cfg_attr(feature = "full", ts(optional))] + pub expires: Option>, + pub when_: DateTime, +} + +#[derive(Clone, Default)] +#[cfg_attr(feature = "full", derive(Insertable, AsChangeset))] +#[cfg_attr(feature = "full", diesel(table_name = admin_block_instance))] +pub struct AdminBlockInstanceForm { + pub instance_id: InstanceId, + pub admin_person_id: PersonId, + pub blocked: bool, + pub reason: Option, + pub when_: Option>, +} diff --git a/crates/db_schema/src/source/mod_log/mod.rs b/crates/db_schema/src/source/mod_log/mod.rs new file mode 100644 index 000000000..54341c69a --- /dev/null +++ b/crates/db_schema/src/source/mod_log/mod.rs @@ -0,0 +1,2 @@ +pub mod admin; +pub mod moderator; diff --git a/crates/db_schema/src/source/moderator.rs b/crates/db_schema/src/source/mod_log/moderator.rs similarity index 75% rename from crates/db_schema/src/source/moderator.rs rename to crates/db_schema/src/source/mod_log/moderator.rs index b4fdcc676..470b643a5 100644 --- a/crates/db_schema/src/source/moderator.rs +++ b/crates/db_schema/src/source/mod_log/moderator.rs @@ -1,10 +1,6 @@ use crate::newtypes::{CommentId, CommunityId, PersonId, PostId}; #[cfg(feature = "full")] use crate::schema::{ - admin_purge_comment, - admin_purge_community, - admin_purge_person, - admin_purge_post, mod_add, mod_add_community, mod_ban, @@ -300,95 +296,3 @@ pub struct ModAddForm { pub other_person_id: PersonId, pub removed: Option, } - -#[skip_serializing_none] -#[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)] -#[cfg_attr(feature = "full", derive(Queryable, Selectable, Identifiable, TS))] -#[cfg_attr(feature = "full", diesel(table_name = admin_purge_person))] -#[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] -#[cfg_attr(feature = "full", ts(export))] -/// When an admin purges a person. -pub struct AdminPurgePerson { - pub id: i32, - pub admin_person_id: PersonId, - #[cfg_attr(feature = "full", ts(optional))] - pub reason: Option, - pub when_: DateTime, -} - -#[cfg_attr(feature = "full", derive(Insertable, AsChangeset))] -#[cfg_attr(feature = "full", diesel(table_name = admin_purge_person))] -pub struct AdminPurgePersonForm { - pub admin_person_id: PersonId, - pub reason: Option, -} - -#[skip_serializing_none] -#[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)] -#[cfg_attr(feature = "full", derive(Queryable, Selectable, Identifiable, TS))] -#[cfg_attr(feature = "full", diesel(table_name = admin_purge_community))] -#[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] -#[cfg_attr(feature = "full", ts(export))] -/// When an admin purges a community. -pub struct AdminPurgeCommunity { - pub id: i32, - pub admin_person_id: PersonId, - #[cfg_attr(feature = "full", ts(optional))] - pub reason: Option, - pub when_: DateTime, -} - -#[cfg_attr(feature = "full", derive(Insertable, AsChangeset))] -#[cfg_attr(feature = "full", diesel(table_name = admin_purge_community))] -pub struct AdminPurgeCommunityForm { - pub admin_person_id: PersonId, - pub reason: Option, -} - -#[skip_serializing_none] -#[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)] -#[cfg_attr(feature = "full", derive(Queryable, Selectable, Identifiable, TS))] -#[cfg_attr(feature = "full", diesel(table_name = admin_purge_post))] -#[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] -#[cfg_attr(feature = "full", ts(export))] -/// When an admin purges a post. -pub struct AdminPurgePost { - pub id: i32, - pub admin_person_id: PersonId, - pub community_id: CommunityId, - #[cfg_attr(feature = "full", ts(optional))] - pub reason: Option, - pub when_: DateTime, -} - -#[cfg_attr(feature = "full", derive(Insertable, AsChangeset))] -#[cfg_attr(feature = "full", diesel(table_name = admin_purge_post))] -pub struct AdminPurgePostForm { - pub admin_person_id: PersonId, - pub community_id: CommunityId, - pub reason: Option, -} - -#[skip_serializing_none] -#[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)] -#[cfg_attr(feature = "full", derive(Queryable, Selectable, Identifiable, TS))] -#[cfg_attr(feature = "full", diesel(table_name = admin_purge_comment))] -#[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] -#[cfg_attr(feature = "full", ts(export))] -/// When an admin purges a comment. -pub struct AdminPurgeComment { - pub id: i32, - pub admin_person_id: PersonId, - pub post_id: PostId, - #[cfg_attr(feature = "full", ts(optional))] - pub reason: Option, - pub when_: DateTime, -} - -#[cfg_attr(feature = "full", derive(Insertable, AsChangeset))] -#[cfg_attr(feature = "full", diesel(table_name = admin_purge_comment))] -pub struct AdminPurgeCommentForm { - pub admin_person_id: PersonId, - pub post_id: PostId, - pub reason: Option, -} diff --git a/crates/db_schema/src/source/oauth_provider.rs b/crates/db_schema/src/source/oauth_provider.rs index a70405a5e..0a82ab9a9 100644 --- a/crates/db_schema/src/source/oauth_provider.rs +++ b/crates/db_schema/src/source/oauth_provider.rs @@ -62,6 +62,8 @@ pub struct OAuthProvider { pub published: DateTime, #[cfg_attr(feature = "full", ts(optional))] pub updated: Option>, + /// switch to enable or disable PKCE + pub use_pkce: bool, } #[derive(Clone, PartialEq, Eq, Debug, Deserialize)] @@ -83,6 +85,7 @@ impl Serialize for PublicOAuthProvider { state.serialize_field("authorization_endpoint", &self.0.authorization_endpoint)?; state.serialize_field("client_id", &self.0.client_id)?; state.serialize_field("scopes", &self.0.scopes)?; + state.serialize_field("use_pkce", &self.0.use_pkce)?; state.end() } } @@ -102,6 +105,7 @@ pub struct OAuthProviderInsertForm { pub scopes: String, pub auto_verify_email: Option, pub account_linking_enabled: Option, + pub use_pkce: Option, pub enabled: Option, } @@ -118,6 +122,7 @@ pub struct OAuthProviderUpdateForm { pub scopes: Option, pub auto_verify_email: Option, pub account_linking_enabled: Option, + pub use_pkce: Option, pub enabled: Option, pub updated: Option>>, } diff --git a/crates/db_schema/src/source/tag.rs b/crates/db_schema/src/source/tag.rs new file mode 100644 index 000000000..265d864c3 --- /dev/null +++ b/crates/db_schema/src/source/tag.rs @@ -0,0 +1,57 @@ +use crate::newtypes::{CommunityId, DbUrl, PostId, TagId}; +#[cfg(feature = "full")] +use crate::schema::{post_tag, tag}; +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; +use serde_with::skip_serializing_none; +#[cfg(feature = "full")] +use ts_rs::TS; + +/// A tag that can be assigned to a post within a community. +/// The tag object is created by the community moderators. +/// The assignment happens by the post creator and can be updated by the community moderators. +/// +/// A tag is a federatable object that gives additional context to another object, which can be +/// displayed and filtered on currently, we only have community post tags, which is a tag that is +/// created by post authors as well as mods of a community, to categorize a post. in the future we +/// may add more tag types, depending on the requirements, this will lead to either expansion of +/// this table (community_id optional, addition of tag_type enum) or split of this table / creation +/// of new tables. +#[skip_serializing_none] +#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)] +#[cfg_attr(feature = "full", derive(TS, Queryable, Selectable, Identifiable))] +#[cfg_attr(feature = "full", diesel(table_name = tag))] +#[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] +#[cfg_attr(feature = "full", ts(export))] +pub struct Tag { + pub id: TagId, + pub ap_id: DbUrl, + pub name: String, + /// the community that owns this tag + pub community_id: CommunityId, + pub published: DateTime, + #[cfg_attr(feature = "full", ts(optional))] + pub updated: Option>, + pub deleted: bool, +} + +#[derive(Debug, Clone)] +#[cfg_attr(feature = "full", derive(Insertable, AsChangeset))] +#[cfg_attr(feature = "full", diesel(table_name = tag))] +pub struct TagInsertForm { + pub ap_id: DbUrl, + pub name: String, + pub community_id: CommunityId, + // default now + pub published: Option>, + pub updated: Option>, + pub deleted: bool, +} + +#[derive(Debug, Clone)] +#[cfg_attr(feature = "full", derive(Insertable, AsChangeset))] +#[cfg_attr(feature = "full", diesel(table_name = post_tag))] +pub struct PostTagInsertForm { + pub post_id: PostId, + pub tag_id: TagId, +} diff --git a/crates/db_schema/src/utils.rs b/crates/db_schema/src/utils.rs index 4619ba7eb..043f669f3 100644 --- a/crates/db_schema/src/utils.rs +++ b/crates/db_schema/src/utils.rs @@ -547,6 +547,11 @@ pub mod functions { // really this function is variadic, this just adds the two-argument version define_sql_function!(fn coalesce(x: diesel::sql_types::Nullable, y: T) -> T); + + define_sql_function! { + #[aggregate] + fn json_agg(obj: T) -> Json + } } pub const DELETED_REPLACEMENT_TEXT: &str = "*Permanently Deleted*"; diff --git a/crates/db_views/Cargo.toml b/crates/db_views/Cargo.toml index df8124c8a..8b0669ff9 100644 --- a/crates/db_views/Cargo.toml +++ b/crates/db_views/Cargo.toml @@ -35,6 +35,7 @@ diesel-async = { workspace = true, optional = true } diesel_ltree = { workspace = true, optional = true } serde = { workspace = true } serde_with = { workspace = true } +serde_json = { workspace = true } tracing = { workspace = true, optional = true } ts-rs = { workspace = true, optional = true } actix-web = { workspace = true, optional = true } @@ -46,3 +47,4 @@ serial_test = { workspace = true } tokio = { workspace = true } pretty_assertions = { workspace = true } url = { workspace = true } +test-context = "0.3.0" diff --git a/crates/db_views/src/comment_view.rs b/crates/db_views/src/comment_view.rs index 1037cf6ff..2cf751f9f 100644 --- a/crates/db_views/src/comment_view.rs +++ b/crates/db_views/src/comment_view.rs @@ -316,17 +316,14 @@ impl CommentView { comment_id: CommentId, my_local_user: Option<&'_ LocalUser>, ) -> Result { + let is_admin = my_local_user.map(|u| u.admin).unwrap_or(false); // If a person is given, then my_vote (res.9), if None, should be 0, not null // Necessary to differentiate between other person's votes - let res = queries().read(pool, (comment_id, my_local_user)).await?; - let mut new_view = res.clone(); + let mut res = queries().read(pool, (comment_id, my_local_user)).await?; if my_local_user.is_some() && res.my_vote.is_none() { - new_view.my_vote = Some(0); + res.my_vote = Some(0); } - if res.comment.deleted || res.comment.removed { - new_view.comment.content = String::new(); - } - Ok(new_view) + Ok(handle_deleted(res, is_admin)) } } @@ -350,22 +347,25 @@ pub struct CommentQuery<'a> { impl CommentQuery<'_> { pub async fn list(self, site: &Site, pool: &mut DbPool<'_>) -> Result, Error> { + let is_admin = self.local_user.map(|u| u.admin).unwrap_or(false); Ok( queries() .list(pool, (self, site)) .await? .into_iter() - .map(|mut c| { - if c.comment.deleted || c.comment.removed { - c.comment.content = String::new(); - } - c - }) + .map(|c| handle_deleted(c, is_admin)) .collect(), ) } } +fn handle_deleted(mut c: CommentView, is_admin: bool) -> CommentView { + if !is_admin && (c.comment.deleted || c.comment.removed) { + c.comment.content = String::new(); + } + c +} + #[cfg(test)] #[expect(clippy::indexing_slicing)] mod tests { @@ -1301,4 +1301,65 @@ mod tests { cleanup(data, pool).await } + + #[tokio::test] + #[serial] + async fn comment_removed() -> LemmyResult<()> { + let pool = &build_db_pool_for_tests(); + let pool = &mut pool.into(); + let mut data = init_data(pool).await?; + + // Mark a comment as removed + let form = CommentUpdateForm { + removed: Some(true), + ..Default::default() + }; + Comment::update(pool, data.inserted_comment_0.id, &form).await?; + + // Read as normal user, content is cleared + data.timmy_local_user_view.local_user.admin = false; + let comment_view = CommentView::read( + pool, + data.inserted_comment_0.id, + Some(&data.timmy_local_user_view.local_user), + ) + .await?; + assert_eq!("", comment_view.comment.content); + let comment_listing = CommentQuery { + community_id: Some(data.inserted_community.id), + local_user: Some(&data.timmy_local_user_view.local_user), + sort: Some(CommentSortType::Old), + ..Default::default() + } + .list(&data.site, pool) + .await?; + assert_eq!("", comment_listing[0].comment.content); + + // Read as admin, content is returned + data.timmy_local_user_view.local_user.admin = true; + let comment_view = CommentView::read( + pool, + data.inserted_comment_0.id, + Some(&data.timmy_local_user_view.local_user), + ) + .await?; + assert_eq!( + data.inserted_comment_0.content, + comment_view.comment.content + ); + let comment_listing = CommentQuery { + community_id: Some(data.inserted_community.id), + local_user: Some(&data.timmy_local_user_view.local_user), + sort: Some(CommentSortType::Old), + ..Default::default() + } + .list(&data.site, pool) + .await?; + assert_eq!( + data.inserted_comment_0.content, + comment_listing[0].comment.content + ); + + cleanup(data, pool).await + } } diff --git a/crates/db_views/src/lib.rs b/crates/db_views/src/lib.rs index e93c7409d..3c1fcd84a 100644 --- a/crates/db_views/src/lib.rs +++ b/crates/db_views/src/lib.rs @@ -14,6 +14,8 @@ pub mod local_user_view; #[cfg(feature = "full")] pub mod post_report_view; #[cfg(feature = "full")] +pub mod post_tags_view; +#[cfg(feature = "full")] pub mod post_view; #[cfg(feature = "full")] pub mod private_message_report_view; diff --git a/crates/db_views/src/post_tags_view.rs b/crates/db_views/src/post_tags_view.rs new file mode 100644 index 000000000..5d1492567 --- /dev/null +++ b/crates/db_views/src/post_tags_view.rs @@ -0,0 +1,30 @@ +//! see post_view.rs for the reason for this json decoding +use crate::structs::PostTags; +use diesel::{ + deserialize::FromSql, + pg::{Pg, PgValue}, + serialize::ToSql, + sql_types::{self, Nullable}, +}; + +impl FromSql, Pg> for PostTags { + fn from_sql(bytes: PgValue) -> diesel::deserialize::Result { + let value = >::from_sql(bytes)?; + Ok(serde_json::from_value::(value)?) + } + fn from_nullable_sql( + bytes: Option<::RawValue<'_>>, + ) -> diesel::deserialize::Result { + match bytes { + Some(bytes) => Self::from_sql(bytes), + None => Ok(Self { tags: vec![] }), + } + } +} + +impl ToSql, Pg> for PostTags { + fn to_sql(&self, out: &mut diesel::serialize::Output) -> diesel::serialize::Result { + let value = serde_json::to_value(self)?; + >::to_sql(&value, &mut out.reborrow()) + } +} diff --git a/crates/db_views/src/post_view.rs b/crates/db_views/src/post_view.rs index c6d1b036f..6ed89e364 100644 --- a/crates/db_views/src/post_view.rs +++ b/crates/db_views/src/post_view.rs @@ -5,7 +5,9 @@ use diesel::{ pg::Pg, query_builder::AsQuery, result::Error, + sql_types, BoolExpressionMethods, + BoxableExpression, ExpressionMethods, JoinOnDsl, NullableExpressionMethods, @@ -32,6 +34,8 @@ use lemmy_db_schema::{ post, post_actions, post_aggregates, + post_tag, + tag, }, source::{ community::{CommunityFollower, CommunityFollowerState}, @@ -80,6 +84,31 @@ fn queries<'a>() -> Queries< // TODO maybe this should go to localuser also let all_joins = move |query: post_aggregates::BoxedQuery<'a, Pg>, my_person_id: Option| { + // We fetch post tags by letting postgresql aggregate them internally in a subquery into JSON. + // This is a simple way to join m rows into n rows without duplicating the data and getting + // complex diesel types. In pure SQL you would usually do this either using a LEFT JOIN + then + // aggregating the results in the application code. But this results in a lot of duplicate + // data transferred (since each post will be returned once per tag that it has) and more + // complicated application code. The diesel docs suggest doing three separate sequential queries + // in this case (see https://diesel.rs/guides/relations.html#many-to-many-or-mn ): First fetch + // the posts, then fetch all relevant post-tag-association tuples from the db, and then fetch + // all the relevant tag objects. + // + // If we want to filter by post tag we will have to add + // separate logic below since this subquery can't affect filtering, but it is simple (`WHERE + // exists (select 1 from post_community_post_tags where community_post_tag_id in (1,2,3,4)`). + let post_tags: Box< + dyn BoxableExpression<_, Pg, SqlType = sql_types::Nullable>, + > = Box::new( + post_tag::table + .inner_join(tag::table) + .select(diesel::dsl::sql::( + "json_agg(tag.*)", + )) + .filter(post_tag::post_id.eq(post_aggregates::post_id)) + .filter(tag::deleted.eq(false)) + .single_value(), + ); query .inner_join(person::table) .inner_join(community::table) @@ -136,6 +165,7 @@ fn queries<'a>() -> Queries< post_aggregates::comments.nullable() - post_actions::read_comments_amount.nullable(), post_aggregates::comments, ), + post_tags, )) }; @@ -603,11 +633,13 @@ impl<'a> PostQuery<'a> { } } +#[allow(clippy::indexing_slicing)] +#[expect(clippy::expect_used)] #[cfg(test)] mod tests { use crate::{ post_view::{PaginationCursorData, PostQuery, PostView}, - structs::LocalUserView, + structs::{LocalUserView, PostTags}, }; use chrono::Utc; use diesel_async::SimpleAsyncConnection; @@ -651,29 +683,33 @@ mod tests { PostUpdateForm, }, site::Site, + tag::{PostTagInsertForm, Tag, TagInsertForm}, }, traits::{Bannable, Blockable, Crud, Followable, Joinable, Likeable, Saveable}, - utils::{build_db_pool, build_db_pool_for_tests, get_conn, uplete, DbPool, RANK_DEFAULT}, + utils::{build_db_pool, get_conn, uplete, ActualDbPool, DbPool, RANK_DEFAULT}, CommunityVisibility, PostSortType, SubscribedType, }; - use lemmy_utils::error::LemmyResult; + use lemmy_utils::error::{LemmyErrorType, LemmyResult}; use pretty_assertions::assert_eq; use serial_test::serial; use std::time::{Duration, Instant}; + use test_context::{test_context, AsyncTestContext}; use url::Url; const POST_WITH_ANOTHER_TITLE: &str = "Another title"; const POST_BY_BLOCKED_PERSON: &str = "post by blocked person"; const POST_BY_BOT: &str = "post by bot"; const POST: &str = "post"; + const POST_WITH_TAGS: &str = "post with tags"; fn names(post_views: &[PostView]) -> Vec<&str> { post_views.iter().map(|i| i.post.name.as_str()).collect() } struct Data { + pool: ActualDbPool, inserted_instance: Instance, local_user_view: LocalUserView, blocked_local_user_view: LocalUserView, @@ -681,10 +717,19 @@ mod tests { inserted_community: Community, inserted_post: Post, inserted_bot_post: Post, + inserted_post_with_tags: Post, + tag_1: Tag, + tag_2: Tag, site: Site, } impl Data { + fn pool(&self) -> ActualDbPool { + self.pool.clone() + } + pub fn pool2(&self) -> DbPool<'_> { + DbPool::Pool(&self.pool) + } fn default_post_query(&self) -> PostQuery<'_> { PostQuery { sort: Some(PostSortType::New), @@ -692,129 +737,206 @@ mod tests { ..Default::default() } } - } - async fn init_data(pool: &mut DbPool<'_>) -> LemmyResult { - let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?; + async fn setup() -> LemmyResult { + let actual_pool = build_db_pool()?; + let pool = &mut (&actual_pool).into(); + let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?; - let new_person = PersonInsertForm::test_form(inserted_instance.id, "tegan"); + let new_person = PersonInsertForm::test_form(inserted_instance.id, "tegan"); - let inserted_person = Person::create(pool, &new_person).await?; + let inserted_person = Person::create(pool, &new_person).await?; - let local_user_form = LocalUserInsertForm { - admin: Some(true), - ..LocalUserInsertForm::test_form(inserted_person.id) - }; - let inserted_local_user = LocalUser::create(pool, &local_user_form, vec![]).await?; + let local_user_form = LocalUserInsertForm { + admin: Some(true), + ..LocalUserInsertForm::test_form(inserted_person.id) + }; + let inserted_local_user = LocalUser::create(pool, &local_user_form, vec![]).await?; - let new_bot = PersonInsertForm { - bot_account: Some(true), - ..PersonInsertForm::test_form(inserted_instance.id, "mybot") - }; + let new_bot = PersonInsertForm { + bot_account: Some(true), + ..PersonInsertForm::test_form(inserted_instance.id, "mybot") + }; - let inserted_bot = Person::create(pool, &new_bot).await?; + let inserted_bot = Person::create(pool, &new_bot).await?; - let new_community = CommunityInsertForm::new( - inserted_instance.id, - "test_community_3".to_string(), - "nada".to_owned(), - "pubkey".to_string(), - ); - let inserted_community = Community::create(pool, &new_community).await?; + let new_community = CommunityInsertForm::new( + inserted_instance.id, + "test_community_3".to_string(), + "nada".to_owned(), + "pubkey".to_string(), + ); + let inserted_community = Community::create(pool, &new_community).await?; - // Test a person block, make sure the post query doesn't include their post - let blocked_person = PersonInsertForm::test_form(inserted_instance.id, "john"); + // Test a person block, make sure the post query doesn't include their post + let blocked_person = PersonInsertForm::test_form(inserted_instance.id, "john"); - let inserted_blocked_person = Person::create(pool, &blocked_person).await?; + let inserted_blocked_person = Person::create(pool, &blocked_person).await?; - let inserted_blocked_local_user = LocalUser::create( - pool, - &LocalUserInsertForm::test_form(inserted_blocked_person.id), - vec![], - ) - .await?; - - let post_from_blocked_person = PostInsertForm { - language_id: Some(LanguageId(1)), - ..PostInsertForm::new( - POST_BY_BLOCKED_PERSON.to_string(), - inserted_blocked_person.id, - inserted_community.id, + let inserted_blocked_local_user = LocalUser::create( + pool, + &LocalUserInsertForm::test_form(inserted_blocked_person.id), + vec![], ) - }; - Post::create(pool, &post_from_blocked_person).await?; + .await?; - // block that person - let person_block = PersonBlockForm { - person_id: inserted_person.id, - target_id: inserted_blocked_person.id, - }; + let post_from_blocked_person = PostInsertForm { + language_id: Some(LanguageId(1)), + ..PostInsertForm::new( + POST_BY_BLOCKED_PERSON.to_string(), + inserted_blocked_person.id, + inserted_community.id, + ) + }; + Post::create(pool, &post_from_blocked_person).await?; - PersonBlock::block(pool, &person_block).await?; + // block that person + let person_block = PersonBlockForm { + person_id: inserted_person.id, + target_id: inserted_blocked_person.id, + }; - // A sample post - let new_post = PostInsertForm { - language_id: Some(LanguageId(47)), - ..PostInsertForm::new(POST.to_string(), inserted_person.id, inserted_community.id) - }; - let inserted_post = Post::create(pool, &new_post).await?; + PersonBlock::block(pool, &person_block).await?; - let new_bot_post = PostInsertForm::new( - POST_BY_BOT.to_string(), - inserted_bot.id, - inserted_community.id, - ); - let inserted_bot_post = Post::create(pool, &new_bot_post).await?; + // Two community post tags + let tag_1 = Tag::create( + pool, + &TagInsertForm { + ap_id: Url::parse(&format!("{}/tags/test_tag1", inserted_community.actor_id))?.into(), + name: "Test Tag 1".into(), + community_id: inserted_community.id, + published: None, + updated: None, + deleted: false, + }, + ) + .await?; + let tag_2 = Tag::create( + pool, + &TagInsertForm { + ap_id: Url::parse(&format!("{}/tags/test_tag2", inserted_community.actor_id))?.into(), + name: "Test Tag 2".into(), + community_id: inserted_community.id, + published: None, + updated: None, + deleted: false, + }, + ) + .await?; - let local_user_view = LocalUserView { - local_user: inserted_local_user, - local_user_vote_display_mode: LocalUserVoteDisplayMode::default(), - person: inserted_person, - counts: Default::default(), - }; - let blocked_local_user_view = LocalUserView { - local_user: inserted_blocked_local_user, - local_user_vote_display_mode: LocalUserVoteDisplayMode::default(), - person: inserted_blocked_person, - counts: Default::default(), - }; + // A sample post + let new_post = PostInsertForm { + language_id: Some(LanguageId(47)), + ..PostInsertForm::new(POST.to_string(), inserted_person.id, inserted_community.id) + }; - let site = Site { - id: Default::default(), - name: String::new(), - sidebar: None, - published: Default::default(), - updated: None, - icon: None, - banner: None, - description: None, - actor_id: Url::parse("http://example.com")?.into(), - last_refreshed_at: Default::default(), - inbox_url: Url::parse("http://example.com")?.into(), - private_key: None, - public_key: String::new(), - instance_id: Default::default(), - content_warning: None, - }; + let inserted_post = Post::create(pool, &new_post).await?; - Ok(Data { - inserted_instance, - local_user_view, - blocked_local_user_view, - inserted_bot, - inserted_community, - inserted_post, - inserted_bot_post, - site, - }) + let new_bot_post = PostInsertForm::new( + POST_BY_BOT.to_string(), + inserted_bot.id, + inserted_community.id, + ); + let inserted_bot_post = Post::create(pool, &new_bot_post).await?; + + // A sample post with tags + let new_post = PostInsertForm { + language_id: Some(LanguageId(47)), + ..PostInsertForm::new( + POST_WITH_TAGS.to_string(), + inserted_person.id, + inserted_community.id, + ) + }; + + let inserted_post_with_tags = Post::create(pool, &new_post).await?; + let inserted_tags = vec![ + PostTagInsertForm { + post_id: inserted_post_with_tags.id, + tag_id: tag_1.id, + }, + PostTagInsertForm { + post_id: inserted_post_with_tags.id, + tag_id: tag_2.id, + }, + ]; + PostTagInsertForm::insert_tag_associations(pool, &inserted_tags).await?; + + let local_user_view = LocalUserView { + local_user: inserted_local_user, + local_user_vote_display_mode: LocalUserVoteDisplayMode::default(), + person: inserted_person, + counts: Default::default(), + }; + let blocked_local_user_view = LocalUserView { + local_user: inserted_blocked_local_user, + local_user_vote_display_mode: LocalUserVoteDisplayMode::default(), + person: inserted_blocked_person, + counts: Default::default(), + }; + + let site = Site { + id: Default::default(), + name: String::new(), + sidebar: None, + published: Default::default(), + updated: None, + icon: None, + banner: None, + description: None, + actor_id: Url::parse("http://example.com")?.into(), + last_refreshed_at: Default::default(), + inbox_url: Url::parse("http://example.com")?.into(), + private_key: None, + public_key: String::new(), + instance_id: Default::default(), + content_warning: None, + }; + + Ok(Data { + pool: actual_pool, + inserted_instance, + local_user_view, + blocked_local_user_view, + inserted_bot, + inserted_community, + inserted_post, + inserted_bot_post, + inserted_post_with_tags, + tag_1, + tag_2, + site, + }) + } + async fn teardown(data: Data) -> LemmyResult<()> { + let pool = &mut data.pool2(); + // let pool = &mut (&pool).into(); + let num_deleted = Post::delete(pool, data.inserted_post.id).await?; + Community::delete(pool, data.inserted_community.id).await?; + Person::delete(pool, data.local_user_view.person.id).await?; + Person::delete(pool, data.inserted_bot.id).await?; + Person::delete(pool, data.blocked_local_user_view.person.id).await?; + Instance::delete(pool, data.inserted_instance.id).await?; + assert_eq!(1, num_deleted); + + Ok(()) + } + } + impl AsyncTestContext for Data { + async fn setup() -> Self { + Data::setup().await.expect("setup failed") + } + async fn teardown(self) { + Data::teardown(self).await.expect("teardown failed") + } } + #[test_context(Data)] #[tokio::test] #[serial] - async fn post_listing_with_person() -> LemmyResult<()> { - let pool = &build_db_pool()?; + async fn post_listing_with_person(data: &mut Data) -> LemmyResult<()> { + let pool = &data.pool(); let pool = &mut pool.into(); - let mut data = init_data(pool).await?; let local_user_form = LocalUserUpdateForm { show_bot_accounts: Some(false), @@ -823,12 +945,14 @@ mod tests { LocalUser::update(pool, data.local_user_view.local_user.id, &local_user_form).await?; data.local_user_view.local_user.show_bot_accounts = false; - let read_post_listing = PostQuery { + let mut read_post_listing = PostQuery { community_id: Some(data.inserted_community.id), ..data.default_post_query() } .list(&data.site, pool) .await?; + // remove tags post + read_post_listing.remove(0); let post_listing_single_with_person = PostView::read( pool, @@ -838,7 +962,7 @@ mod tests { ) .await?; - let expected_post_listing_with_user = expected_post_view(&data, pool).await?; + let expected_post_listing_with_user = expected_post_view(data, pool).await?; // Should be only one person, IE the bot post, and blocked should be missing assert_eq!( @@ -864,17 +988,19 @@ mod tests { .list(&data.site, pool) .await?; // should include bot post which has "undetermined" language - assert_eq!(vec![POST_BY_BOT, POST], names(&post_listings_with_bots)); - - cleanup(data, pool).await + assert_eq!( + vec![POST_WITH_TAGS, POST_BY_BOT, POST], + names(&post_listings_with_bots) + ); + Ok(()) } + #[test_context(Data)] #[tokio::test] #[serial] - async fn post_listing_no_person() -> LemmyResult<()> { - let pool = &build_db_pool()?; + async fn post_listing_no_person(data: &mut Data) -> LemmyResult<()> { + let pool = &data.pool(); let pool = &mut pool.into(); - let data = init_data(pool).await?; let read_post_listing_multiple_no_person = PostQuery { community_id: Some(data.inserted_community.id), @@ -887,32 +1013,31 @@ mod tests { let read_post_listing_single_no_person = PostView::read(pool, data.inserted_post.id, None, false).await?; - let expected_post_listing_no_person = expected_post_view(&data, pool).await?; + let expected_post_listing_no_person = expected_post_view(data, pool).await?; // Should be 2 posts, with the bot post, and the blocked assert_eq!( - vec![POST_BY_BOT, POST, POST_BY_BLOCKED_PERSON], + vec![POST_WITH_TAGS, POST_BY_BOT, POST, POST_BY_BLOCKED_PERSON], names(&read_post_listing_multiple_no_person) ); assert_eq!( Some(&expected_post_listing_no_person), - read_post_listing_multiple_no_person.get(1) + read_post_listing_multiple_no_person.get(2) ); assert_eq!( expected_post_listing_no_person, read_post_listing_single_no_person ); - - cleanup(data, pool).await + Ok(()) } + #[test_context(Data)] #[tokio::test] #[serial] - async fn post_listing_title_only() -> LemmyResult<()> { - let pool = &build_db_pool()?; + async fn post_listing_title_only(data: &mut Data) -> LemmyResult<()> { + let pool = &data.pool(); let pool = &mut pool.into(); - let data = init_data(pool).await?; // A post which contains the search them 'Post' not in the title (but in the body) let new_post = PostInsertForm { @@ -950,6 +1075,7 @@ mod tests { assert_eq!( vec![ POST_WITH_ANOTHER_TITLE, + POST_WITH_TAGS, POST_BY_BOT, POST, POST_BY_BLOCKED_PERSON @@ -959,19 +1085,19 @@ mod tests { // Should be 3 posts when we search for title only assert_eq!( - vec![POST_BY_BOT, POST, POST_BY_BLOCKED_PERSON], + vec![POST_WITH_TAGS, POST_BY_BOT, POST, POST_BY_BLOCKED_PERSON], names(&read_post_listing_by_title_only) ); Post::delete(pool, inserted_post.id).await?; - cleanup(data, pool).await + Ok(()) } + #[test_context(Data)] #[tokio::test] #[serial] - async fn post_listing_block_community() -> LemmyResult<()> { - let pool = &build_db_pool()?; + async fn post_listing_block_community(data: &mut Data) -> LemmyResult<()> { + let pool = &data.pool(); let pool = &mut pool.into(); - let data = init_data(pool).await?; let community_block = CommunityBlockForm { person_id: data.local_user_view.person.id, @@ -989,15 +1115,15 @@ mod tests { assert_eq!(read_post_listings_with_person_after_block, vec![]); CommunityBlock::unblock(pool, &community_block).await?; - cleanup(data, pool).await + Ok(()) } + #[test_context(Data)] #[tokio::test] #[serial] - async fn post_listing_like() -> LemmyResult<()> { - let pool = &build_db_pool()?; + async fn post_listing_like(data: &mut Data) -> LemmyResult<()> { + let pool = &data.pool(); let pool = &mut pool.into(); - let mut data = init_data(pool).await?; let post_like_form = PostLikeForm::new(data.inserted_post.id, data.local_user_view.person.id, 1); @@ -1020,7 +1146,7 @@ mod tests { ) .await?; - let mut expected_post_with_upvote = expected_post_view(&data, pool).await?; + let mut expected_post_with_upvote = expected_post_view(data, pool).await?; expected_post_with_upvote.my_vote = Some(1); expected_post_with_upvote.counts.score = 1; expected_post_with_upvote.counts.upvotes = 1; @@ -1033,26 +1159,27 @@ mod tests { LocalUser::update(pool, data.local_user_view.local_user.id, &local_user_form).await?; data.local_user_view.local_user.show_bot_accounts = false; - let read_post_listing = PostQuery { + let mut read_post_listing = PostQuery { community_id: Some(data.inserted_community.id), ..data.default_post_query() } .list(&data.site, pool) .await?; + read_post_listing.remove(0); assert_eq!(vec![expected_post_with_upvote], read_post_listing); let like_removed = PostLike::remove(pool, data.local_user_view.person.id, data.inserted_post.id).await?; assert_eq!(uplete::Count::only_deleted(1), like_removed); - cleanup(data, pool).await + Ok(()) } + #[test_context(Data)] #[tokio::test] #[serial] - async fn post_listing_liked_only() -> LemmyResult<()> { - let pool = &build_db_pool()?; + async fn post_listing_liked_only(data: &mut Data) -> LemmyResult<()> { + let pool = &data.pool(); let pool = &mut pool.into(); - let data = init_data(pool).await?; // Like both the bot post, and your own // The liked_only should not show your own post @@ -1087,15 +1214,15 @@ mod tests { // Should be no posts assert_eq!(read_disliked_post_listing, vec![]); - cleanup(data, pool).await + Ok(()) } + #[test_context(Data)] #[tokio::test] #[serial] - async fn post_listing_saved_only() -> LemmyResult<()> { - let pool = &build_db_pool()?; + async fn post_listing_saved_only(data: &mut Data) -> LemmyResult<()> { + let pool = &data.pool(); let pool = &mut pool.into(); - let data = init_data(pool).await?; // Save only the bot post // The saved_only should only show the bot post @@ -1115,15 +1242,15 @@ mod tests { // This should only include the bot post, not the one you created assert_eq!(vec![POST_BY_BOT], names(&read_saved_post_listing)); - cleanup(data, pool).await + Ok(()) } + #[test_context(Data)] #[tokio::test] #[serial] - async fn creator_info() -> LemmyResult<()> { - let pool = &build_db_pool()?; + async fn creator_info(data: &mut Data) -> LemmyResult<()> { + let pool = &data.pool(); let pool = &mut pool.into(); - let data = init_data(pool).await?; // Make one of the inserted persons a moderator let person_id = data.local_user_view.person.id; @@ -1145,23 +1272,24 @@ mod tests { .collect::>(); let expected_post_listing = vec![ + ("tegan".to_owned(), true, true), ("mybot".to_owned(), false, false), ("tegan".to_owned(), true, true), ]; assert_eq!(expected_post_listing, post_listing); - cleanup(data, pool).await + Ok(()) } + #[test_context(Data)] #[tokio::test] #[serial] - async fn post_listing_person_language() -> LemmyResult<()> { + async fn post_listing_person_language(data: &mut Data) -> LemmyResult<()> { const EL_POSTO: &str = "el posto"; - let pool = &build_db_pool()?; + let pool = &data.pool(); let pool = &mut pool.into(); - let data = init_data(pool).await?; let spanish_id = Language::read_id_from_code(pool, "es").await?; @@ -1180,17 +1308,23 @@ mod tests { let post_listings_all = data.default_post_query().list(&data.site, pool).await?; // no language filters specified, all posts should be returned - assert_eq!(vec![EL_POSTO, POST_BY_BOT, POST], names(&post_listings_all)); + assert_eq!( + vec![EL_POSTO, POST_WITH_TAGS, POST_BY_BOT, POST], + names(&post_listings_all) + ); LocalUserLanguage::update(pool, vec![french_id], data.local_user_view.local_user.id).await?; let post_listing_french = data.default_post_query().list(&data.site, pool).await?; // only one post in french and one undetermined should be returned - assert_eq!(vec![POST_BY_BOT, POST], names(&post_listing_french)); + assert_eq!( + vec![POST_WITH_TAGS, POST_BY_BOT, POST], + names(&post_listing_french) + ); assert_eq!( Some(french_id), - post_listing_french.get(1).map(|p| p.post.language_id) + post_listing_french.get(2).map(|p| p.post.language_id) ); LocalUserLanguage::update( @@ -1207,6 +1341,7 @@ mod tests { .map(|p| (p.post.name, p.post.language_id)) .collect::>(); let expected_post_listings_french_und = vec![ + (POST_WITH_TAGS.to_owned(), french_id), (POST_BY_BOT.to_owned(), UNDETERMINED_ID), (POST.to_owned(), french_id), ]; @@ -1214,15 +1349,15 @@ mod tests { // french post and undetermined language post should be returned assert_eq!(expected_post_listings_french_und, post_listings_french_und); - cleanup(data, pool).await + Ok(()) } + #[test_context(Data)] #[tokio::test] #[serial] - async fn post_listings_removed() -> LemmyResult<()> { - let pool = &build_db_pool()?; + async fn post_listings_removed(data: &mut Data) -> LemmyResult<()> { + let pool = &data.pool(); let pool = &mut pool.into(); - let mut data = init_data(pool).await?; // Remove the post Post::update( @@ -1237,7 +1372,7 @@ mod tests { // Make sure you don't see the removed post in the results let post_listings_no_admin = data.default_post_query().list(&data.site, pool).await?; - assert_eq!(vec![POST], names(&post_listings_no_admin)); + assert_eq!(vec![POST_WITH_TAGS, POST], names(&post_listings_no_admin)); // Removed bot post is shown to admins on its profile page data.local_user_view.local_user.admin = true; @@ -1249,15 +1384,15 @@ mod tests { .await?; assert_eq!(vec![POST_BY_BOT], names(&post_listings_is_admin)); - cleanup(data, pool).await + Ok(()) } + #[test_context(Data)] #[tokio::test] #[serial] - async fn post_listings_deleted() -> LemmyResult<()> { - let pool = &build_db_pool()?; + async fn post_listings_deleted(data: &mut Data) -> LemmyResult<()> { + let pool = &data.pool(); let pool = &mut pool.into(); - let data = init_data(pool).await?; // Delete the post Post::update( @@ -1288,15 +1423,15 @@ mod tests { assert_eq!(expect_contains_deleted, contains_deleted); } - cleanup(data, pool).await + Ok(()) } + #[test_context(Data)] #[tokio::test] #[serial] - async fn post_listings_hidden_community() -> LemmyResult<()> { - let pool = &build_db_pool()?; + async fn post_listings_hidden_community(data: &mut Data) -> LemmyResult<()> { + let pool = &data.pool(); let pool = &mut pool.into(); - let data = init_data(pool).await?; Community::update( pool, @@ -1324,17 +1459,17 @@ mod tests { let posts = data.default_post_query().list(&data.site, pool).await?; assert!(!posts.is_empty()); - cleanup(data, pool).await + Ok(()) } + #[test_context(Data)] #[tokio::test] #[serial] - async fn post_listing_instance_block() -> LemmyResult<()> { + async fn post_listing_instance_block(data: &mut Data) -> LemmyResult<()> { const POST_FROM_BLOCKED_INSTANCE: &str = "post on blocked instance"; - let pool = &build_db_pool()?; + let pool = &data.pool(); let pool = &mut pool.into(); - let data = init_data(pool).await?; let blocked_instance = Instance::read_or_create(pool, "another_domain.tld".to_string()).await?; @@ -1359,7 +1494,12 @@ mod tests { // no instance block, should return all posts let post_listings_all = data.default_post_query().list(&data.site, pool).await?; assert_eq!( - vec![POST_FROM_BLOCKED_INSTANCE, POST_BY_BOT, POST], + vec![ + POST_FROM_BLOCKED_INSTANCE, + POST_WITH_TAGS, + POST_BY_BOT, + POST + ], names(&post_listings_all) ); @@ -1372,7 +1512,10 @@ mod tests { // now posts from communities on that instance should be hidden let post_listings_blocked = data.default_post_query().list(&data.site, pool).await?; - assert_eq!(vec![POST_BY_BOT, POST], names(&post_listings_blocked)); + assert_eq!( + vec![POST_WITH_TAGS, POST_BY_BOT, POST], + names(&post_listings_blocked) + ); assert!(post_listings_blocked .iter() .all(|p| p.post.id != post_from_blocked_instance.id)); @@ -1381,20 +1524,25 @@ mod tests { InstanceBlock::unblock(pool, &block_form).await?; let post_listings_blocked = data.default_post_query().list(&data.site, pool).await?; assert_eq!( - vec![POST_FROM_BLOCKED_INSTANCE, POST_BY_BOT, POST], + vec![ + POST_FROM_BLOCKED_INSTANCE, + POST_WITH_TAGS, + POST_BY_BOT, + POST + ], names(&post_listings_blocked) ); Instance::delete(pool, blocked_instance.id).await?; - cleanup(data, pool).await + Ok(()) } + #[test_context(Data)] #[tokio::test] #[serial] - async fn pagination_includes_each_post_once() -> LemmyResult<()> { - let pool = &build_db_pool()?; + async fn pagination_includes_each_post_once(data: &mut Data) -> LemmyResult<()> { + let pool = &data.pool(); let pool = &mut pool.into(); - let data = init_data(pool).await?; let community_form = CommunityInsertForm::new( data.inserted_instance.id, @@ -1496,15 +1644,15 @@ mod tests { assert_eq!(inserted_post_ids, listed_post_ids); Community::delete(pool, inserted_community.id).await?; - cleanup(data, pool).await + Ok(()) } + #[test_context(Data)] #[tokio::test] #[serial] - async fn post_listings_hide_read() -> LemmyResult<()> { - let pool = &build_db_pool()?; + async fn post_listings_hide_read(data: &mut Data) -> LemmyResult<()> { + let pool = &data.pool(); let pool = &mut pool.into(); - let mut data = init_data(pool).await?; // Make sure local user hides read posts let local_user_form = LocalUserUpdateForm { @@ -1520,7 +1668,7 @@ mod tests { // Make sure you don't see the read post in the results let post_listings_hide_read = data.default_post_query().list(&data.site, pool).await?; - assert_eq!(vec![POST], names(&post_listings_hide_read)); + assert_eq!(vec![POST_WITH_TAGS, POST], names(&post_listings_hide_read)); // Test with the show_read override as true let post_listings_show_read_true = PostQuery { @@ -1530,7 +1678,7 @@ mod tests { .list(&data.site, pool) .await?; assert_eq!( - vec![POST_BY_BOT, POST], + vec![POST_WITH_TAGS, POST_BY_BOT, POST], names(&post_listings_show_read_true) ); @@ -1541,16 +1689,19 @@ mod tests { } .list(&data.site, pool) .await?; - assert_eq!(vec![POST], names(&post_listings_show_read_false)); - cleanup(data, pool).await + assert_eq!( + vec![POST_WITH_TAGS, POST], + names(&post_listings_show_read_false) + ); + Ok(()) } + #[test_context(Data)] #[tokio::test] #[serial] - async fn post_listings_hide_hidden() -> LemmyResult<()> { - let pool = &build_db_pool()?; + async fn post_listings_hide_hidden(data: &mut Data) -> LemmyResult<()> { + let pool = &data.pool(); let pool = &mut pool.into(); - let data = init_data(pool).await?; // Mark a post as hidden PostHide::hide( @@ -1562,7 +1713,10 @@ mod tests { // Make sure you don't see the hidden post in the results let post_listings_hide_hidden = data.default_post_query().list(&data.site, pool).await?; - assert_eq!(vec![POST], names(&post_listings_hide_hidden)); + assert_eq!( + vec![POST_WITH_TAGS, POST], + names(&post_listings_hide_hidden) + ); // Make sure it does come back with the show_hidden option let post_listings_show_hidden = PostQuery { @@ -1573,20 +1727,23 @@ mod tests { } .list(&data.site, pool) .await?; - assert_eq!(vec![POST_BY_BOT, POST], names(&post_listings_show_hidden)); + assert_eq!( + vec![POST_WITH_TAGS, POST_BY_BOT, POST], + names(&post_listings_show_hidden) + ); // Make sure that hidden field is true. - assert!(&post_listings_show_hidden.first().is_some_and(|p| p.hidden)); + assert!(&post_listings_show_hidden.get(1).is_some_and(|p| p.hidden)); - cleanup(data, pool).await + Ok(()) } + #[test_context(Data)] #[tokio::test] #[serial] - async fn post_listings_hide_nsfw() -> LemmyResult<()> { - let pool = &build_db_pool()?; + async fn post_listings_hide_nsfw(data: &mut Data) -> LemmyResult<()> { + let pool = &data.pool(); let pool = &mut pool.into(); - let data = init_data(pool).await?; // Mark a post as nsfw let update_form = PostUpdateForm { @@ -1594,11 +1751,11 @@ mod tests { ..Default::default() }; - Post::update(pool, data.inserted_bot_post.id, &update_form).await?; + Post::update(pool, data.inserted_post_with_tags.id, &update_form).await?; // Make sure you don't see the nsfw post in the regular results let post_listings_hide_nsfw = data.default_post_query().list(&data.site, pool).await?; - assert_eq!(vec![POST], names(&post_listings_hide_nsfw)); + assert_eq!(vec![POST_BY_BOT, POST], names(&post_listings_hide_nsfw)); // Make sure it does come back with the show_nsfw option let post_listings_show_nsfw = PostQuery { @@ -1609,22 +1766,19 @@ mod tests { } .list(&data.site, pool) .await?; - assert_eq!(vec![POST_BY_BOT, POST], names(&post_listings_show_nsfw)); + assert_eq!( + vec![POST_WITH_TAGS, POST_BY_BOT, POST], + names(&post_listings_show_nsfw) + ); // Make sure that nsfw field is true. - assert!(&post_listings_show_nsfw.first().is_some_and(|p| p.post.nsfw)); - - cleanup(data, pool).await - } - - async fn cleanup(data: Data, pool: &mut DbPool<'_>) -> LemmyResult<()> { - let num_deleted = Post::delete(pool, data.inserted_post.id).await?; - Community::delete(pool, data.inserted_community.id).await?; - Person::delete(pool, data.local_user_view.person.id).await?; - Person::delete(pool, data.inserted_bot.id).await?; - Person::delete(pool, data.blocked_local_user_view.person.id).await?; - Instance::delete(pool, data.inserted_instance.id).await?; - assert_eq!(1, num_deleted); + assert!( + &post_listings_show_nsfw + .first() + .ok_or(LemmyErrorType::NotFound)? + .post + .nsfw + ); Ok(()) } @@ -1746,15 +1900,16 @@ mod tests { hidden: false, saved: false, creator_blocked: false, + tags: PostTags::default(), }) } + #[test_context(Data)] #[tokio::test] #[serial] - async fn local_only_instance() -> LemmyResult<()> { - let pool = &build_db_pool_for_tests(); + async fn local_only_instance(data: &mut Data) -> LemmyResult<()> { + let pool = &data.pool(); let pool = &mut pool.into(); - let data = init_data(pool).await?; Community::update( pool, @@ -1779,7 +1934,7 @@ mod tests { } .list(&data.site, pool) .await?; - assert_eq!(2, authenticated_query.len()); + assert_eq!(3, authenticated_query.len()); let unauthenticated_post = PostView::read(pool, data.inserted_post.id, None, false).await; assert!(unauthenticated_post.is_err()); @@ -1793,16 +1948,15 @@ mod tests { .await; assert!(authenticated_post.is_ok()); - cleanup(data, pool).await?; Ok(()) } + #[test_context(Data)] #[tokio::test] #[serial] - async fn post_listing_local_user_banned_from_community() -> LemmyResult<()> { - let pool = &build_db_pool()?; + async fn post_listing_local_user_banned_from_community(data: &mut Data) -> LemmyResult<()> { + let pool = &data.pool(); let pool = &mut pool.into(); - let data = init_data(pool).await?; // Test that post view shows if local user is blocked from community let banned_from_comm_person = PersonInsertForm::test_form(data.inserted_instance.id, "jill"); @@ -1837,15 +1991,15 @@ mod tests { assert!(post_view.banned_from_community); Person::delete(pool, inserted_banned_from_comm_person.id).await?; - cleanup(data, pool).await + Ok(()) } + #[test_context(Data)] #[tokio::test] #[serial] - async fn post_listing_local_user_not_banned_from_community() -> LemmyResult<()> { - let pool = &build_db_pool()?; + async fn post_listing_local_user_not_banned_from_community(data: &mut Data) -> LemmyResult<()> { + let pool = &data.pool(); let pool = &mut pool.into(); - let data = init_data(pool).await?; let post_view = PostView::read( pool, @@ -1857,15 +2011,15 @@ mod tests { assert!(!post_view.banned_from_community); - cleanup(data, pool).await + Ok(()) } + #[test_context(Data)] #[tokio::test] #[serial] - async fn speed_check() -> LemmyResult<()> { - let pool = &build_db_pool()?; + async fn speed_check(data: &mut Data) -> LemmyResult<()> { + let pool = &data.pool(); let pool = &mut pool.into(); - let data = init_data(pool).await?; // Make sure the post_view query is less than this time let duration_max = Duration::from_millis(80); @@ -1913,15 +2067,15 @@ mod tests { duration_max ); - cleanup(data, pool).await + Ok(()) } + #[test_context(Data)] #[tokio::test] #[serial] - async fn post_listings_no_comments_only() -> LemmyResult<()> { - let pool = &build_db_pool()?; + async fn post_listings_no_comments_only(data: &mut Data) -> LemmyResult<()> { + let pool = &data.pool(); let pool = &mut pool.into(); - let data = init_data(pool).await?; // Create a comment for a post let comment_form = CommentInsertForm::new( @@ -1941,17 +2095,20 @@ mod tests { .list(&data.site, pool) .await?; - assert_eq!(vec![POST_BY_BOT], names(&post_listings_no_comments)); + assert_eq!( + vec![POST_WITH_TAGS, POST_BY_BOT], + names(&post_listings_no_comments) + ); - cleanup(data, pool).await + Ok(()) } + #[test_context(Data)] #[tokio::test] #[serial] - async fn post_listing_private_community() -> LemmyResult<()> { - let pool = &build_db_pool()?; + async fn post_listing_private_community(data: &mut Data) -> LemmyResult<()> { + let pool = &data.pool(); let pool = &mut pool.into(); - let mut data = init_data(pool).await?; // Mark community as private Community::update( @@ -2003,7 +2160,7 @@ mod tests { } .list(&data.site, pool) .await?; - assert_eq!(2, read_post_listing.len()); + assert_eq!(3, read_post_listing.len()); let post_view = PostView::read( pool, data.inserted_post.id, @@ -2030,7 +2187,7 @@ mod tests { } .list(&data.site, pool) .await?; - assert_eq!(2, read_post_listing.len()); + assert_eq!(3, read_post_listing.len()); let post_view = PostView::read( pool, data.inserted_post.id, @@ -2040,6 +2197,33 @@ mod tests { .await; assert!(post_view.is_ok()); - cleanup(data, pool).await + Ok(()) + } + + #[test_context(Data)] + #[tokio::test] + #[serial] + async fn post_tags_present(data: &mut Data) -> LemmyResult<()> { + let pool = &data.pool(); + let pool = &mut pool.into(); + + let post_view = PostView::read( + pool, + data.inserted_post_with_tags.id, + Some(&data.local_user_view.local_user), + false, + ) + .await?; + + assert_eq!(2, post_view.tags.tags.len()); + assert_eq!(data.tag_1.name, post_view.tags.tags[0].name); + assert_eq!(data.tag_2.name, post_view.tags.tags[1].name); + + let all_posts = data.default_post_query().list(&data.site, pool).await?; + assert_eq!(2, all_posts[0].tags.tags.len()); // post with tags + assert_eq!(0, all_posts[1].tags.tags.len()); // bot post + assert_eq!(0, all_posts[2].tags.tags.len()); // normal post + + Ok(()) } } diff --git a/crates/db_views/src/structs.rs b/crates/db_views/src/structs.rs index 4586fbcac..a95376a1a 100644 --- a/crates/db_views/src/structs.rs +++ b/crates/db_views/src/structs.rs @@ -1,5 +1,7 @@ #[cfg(feature = "full")] use diesel::Queryable; +#[cfg(feature = "full")] +use diesel::{deserialize::FromSqlRow, expression::AsExpression, sql_types}; use lemmy_db_schema::{ aggregates::structs::{CommentAggregates, PersonAggregates, PostAggregates, SiteAggregates}, source::{ @@ -20,6 +22,7 @@ use lemmy_db_schema::{ private_message_report::PrivateMessageReport, registration_application::RegistrationApplication, site::Site, + tag::Tag, }, SubscribedType, }; @@ -151,6 +154,7 @@ pub struct PostView { #[cfg_attr(feature = "full", ts(optional))] pub my_vote: Option, pub unread_comments: i64, + pub tags: PostTags, } #[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Clone)] @@ -237,3 +241,12 @@ pub struct LocalImageView { pub local_image: LocalImage, pub person: Person, } + +#[derive(Clone, serde::Serialize, serde::Deserialize, Debug, PartialEq, Default)] +#[cfg_attr(feature = "full", derive(TS, FromSqlRow, AsExpression))] +#[serde(transparent)] +#[cfg_attr(feature = "full", diesel(sql_type = Nullable))] +/// we wrap this in a struct so we can implement FromSqlRow for it +pub struct PostTags { + pub tags: Vec, +} diff --git a/crates/db_views_actor/src/community_view.rs b/crates/db_views_actor/src/community_view.rs index f6ce82d37..8bcf50ba3 100644 --- a/crates/db_views_actor/src/community_view.rs +++ b/crates/db_views_actor/src/community_view.rs @@ -188,7 +188,7 @@ impl CommunityView { let is_mod = CommunityModeratorView::check_is_community_moderator(pool, community_id, person_id).await; if is_mod.is_ok() - || PersonView::read(pool, person_id) + || PersonView::read(pool, person_id, false) .await .is_ok_and(|t| t.is_admin) { @@ -206,7 +206,7 @@ impl CommunityView { let is_mod_of_any = CommunityModeratorView::is_community_moderator_of_any(pool, person_id).await; if is_mod_of_any.is_ok() - || PersonView::read(pool, person_id) + || PersonView::read(pool, person_id, false) .await .is_ok_and(|t| t.is_admin) { diff --git a/crates/db_views_actor/src/person_view.rs b/crates/db_views_actor/src/person_view.rs index 39d1ac27c..b90ab7811 100644 --- a/crates/db_views_actor/src/person_view.rs +++ b/crates/db_views_actor/src/person_view.rs @@ -58,12 +58,11 @@ fn post_to_person_sort_type(sort: PostSortType) -> PersonSortType { } fn queries<'a>( -) -> Queries, impl ListFn<'a, PersonView, ListMode>> { +) -> Queries, impl ListFn<'a, PersonView, ListMode>> { let all_joins = move |query: person::BoxedQuery<'a, Pg>| { query .inner_join(person_aggregates::table) .left_join(local_user::table) - .filter(person::deleted.eq(false)) .select(( person::all_columns, person_aggregates::all_columns, @@ -71,14 +70,17 @@ fn queries<'a>( )) }; - let read = move |mut conn: DbConn<'a>, person_id: PersonId| async move { - all_joins(person::table.find(person_id).into_boxed()) - .first(&mut conn) - .await + let read = move |mut conn: DbConn<'a>, params: (PersonId, bool)| async move { + let (person_id, is_admin) = params; + let mut query = all_joins(person::table.find(person_id).into_boxed()); + if !is_admin { + query = query.filter(person::deleted.eq(false)); + } + query.first(&mut conn).await }; let list = move |mut conn: DbConn<'a>, mode: ListMode| async move { - let mut query = all_joins(person::table.into_boxed()); + let mut query = all_joins(person::table.into_boxed()).filter(person::deleted.eq(false)); match mode { ListMode::Admins => { query = query @@ -135,8 +137,12 @@ fn queries<'a>( } impl PersonView { - pub async fn read(pool: &mut DbPool<'_>, person_id: PersonId) -> Result { - queries().read(pool, person_id).await + pub async fn read( + pool: &mut DbPool<'_>, + person_id: PersonId, + is_admin: bool, + ) -> Result { + queries().read(pool, (person_id, is_admin)).await } pub async fn admins(pool: &mut DbPool<'_>) -> Result, Error> { @@ -243,9 +249,13 @@ mod tests { ) .await?; - let read = PersonView::read(pool, data.alice.id).await; + let read = PersonView::read(pool, data.alice.id, false).await; assert!(read.is_err()); + // only admin can view deleted users + let read = PersonView::read(pool, data.alice.id, true).await; + assert!(read.is_ok()); + let list = PersonQuery { sort: Some(PostSortType::New), ..Default::default() @@ -303,10 +313,10 @@ mod tests { assert_length!(1, list); assert_eq!(list[0].person.id, data.alice.id); - let is_admin = PersonView::read(pool, data.alice.id).await?.is_admin; + let is_admin = PersonView::read(pool, data.alice.id, false).await?.is_admin; assert!(is_admin); - let is_admin = PersonView::read(pool, data.bob.id).await?.is_admin; + let is_admin = PersonView::read(pool, data.bob.id, false).await?.is_admin; assert!(!is_admin); cleanup(data, pool).await diff --git a/crates/db_views_moderator/src/admin_allow_instance.rs b/crates/db_views_moderator/src/admin_allow_instance.rs new file mode 100644 index 000000000..2a0aaad14 --- /dev/null +++ b/crates/db_views_moderator/src/admin_allow_instance.rs @@ -0,0 +1,52 @@ +use crate::structs::{AdminAllowInstanceView, ModlogListParams}; +use diesel::{ + result::Error, + BoolExpressionMethods, + ExpressionMethods, + IntoSql, + JoinOnDsl, + NullableExpressionMethods, + QueryDsl, +}; +use diesel_async::RunQueryDsl; +use lemmy_db_schema::{ + newtypes::PersonId, + schema::{admin_allow_instance, instance, person}, + utils::{get_conn, limit_and_offset, DbPool}, +}; + +impl AdminAllowInstanceView { + pub async fn list(pool: &mut DbPool<'_>, params: ModlogListParams) -> Result, Error> { + let conn = &mut get_conn(pool).await?; + + let admin_person_id_join = params.mod_person_id.unwrap_or(PersonId(-1)); + let show_mod_names = !params.hide_modlog_names; + let show_mod_names_expr = show_mod_names.as_sql::(); + + let admin_names_join = admin_allow_instance::admin_person_id + .eq(person::id) + .and(show_mod_names_expr.or(person::id.eq(admin_person_id_join))); + let mut query = admin_allow_instance::table + .left_join(person::table.on(admin_names_join)) + .inner_join(instance::table) + .select(( + admin_allow_instance::all_columns, + instance::all_columns, + person::all_columns.nullable(), + )) + .into_boxed(); + + if let Some(admin_person_id) = params.mod_person_id { + query = query.filter(admin_allow_instance::admin_person_id.eq(admin_person_id)); + }; + + let (limit, offset) = limit_and_offset(params.page, params.limit)?; + + query + .limit(limit) + .offset(offset) + .order_by(admin_allow_instance::when_.desc()) + .load::(conn) + .await + } +} diff --git a/crates/db_views_moderator/src/admin_block_instance.rs b/crates/db_views_moderator/src/admin_block_instance.rs new file mode 100644 index 000000000..e9d7c8b0d --- /dev/null +++ b/crates/db_views_moderator/src/admin_block_instance.rs @@ -0,0 +1,52 @@ +use crate::structs::{AdminBlockInstanceView, ModlogListParams}; +use diesel::{ + result::Error, + BoolExpressionMethods, + ExpressionMethods, + IntoSql, + JoinOnDsl, + NullableExpressionMethods, + QueryDsl, +}; +use diesel_async::RunQueryDsl; +use lemmy_db_schema::{ + newtypes::PersonId, + schema::{admin_block_instance, instance, person}, + utils::{get_conn, limit_and_offset, DbPool}, +}; + +impl AdminBlockInstanceView { + pub async fn list(pool: &mut DbPool<'_>, params: ModlogListParams) -> Result, Error> { + let conn = &mut get_conn(pool).await?; + + let admin_person_id_join = params.mod_person_id.unwrap_or(PersonId(-1)); + let show_mod_names = !params.hide_modlog_names; + let show_mod_names_expr = show_mod_names.as_sql::(); + + let admin_names_join = admin_block_instance::admin_person_id + .eq(person::id) + .and(show_mod_names_expr.or(person::id.eq(admin_person_id_join))); + let mut query = admin_block_instance::table + .left_join(person::table.on(admin_names_join)) + .inner_join(instance::table) + .select(( + admin_block_instance::all_columns, + instance::all_columns, + person::all_columns.nullable(), + )) + .into_boxed(); + + if let Some(admin_person_id) = params.mod_person_id { + query = query.filter(admin_block_instance::admin_person_id.eq(admin_person_id)); + }; + + let (limit, offset) = limit_and_offset(params.page, params.limit)?; + + query + .limit(limit) + .offset(offset) + .order_by(admin_block_instance::when_.desc()) + .load::(conn) + .await + } +} diff --git a/crates/db_views_moderator/src/lib.rs b/crates/db_views_moderator/src/lib.rs index d3e7efffd..5748707c6 100644 --- a/crates/db_views_moderator/src/lib.rs +++ b/crates/db_views_moderator/src/lib.rs @@ -1,4 +1,8 @@ #[cfg(feature = "full")] +pub mod admin_allow_instance; +#[cfg(feature = "full")] +pub mod admin_block_instance; +#[cfg(feature = "full")] pub mod admin_purge_comment_view; #[cfg(feature = "full")] pub mod admin_purge_community_view; diff --git a/crates/db_views_moderator/src/structs.rs b/crates/db_views_moderator/src/structs.rs index 27ee82522..06e9f099a 100644 --- a/crates/db_views_moderator/src/structs.rs +++ b/crates/db_views_moderator/src/structs.rs @@ -5,22 +5,29 @@ use lemmy_db_schema::{ source::{ comment::Comment, community::Community, - moderator::{ - AdminPurgeComment, - AdminPurgeCommunity, - AdminPurgePerson, - AdminPurgePost, - ModAdd, - ModAddCommunity, - ModBan, - ModBanFromCommunity, - ModFeaturePost, - ModHideCommunity, - ModLockPost, - ModRemoveComment, - ModRemoveCommunity, - ModRemovePost, - ModTransferCommunity, + instance::Instance, + mod_log::{ + admin::{ + AdminAllowInstance, + AdminBlockInstance, + AdminPurgeComment, + AdminPurgeCommunity, + AdminPurgePerson, + AdminPurgePost, + }, + moderator::{ + ModAdd, + ModAddCommunity, + ModBan, + ModBanFromCommunity, + ModFeaturePost, + ModHideCommunity, + ModLockPost, + ModRemoveComment, + ModRemoveCommunity, + ModRemovePost, + ModTransferCommunity, + }, }, person::Person, post::Post, @@ -233,6 +240,32 @@ pub struct AdminPurgePostView { pub community: Community, } +#[skip_serializing_none] +#[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))] +/// When an admin purges a post. +pub struct AdminBlockInstanceView { + pub admin_block_instance: AdminBlockInstance, + pub instance: Instance, + #[cfg_attr(feature = "full", ts(optional))] + pub admin: Option, +} + +#[skip_serializing_none] +#[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))] +/// When an admin purges a post. +pub struct AdminAllowInstanceView { + pub admin_block_instance: AdminAllowInstance, + pub instance: Instance, + #[cfg_attr(feature = "full", ts(optional))] + pub admin: Option, +} + #[skip_serializing_none] #[derive(Debug, Serialize, Deserialize, Clone, Copy)] #[cfg_attr(feature = "full", derive(TS, Queryable))] diff --git a/crates/federate/src/lib.rs b/crates/federate/src/lib.rs index 983749de3..dbb92949e 100644 --- a/crates/federate/src/lib.rs +++ b/crates/federate/src/lib.rs @@ -199,10 +199,14 @@ mod test { use super::*; use activitypub_federation::config::Data; use chrono::DateTime; - use lemmy_db_schema::source::{ - federation_allowlist::FederationAllowList, - federation_blocklist::FederationBlockList, - instance::InstanceForm, + use lemmy_db_schema::{ + source::{ + federation_allowlist::{FederationAllowList, FederationAllowListForm}, + federation_blocklist::{FederationBlockList, FederationBlockListForm}, + instance::InstanceForm, + person::{Person, PersonInsertForm}, + }, + traits::Crud, }; use lemmy_utils::error::LemmyError; use serial_test::serial; @@ -318,14 +322,22 @@ mod test { async fn test_send_manager_blocked() -> LemmyResult<()> { let mut data = TestData::init(1, 1).await?; - let domain = data.instances[0].domain.clone(); - FederationBlockList::replace(&mut data.context.pool(), Some(vec![domain])).await?; + let instance_id = data.instances[0].id; + let form = PersonInsertForm::new("tim".to_string(), String::new(), instance_id); + let person = Person::create(&mut data.context.pool(), &form).await?; + let form = FederationBlockListForm { + instance_id, + updated: None, + expires: None, + }; + FederationBlockList::block(&mut data.context.pool(), &form).await?; data.run().await?; let workers = &data.send_manager.workers; assert_eq!(2, workers.len()); assert!(workers.contains_key(&data.instances[1].id)); assert!(workers.contains_key(&data.instances[2].id)); + Person::delete(&mut data.context.pool(), person.id).await?; data.cleanup().await?; Ok(()) } @@ -336,13 +348,20 @@ mod test { async fn test_send_manager_allowed() -> LemmyResult<()> { let mut data = TestData::init(1, 1).await?; - let domain = data.instances[0].domain.clone(); - FederationAllowList::replace(&mut data.context.pool(), Some(vec![domain])).await?; + let instance_id = data.instances[0].id; + let form = PersonInsertForm::new("tim".to_string(), String::new(), instance_id); + let person = Person::create(&mut data.context.pool(), &form).await?; + let form = FederationAllowListForm { + instance_id: data.instances[0].id, + updated: None, + }; + FederationAllowList::allow(&mut data.context.pool(), &form).await?; data.run().await?; let workers = &data.send_manager.workers; assert_eq!(1, workers.len()); assert!(workers.contains_key(&data.instances[0].id)); + Person::delete(&mut data.context.pool(), person.id).await?; data.cleanup().await?; Ok(()) } diff --git a/crates/federate/src/worker.rs b/crates/federate/src/worker.rs index 047c5a5d6..1d666cfa3 100644 --- a/crates/federate/src/worker.rs +++ b/crates/federate/src/worker.rs @@ -44,9 +44,9 @@ const MAX_SUCCESSFULS: usize = 1000; /// in prod mode, try to collect multiple send results at the same time to reduce load #[cfg(not(test))] -static MIN_ACTIVITY_SEND_RESULTS_TO_HANDLE: usize = 4; +const MIN_ACTIVITY_SEND_RESULTS_TO_HANDLE: usize = 4; #[cfg(test)] -static MIN_ACTIVITY_SEND_RESULTS_TO_HANDLE: usize = 0; +const MIN_ACTIVITY_SEND_RESULTS_TO_HANDLE: usize = 0; /// /// SendManager --(has many)--> InstanceWorker --(has many)--> SendRetryTask diff --git a/crates/routes/src/feeds.rs b/crates/routes/src/feeds.rs index 55e9cc7f3..cd1ca3e98 100644 --- a/crates/routes/src/feeds.rs +++ b/crates/routes/src/feeds.rs @@ -454,7 +454,6 @@ fn build_item( protocol_and_hostname: &str, ) -> LemmyResult { // TODO add images - let author_url = format!("{protocol_and_hostname}/u/{creator_name}"); let guid = Some(Guid { permalink: true, value: url.to_owned(), @@ -464,7 +463,8 @@ fn build_item( Ok(Item { title: Some(format!("Reply from {creator_name}")), author: Some(format!( - "/u/{creator_name} (link)" + "/u/{creator_name} (link)", + format_args!("{protocol_and_hostname}/u/{creator_name}") )), pub_date: Some(published.to_rfc2822()), comments: Some(url.to_owned()), diff --git a/crates/routes/src/images.rs b/crates/routes/src/images.rs index fe91fa42c..50897b95d 100644 --- a/crates/routes/src/images.rs +++ b/crates/routes/src/images.rs @@ -1,13 +1,14 @@ use actix_web::{ - body::BodyStream, + body::{BodyStream, BoxBody}, http::{ header::{HeaderName, ACCEPT_ENCODING, HOST}, Method, StatusCode, }, - web::{self, Query}, + web::*, HttpRequest, HttpResponse, + Responder, }; use futures::stream::{Stream, StreamExt}; use http::HeaderValue; @@ -24,22 +25,18 @@ use serde::Deserialize; use std::time::Duration; use url::Url; -pub fn config( - cfg: &mut web::ServiceConfig, - client: ClientWithMiddleware, - rate_limit: &RateLimitCell, -) { +pub fn config(cfg: &mut ServiceConfig, client: ClientWithMiddleware, rate_limit: &RateLimitCell) { cfg - .app_data(web::Data::new(client)) + .app_data(Data::new(client)) .service( - web::resource("/pictrs/image") + resource("/pictrs/image") .wrap(rate_limit.image()) - .route(web::post().to(upload)), + .route(post().to(upload)), ) // This has optional query params: /image/{filename}?format=jpg&thumbnail=256 - .service(web::resource("/pictrs/image/{filename}").route(web::get().to(full_res))) - .service(web::resource("/pictrs/image/delete/{token}/{filename}").route(web::get().to(delete))) - .service(web::resource("/pictrs/healthz").route(web::get().to(healthz))); + .service(resource("/pictrs/image/{filename}").route(get().to(full_res))) + .service(resource("/pictrs/image/delete/{token}/{filename}").route(get().to(delete))) + .service(resource("/pictrs/healthz").route(get().to(healthz))); } trait ProcessUrl { @@ -129,11 +126,11 @@ fn adapt_request( async fn upload( req: HttpRequest, - body: web::Payload, + body: Payload, // require login local_user_view: LocalUserView, - client: web::Data, - context: web::Data, + client: Data, + context: Data, ) -> LemmyResult { // TODO: check rate limit here let pictrs_config = context.settings().pictrs_config()?; @@ -173,11 +170,11 @@ async fn upload( } async fn full_res( - filename: web::Path, - web::Query(params): web::Query, + filename: Path, + Query(params): Query, req: HttpRequest, - client: web::Data, - context: web::Data, + client: Data, + context: Data, local_user_view: Option, ) -> LemmyResult { // block access to images if instance is private and unauthorized, public @@ -226,10 +223,10 @@ async fn image( } async fn delete( - components: web::Path<(String, String)>, + components: Path<(String, String)>, req: HttpRequest, - client: web::Data, - context: web::Data, + client: Data, + context: Data, // require login _local_user_view: LocalUserView, ) -> LemmyResult { @@ -253,8 +250,8 @@ async fn delete( async fn healthz( req: HttpRequest, - client: web::Data, - context: web::Data, + client: Data, + context: Data, ) -> LemmyResult { let pictrs_config = context.settings().pictrs_config()?; let url = format!("{}healthz", pictrs_config.url); @@ -273,9 +270,9 @@ async fn healthz( pub async fn image_proxy( Query(params): Query, req: HttpRequest, - client: web::Data, - context: web::Data, -) -> LemmyResult { + client: Data, + context: Data, +) -> LemmyResult, HttpResponse>> { let url = Url::parse(¶ms.url)?; // Check that url corresponds to a federated image so that this can't be abused as a proxy @@ -283,10 +280,19 @@ pub async fn image_proxy( RemoteImage::validate(&mut context.pool(), url.clone().into()).await?; let pictrs_config = context.settings().pictrs_config()?; - let processed_url = params.process_url(¶ms.url, &pictrs_config.url); - image(processed_url, req, &client).await + let bypass_proxy = pictrs_config + .proxy_bypass_domains + .iter() + .any(|s| url.domain().is_some_and(|d| d == s)); + if bypass_proxy { + // Bypass proxy and redirect user to original image + Ok(Either::Left(Redirect::to(url.to_string()).respond_to(&req))) + } else { + // Proxy the image data through Lemmy + Ok(Either::Right(image(processed_url, req, &client).await?)) + } } fn make_send(mut stream: S) -> impl Stream + Send + Unpin + 'static diff --git a/crates/utils/src/error.rs b/crates/utils/src/error.rs index 906a9006d..4f28aaa32 100644 --- a/crates/utils/src/error.rs +++ b/crates/utils/src/error.rs @@ -76,6 +76,7 @@ pub enum LemmyErrorType { InvalidEmailAddress(String), RateLimitError, InvalidName, + InvalidCodeVerifier, InvalidDisplayName, InvalidMatrixId, InvalidPostTitle, @@ -112,7 +113,6 @@ pub enum LemmyErrorType { SystemErrLogin, CouldntSetAllRegistrationsAccepted, CouldntSetAllEmailVerified, - Banned, BlockedUrl, CouldntGetComments, CouldntGetPosts, @@ -151,6 +151,7 @@ pub enum LemmyErrorType { CommunityHasNoFollowers, PostScheduleTimeMustBeInFuture, TooManyScheduledPosts, + CannotCombineFederationBlocklistAndAllowlist, FederationError { #[cfg_attr(feature = "full", ts(optional))] error: Option, @@ -326,9 +327,9 @@ cfg_if! { #[test] fn deserializes_no_message() -> LemmyResult<()> { - let err = LemmyError::from(LemmyErrorType::Banned).error_response(); + let err = LemmyError::from(LemmyErrorType::BlockedUrl).error_response(); let json = String::from_utf8(err.into_body().try_into_bytes().unwrap_or_default().to_vec())?; - assert_eq!(&json, "{\"error\":\"banned\"}"); + assert_eq!(&json, "{\"error\":\"blocked_url\"}"); Ok(()) } diff --git a/crates/utils/src/settings/mod.rs b/crates/utils/src/settings/mod.rs index 986c19059..72d986d2d 100644 --- a/crates/utils/src/settings/mod.rs +++ b/crates/utils/src/settings/mod.rs @@ -3,14 +3,12 @@ use anyhow::{anyhow, Context}; use deser_hjson::from_str; use regex::Regex; use std::{env, fs, io::Error, sync::LazyLock}; +use structs::{PictrsConfig, PictrsImageMode, Settings}; use url::Url; -use urlencoding::encode; pub mod structs; -use structs::{DatabaseConnection, PictrsConfig, PictrsImageMode, Settings}; - -static DEFAULT_CONFIG_FILE: &str = "config/config.hjson"; +const DEFAULT_CONFIG_FILE: &str = "config/config.hjson"; #[allow(clippy::expect_used)] pub static SETTINGS: LazyLock = LazyLock::new(|| { @@ -51,20 +49,9 @@ impl Settings { pub fn get_database_url(&self) -> String { if let Ok(url) = env::var("LEMMY_DATABASE_URL") { - return url; - } - match &self.database.connection { - DatabaseConnection::Uri { uri } => uri.clone(), - DatabaseConnection::Parts(parts) => { - format!( - "postgres://{}:{}@{}:{}/{}", - encode(&parts.user), - encode(&parts.password), - parts.host, - parts.port, - encode(&parts.database), - ) - } + url + } else { + self.database.connection.clone() } } diff --git a/crates/utils/src/settings/structs.rs b/crates/utils/src/settings/structs.rs index fdbec4a95..1aef9f79b 100644 --- a/crates/utils/src/settings/structs.rs +++ b/crates/utils/src/settings/structs.rs @@ -88,6 +88,15 @@ pub struct PictrsConfig { #[default(PictrsImageMode::StoreLinkPreviews)] pub(super) image_mode: PictrsImageMode, + /// Allows bypassing proxy for specific image hosts when using ProxyAllImages. + /// + /// imgur.com is bypassed by default to avoid rate limit errors. When specifying any bypass + /// in the config, this default is ignored and you need to list imgur explicitly. To proxy imgur + /// requests, specify a noop bypass list, eg `proxy_bypass_domains ["example.org"]`. + #[default(vec!["i.imgur.com".to_string()])] + #[doku(example = "i.imgur.com")] + pub proxy_bypass_domains: Vec, + /// Timeout for uploading images to pictrs (in seconds) #[default(30)] pub upload_timeout: u64, @@ -111,7 +120,7 @@ pub enum PictrsImageMode { #[default] StoreLinkPreviews, /// If enabled, all images from remote domains are rewritten to pass through - /// `/api/v3/image_proxy`, including embedded images in markdown. Images are stored temporarily + /// `/api/v4/image_proxy`, including embedded images in markdown. Images are stored temporarily /// in pict-rs for caching. This improves privacy as users don't expose their IP to untrusted /// servers, and decreases load on other servers. However it increases bandwidth use for the /// local server. @@ -123,64 +132,24 @@ pub enum PictrsImageMode { #[derive(Debug, Deserialize, Serialize, Clone, SmartDefault, Document)] #[serde(default)] pub struct DatabaseConfig { - #[serde(flatten, default)] - pub(crate) connection: DatabaseConnection, + /// Configure the database by specifying URI pointing to a postgres instance + /// + /// This example uses peer authentication to obviate the need for creating, + /// configuring, and managing passwords. + /// + /// For an explanation of how to use connection URIs, see [here][0] in + /// PostgreSQL's documentation. + /// + /// [0]: https://www.postgresql.org/docs/current/libpq-connect.html#id-1.7.3.8.3.6 + #[default("postgres://lemmy:password@localhost:5432/lemmy")] + #[doku(example = "postgresql:///lemmy?user=lemmy&host=/var/run/postgresql")] + pub(crate) connection: String, /// Maximum number of active sql connections #[default(30)] pub pool_size: usize, } -#[derive(Debug, Deserialize, Serialize, Clone, SmartDefault, Document)] -#[serde(untagged)] -pub enum DatabaseConnection { - /// Configure the database by specifying a URI - /// - /// This is the preferred method to specify database connection details since - /// it is the most flexible. - Uri { - /// Connection URI pointing to a postgres instance - /// - /// This example uses peer authentication to obviate the need for creating, - /// configuring, and managing passwords. - /// - /// For an explanation of how to use connection URIs, see [here][0] in - /// PostgreSQL's documentation. - /// - /// [0]: https://www.postgresql.org/docs/current/libpq-connect.html#id-1.7.3.8.3.6 - #[doku(example = "postgresql:///lemmy?user=lemmy&host=/var/run/postgresql")] - uri: String, - }, - - /// Configure the database by specifying parts of a URI - /// - /// Note that specifying the `uri` field should be preferred since it provides - /// greater control over how the connection is made. This merely exists for - /// backwards-compatibility. - #[default] - Parts(DatabaseConnectionParts), -} - -#[derive(Debug, Deserialize, Serialize, Clone, SmartDefault, Document)] -#[serde(default)] -pub struct DatabaseConnectionParts { - /// Username to connect to postgres - #[default("lemmy")] - pub(super) user: String, - /// Password to connect to postgres - #[default("password")] - pub(super) password: String, - #[default("localhost")] - /// Host where postgres is running - pub(super) host: String, - /// Port where postgres can be accessed - #[default(5432)] - pub(super) port: i32, - /// Name of the postgres database for lemmy - #[default("lemmy")] - pub(super) database: String, -} - #[derive(Debug, Deserialize, Serialize, Clone, Document, SmartDefault)] #[serde(deny_unknown_fields)] pub struct EmailConfig { diff --git a/crates/utils/src/utils/markdown/image_links.rs b/crates/utils/src/utils/markdown/image_links.rs index 9dcea8da7..7914452ff 100644 --- a/crates/utils/src/utils/markdown/image_links.rs +++ b/crates/utils/src/utils/markdown/image_links.rs @@ -4,7 +4,7 @@ use markdown_it::{plugins::cmark::inline::image::Image, NodeValue}; use url::Url; use urlencoding::encode; -/// Rewrites all links to remote domains in markdown, so they go through `/api/v3/image_proxy`. +/// Rewrites all links to remote domains in markdown, so they go through `/api/v4/image_proxy`. pub fn markdown_rewrite_image_links(mut src: String) -> (String, Vec) { let links_offsets = find_urls::(&src); @@ -18,13 +18,14 @@ pub fn markdown_rewrite_image_links(mut src: String) -> (String, Vec) { // If link points to remote domain, replace with proxied link if parsed.domain() != Some(&SETTINGS.hostname) { let mut proxied = format!( - "{}/api/v3/image_proxy?url={}", + "{}/api/v4/image_proxy?url={}", SETTINGS.get_protocol_and_hostname(), encode(url), ); // restore custom emoji format if let Some(extra) = extra { - proxied = format!("{proxied} {extra}"); + proxied.push(' '); + proxied.push_str(extra); } src.replace_range(start..end, &proxied); } @@ -115,7 +116,7 @@ mod tests { ( "remote image proxied", "![link](http://example.com/image.jpg)", - "![link](https://lemmy-alpha/api/v3/image_proxy?url=http%3A%2F%2Fexample.com%2Fimage.jpg)", + "![link](https://lemmy-alpha/api/v4/image_proxy?url=http%3A%2F%2Fexample.com%2Fimage.jpg)", ), ( "local image unproxied", @@ -125,7 +126,7 @@ mod tests { ( "multiple image links", "![link](http://example.com/image1.jpg) ![link](http://example.com/image2.jpg)", - "![link](https://lemmy-alpha/api/v3/image_proxy?url=http%3A%2F%2Fexample.com%2Fimage1.jpg) ![link](https://lemmy-alpha/api/v3/image_proxy?url=http%3A%2F%2Fexample.com%2Fimage2.jpg)", + "![link](https://lemmy-alpha/api/v4/image_proxy?url=http%3A%2F%2Fexample.com%2Fimage1.jpg) ![link](https://lemmy-alpha/api/v4/image_proxy?url=http%3A%2F%2Fexample.com%2Fimage2.jpg)", ), ( "empty link handled", @@ -135,7 +136,7 @@ mod tests { ( "empty label handled", "![](http://example.com/image.jpg)", - "![](https://lemmy-alpha/api/v3/image_proxy?url=http%3A%2F%2Fexample.com%2Fimage.jpg)" + "![](https://lemmy-alpha/api/v4/image_proxy?url=http%3A%2F%2Fexample.com%2Fimage.jpg)" ), ( "invalid image link removed", @@ -145,12 +146,12 @@ mod tests { ( "label with nested markdown handled", "![a *b* c](http://example.com/image.jpg)", - "![a *b* c](https://lemmy-alpha/api/v3/image_proxy?url=http%3A%2F%2Fexample.com%2Fimage.jpg)" + "![a *b* c](https://lemmy-alpha/api/v4/image_proxy?url=http%3A%2F%2Fexample.com%2Fimage.jpg)" ), ( "custom emoji support", r#"![party-blob](https://www.hexbear.net/pictrs/image/83405746-0620-4728-9358-5f51b040ffee.gif "emoji party-blob")"#, - r#"![party-blob](https://lemmy-alpha/api/v3/image_proxy?url=https%3A%2F%2Fwww.hexbear.net%2Fpictrs%2Fimage%2F83405746-0620-4728-9358-5f51b040ffee.gif "emoji party-blob")"# + r#"![party-blob](https://lemmy-alpha/api/v4/image_proxy?url=https%3A%2F%2Fwww.hexbear.net%2Fpictrs%2Fimage%2F83405746-0620-4728-9358-5f51b040ffee.gif "emoji party-blob")"# ) ]; diff --git a/crates/utils/src/utils/markdown/mod.rs b/crates/utils/src/utils/markdown/mod.rs index 3dfa8e9f1..ba509596e 100644 --- a/crates/utils/src/utils/markdown/mod.rs +++ b/crates/utils/src/utils/markdown/mod.rs @@ -141,7 +141,7 @@ mod tests { ( "remote image proxied", "![link](http://example.com/image.jpg)", - "![link](https://lemmy-alpha/api/v3/image_proxy?url=http%3A%2F%2Fexample.com%2Fimage.jpg)", + "![link](https://lemmy-alpha/api/v4/image_proxy?url=http%3A%2F%2Fexample.com%2Fimage.jpg)", ), ( "local image unproxied", @@ -151,7 +151,7 @@ mod tests { ( "multiple image links", "![link](http://example.com/image1.jpg) ![link](http://example.com/image2.jpg)", - "![link](https://lemmy-alpha/api/v3/image_proxy?url=http%3A%2F%2Fexample.com%2Fimage1.jpg) ![link](https://lemmy-alpha/api/v3/image_proxy?url=http%3A%2F%2Fexample.com%2Fimage2.jpg)", + "![link](https://lemmy-alpha/api/v4/image_proxy?url=http%3A%2F%2Fexample.com%2Fimage1.jpg) ![link](https://lemmy-alpha/api/v4/image_proxy?url=http%3A%2F%2Fexample.com%2Fimage2.jpg)", ), ( "empty link handled", @@ -161,7 +161,7 @@ mod tests { ( "empty label handled", "![](http://example.com/image.jpg)", - "![](https://lemmy-alpha/api/v3/image_proxy?url=http%3A%2F%2Fexample.com%2Fimage.jpg)" + "![](https://lemmy-alpha/api/v4/image_proxy?url=http%3A%2F%2Fexample.com%2Fimage.jpg)" ), ( "invalid image link removed", @@ -171,12 +171,12 @@ mod tests { ( "label with nested markdown handled", "![a *b* c](http://example.com/image.jpg)", - "![a *b* c](https://lemmy-alpha/api/v3/image_proxy?url=http%3A%2F%2Fexample.com%2Fimage.jpg)" + "![a *b* c](https://lemmy-alpha/api/v4/image_proxy?url=http%3A%2F%2Fexample.com%2Fimage.jpg)" ), ( "custom emoji support", r#"![party-blob](https://www.hexbear.net/pictrs/image/83405746-0620-4728-9358-5f51b040ffee.gif "emoji party-blob")"#, - r#"![party-blob](https://lemmy-alpha/api/v3/image_proxy?url=https%3A%2F%2Fwww.hexbear.net%2Fpictrs%2Fimage%2F83405746-0620-4728-9358-5f51b040ffee.gif "emoji party-blob")"# + r#"![party-blob](https://lemmy-alpha/api/v4/image_proxy?url=https%3A%2F%2Fwww.hexbear.net%2Fpictrs%2Fimage%2F83405746-0620-4728-9358-5f51b040ffee.gif "emoji party-blob")"# ) ]; diff --git a/docker/Dockerfile b/docker/Dockerfile index 9701b4ad6..93f17bb95 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -5,7 +5,7 @@ ARG RUST_RELEASE_MODE=debug ARG AMD_BUILDER_IMAGE=rust:${RUST_VERSION} # Repo: https://github.com/raskyld/lemmy-cross-toolchains -ARG ARM_BUILDER_IMAGE="ghcr.io/raskyld/aarch64-lemmy-linux-gnu:v0.4.0" +ARG ARM_BUILDER_IMAGE="ghcr.io/raskyld/aarch64-lemmy-linux-gnu:v0.5.0" ARG AMD_RUNNER_IMAGE=debian:bookworm-slim ARG ARM_RUNNER_IMAGE=debian:bookworm-slim diff --git a/docker/federation/lemmy_alpha.hjson b/docker/federation/lemmy_alpha.hjson index 84ef1a16e..a3bf2bb21 100644 --- a/docker/federation/lemmy_alpha.hjson +++ b/docker/federation/lemmy_alpha.hjson @@ -8,7 +8,7 @@ site_name: lemmy-alpha } database: { - host: postgres_alpha + connection: "postgres://lemmy:password@postgres_alpha:5432/lemmy" } pictrs: { api_key: "my-pictrs-key" diff --git a/docker/federation/lemmy_beta.hjson b/docker/federation/lemmy_beta.hjson index 1b4508a43..c026b2f71 100644 --- a/docker/federation/lemmy_beta.hjson +++ b/docker/federation/lemmy_beta.hjson @@ -8,7 +8,7 @@ site_name: lemmy-beta } database: { - host: postgres_beta + connection: "postgres://lemmy:password@postgres_beta:5432/lemmy" } pictrs: { api_key: "my-pictrs-key" diff --git a/docker/federation/lemmy_delta.hjson b/docker/federation/lemmy_delta.hjson index d05e4121f..acfddc304 100644 --- a/docker/federation/lemmy_delta.hjson +++ b/docker/federation/lemmy_delta.hjson @@ -8,6 +8,6 @@ site_name: lemmy-delta } database: { - host: postgres_delta + connection: "postgres://lemmy:password@postgres_delta:5432/lemmy" } } diff --git a/docker/federation/lemmy_epsilon.hjson b/docker/federation/lemmy_epsilon.hjson index c24baa9f8..a607353a6 100644 --- a/docker/federation/lemmy_epsilon.hjson +++ b/docker/federation/lemmy_epsilon.hjson @@ -8,7 +8,7 @@ site_name: lemmy-epsilon } database: { - host: postgres_epsilon + connection: "postgres://lemmy:password@postgres_epsilon:5432/lemmy" } pictrs: { api_key: "my-pictrs-key" diff --git a/docker/federation/lemmy_gamma.hjson b/docker/federation/lemmy_gamma.hjson index d7e5b6065..7db9a5065 100644 --- a/docker/federation/lemmy_gamma.hjson +++ b/docker/federation/lemmy_gamma.hjson @@ -8,7 +8,7 @@ site_name: lemmy-gamma } database: { - host: postgres_gamma + connection: "postgres://lemmy:password@postgres_gamma:5432/lemmy" } pictrs: { api_key: "my-pictrs-key" diff --git a/docker/lemmy.hjson b/docker/lemmy.hjson index 83adf9c0c..e28a49b6d 100644 --- a/docker/lemmy.hjson +++ b/docker/lemmy.hjson @@ -11,7 +11,7 @@ site_name: "lemmy-dev" } database: { - host: postgres + connection: "postgres://lemmy:password@postgres:5432/lemmy" } hostname: "localhost" diff --git a/migrations/2024-11-23-234637_oauth_pkce/down.sql b/migrations/2024-11-23-234637_oauth_pkce/down.sql new file mode 100644 index 000000000..50c09050a --- /dev/null +++ b/migrations/2024-11-23-234637_oauth_pkce/down.sql @@ -0,0 +1,3 @@ +ALTER TABLE oauth_provider + DROP COLUMN use_pkce; + diff --git a/migrations/2024-11-23-234637_oauth_pkce/up.sql b/migrations/2024-11-23-234637_oauth_pkce/up.sql new file mode 100644 index 000000000..b03d74f7f --- /dev/null +++ b/migrations/2024-11-23-234637_oauth_pkce/up.sql @@ -0,0 +1,3 @@ +ALTER TABLE oauth_provider + ADD COLUMN use_pkce boolean DEFAULT FALSE NOT NULL; + diff --git a/migrations/2024-11-28-142005_instance-block-mod-log/down.sql b/migrations/2024-11-28-142005_instance-block-mod-log/down.sql new file mode 100644 index 000000000..7936cfe7c --- /dev/null +++ b/migrations/2024-11-28-142005_instance-block-mod-log/down.sql @@ -0,0 +1,7 @@ +ALTER TABLE federation_blocklist + DROP expires; + +DROP TABLE admin_block_instance; + +DROP TABLE admin_allow_instance; + diff --git a/migrations/2024-11-28-142005_instance-block-mod-log/up.sql b/migrations/2024-11-28-142005_instance-block-mod-log/up.sql new file mode 100644 index 000000000..f537f5d32 --- /dev/null +++ b/migrations/2024-11-28-142005_instance-block-mod-log/up.sql @@ -0,0 +1,22 @@ +ALTER TABLE federation_blocklist + ADD COLUMN expires timestamptz; + +CREATE TABLE admin_block_instance ( + id serial PRIMARY KEY, + instance_id int NOT NULL REFERENCES instance (id) ON UPDATE CASCADE ON DELETE CASCADE, + admin_person_id int NOT NULL REFERENCES person (id) ON UPDATE CASCADE ON DELETE CASCADE, + blocked bool NOT NULL, + reason text, + expires timestamptz, + when_ timestamptz NOT NULL DEFAULT now() +); + +CREATE TABLE admin_allow_instance ( + id serial PRIMARY KEY, + instance_id int NOT NULL REFERENCES instance (id) ON UPDATE CASCADE ON DELETE CASCADE, + admin_person_id int NOT NULL REFERENCES person (id) ON UPDATE CASCADE ON DELETE CASCADE, + allowed bool NOT NULL, + reason text, + when_ timestamptz NOT NULL DEFAULT now() +); + diff --git a/migrations/2024-12-17-144959_community-post-tags/down.sql b/migrations/2024-12-17-144959_community-post-tags/down.sql new file mode 100644 index 000000000..9e6e2299f --- /dev/null +++ b/migrations/2024-12-17-144959_community-post-tags/down.sql @@ -0,0 +1,4 @@ +DROP TABLE post_tag; + +DROP TABLE tag; + diff --git a/migrations/2024-12-17-144959_community-post-tags/up.sql b/migrations/2024-12-17-144959_community-post-tags/up.sql new file mode 100644 index 000000000..f0c596e09 --- /dev/null +++ b/migrations/2024-12-17-144959_community-post-tags/up.sql @@ -0,0 +1,23 @@ +-- a tag is a federatable object that gives additional context to another object, which can be displayed and filtered on +-- currently, we only have community post tags, which is a tag that is created by post authors as well as mods of a community, +-- to categorize a post. in the future we may add more tag types, depending on the requirements, +-- this will lead to either expansion of this table (community_id optional, addition of tag_type enum) +-- or split of this table / creation of new tables. +CREATE TABLE tag ( + id serial PRIMARY KEY, + ap_id text NOT NULL UNIQUE, + name text NOT NULL, + community_id int NOT NULL REFERENCES community (id) ON UPDATE CASCADE ON DELETE CASCADE, + published timestamptz NOT NULL DEFAULT now(), + updated timestamptz, + deleted boolean NOT NULL DEFAULT FALSE +); + +-- an association between a post and a tag. created/updated by the post author or mods of a community +CREATE TABLE post_tag ( + post_id int NOT NULL REFERENCES post (id) ON UPDATE CASCADE ON DELETE CASCADE, + tag_id int NOT NULL REFERENCES tag (id) ON UPDATE CASCADE ON DELETE CASCADE, + published timestamptz NOT NULL DEFAULT now(), + PRIMARY KEY (post_id, tag_id) +); + diff --git a/src/api_routes_http.rs b/src/api_routes_http.rs deleted file mode 100644 index 2f431419c..000000000 --- a/src/api_routes_http.rs +++ /dev/null @@ -1,443 +0,0 @@ -use actix_web::{guard, web}; -use lemmy_api::{ - comment::{ - distinguish::distinguish_comment, - like::like_comment, - list_comment_likes::list_comment_likes, - save::save_comment, - }, - comment_report::{ - create::create_comment_report, - list::list_comment_reports, - resolve::resolve_comment_report, - }, - community::{ - add_mod::add_mod_to_community, - ban::ban_from_community, - 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, - }, - local_user::{ - add_admin::add_admin, - ban_person::ban_from_site, - block::block_person, - change_password::change_password, - change_password_after_reset::change_password_after_reset, - generate_totp_secret::generate_totp_secret, - get_captcha::get_captcha, - list_banned::list_banned_users, - list_logins::list_logins, - list_media::list_media, - login::login, - logout::logout, - notifications::{ - list_mentions::list_mentions, - list_replies::list_replies, - mark_all_read::mark_all_notifications_read, - mark_mention_read::mark_person_mention_as_read, - mark_reply_read::mark_reply_as_read, - unread_count::unread_count, - }, - report_count::report_count, - reset_password::reset_password, - save_settings::save_user_settings, - update_totp::update_totp, - validate_auth::validate_auth, - verify_email::verify_email, - }, - post::{ - feature::feature_post, - get_link_metadata::get_link_metadata, - hide::hide_post, - like::like_post, - list_post_likes::list_post_likes, - lock::lock_post, - mark_many_read::mark_posts_as_read, - mark_read::mark_post_as_read, - save::save_post, - }, - post_report::{ - create::create_post_report, - list::list_post_reports, - resolve::resolve_post_report, - }, - private_message::mark_read::mark_pm_as_read, - private_message_report::{ - create::create_pm_report, - list::list_pm_reports, - resolve::resolve_pm_report, - }, - site::{ - block::block_instance, - federated_instances::get_federated_instances, - leave_admin::leave_admin, - list_all_media::list_all_media, - mod_log::get_mod_log, - purge::{ - comment::purge_comment, - community::purge_community, - person::purge_person, - post::purge_post, - }, - registration_applications::{ - approve::approve_registration_application, - get::get_registration_application, - list::list_registration_applications, - unread_count::get_unread_registration_application_count, - }, - }, - sitemap::get_sitemap, -}; -use lemmy_api_crud::{ - comment::{ - create::create_comment, - delete::delete_comment, - read::get_comment, - remove::remove_comment, - update::update_comment, - }, - community::{ - create::create_community, - delete::delete_community, - list::list_communities, - remove::remove_community, - update::update_community, - }, - custom_emoji::{ - create::create_custom_emoji, - delete::delete_custom_emoji, - list::list_custom_emojis, - update::update_custom_emoji, - }, - oauth_provider::{ - create::create_oauth_provider, - delete::delete_oauth_provider, - update::update_oauth_provider, - }, - post::{ - create::create_post, - delete::delete_post, - read::get_post, - remove::remove_post, - update::update_post, - }, - private_message::{ - create::create_private_message, - delete::delete_private_message, - read::get_private_message, - update::update_private_message, - }, - site::{create::create_site, read::get_site, update::update_site}, - tagline::{ - create::create_tagline, - delete::delete_tagline, - list::list_taglines, - update::update_tagline, - }, - user::{ - create::{authenticate_with_oauth, register}, - delete::delete_account, - }, -}; -use lemmy_apub::api::{ - list_comments::list_comments, - list_posts::list_posts, - read_community::get_community, - read_person::read_person, - resolve_object::resolve_object, - search::search, - user_settings_backup::{export_settings, import_settings}, -}; -use lemmy_routes::images::image_proxy; -use lemmy_utils::rate_limit::RateLimitCell; - -pub fn config(cfg: &mut web::ServiceConfig, rate_limit: &RateLimitCell) { - cfg.service( - web::scope("/api/v3") - .route("/image_proxy", web::get().to(image_proxy)) - // Site - .service( - web::scope("/site") - .wrap(rate_limit.message()) - .route("", web::get().to(get_site)) - // Admin Actions - .route("", web::post().to(create_site)) - .route("", web::put().to(update_site)) - .route("/block", web::post().to(block_instance)), - ) - .service( - web::resource("/modlog") - .wrap(rate_limit.message()) - .route(web::get().to(get_mod_log)), - ) - .service( - web::resource("/search") - .wrap(rate_limit.search()) - .route(web::get().to(search)), - ) - .service( - web::resource("/resolve_object") - .wrap(rate_limit.message()) - .route(web::get().to(resolve_object)), - ) - // Community - .service( - web::resource("/community") - .guard(guard::Post()) - .wrap(rate_limit.register()) - .route(web::post().to(create_community)), - ) - .service( - web::scope("/community") - .wrap(rate_limit.message()) - .route("", web::get().to(get_community)) - .route("", web::put().to(update_community)) - .route("/random", web::get().to(get_random_community)) - .route("/hide", web::put().to(hide_community)) - .route("/list", web::get().to(list_communities)) - .route("/follow", web::post().to(follow_community)) - .route("/block", web::post().to(block_community)) - .route("/delete", web::post().to(delete_community)) - // Mod Actions - .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)) - .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") - .wrap(rate_limit.message()) - .route("", web::get().to(get_federated_instances)), - ) - // Post - .service( - // Handle POST to /post separately to add the post() rate limitter - web::resource("/post") - .guard(guard::Post()) - .wrap(rate_limit.post()) - .route(web::post().to(create_post)), - ) - .service( - web::scope("/post") - .wrap(rate_limit.message()) - .route("", web::get().to(get_post)) - .route("", web::put().to(update_post)) - .route("/delete", web::post().to(delete_post)) - .route("/remove", web::post().to(remove_post)) - .route("/mark_as_read", web::post().to(mark_post_as_read)) - .route("/mark_many_as_read", web::post().to(mark_posts_as_read)) - .route("/hide", web::post().to(hide_post)) - .route("/lock", web::post().to(lock_post)) - .route("/feature", web::post().to(feature_post)) - .route("/list", web::get().to(list_posts)) - .route("/like", web::post().to(like_post)) - .route("/like/list", web::get().to(list_post_likes)) - .route("/save", web::put().to(save_post)) - .route("/report", web::post().to(create_post_report)) - .route("/report/resolve", web::put().to(resolve_post_report)) - .route("/report/list", web::get().to(list_post_reports)) - .route("/site_metadata", web::get().to(get_link_metadata)), - ) - // Comment - .service( - // Handle POST to /comment separately to add the comment() rate limitter - web::resource("/comment") - .guard(guard::Post()) - .wrap(rate_limit.comment()) - .route(web::post().to(create_comment)), - ) - .service( - web::scope("/comment") - .wrap(rate_limit.message()) - .route("", web::get().to(get_comment)) - .route("", web::put().to(update_comment)) - .route("/delete", web::post().to(delete_comment)) - .route("/remove", web::post().to(remove_comment)) - .route("/mark_as_read", web::post().to(mark_reply_as_read)) - .route("/distinguish", web::post().to(distinguish_comment)) - .route("/like", web::post().to(like_comment)) - .route("/like/list", web::get().to(list_comment_likes)) - .route("/save", web::put().to(save_comment)) - .route("/list", web::get().to(list_comments)) - .route("/report", web::post().to(create_comment_report)) - .route("/report/resolve", web::put().to(resolve_comment_report)) - .route("/report/list", web::get().to(list_comment_reports)), - ) - // Private Message - .service( - web::scope("/private_message") - .wrap(rate_limit.message()) - .route("/list", web::get().to(get_private_message)) - .route("", web::post().to(create_private_message)) - .route("", web::put().to(update_private_message)) - .route("/delete", web::post().to(delete_private_message)) - .route("/mark_as_read", web::post().to(mark_pm_as_read)) - .route("/report", web::post().to(create_pm_report)) - .route("/report/resolve", web::put().to(resolve_pm_report)) - .route("/report/list", web::get().to(list_pm_reports)), - ) - // User - .service( - // Account action, I don't like that it's in /user maybe /accounts - // Handle /user/register separately to add the register() rate limiter - web::resource("/user/register") - .guard(guard::Post()) - .wrap(rate_limit.register()) - .route(web::post().to(register)), - ) - // User - .service( - // Handle /user/login separately to add the register() rate limiter - // TODO: pretty annoying way to apply rate limits for register and login, we should - // group them under a common path so that rate limit is only applied once (eg under - // /account). - web::resource("/user/login") - .guard(guard::Post()) - .wrap(rate_limit.register()) - .route(web::post().to(login)), - ) - .service( - web::resource("/user/password_reset") - .wrap(rate_limit.register()) - .route(web::post().to(reset_password)), - ) - .service( - // Handle captcha separately - web::resource("/user/get_captcha") - .wrap(rate_limit.post()) - .route(web::get().to(get_captcha)), - ) - .service( - web::resource("/user/export_settings") - .wrap(rate_limit.import_user_settings()) - .route(web::get().to(export_settings)), - ) - .service( - web::resource("/user/import_settings") - .wrap(rate_limit.import_user_settings()) - .route(web::post().to(import_settings)), - ) - // TODO, all the current account related actions under /user need to get moved here eventually - .service( - web::scope("/account") - .wrap(rate_limit.message()) - .route("/list_media", web::get().to(list_media)), - ) - // User actions - .service( - web::scope("/user") - .wrap(rate_limit.message()) - .route("", web::get().to(read_person)) - .route("/mention", web::get().to(list_mentions)) - .route( - "/mention/mark_as_read", - web::post().to(mark_person_mention_as_read), - ) - .route("/replies", web::get().to(list_replies)) - // Admin action. I don't like that it's in /user - .route("/ban", web::post().to(ban_from_site)) - .route("/banned", web::get().to(list_banned_users)) - .route("/block", web::post().to(block_person)) - // TODO Account actions. I don't like that they're in /user maybe /accounts - .route("/logout", web::post().to(logout)) - .route("/delete_account", web::post().to(delete_account)) - .route( - "/password_change", - web::post().to(change_password_after_reset), - ) - // TODO mark_all_as_read feels off being in this section as well - .route( - "/mark_all_as_read", - web::post().to(mark_all_notifications_read), - ) - .route("/save_user_settings", web::put().to(save_user_settings)) - .route("/change_password", web::put().to(change_password)) - .route("/report_count", web::get().to(report_count)) - .route("/unread_count", web::get().to(unread_count)) - .route("/verify_email", web::post().to(verify_email)) - .route("/leave_admin", web::post().to(leave_admin)) - .route("/totp/generate", web::post().to(generate_totp_secret)) - .route("/totp/update", web::post().to(update_totp)) - .route("/list_logins", web::get().to(list_logins)) - .route("/validate_auth", web::get().to(validate_auth)), - ) - // Admin Actions - .service( - web::scope("/admin") - .wrap(rate_limit.message()) - .route("/add", web::post().to(add_admin)) - .route( - "/registration_application/count", - web::get().to(get_unread_registration_application_count), - ) - .route( - "/registration_application/list", - web::get().to(list_registration_applications), - ) - .route( - "/registration_application/approve", - web::put().to(approve_registration_application), - ) - .route( - "/registration_application", - web::get().to(get_registration_application), - ) - .route("/list_all_media", web::get().to(list_all_media)) - .service( - web::scope("/purge") - .route("/person", web::post().to(purge_person)) - .route("/community", web::post().to(purge_community)) - .route("/post", web::post().to(purge_post)) - .route("/comment", web::post().to(purge_comment)), - ) - .service( - web::scope("/tagline") - .wrap(rate_limit.message()) - .route("", web::post().to(create_tagline)) - .route("", web::put().to(update_tagline)) - .route("/delete", web::post().to(delete_tagline)) - .route("/list", web::get().to(list_taglines)), - ), - ) - .service( - web::scope("/custom_emoji") - .wrap(rate_limit.message()) - .route("", web::post().to(create_custom_emoji)) - .route("", web::put().to(update_custom_emoji)) - .route("/delete", web::post().to(delete_custom_emoji)) - .route("/list", web::get().to(list_custom_emojis)), - ) - .service( - web::scope("/oauth_provider") - .wrap(rate_limit.message()) - .route("", web::post().to(create_oauth_provider)) - .route("", web::put().to(update_oauth_provider)) - .route("/delete", web::post().to(delete_oauth_provider)), - ) - .service( - web::scope("/oauth") - .wrap(rate_limit.register()) - .route("/authenticate", web::post().to(authenticate_with_oauth)), - ), - ); - cfg.service( - web::scope("/sitemap.xml") - .wrap(rate_limit.message()) - .route("", web::get().to(get_sitemap)), - ); -} diff --git a/src/api_routes_v3.rs b/src/api_routes_v3.rs new file mode 100644 index 000000000..eefaf5b87 --- /dev/null +++ b/src/api_routes_v3.rs @@ -0,0 +1,388 @@ +use actix_web::{guard, web::*}; +use lemmy_api::{ + comment::{ + distinguish::distinguish_comment, + like::like_comment, + list_comment_likes::list_comment_likes, + save::save_comment, + }, + comment_report::{ + create::create_comment_report, + list::list_comment_reports, + resolve::resolve_comment_report, + }, + community::{ + add_mod::add_mod_to_community, + ban::ban_from_community, + block::user_block_community, + follow::follow_community, + hide::hide_community, + transfer::transfer_community, + }, + local_user::{ + add_admin::add_admin, + ban_person::ban_from_site, + block::user_block_person, + change_password::change_password, + change_password_after_reset::change_password_after_reset, + generate_totp_secret::generate_totp_secret, + get_captcha::get_captcha, + list_banned::list_banned_users, + list_logins::list_logins, + list_media::list_media, + login::login, + logout::logout, + notifications::{ + list_mentions::list_mentions, + list_replies::list_replies, + mark_all_read::mark_all_notifications_read, + mark_mention_read::mark_person_mention_as_read, + mark_reply_read::mark_reply_as_read, + unread_count::unread_count, + }, + report_count::report_count, + reset_password::reset_password, + save_settings::save_user_settings, + update_totp::update_totp, + user_block_instance::user_block_instance, + validate_auth::validate_auth, + verify_email::verify_email, + }, + post::{ + feature::feature_post, + get_link_metadata::get_link_metadata, + hide::hide_post, + like::like_post, + list_post_likes::list_post_likes, + lock::lock_post, + mark_read::mark_post_as_read, + save::save_post, + }, + post_report::{ + create::create_post_report, + list::list_post_reports, + resolve::resolve_post_report, + }, + private_message::mark_read::mark_pm_as_read, + private_message_report::{ + create::create_pm_report, + list::list_pm_reports, + resolve::resolve_pm_report, + }, + site::{ + federated_instances::get_federated_instances, + leave_admin::leave_admin, + list_all_media::list_all_media, + mod_log::get_mod_log, + purge::{ + comment::purge_comment, + community::purge_community, + person::purge_person, + post::purge_post, + }, + registration_applications::{ + approve::approve_registration_application, + get::get_registration_application, + list::list_registration_applications, + unread_count::get_unread_registration_application_count, + }, + }, + sitemap::get_sitemap, +}; +use lemmy_api_crud::{ + comment::{ + create::create_comment, + delete::delete_comment, + read::get_comment, + remove::remove_comment, + update::update_comment, + }, + community::{ + create::create_community, + delete::delete_community, + list::list_communities, + remove::remove_community, + update::update_community, + }, + custom_emoji::{ + create::create_custom_emoji, + delete::delete_custom_emoji, + update::update_custom_emoji, + }, + post::{ + create::create_post, + delete::delete_post, + read::get_post, + remove::remove_post, + update::update_post, + }, + private_message::{ + create::create_private_message, + delete::delete_private_message, + read::get_private_message, + update::update_private_message, + }, + site::{create::create_site, read::get_site_v3, update::update_site}, + user::{create::register, delete::delete_account}, +}; +use lemmy_apub::api::{ + list_comments::list_comments, + list_posts::list_posts, + read_community::get_community, + read_person::read_person, + resolve_object::resolve_object, + search::search, + user_settings_backup::{export_settings, import_settings}, +}; +use lemmy_routes::images::image_proxy; +use lemmy_utils::rate_limit::RateLimitCell; + +// Deprecated, use api v4 instead. +// When removing api v3, we also need to rewrite all links in database with +// `/api/v3/image_proxy` to use `/api/v4/image_proxy` instead. +pub fn config(cfg: &mut ServiceConfig, rate_limit: &RateLimitCell) { + cfg.service( + scope("/api/v3") + .route("/image_proxy", get().to(image_proxy)) + // Site + .service( + scope("/site") + .wrap(rate_limit.message()) + .route("", get().to(get_site_v3)) + // Admin Actions + .route("", post().to(create_site)) + .route("", put().to(update_site)) + .route("/block", post().to(user_block_instance)), + ) + .service( + resource("/modlog") + .wrap(rate_limit.message()) + .route(get().to(get_mod_log)), + ) + .service( + resource("/search") + .wrap(rate_limit.search()) + .route(get().to(search)), + ) + .service( + resource("/resolve_object") + .wrap(rate_limit.message()) + .route(get().to(resolve_object)), + ) + // Community + .service( + resource("/community") + .guard(guard::Post()) + .wrap(rate_limit.register()) + .route(post().to(create_community)), + ) + .service( + scope("/community") + .wrap(rate_limit.message()) + .route("", get().to(get_community)) + .route("", put().to(update_community)) + .route("/hide", put().to(hide_community)) + .route("/list", get().to(list_communities)) + .route("/follow", post().to(follow_community)) + .route("/block", post().to(user_block_community)) + .route("/delete", post().to(delete_community)) + // Mod Actions + .route("/remove", post().to(remove_community)) + .route("/transfer", post().to(transfer_community)) + .route("/ban_user", post().to(ban_from_community)) + .route("/mod", post().to(add_mod_to_community)), + ) + .service( + scope("/federated_instances") + .wrap(rate_limit.message()) + .route("", get().to(get_federated_instances)), + ) + // Post + .service( + // Handle POST to /post separately to add the post() rate limitter + resource("/post") + .guard(guard::Post()) + .wrap(rate_limit.post()) + .route(post().to(create_post)), + ) + .service( + scope("/post") + .wrap(rate_limit.message()) + .route("", get().to(get_post)) + .route("", put().to(update_post)) + .route("/delete", post().to(delete_post)) + .route("/remove", post().to(remove_post)) + .route("/mark_as_read", post().to(mark_post_as_read)) + .route("/hide", post().to(hide_post)) + .route("/lock", post().to(lock_post)) + .route("/feature", post().to(feature_post)) + .route("/list", get().to(list_posts)) + .route("/like", post().to(like_post)) + .route("/like/list", get().to(list_post_likes)) + .route("/save", put().to(save_post)) + .route("/report", post().to(create_post_report)) + .route("/report/resolve", put().to(resolve_post_report)) + .route("/report/list", get().to(list_post_reports)) + .route("/site_metadata", get().to(get_link_metadata)), + ) + // Comment + .service( + // Handle POST to /comment separately to add the comment() rate limitter + resource("/comment") + .guard(guard::Post()) + .wrap(rate_limit.comment()) + .route(post().to(create_comment)), + ) + .service( + scope("/comment") + .wrap(rate_limit.message()) + .route("", get().to(get_comment)) + .route("", put().to(update_comment)) + .route("/delete", post().to(delete_comment)) + .route("/remove", post().to(remove_comment)) + .route("/mark_as_read", post().to(mark_reply_as_read)) + .route("/distinguish", post().to(distinguish_comment)) + .route("/like", post().to(like_comment)) + .route("/like/list", get().to(list_comment_likes)) + .route("/save", put().to(save_comment)) + .route("/list", get().to(list_comments)) + .route("/report", post().to(create_comment_report)) + .route("/report/resolve", put().to(resolve_comment_report)) + .route("/report/list", get().to(list_comment_reports)), + ) + // Private Message + .service( + scope("/private_message") + .wrap(rate_limit.message()) + .route("/list", get().to(get_private_message)) + .route("", post().to(create_private_message)) + .route("", put().to(update_private_message)) + .route("/delete", post().to(delete_private_message)) + .route("/mark_as_read", post().to(mark_pm_as_read)) + .route("/report", post().to(create_pm_report)) + .route("/report/resolve", put().to(resolve_pm_report)) + .route("/report/list", get().to(list_pm_reports)), + ) + // User + .service( + // Account action, I don't like that it's in /user maybe /accounts + // Handle /user/register separately to add the register() rate limiter + resource("/user/register") + .guard(guard::Post()) + .wrap(rate_limit.register()) + .route(post().to(register)), + ) + // User + .service( + // Handle /user/login separately to add the register() rate limiter + // TODO: pretty annoying way to apply rate limits for register and login, we should + // group them under a common path so that rate limit is only applied once (eg under + // /account). + resource("/user/login") + .guard(guard::Post()) + .wrap(rate_limit.register()) + .route(post().to(login)), + ) + .service( + resource("/user/password_reset") + .wrap(rate_limit.register()) + .route(post().to(reset_password)), + ) + .service( + // Handle captcha separately + resource("/user/get_captcha") + .wrap(rate_limit.post()) + .route(get().to(get_captcha)), + ) + .service( + resource("/user/export_settings") + .wrap(rate_limit.import_user_settings()) + .route(get().to(export_settings)), + ) + .service( + resource("/user/import_settings") + .wrap(rate_limit.import_user_settings()) + .route(post().to(import_settings)), + ) + // TODO, all the current account related actions under /user need to get moved here eventually + .service( + scope("/account") + .wrap(rate_limit.message()) + .route("/list_media", get().to(list_media)), + ) + // User actions + .service( + scope("/user") + .wrap(rate_limit.message()) + .route("", get().to(read_person)) + .route("/mention", get().to(list_mentions)) + .route( + "/mention/mark_as_read", + post().to(mark_person_mention_as_read), + ) + .route("/replies", get().to(list_replies)) + // Admin action. I don't like that it's in /user + .route("/ban", post().to(ban_from_site)) + .route("/banned", get().to(list_banned_users)) + .route("/block", post().to(user_block_person)) + // TODO Account actions. I don't like that they're in /user maybe /accounts + .route("/logout", post().to(logout)) + .route("/delete_account", post().to(delete_account)) + .route("/password_change", post().to(change_password_after_reset)) + // TODO mark_all_as_read feels off being in this section as well + .route("/mark_all_as_read", post().to(mark_all_notifications_read)) + .route("/save_user_settings", put().to(save_user_settings)) + .route("/change_password", put().to(change_password)) + .route("/report_count", get().to(report_count)) + .route("/unread_count", get().to(unread_count)) + .route("/verify_email", post().to(verify_email)) + .route("/leave_admin", post().to(leave_admin)) + .route("/totp/generate", post().to(generate_totp_secret)) + .route("/totp/update", post().to(update_totp)) + .route("/list_logins", get().to(list_logins)) + .route("/validate_auth", get().to(validate_auth)), + ) + // Admin Actions + .service( + scope("/admin") + .wrap(rate_limit.message()) + .route("/add", post().to(add_admin)) + .route( + "/registration_application/count", + get().to(get_unread_registration_application_count), + ) + .route( + "/registration_application/list", + get().to(list_registration_applications), + ) + .route( + "/registration_application/approve", + put().to(approve_registration_application), + ) + .route( + "/registration_application", + get().to(get_registration_application), + ) + .route("/list_all_media", get().to(list_all_media)) + .service( + scope("/purge") + .route("/person", post().to(purge_person)) + .route("/community", post().to(purge_community)) + .route("/post", post().to(purge_post)) + .route("/comment", post().to(purge_comment)), + ), + ) + .service( + scope("/custom_emoji") + .wrap(rate_limit.message()) + .route("", post().to(create_custom_emoji)) + .route("", put().to(update_custom_emoji)) + .route("/delete", post().to(delete_custom_emoji)), + ), + ); + cfg.service( + scope("/sitemap.xml") + .wrap(rate_limit.message()) + .route("", get().to(get_sitemap)), + ); +} diff --git a/src/api_routes_v4.rs b/src/api_routes_v4.rs new file mode 100644 index 000000000..a9f71c9da --- /dev/null +++ b/src/api_routes_v4.rs @@ -0,0 +1,392 @@ +use actix_web::{guard, web::*}; +use lemmy_api::{ + comment::{ + distinguish::distinguish_comment, + like::like_comment, + list_comment_likes::list_comment_likes, + save::save_comment, + }, + comment_report::{ + create::create_comment_report, + list::list_comment_reports, + resolve::resolve_comment_report, + }, + community::{ + add_mod::add_mod_to_community, + ban::ban_from_community, + block::user_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, + }, + local_user::{ + add_admin::add_admin, + ban_person::ban_from_site, + block::user_block_person, + change_password::change_password, + change_password_after_reset::change_password_after_reset, + generate_totp_secret::generate_totp_secret, + get_captcha::get_captcha, + list_banned::list_banned_users, + list_logins::list_logins, + list_media::list_media, + login::login, + logout::logout, + notifications::{ + list_mentions::list_mentions, + list_replies::list_replies, + mark_all_read::mark_all_notifications_read, + mark_mention_read::mark_person_mention_as_read, + mark_reply_read::mark_reply_as_read, + unread_count::unread_count, + }, + report_count::report_count, + reset_password::reset_password, + save_settings::save_user_settings, + update_totp::update_totp, + user_block_instance::user_block_instance, + validate_auth::validate_auth, + verify_email::verify_email, + }, + post::{ + feature::feature_post, + get_link_metadata::get_link_metadata, + hide::hide_post, + like::like_post, + list_post_likes::list_post_likes, + lock::lock_post, + mark_many_read::mark_posts_as_read, + mark_read::mark_post_as_read, + save::save_post, + }, + post_report::{ + create::create_post_report, + list::list_post_reports, + resolve::resolve_post_report, + }, + private_message::mark_read::mark_pm_as_read, + private_message_report::{ + create::create_pm_report, + list::list_pm_reports, + resolve::resolve_pm_report, + }, + site::{ + admin_allow_instance::admin_allow_instance, + admin_block_instance::admin_block_instance, + federated_instances::get_federated_instances, + leave_admin::leave_admin, + list_all_media::list_all_media, + mod_log::get_mod_log, + purge::{ + comment::purge_comment, + community::purge_community, + person::purge_person, + post::purge_post, + }, + registration_applications::{ + approve::approve_registration_application, + get::get_registration_application, + list::list_registration_applications, + unread_count::get_unread_registration_application_count, + }, + }, + sitemap::get_sitemap, +}; +use lemmy_api_crud::{ + comment::{ + create::create_comment, + delete::delete_comment, + read::get_comment, + remove::remove_comment, + update::update_comment, + }, + community::{ + create::create_community, + delete::delete_community, + list::list_communities, + remove::remove_community, + update::update_community, + }, + custom_emoji::{ + create::create_custom_emoji, + delete::delete_custom_emoji, + list::list_custom_emojis, + update::update_custom_emoji, + }, + oauth_provider::{ + create::create_oauth_provider, + delete::delete_oauth_provider, + update::update_oauth_provider, + }, + post::{ + create::create_post, + delete::delete_post, + read::get_post, + remove::remove_post, + update::update_post, + }, + private_message::{ + create::create_private_message, + delete::delete_private_message, + read::get_private_message, + update::update_private_message, + }, + site::{create::create_site, read::get_site_v4, update::update_site}, + tagline::{ + create::create_tagline, + delete::delete_tagline, + list::list_taglines, + update::update_tagline, + }, + user::{ + create::{authenticate_with_oauth, register}, + delete::delete_account, + my_user::get_my_user, + }, +}; +use lemmy_apub::api::{ + list_comments::list_comments, + list_posts::list_posts, + read_community::get_community, + read_person::read_person, + resolve_object::resolve_object, + search::search, + user_settings_backup::{export_settings, import_settings}, +}; +use lemmy_routes::images::image_proxy; +use lemmy_utils::rate_limit::RateLimitCell; + +pub fn config(cfg: &mut ServiceConfig, rate_limit: &RateLimitCell) { + cfg.service( + scope("/api/v4") + .wrap(rate_limit.message()) + .route("/image_proxy", get().to(image_proxy)) + // Site + .service( + scope("/site") + .route("", get().to(get_site_v4)) + .route("", post().to(create_site)) + .route("", put().to(update_site)), + ) + .route("/modlog", get().to(get_mod_log)) + .service( + resource("/search") + .wrap(rate_limit.search()) + .route(get().to(search)), + ) + .route("/resolve_object", get().to(resolve_object)) + // Community + .service( + resource("/community") + .guard(guard::Post()) + .wrap(rate_limit.register()) + .route(post().to(create_community)), + ) + .service( + scope("/community") + .route("", get().to(get_community)) + .route("", put().to(update_community)) + .route("/random", get().to(get_random_community)) + .route("/hide", put().to(hide_community)) + .route("/list", get().to(list_communities)) + .route("/follow", post().to(follow_community)) + .route("/delete", post().to(delete_community)) + // Mod Actions + .route("/remove", post().to(remove_community)) + .route("/transfer", post().to(transfer_community)) + .route("/ban_user", post().to(ban_from_community)) + .route("/mod", post().to(add_mod_to_community)) + .service( + scope("/pending_follows") + .route("/count", get().to(get_pending_follows_count)) + .route("/list", get().to(get_pending_follows_list)) + .route("/approve", post().to(post_pending_follows_approve)), + ), + ) + .route("/federated_instances", get().to(get_federated_instances)) + // Post + .service( + // Handle POST to /post separately to add the post() rate limitter + resource("/post") + .guard(guard::Post()) + .wrap(rate_limit.post()) + .route(post().to(create_post)), + ) + .service( + scope("/post") + .route("", get().to(get_post)) + .route("", put().to(update_post)) + .route("/delete", post().to(delete_post)) + .route("/remove", post().to(remove_post)) + .route("/mark_as_read", post().to(mark_post_as_read)) + .route("/mark_as_read/many", post().to(mark_posts_as_read)) + .route("/hide", post().to(hide_post)) + .route("/lock", post().to(lock_post)) + .route("/feature", post().to(feature_post)) + .route("/list", get().to(list_posts)) + .route("/like", post().to(like_post)) + .route("/like/list", get().to(list_post_likes)) + .route("/save", put().to(save_post)) + .route("/report", post().to(create_post_report)) + .route("/report/resolve", put().to(resolve_post_report)) + .route("/report/list", get().to(list_post_reports)) + .route("/site_metadata", get().to(get_link_metadata)), + ) + // Comment + .service( + // Handle POST to /comment separately to add the comment() rate limitter + resource("/comment") + .guard(guard::Post()) + .wrap(rate_limit.comment()) + .route(post().to(create_comment)), + ) + .service( + scope("/comment") + .route("", get().to(get_comment)) + .route("", put().to(update_comment)) + .route("/delete", post().to(delete_comment)) + .route("/remove", post().to(remove_comment)) + .route("/mark_as_read", post().to(mark_reply_as_read)) + .route("/distinguish", post().to(distinguish_comment)) + .route("/like", post().to(like_comment)) + .route("/like/list", get().to(list_comment_likes)) + .route("/save", put().to(save_comment)) + .route("/list", get().to(list_comments)) + .route("/report", post().to(create_comment_report)) + .route("/report/resolve", put().to(resolve_comment_report)) + .route("/report/list", get().to(list_comment_reports)), + ) + // Private Message + .service( + scope("/private_message") + .route("/list", get().to(get_private_message)) + .route("", post().to(create_private_message)) + .route("", put().to(update_private_message)) + .route("/delete", post().to(delete_private_message)) + .route("/mark_as_read", post().to(mark_pm_as_read)) + .route("/report", post().to(create_pm_report)) + .route("/report/resolve", put().to(resolve_pm_report)) + .route("/report/list", get().to(list_pm_reports)), + ) + // User + .service( + scope("/account/auth") + .guard(guard::Post()) + .wrap(rate_limit.register()) + .route("/register", post().to(register)) + .route("/login", post().to(login)) + .route("/logout", post().to(logout)) + .route("/password_reset", post().to(reset_password)) + .route("/get_captcha", get().to(get_captcha)) + .route("/password_change", post().to(change_password_after_reset)) + .route("/change_password", put().to(change_password)) + .route("/totp/generate", post().to(generate_totp_secret)) + .route("/totp/update", post().to(update_totp)) + .route("/verify_email", post().to(verify_email)), + ) + .route("/account/settings/save", put().to(save_user_settings)) + .service( + scope("/account/settings") + .wrap(rate_limit.import_user_settings()) + .route("/export", get().to(export_settings)) + .route("/import", post().to(import_settings)), + ) + .service( + scope("/account") + .route("", get().to(get_my_user)) + .route("/list_media", get().to(list_media)) + .route("/mention", get().to(list_mentions)) + .route("/replies", get().to(list_replies)) + .route("/delete", post().to(delete_account)) + .route( + "/mention/mark_as_read", + post().to(mark_person_mention_as_read), + ) + .route( + "/mention/mark_as_read/all", + post().to(mark_all_notifications_read), + ) + .route("/report_count", get().to(report_count)) + .route("/unread_count", get().to(unread_count)) + .route("/list_logins", get().to(list_logins)) + .route("/validate_auth", get().to(validate_auth)) + .service( + scope("/block") + .route("/person", post().to(user_block_person)) + .route("/community", post().to(user_block_community)) + .route("/instance", post().to(user_block_instance)), + ), + ) + // User actions + .route("/person", get().to(read_person)) + // Admin Actions + .service( + scope("/admin") + .route("/add", post().to(add_admin)) + .route( + "/registration_application/count", + get().to(get_unread_registration_application_count), + ) + .route( + "/registration_application/list", + get().to(list_registration_applications), + ) + .route( + "/registration_application/approve", + put().to(approve_registration_application), + ) + .route( + "/registration_application", + get().to(get_registration_application), + ) + .route("/list_all_media", get().to(list_all_media)) + .service( + scope("/purge") + .route("/person", post().to(purge_person)) + .route("/community", post().to(purge_community)) + .route("/post", post().to(purge_post)) + .route("/comment", post().to(purge_comment)), + ) + .service( + scope("/tagline") + .route("", post().to(create_tagline)) + .route("", put().to(update_tagline)) + .route("/delete", post().to(delete_tagline)) + .route("/list", get().to(list_taglines)), + ) + .route("/ban", post().to(ban_from_site)) + .route("/banned", get().to(list_banned_users)) + .route("/leave", post().to(leave_admin)) + .service( + scope("/instance") + .route("/block", post().to(admin_block_instance)) + .route("/allow", post().to(admin_allow_instance)), + ), + ) + .service( + scope("/custom_emoji") + .route("", post().to(create_custom_emoji)) + .route("", put().to(update_custom_emoji)) + .route("/delete", post().to(delete_custom_emoji)) + .route("/list", get().to(list_custom_emojis)), + ) + .service( + scope("/oauth_provider") + .route("", post().to(create_oauth_provider)) + .route("", put().to(update_oauth_provider)) + .route("/delete", post().to(delete_oauth_provider)), + ) + .service( + scope("/oauth") + .wrap(rate_limit.register()) + .route("/authenticate", post().to(authenticate_with_oauth)), + ) + .route("/sitemap.xml", get().to(get_sitemap)), + ); +} diff --git a/src/lib.rs b/src/lib.rs index 2e04f5db8..2d140b849 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,5 @@ -pub mod api_routes_http; +pub mod api_routes_v3; +pub mod api_routes_v4; pub mod code_migrations; pub mod prometheus_metrics; pub mod scheduled_tasks; @@ -360,7 +361,8 @@ fn create_http_server( // The routes app - .configure(|cfg| api_routes_http::config(cfg, &rate_limit_cell)) + .configure(|cfg| api_routes_v3::config(cfg, &rate_limit_cell)) + .configure(|cfg| api_routes_v4::config(cfg, &rate_limit_cell)) .configure(|cfg| { if federation_enabled { lemmy_apub::http::routes::config(cfg); diff --git a/src/scheduled_tasks.rs b/src/scheduled_tasks.rs index 5900fe39f..53c0b888b 100644 --- a/src/scheduled_tasks.rs +++ b/src/scheduled_tasks.rs @@ -23,6 +23,7 @@ use lemmy_db_schema::{ comment, community, community_actions, + federation_blocklist, instance, person, post, @@ -58,6 +59,7 @@ pub async fn setup(context: Data) -> LemmyResult<()> { async move { active_counts(&mut context.pool()).await; update_banned_when_expired(&mut context.pool()).await; + delete_instance_block_when_expired(&mut context.pool()).await; } }); @@ -113,6 +115,7 @@ async fn startup_jobs(pool: &mut DbPool<'_>) { active_counts(pool).await; update_hot_ranks(pool).await; update_banned_when_expired(pool).await; + delete_instance_block_when_expired(pool).await; clear_old_activities(pool).await; overwrite_deleted_posts_and_comments(pool).await; delete_old_denied_users(pool).await; @@ -187,10 +190,8 @@ async fn process_ranks_in_batches( UPDATE {aggregates_table} a {set_clause} FROM batch WHERE a.{id_column} = batch.{id_column} RETURNING a.published; "#, - id_column = format!("{table_name}_id"), - aggregates_table = format!("{table_name}_aggregates"), - set_clause = set_clause, - where_clause = where_clause + id_column = format_args!("{table_name}_id"), + aggregates_table = format_args!("{table_name}_aggregates"), )) .bind::(previous_batch_last_published) .bind::(update_batch_size) @@ -446,6 +447,27 @@ async fn update_banned_when_expired(pool: &mut DbPool<'_>) { } } +/// Set banned to false after ban expires +async fn delete_instance_block_when_expired(pool: &mut DbPool<'_>) { + info!("Delete instance blocks when expired ..."); + let conn = get_conn(pool).await; + + match conn { + Ok(mut conn) => { + diesel::delete( + federation_blocklist::table.filter(federation_blocklist::expires.lt(now().nullable())), + ) + .execute(&mut conn) + .await + .inspect_err(|e| error!("Failed to remove federation_blocklist expired rows: {e}")) + .ok(); + } + Err(e) => { + error!("Failed to get connection from pool: {e}"); + } + } +} + /// Find all unpublished posts with scheduled date in the future, and publish them. async fn publish_scheduled_posts(context: &Data) { let pool = &mut context.pool(); @@ -555,13 +577,13 @@ async fn build_update_instance_form( // This is the only kind of error that means the instance is dead return None; }; + let status = res.status(); + if status.is_client_error() || status.is_server_error() { + return None; + } // In this block, returning `None` is ignored, and only means not writing nodeinfo to db async { - if res.status().is_client_error() { - return None; - } - let node_info_url = res .json::() .await