diff --git a/.woodpecker.yml b/.woodpecker.yml index cb6580e32..060cc3e26 100644 --- a/.woodpecker.yml +++ b/.woodpecker.yml @@ -73,12 +73,12 @@ steps: when: - event: pull_request - cargo_machete: + cargo_shear: image: *rust_nightly_image commands: - *install_binstall - - cargo binstall -y cargo-machete - - cargo machete + - cargo binstall -y cargo-shear + - cargo shear when: - event: pull_request @@ -122,7 +122,6 @@ steps: environment: CARGO_HOME: .cargo_home commands: - - export LEMMY_CONFIG_LOCATION=./config/config.hjson - ./scripts/update_config_defaults.sh config/defaults_current.hjson - diff config/defaults.hjson config/defaults_current.hjson when: *slow_check_paths @@ -134,8 +133,8 @@ steps: DATABASE_URL: postgres://lemmy:password@database:5432/lemmy commands: - <<: *install_diesel_cli + - cp crates/db_schema/src/schema.rs tmp.schema - diesel migration run - - diesel print-schema --config-file=diesel.toml > tmp.schema - diff tmp.schema crates/db_schema/src/schema.rs when: *slow_check_paths @@ -147,7 +146,6 @@ steps: CARGO_HOME: .cargo_home commands: # same as scripts/db_perf.sh but without creating a new database server - - export LEMMY_CONFIG_LOCATION=config/config.hjson - cargo run --package lemmy_db_perf -- --posts 10 --read-post-pages 1 when: *slow_check_paths @@ -176,11 +174,20 @@ steps: RUST_BACKTRACE: "1" CARGO_HOME: .cargo_home LEMMY_TEST_FAST_FEDERATION: "1" + LEMMY_CONFIG_LOCATION: ../../config/config.hjson commands: - - export LEMMY_CONFIG_LOCATION=../../config/config.hjson - cargo test --workspace --no-fail-fast when: *slow_check_paths + check_ts_bindings: + image: *rust_image + environment: + CARGO_HOME: .cargo_home + commands: + - ./scripts/ts_bindings_check.sh + when: + - event: pull_request + check_diesel_migration: # TODO: use willsquire/diesel-cli image when shared libraries become optional in lemmy_server image: *rust_image @@ -215,7 +222,7 @@ steps: when: *slow_check_paths run_federation_tests: - image: node:20-bookworm-slim + image: node:22-bookworm-slim environment: LEMMY_DATABASE_URL: postgres://lemmy:password@database:5432 DO_WRITE_HOSTS_FILE: "1" diff --git a/Cargo.lock b/Cargo.lock index c38a00d0c..caef131ea 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,12 +2,6 @@ # It is not intended for manual editing. version = 3 -[[package]] -name = "Inflector" -version = "0.11.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3" - [[package]] name = "accept-language" version = "3.1.0" @@ -1287,6 +1281,15 @@ dependencies = [ "tokio-postgres", ] +[[package]] +name = "diesel-bind-if-some" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ed8ce9db476124d2eaf4c9db45dc6581b8e8c4c4d47d5e0f39de1fb55dfb2a7" +dependencies = [ + "diesel", +] + [[package]] name = "diesel-derive-enum" version = "2.1.0" @@ -2570,7 +2573,6 @@ dependencies = [ "futures", "html2md", "html2text", - "http 1.1.0", "itertools 0.13.0", "lemmy_api_common", "lemmy_db_schema", @@ -2620,6 +2622,7 @@ dependencies = [ "derive-new", "diesel", "diesel-async", + "diesel-bind-if-some", "diesel-derive-enum", "diesel-derive-newtype", "diesel_ltree", @@ -2641,6 +2644,7 @@ dependencies = [ "tokio-postgres-rustls", "tracing", "ts-rs", + "tuplex", "url", "uuid", ] @@ -2809,9 +2813,12 @@ dependencies = [ "itertools 0.13.0", "lettre", "markdown-it", + "markdown-it-block-spoiler", + "markdown-it-ruby", + "markdown-it-sub", + "markdown-it-sup", "pretty_assertions", "regex", - "reqwest 0.12.8", "reqwest-middleware", "rosetta-build", "rosetta-i18n", @@ -2980,6 +2987,44 @@ dependencies = [ "unicode-general-category", ] +[[package]] +name = "markdown-it-block-spoiler" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "008a8e4184fd08b5dca0f2b5b2ef8f126c1e83ca797c44ee41f8d7765951360c" +dependencies = [ + "itertools 0.13.0", + "markdown-it", +] + +[[package]] +name = "markdown-it-ruby" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3505f4ada7c372e7f5eb4b07850bf5921193bc0bd43cb18991233999c9134d4" +dependencies = [ + "itertools 0.13.0", + "markdown-it", +] + +[[package]] +name = "markdown-it-sub" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8abe3aa8927af2314644b3aae37393241a229e869ff9c95ac640749e08357d2a" +dependencies = [ + "markdown-it", +] + +[[package]] +name = "markdown-it-sup" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ae949e78c7a615f88a47019d51b65962bfc5c4cbc65fa81eae8b9b2506d1cb1" +dependencies = [ + "markdown-it", +] + [[package]] name = "markup5ever" version = "0.11.0" @@ -5215,28 +5260,35 @@ checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] name = "ts-rs" -version = "7.1.1" +version = "10.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc2cae1fc5d05d47aa24b64f9a4f7cba24cdc9187a2084dd97ac57bef5eccae6" +checksum = "3a2f31991cee3dce1ca4f929a8a04fdd11fd8801aac0f2030b0fa8a0a3fef6b9" dependencies = [ "chrono", + "lazy_static", "thiserror", "ts-rs-macros", + "url", ] [[package]] name = "ts-rs-macros" -version = "7.1.1" +version = "10.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73f7f9b821696963053a89a7bd8b292dc34420aea8294d7b225274d488f3ec92" +checksum = "0ea0b99e8ec44abd6f94a18f28f7934437809dd062820797c52401298116f70e" dependencies = [ - "Inflector", "proc-macro2", "quote", "syn 2.0.77", "termcolor", ] +[[package]] +name = "tuplex" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "676ac81d5454c4dcf37955d34fa8626ede3490f744b86ca14a7b90168d2a08aa" + [[package]] name = "typenum" version = "1.17.0" @@ -5579,7 +5631,7 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.59.0", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index fc22d0d53..807e24e3f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,11 +24,12 @@ doctest = false [lints] workspace = true +# See https://github.com/johnthagen/min-sized-rust for additional optimizations [profile.release] debug = 0 -lto = "thin" -strip = true # Automatically strip symbols from the binary. -opt-level = "z" # Optimize for size. +lto = "fat" +opt-level = 3 # Optimize for speed, not size. +codegen-units = 1 # Reduce parallel code generation. # This profile significantly speeds up build time. If debug info is needed you can comment the line # out temporarily, but make sure to leave this in the main branch. @@ -77,6 +78,7 @@ uninlined_format_args = "allow" unused_self = "deny" unwrap_used = "deny" unimplemented = "deny" +unused_async = "deny" [workspace.dependencies] lemmy_api = { version = "=0.19.6-beta.7", path = "./crates/api" } @@ -141,10 +143,11 @@ itertools = "0.13.0" futures = "0.3.30" http = "1.1" rosetta-i18n = "0.1.3" -ts-rs = { version = "7.1.1", features = [ +ts-rs = { version = "10.0.0", features = [ "serde-compat", "chrono-impl", "no-serde-warnings", + "url-impl", ] } rustls = { version = "0.23.12", features = ["ring"] } futures-util = "0.3.30" @@ -157,6 +160,8 @@ i-love-jesus = { version = "0.1.0" } clap = { version = "4.5.13", features = ["derive", "env"] } pretty_assertions = "1.4.0" derive-new = "0.7.0" +diesel-bind-if-some = "0.1.0" +tuplex = "0.1.2" [dependencies] lemmy_api = { workspace = true } diff --git a/api_tests/package.json b/api_tests/package.json index 63c212d01..9a5057c00 100644 --- a/api_tests/package.json +++ b/api_tests/package.json @@ -6,16 +6,17 @@ "repository": "https://github.com/LemmyNet/lemmy", "author": "Dessalines", "license": "AGPL-3.0", - "packageManager": "pnpm@9.12.0", + "packageManager": "pnpm@9.12.3", "scripts": { "lint": "tsc --noEmit && eslint --report-unused-disable-directives && prettier --check 'src/**/*.ts'", "fix": "prettier --write src && eslint --fix src", - "api-test": "jest -i follow.spec.ts && jest -i image.spec.ts && jest -i user.spec.ts && jest -i private_message.spec.ts && jest -i community.spec.ts && jest -i post.spec.ts && jest -i comment.spec.ts ", + "api-test": "jest -i follow.spec.ts && jest -i image.spec.ts && jest -i user.spec.ts && jest -i private_message.spec.ts && jest -i community.spec.ts && jest -i private_community.spec.ts && jest -i post.spec.ts && jest -i comment.spec.ts ", "api-test-follow": "jest -i follow.spec.ts", "api-test-comment": "jest -i comment.spec.ts", "api-test-post": "jest -i post.spec.ts", "api-test-user": "jest -i user.spec.ts", "api-test-community": "jest -i community.spec.ts", + "api-test-private-community": "jest -i private_community.spec.ts", "api-test-private-message": "jest -i private_message.spec.ts", "api-test-image": "jest -i image.spec.ts" }, @@ -27,7 +28,7 @@ "eslint": "^9.9.0", "eslint-plugin-prettier": "^5.1.3", "jest": "^29.5.0", - "lemmy-js-client": "0.20.0-alpha.11", + "lemmy-js-client": "0.20.0-private-community.9", "prettier": "^3.2.5", "ts-jest": "^29.1.0", "typescript": "^5.5.4", diff --git a/api_tests/pnpm-lock.yaml b/api_tests/pnpm-lock.yaml index 6807f43f8..b1f18622e 100644 --- a/api_tests/pnpm-lock.yaml +++ b/api_tests/pnpm-lock.yaml @@ -10,40 +10,40 @@ importers: devDependencies: '@types/jest': specifier: ^29.5.12 - version: 29.5.13 + version: 29.5.14 '@types/node': specifier: ^22.3.0 - version: 22.7.4 + version: 22.8.6 '@typescript-eslint/eslint-plugin': specifier: ^8.1.0 - version: 8.8.1(@typescript-eslint/parser@8.8.1(eslint@9.12.0)(typescript@5.6.2))(eslint@9.12.0)(typescript@5.6.2) + version: 8.12.2(@typescript-eslint/parser@8.12.2(eslint@9.13.0)(typescript@5.6.3))(eslint@9.13.0)(typescript@5.6.3) '@typescript-eslint/parser': specifier: ^8.1.0 - version: 8.8.1(eslint@9.12.0)(typescript@5.6.2) + version: 8.12.2(eslint@9.13.0)(typescript@5.6.3) eslint: specifier: ^9.9.0 - version: 9.12.0 + version: 9.13.0 eslint-plugin-prettier: specifier: ^5.1.3 - version: 5.2.1(eslint@9.12.0)(prettier@3.3.3) + version: 5.2.1(eslint@9.13.0)(prettier@3.3.3) jest: specifier: ^29.5.0 - version: 29.7.0(@types/node@22.7.4) + version: 29.7.0(@types/node@22.8.6) lemmy-js-client: - specifier: 0.20.0-alpha.11 - version: 0.20.0-alpha.11 + specifier: 0.20.0-private-community.9 + version: 0.20.0-private-community.9 prettier: specifier: ^3.2.5 version: 3.3.3 ts-jest: specifier: ^29.1.0 - version: 29.2.5(@babel/core@7.23.9)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(jest@29.7.0(@types/node@22.7.4))(typescript@5.6.2) + version: 29.2.5(@babel/core@7.23.9)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(jest@29.7.0(@types/node@22.8.6))(typescript@5.6.3) typescript: specifier: ^5.5.4 - version: 5.6.2 + version: 5.6.3 typescript-eslint: specifier: ^8.1.0 - version: 8.8.1(eslint@9.12.0)(typescript@5.6.2) + version: 8.12.2(eslint@9.13.0)(typescript@5.6.3) packages: @@ -51,8 +51,8 @@ packages: resolution: {integrity: sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==} engines: {node: '>=6.0.0'} - '@babel/code-frame@7.24.7': - resolution: {integrity: sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==} + '@babel/code-frame@7.26.2': + resolution: {integrity: sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==} engines: {node: '>=6.9.0'} '@babel/compat-data@7.23.5': @@ -113,8 +113,8 @@ packages: resolution: {integrity: sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==} engines: {node: '>=6.9.0'} - '@babel/helper-validator-identifier@7.24.7': - resolution: {integrity: sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==} + '@babel/helper-validator-identifier@7.25.9': + resolution: {integrity: sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==} engines: {node: '>=6.9.0'} '@babel/helper-validator-option@7.23.5': @@ -125,10 +125,6 @@ packages: resolution: {integrity: sha512-87ICKgU5t5SzOT7sBMfCOZQ2rHjRU+Pcb9BoILMYz600W6DkVRLFBPwQ18gwUVvggqXivaUakpnxWQGbpywbBQ==} engines: {node: '>=6.9.0'} - '@babel/highlight@7.24.7': - resolution: {integrity: sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==} - engines: {node: '>=6.9.0'} - '@babel/parser@7.23.9': resolution: {integrity: sha512-9tcKgqKbs3xGJ+NtKF2ndOBBLVwPjl1SHxPQkd36r3Dlirw3xWUeGaTbqr7uGZcTaxkVNwc+03SVP7aCdWrTlA==} engines: {node: '>=6.0.0'} @@ -222,46 +218,46 @@ packages: '@bcoe/v8-coverage@0.2.3': resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} - '@eslint-community/eslint-utils@4.4.0': - resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==} + '@eslint-community/eslint-utils@4.4.1': + resolution: {integrity: sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 - '@eslint-community/regexpp@4.11.1': - resolution: {integrity: sha512-m4DVN9ZqskZoLU5GlWZadwDnYo3vAEydiUayB9widCl9ffWx2IvPnp6n3on5rJmziJSw9Bv+Z3ChDVdMwXCY8Q==} + '@eslint-community/regexpp@4.12.1': + resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==} engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} '@eslint/config-array@0.18.0': resolution: {integrity: sha512-fTxvnS1sRMu3+JjXwJG0j/i4RT9u4qJ+lqS/yCGap4lH4zZGzQ7tu+xZqQmcMZq5OBZDL4QRxQzRjkWcGt8IVw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/core@0.6.0': - resolution: {integrity: sha512-8I2Q8ykA4J0x0o7cg67FPVnehcqWTBehu/lmY+bolPFHGjh49YzGBMXTvpqVgEbBdvNCSxj6iFgiIyHzf03lzg==} + '@eslint/core@0.7.0': + resolution: {integrity: sha512-xp5Jirz5DyPYlPiKat8jaq0EmYvDXKKpzTbxXMpT9eqlRJkRKIz9AGMdlvYjih+im+QlhWrpvVjl8IPC/lHlUw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@eslint/eslintrc@3.1.0': resolution: {integrity: sha512-4Bfj15dVJdoy3RfZmmo86RK1Fwzn6SstsvK9JS+BaVKqC6QQQQyXekNaC+g+LKNgkQ+2VhGAzm6hO40AhMR3zQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/js@9.12.0': - resolution: {integrity: sha512-eohesHH8WFRUprDNyEREgqP6beG6htMeUYeCpkEgBCieCMme5r9zFWjzAJp//9S+Kub4rqE+jXe9Cp1a7IYIIA==} + '@eslint/js@9.13.0': + resolution: {integrity: sha512-IFLyoY4d72Z5y/6o/BazFBezupzI/taV8sGumxTAVw3lXG9A6md1Dc34T9s1FoD/an9pJH8RHbAxsaEbBed9lA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@eslint/object-schema@2.1.4': resolution: {integrity: sha512-BsWiH1yFGjXXS2yvrf5LyuoSIIbPrGUWob917o+BTKuZ7qJdxX8aJLRxs1fS9n6r7vESrq1OUqb68dANcFXuQQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/plugin-kit@0.2.0': - resolution: {integrity: sha512-vH9PiIMMwvhCx31Af3HiGzsVNULDbyVkHXwlemn/B0TFj/00ho3y55efXrUZTfQipxoHC5u4xq6zblww1zm1Ig==} + '@eslint/plugin-kit@0.2.2': + resolution: {integrity: sha512-CXtq5nR4Su+2I47WPOlWud98Y5Lv8Kyxp2ukhgFx/eW6Blm18VXJO5WuQylPugRo8nbluoi6GvvxBLqHcvqUUw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@humanfs/core@0.19.0': - resolution: {integrity: sha512-2cbWIHbZVEweE853g8jymffCA+NCMiuqeECeBBLm8dg2oFdjuGJhgN4UAbI+6v0CKbbhvtXA4qV8YR5Ji86nmw==} + '@humanfs/core@0.19.1': + resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} engines: {node: '>=18.18.0'} - '@humanfs/node@0.16.5': - resolution: {integrity: sha512-KSPA4umqSG4LHYRodq31VDwKAvaTF4xmVlzM8Aeh4PlU1JQ3IG0wiA8C25d3RQ9nJyM3mBHyI53K06VVL/oFFg==} + '@humanfs/node@0.16.6': + resolution: {integrity: sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==} engines: {node: '>=18.18.0'} '@humanwhocodes/module-importer@1.0.1': @@ -416,14 +412,14 @@ packages: '@types/istanbul-reports@3.0.4': resolution: {integrity: sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==} - '@types/jest@29.5.13': - resolution: {integrity: sha512-wd+MVEZCHt23V0/L642O5APvspWply/rGY5BcW4SUETo2UzPU3Z26qr8jC2qxpimI2jjx9h7+2cj2FwIr01bXg==} + '@types/jest@29.5.14': + resolution: {integrity: sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ==} '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} - '@types/node@22.7.4': - resolution: {integrity: sha512-y+NPi1rFzDs1NdQHHToqeiX2TIS79SWEAw9GYhkkx8bD0ChpfqC+n2j5OXOCpzfojBEBt6DnEnnG9MY0zk1XLg==} + '@types/node@22.8.6': + resolution: {integrity: sha512-tosuJYKrIqjQIlVCM4PEGxOmyg3FCPa/fViuJChnGeEIhjA46oy8FMVoF9su1/v8PNs2a8Q0iFNyOx0uOF91nw==} '@types/stack-utils@2.0.3': resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==} @@ -434,8 +430,8 @@ packages: '@types/yargs@17.0.32': resolution: {integrity: sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==} - '@typescript-eslint/eslint-plugin@8.8.1': - resolution: {integrity: sha512-xfvdgA8AP/vxHgtgU310+WBnLB4uJQ9XdyP17RebG26rLtDrQJV3ZYrcopX91GrHmMoH8bdSwMRh2a//TiJ1jQ==} + '@typescript-eslint/eslint-plugin@8.12.2': + resolution: {integrity: sha512-gQxbxM8mcxBwaEmWdtLCIGLfixBMHhQjBqR8sVWNTPpcj45WlYL2IObS/DNMLH1DBP0n8qz+aiiLTGfopPEebw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: '@typescript-eslint/parser': ^8.0.0 || ^8.0.0-alpha.0 @@ -445,8 +441,8 @@ packages: typescript: optional: true - '@typescript-eslint/parser@8.8.1': - resolution: {integrity: sha512-hQUVn2Lij2NAxVFEdvIGxT9gP1tq2yM83m+by3whWFsWC+1y8pxxxHUFE1UqDu2VsGi2i6RLcv4QvouM84U+ow==} + '@typescript-eslint/parser@8.12.2': + resolution: {integrity: sha512-MrvlXNfGPLH3Z+r7Tk+Z5moZAc0dzdVjTgUgwsdGweH7lydysQsnSww3nAmsq8blFuRD5VRlAr9YdEFw3e6PBw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 @@ -455,12 +451,12 @@ packages: typescript: optional: true - '@typescript-eslint/scope-manager@8.8.1': - resolution: {integrity: sha512-X4JdU+66Mazev/J0gfXlcC/dV6JI37h+93W9BRYXrSn0hrE64IoWgVkO9MSJgEzoWkxONgaQpICWg8vAN74wlA==} + '@typescript-eslint/scope-manager@8.12.2': + resolution: {integrity: sha512-gPLpLtrj9aMHOvxJkSbDBmbRuYdtiEbnvO25bCMza3DhMjTQw0u7Y1M+YR5JPbMsXXnSPuCf5hfq0nEkQDL/JQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/type-utils@8.8.1': - resolution: {integrity: sha512-qSVnpcbLP8CALORf0za+vjLYj1Wp8HSoiI8zYU5tHxRVj30702Z1Yw4cLwfNKhTPWp5+P+k1pjmD5Zd1nhxiZA==} + '@typescript-eslint/type-utils@8.12.2': + resolution: {integrity: sha512-bwuU4TAogPI+1q/IJSKuD4shBLc/d2vGcRT588q+jzayQyjVK2X6v/fbR4InY2U2sgf8MEvVCqEWUzYzgBNcGQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '*' @@ -468,12 +464,12 @@ packages: typescript: optional: true - '@typescript-eslint/types@8.8.1': - resolution: {integrity: sha512-WCcTP4SDXzMd23N27u66zTKMuEevH4uzU8C9jf0RO4E04yVHgQgW+r+TeVTNnO1KIfrL8ebgVVYYMMO3+jC55Q==} + '@typescript-eslint/types@8.12.2': + resolution: {integrity: sha512-VwDwMF1SZ7wPBUZwmMdnDJ6sIFk4K4s+ALKLP6aIQsISkPv8jhiw65sAK6SuWODN/ix+m+HgbYDkH+zLjrzvOA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/typescript-estree@8.8.1': - resolution: {integrity: sha512-A5d1R9p+X+1js4JogdNilDuuq+EHZdsH9MjTVxXOdVFfTJXunKJR/v+fNNyO4TnoOn5HqobzfRlc70NC6HTcdg==} + '@typescript-eslint/typescript-estree@8.12.2': + resolution: {integrity: sha512-mME5MDwGe30Pq9zKPvyduyU86PH7aixwqYR2grTglAdB+AN8xXQ1vFGpYaUSJ5o5P/5znsSBeNcs5g5/2aQwow==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '*' @@ -481,14 +477,14 @@ packages: typescript: optional: true - '@typescript-eslint/utils@8.8.1': - resolution: {integrity: sha512-/QkNJDbV0bdL7H7d0/y0qBbV2HTtf0TIyjSDTvvmQEzeVx8jEImEbLuOA4EsvE8gIgqMitns0ifb5uQhMj8d9w==} + '@typescript-eslint/utils@8.12.2': + resolution: {integrity: sha512-UTTuDIX3fkfAz6iSVa5rTuSfWIYZ6ATtEocQ/umkRSyC9O919lbZ8dcH7mysshrCdrAM03skJOEYaBugxN+M6A==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 - '@typescript-eslint/visitor-keys@8.8.1': - resolution: {integrity: sha512-0/TdC3aeRAsW7MDvYRwEc1Uwm0TIBfzjPFgg60UU2Haj5qsCs9cc3zNgY71edqE3LbWfF/WoZQd3lJoDXFQpag==} + '@typescript-eslint/visitor-keys@8.12.2': + resolution: {integrity: sha512-PChz8UaKQAVNHghsHcPyx1OMHoFRUEA7rJSK/mDhdq85bk+PLsUHUBqTQTFt18VJZbmxBovM65fezlheQRsSDA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} acorn-jsx@5.3.2: @@ -496,8 +492,8 @@ packages: peerDependencies: acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 - acorn@8.12.1: - resolution: {integrity: sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==} + acorn@8.14.0: + resolution: {integrity: sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==} engines: {node: '>=0.4.0'} hasBin: true @@ -512,10 +508,6 @@ packages: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} - ansi-styles@3.2.1: - resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} - engines: {node: '>=4'} - ansi-styles@4.3.0: resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} engines: {node: '>=8'} @@ -609,10 +601,6 @@ packages: caniuse-lite@1.0.30001581: resolution: {integrity: sha512-whlTkwhqV2tUmP3oYhtNfaWGYHDdS3JYFQBKXxcUR9qqPWsRhFHhoISO2Xnl/g0xyKzht9mI1LZpiNWfMzHixQ==} - chalk@2.4.2: - resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} - engines: {node: '>=4'} - chalk@4.1.2: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} @@ -639,16 +627,10 @@ packages: collect-v8-coverage@1.0.2: resolution: {integrity: sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==} - color-convert@1.9.3: - resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} - color-convert@2.0.1: resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} engines: {node: '>=7.0.0'} - color-name@1.1.3: - resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==} - color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} @@ -721,10 +703,6 @@ packages: resolution: {integrity: sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==} engines: {node: '>=6'} - escape-string-regexp@1.0.5: - resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} - engines: {node: '>=0.8.0'} - escape-string-regexp@2.0.0: resolution: {integrity: sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==} engines: {node: '>=8'} @@ -747,20 +725,20 @@ packages: eslint-config-prettier: optional: true - eslint-scope@8.1.0: - resolution: {integrity: sha512-14dSvlhaVhKKsa9Fx1l8A17s7ah7Ef7wCakJ10LYk6+GYmP9yDti2oq2SEwcyndt6knfcZyhyxwY3i9yL78EQw==} + eslint-scope@8.2.0: + resolution: {integrity: sha512-PHlWUfG6lvPc3yvP5A4PNyBL1W8fkDUccmI21JUu/+GKZBoH/W5u6usENXUrWFRsyoW5ACUjFGgAFQp5gUlb/A==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} eslint-visitor-keys@3.4.3: resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - eslint-visitor-keys@4.1.0: - resolution: {integrity: sha512-Q7lok0mqMUSf5a/AdAZkA5a/gHcO6snwQClVNNvFKCAVlxXucdU8pKydU5ZVZjBx5xr37vGbFFWtLQYreLzrZg==} + eslint-visitor-keys@4.2.0: + resolution: {integrity: sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - eslint@9.12.0: - resolution: {integrity: sha512-UVIOlTEWxwIopRL1wgSQYdnVDcEvs2wyaO6DGo5mXqe3r16IoCNWkR29iHhyaP4cICWjbgbmFUGAhh0GJRuGZw==} + eslint@9.13.0: + resolution: {integrity: sha512-EYZK6SX6zjFHST/HRytOdA/zE72Cq/bfw45LSyuwrdvcclb/gqV8RRQxywOBEWO2+WDpva6UZa4CcDeJKzUCFA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} hasBin: true peerDependencies: @@ -769,8 +747,8 @@ packages: jiti: optional: true - espree@10.2.0: - resolution: {integrity: sha512-upbkBJbckcCNBDBDXEbuhjbP68n+scUd3k/U2EkyM9nw+I/jPiL4cLF/Al06CF96wRltFda16sxDFrxsI1v0/g==} + espree@10.3.0: + resolution: {integrity: sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} esprima@4.0.1: @@ -911,10 +889,6 @@ packages: graphemer@1.4.0: resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} - has-flag@3.0.0: - resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} - engines: {node: '>=4'} - has-flag@4.0.0: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} @@ -1185,8 +1159,8 @@ packages: resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} engines: {node: '>=6'} - lemmy-js-client@0.20.0-alpha.11: - resolution: {integrity: sha512-iRSG4xHMjPDIreQqVIoJ5JrMY71uk07G0Zbgyf068xKbib22J3+i1x/XgCTs6tiHlqTnw1Ig/KRq7p7qJoA4uw==} + lemmy-js-client@0.20.0-private-community.9: + resolution: {integrity: sha512-iuFezswCzIco5U5Q4Eo8HAWVE65pDW2zeO+fYLEyFl30SLw9a3gqJkip2vbDfVvoAjDXyUskZKddf1Nnj8mVcg==} leven@3.1.0: resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==} @@ -1330,8 +1304,8 @@ packages: path-parse@1.0.7: resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} - picocolors@1.1.0: - resolution: {integrity: sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==} + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} picomatch@2.3.1: resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} @@ -1480,10 +1454,6 @@ packages: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} - supports-color@5.5.0: - resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} - engines: {node: '>=4'} - supports-color@7.2.0: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} engines: {node: '>=8'} @@ -1518,8 +1488,8 @@ packages: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} - ts-api-utils@1.3.0: - resolution: {integrity: sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==} + ts-api-utils@1.4.0: + resolution: {integrity: sha512-032cPxaEKwM+GT3vA5JXNzIaizx388rhsSW79vGRNGXfRRAdEAn2mvk36PvK5HnOchyWZ7afLEXqYCvPCrzuzQ==} engines: {node: '>=16'} peerDependencies: typescript: '>=4.2.0' @@ -1563,8 +1533,8 @@ packages: resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} engines: {node: '>=10'} - typescript-eslint@8.8.1: - resolution: {integrity: sha512-R0dsXFt6t4SAFjUSKFjMh4pXDtq04SsFKCVGDP3ZOzNP7itF0jBcZYU4fMsZr4y7O7V7Nc751dDeESbe4PbQMQ==} + typescript-eslint@8.12.2: + resolution: {integrity: sha512-UbuVUWSrHVR03q9CWx+JDHeO6B/Hr9p4U5lRH++5tq/EbFq1faYZe50ZSBePptgfIKLEti0aPQ3hFgnPVcd8ZQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '*' @@ -1572,8 +1542,8 @@ packages: typescript: optional: true - typescript@5.6.2: - resolution: {integrity: sha512-NW8ByodCSNCwZeghjN3o+JX5OFH0Ojg6sadjEKY4huZ52TqbJTJnDo5+Tw98lSy63NZvi4n+ez5m2u5d4PkZyw==} + typescript@5.6.3: + resolution: {integrity: sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==} engines: {node: '>=14.17'} hasBin: true @@ -1642,17 +1612,18 @@ snapshots: '@jridgewell/gen-mapping': 0.3.3 '@jridgewell/trace-mapping': 0.3.22 - '@babel/code-frame@7.24.7': + '@babel/code-frame@7.26.2': dependencies: - '@babel/highlight': 7.24.7 - picocolors: 1.1.0 + '@babel/helper-validator-identifier': 7.25.9 + js-tokens: 4.0.0 + picocolors: 1.1.1 '@babel/compat-data@7.23.5': {} '@babel/core@7.23.9': dependencies: '@ampproject/remapping': 2.2.1 - '@babel/code-frame': 7.24.7 + '@babel/code-frame': 7.26.2 '@babel/generator': 7.23.6 '@babel/helper-compilation-targets': 7.23.6 '@babel/helper-module-transforms': 7.23.3(@babel/core@7.23.9) @@ -1706,7 +1677,7 @@ snapshots: '@babel/helper-module-imports': 7.22.15 '@babel/helper-simple-access': 7.22.5 '@babel/helper-split-export-declaration': 7.22.6 - '@babel/helper-validator-identifier': 7.22.20 + '@babel/helper-validator-identifier': 7.25.9 '@babel/helper-plugin-utils@7.22.5': {} @@ -1722,7 +1693,7 @@ snapshots: '@babel/helper-validator-identifier@7.22.20': {} - '@babel/helper-validator-identifier@7.24.7': {} + '@babel/helper-validator-identifier@7.25.9': {} '@babel/helper-validator-option@7.23.5': {} @@ -1734,13 +1705,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/highlight@7.24.7': - dependencies: - '@babel/helper-validator-identifier': 7.24.7 - chalk: 2.4.2 - js-tokens: 4.0.0 - picocolors: 1.1.0 - '@babel/parser@7.23.9': dependencies: '@babel/types': 7.23.9 @@ -1817,13 +1781,13 @@ snapshots: '@babel/template@7.23.9': dependencies: - '@babel/code-frame': 7.24.7 + '@babel/code-frame': 7.26.2 '@babel/parser': 7.23.9 '@babel/types': 7.23.9 '@babel/traverse@7.23.9': dependencies: - '@babel/code-frame': 7.24.7 + '@babel/code-frame': 7.26.2 '@babel/generator': 7.23.6 '@babel/helper-environment-visitor': 7.22.20 '@babel/helper-function-name': 7.23.0 @@ -1844,12 +1808,12 @@ snapshots: '@bcoe/v8-coverage@0.2.3': {} - '@eslint-community/eslint-utils@4.4.0(eslint@9.12.0)': + '@eslint-community/eslint-utils@4.4.1(eslint@9.13.0)': dependencies: - eslint: 9.12.0 + eslint: 9.13.0 eslint-visitor-keys: 3.4.3 - '@eslint-community/regexpp@4.11.1': {} + '@eslint-community/regexpp@4.12.1': {} '@eslint/config-array@0.18.0': dependencies: @@ -1859,13 +1823,13 @@ snapshots: transitivePeerDependencies: - supports-color - '@eslint/core@0.6.0': {} + '@eslint/core@0.7.0': {} '@eslint/eslintrc@3.1.0': dependencies: ajv: 6.12.6 debug: 4.3.7 - espree: 10.2.0 + espree: 10.3.0 globals: 14.0.0 ignore: 5.3.2 import-fresh: 3.3.0 @@ -1875,19 +1839,19 @@ snapshots: transitivePeerDependencies: - supports-color - '@eslint/js@9.12.0': {} + '@eslint/js@9.13.0': {} '@eslint/object-schema@2.1.4': {} - '@eslint/plugin-kit@0.2.0': + '@eslint/plugin-kit@0.2.2': dependencies: levn: 0.4.1 - '@humanfs/core@0.19.0': {} + '@humanfs/core@0.19.1': {} - '@humanfs/node@0.16.5': + '@humanfs/node@0.16.6': dependencies: - '@humanfs/core': 0.19.0 + '@humanfs/core': 0.19.1 '@humanwhocodes/retry': 0.3.1 '@humanwhocodes/module-importer@1.0.1': {} @@ -1907,7 +1871,7 @@ snapshots: '@jest/console@29.7.0': dependencies: '@jest/types': 29.6.3 - '@types/node': 22.7.4 + '@types/node': 22.8.6 chalk: 4.1.2 jest-message-util: 29.7.0 jest-util: 29.7.0 @@ -1920,14 +1884,14 @@ snapshots: '@jest/test-result': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 22.7.4 + '@types/node': 22.8.6 ansi-escapes: 4.3.2 chalk: 4.1.2 ci-info: 3.9.0 exit: 0.1.2 graceful-fs: 4.2.11 jest-changed-files: 29.7.0 - jest-config: 29.7.0(@types/node@22.7.4) + jest-config: 29.7.0(@types/node@22.8.6) jest-haste-map: 29.7.0 jest-message-util: 29.7.0 jest-regex-util: 29.6.3 @@ -1952,7 +1916,7 @@ snapshots: dependencies: '@jest/fake-timers': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 22.7.4 + '@types/node': 22.8.6 jest-mock: 29.7.0 '@jest/expect-utils@29.7.0': @@ -1970,7 +1934,7 @@ snapshots: dependencies: '@jest/types': 29.6.3 '@sinonjs/fake-timers': 10.3.0 - '@types/node': 22.7.4 + '@types/node': 22.8.6 jest-message-util: 29.7.0 jest-mock: 29.7.0 jest-util: 29.7.0 @@ -1992,7 +1956,7 @@ snapshots: '@jest/transform': 29.7.0 '@jest/types': 29.6.3 '@jridgewell/trace-mapping': 0.3.22 - '@types/node': 22.7.4 + '@types/node': 22.8.6 chalk: 4.1.2 collect-v8-coverage: 1.0.2 exit: 0.1.2 @@ -2062,7 +2026,7 @@ snapshots: '@jest/schemas': 29.6.3 '@types/istanbul-lib-coverage': 2.0.6 '@types/istanbul-reports': 3.0.4 - '@types/node': 22.7.4 + '@types/node': 22.8.6 '@types/yargs': 17.0.32 chalk: 4.1.2 @@ -2132,7 +2096,7 @@ snapshots: '@types/graceful-fs@4.1.9': dependencies: - '@types/node': 22.7.4 + '@types/node': 22.8.6 '@types/istanbul-lib-coverage@2.0.6': {} @@ -2144,14 +2108,14 @@ snapshots: dependencies: '@types/istanbul-lib-report': 3.0.3 - '@types/jest@29.5.13': + '@types/jest@29.5.14': dependencies: expect: 29.7.0 pretty-format: 29.7.0 '@types/json-schema@7.0.15': {} - '@types/node@22.7.4': + '@types/node@22.8.6': dependencies: undici-types: 6.19.8 @@ -2163,92 +2127,92 @@ snapshots: dependencies: '@types/yargs-parser': 21.0.3 - '@typescript-eslint/eslint-plugin@8.8.1(@typescript-eslint/parser@8.8.1(eslint@9.12.0)(typescript@5.6.2))(eslint@9.12.0)(typescript@5.6.2)': + '@typescript-eslint/eslint-plugin@8.12.2(@typescript-eslint/parser@8.12.2(eslint@9.13.0)(typescript@5.6.3))(eslint@9.13.0)(typescript@5.6.3)': dependencies: - '@eslint-community/regexpp': 4.11.1 - '@typescript-eslint/parser': 8.8.1(eslint@9.12.0)(typescript@5.6.2) - '@typescript-eslint/scope-manager': 8.8.1 - '@typescript-eslint/type-utils': 8.8.1(eslint@9.12.0)(typescript@5.6.2) - '@typescript-eslint/utils': 8.8.1(eslint@9.12.0)(typescript@5.6.2) - '@typescript-eslint/visitor-keys': 8.8.1 - eslint: 9.12.0 + '@eslint-community/regexpp': 4.12.1 + '@typescript-eslint/parser': 8.12.2(eslint@9.13.0)(typescript@5.6.3) + '@typescript-eslint/scope-manager': 8.12.2 + '@typescript-eslint/type-utils': 8.12.2(eslint@9.13.0)(typescript@5.6.3) + '@typescript-eslint/utils': 8.12.2(eslint@9.13.0)(typescript@5.6.3) + '@typescript-eslint/visitor-keys': 8.12.2 + eslint: 9.13.0 graphemer: 1.4.0 ignore: 5.3.2 natural-compare: 1.4.0 - ts-api-utils: 1.3.0(typescript@5.6.2) + ts-api-utils: 1.4.0(typescript@5.6.3) optionalDependencies: - typescript: 5.6.2 + typescript: 5.6.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.8.1(eslint@9.12.0)(typescript@5.6.2)': + '@typescript-eslint/parser@8.12.2(eslint@9.13.0)(typescript@5.6.3)': dependencies: - '@typescript-eslint/scope-manager': 8.8.1 - '@typescript-eslint/types': 8.8.1 - '@typescript-eslint/typescript-estree': 8.8.1(typescript@5.6.2) - '@typescript-eslint/visitor-keys': 8.8.1 + '@typescript-eslint/scope-manager': 8.12.2 + '@typescript-eslint/types': 8.12.2 + '@typescript-eslint/typescript-estree': 8.12.2(typescript@5.6.3) + '@typescript-eslint/visitor-keys': 8.12.2 debug: 4.3.7 - eslint: 9.12.0 + eslint: 9.13.0 optionalDependencies: - typescript: 5.6.2 + typescript: 5.6.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/scope-manager@8.8.1': + '@typescript-eslint/scope-manager@8.12.2': dependencies: - '@typescript-eslint/types': 8.8.1 - '@typescript-eslint/visitor-keys': 8.8.1 + '@typescript-eslint/types': 8.12.2 + '@typescript-eslint/visitor-keys': 8.12.2 - '@typescript-eslint/type-utils@8.8.1(eslint@9.12.0)(typescript@5.6.2)': + '@typescript-eslint/type-utils@8.12.2(eslint@9.13.0)(typescript@5.6.3)': dependencies: - '@typescript-eslint/typescript-estree': 8.8.1(typescript@5.6.2) - '@typescript-eslint/utils': 8.8.1(eslint@9.12.0)(typescript@5.6.2) + '@typescript-eslint/typescript-estree': 8.12.2(typescript@5.6.3) + '@typescript-eslint/utils': 8.12.2(eslint@9.13.0)(typescript@5.6.3) debug: 4.3.7 - ts-api-utils: 1.3.0(typescript@5.6.2) + ts-api-utils: 1.4.0(typescript@5.6.3) optionalDependencies: - typescript: 5.6.2 + typescript: 5.6.3 transitivePeerDependencies: - eslint - supports-color - '@typescript-eslint/types@8.8.1': {} + '@typescript-eslint/types@8.12.2': {} - '@typescript-eslint/typescript-estree@8.8.1(typescript@5.6.2)': + '@typescript-eslint/typescript-estree@8.12.2(typescript@5.6.3)': dependencies: - '@typescript-eslint/types': 8.8.1 - '@typescript-eslint/visitor-keys': 8.8.1 + '@typescript-eslint/types': 8.12.2 + '@typescript-eslint/visitor-keys': 8.12.2 debug: 4.3.7 fast-glob: 3.3.2 is-glob: 4.0.3 minimatch: 9.0.5 semver: 7.6.3 - ts-api-utils: 1.3.0(typescript@5.6.2) + ts-api-utils: 1.4.0(typescript@5.6.3) optionalDependencies: - typescript: 5.6.2 + typescript: 5.6.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.8.1(eslint@9.12.0)(typescript@5.6.2)': + '@typescript-eslint/utils@8.12.2(eslint@9.13.0)(typescript@5.6.3)': dependencies: - '@eslint-community/eslint-utils': 4.4.0(eslint@9.12.0) - '@typescript-eslint/scope-manager': 8.8.1 - '@typescript-eslint/types': 8.8.1 - '@typescript-eslint/typescript-estree': 8.8.1(typescript@5.6.2) - eslint: 9.12.0 + '@eslint-community/eslint-utils': 4.4.1(eslint@9.13.0) + '@typescript-eslint/scope-manager': 8.12.2 + '@typescript-eslint/types': 8.12.2 + '@typescript-eslint/typescript-estree': 8.12.2(typescript@5.6.3) + eslint: 9.13.0 transitivePeerDependencies: - supports-color - typescript - '@typescript-eslint/visitor-keys@8.8.1': + '@typescript-eslint/visitor-keys@8.12.2': dependencies: - '@typescript-eslint/types': 8.8.1 + '@typescript-eslint/types': 8.12.2 eslint-visitor-keys: 3.4.3 - acorn-jsx@5.3.2(acorn@8.12.1): + acorn-jsx@5.3.2(acorn@8.14.0): dependencies: - acorn: 8.12.1 + acorn: 8.14.0 - acorn@8.12.1: {} + acorn@8.14.0: {} ajv@6.12.6: dependencies: @@ -2263,10 +2227,6 @@ snapshots: ansi-regex@5.0.1: {} - ansi-styles@3.2.1: - dependencies: - color-convert: 1.9.3 - ansi-styles@4.3.0: dependencies: color-convert: 2.0.1 @@ -2382,12 +2342,6 @@ snapshots: caniuse-lite@1.0.30001581: {} - chalk@2.4.2: - dependencies: - ansi-styles: 3.2.1 - escape-string-regexp: 1.0.5 - supports-color: 5.5.0 - chalk@4.1.2: dependencies: ansi-styles: 4.3.0 @@ -2409,29 +2363,23 @@ snapshots: collect-v8-coverage@1.0.2: {} - color-convert@1.9.3: - dependencies: - color-name: 1.1.3 - color-convert@2.0.1: dependencies: color-name: 1.1.4 - color-name@1.1.3: {} - color-name@1.1.4: {} concat-map@0.0.1: {} convert-source-map@2.0.0: {} - create-jest@29.7.0(@types/node@22.7.4): + create-jest@29.7.0(@types/node@22.8.6): dependencies: '@jest/types': 29.6.3 chalk: 4.1.2 exit: 0.1.2 graceful-fs: 4.2.11 - jest-config: 29.7.0(@types/node@22.7.4) + jest-config: 29.7.0(@types/node@22.8.6) jest-util: 29.7.0 prompts: 2.4.2 transitivePeerDependencies: @@ -2476,38 +2424,36 @@ snapshots: escalade@3.1.1: {} - escape-string-regexp@1.0.5: {} - escape-string-regexp@2.0.0: {} escape-string-regexp@4.0.0: {} - eslint-plugin-prettier@5.2.1(eslint@9.12.0)(prettier@3.3.3): + eslint-plugin-prettier@5.2.1(eslint@9.13.0)(prettier@3.3.3): dependencies: - eslint: 9.12.0 + eslint: 9.13.0 prettier: 3.3.3 prettier-linter-helpers: 1.0.0 synckit: 0.9.1 - eslint-scope@8.1.0: + eslint-scope@8.2.0: dependencies: esrecurse: 4.3.0 estraverse: 5.3.0 eslint-visitor-keys@3.4.3: {} - eslint-visitor-keys@4.1.0: {} + eslint-visitor-keys@4.2.0: {} - eslint@9.12.0: + eslint@9.13.0: dependencies: - '@eslint-community/eslint-utils': 4.4.0(eslint@9.12.0) - '@eslint-community/regexpp': 4.11.1 + '@eslint-community/eslint-utils': 4.4.1(eslint@9.13.0) + '@eslint-community/regexpp': 4.12.1 '@eslint/config-array': 0.18.0 - '@eslint/core': 0.6.0 + '@eslint/core': 0.7.0 '@eslint/eslintrc': 3.1.0 - '@eslint/js': 9.12.0 - '@eslint/plugin-kit': 0.2.0 - '@humanfs/node': 0.16.5 + '@eslint/js': 9.13.0 + '@eslint/plugin-kit': 0.2.2 + '@humanfs/node': 0.16.6 '@humanwhocodes/module-importer': 1.0.1 '@humanwhocodes/retry': 0.3.1 '@types/estree': 1.0.6 @@ -2517,9 +2463,9 @@ snapshots: cross-spawn: 7.0.3 debug: 4.3.7 escape-string-regexp: 4.0.0 - eslint-scope: 8.1.0 - eslint-visitor-keys: 4.1.0 - espree: 10.2.0 + eslint-scope: 8.2.0 + eslint-visitor-keys: 4.2.0 + espree: 10.3.0 esquery: 1.6.0 esutils: 2.0.3 fast-deep-equal: 3.1.3 @@ -2538,11 +2484,11 @@ snapshots: transitivePeerDependencies: - supports-color - espree@10.2.0: + espree@10.3.0: dependencies: - acorn: 8.12.1 - acorn-jsx: 5.3.2(acorn@8.12.1) - eslint-visitor-keys: 4.1.0 + acorn: 8.14.0 + acorn-jsx: 5.3.2(acorn@8.14.0) + eslint-visitor-keys: 4.2.0 esprima@4.0.1: {} @@ -2677,8 +2623,6 @@ snapshots: graphemer@1.4.0: {} - has-flag@3.0.0: {} - has-flag@4.0.0: {} hasown@2.0.0: @@ -2792,7 +2736,7 @@ snapshots: '@jest/expect': 29.7.0 '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 22.7.4 + '@types/node': 22.8.6 chalk: 4.1.2 co: 4.6.0 dedent: 1.5.1 @@ -2812,16 +2756,16 @@ snapshots: - babel-plugin-macros - supports-color - jest-cli@29.7.0(@types/node@22.7.4): + jest-cli@29.7.0(@types/node@22.8.6): dependencies: '@jest/core': 29.7.0 '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 chalk: 4.1.2 - create-jest: 29.7.0(@types/node@22.7.4) + create-jest: 29.7.0(@types/node@22.8.6) exit: 0.1.2 import-local: 3.1.0 - jest-config: 29.7.0(@types/node@22.7.4) + jest-config: 29.7.0(@types/node@22.8.6) jest-util: 29.7.0 jest-validate: 29.7.0 yargs: 17.7.2 @@ -2831,7 +2775,7 @@ snapshots: - supports-color - ts-node - jest-config@29.7.0(@types/node@22.7.4): + jest-config@29.7.0(@types/node@22.8.6): dependencies: '@babel/core': 7.23.9 '@jest/test-sequencer': 29.7.0 @@ -2856,7 +2800,7 @@ snapshots: slash: 3.0.0 strip-json-comments: 3.1.1 optionalDependencies: - '@types/node': 22.7.4 + '@types/node': 22.8.6 transitivePeerDependencies: - babel-plugin-macros - supports-color @@ -2885,7 +2829,7 @@ snapshots: '@jest/environment': 29.7.0 '@jest/fake-timers': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 22.7.4 + '@types/node': 22.8.6 jest-mock: 29.7.0 jest-util: 29.7.0 @@ -2895,7 +2839,7 @@ snapshots: dependencies: '@jest/types': 29.6.3 '@types/graceful-fs': 4.1.9 - '@types/node': 22.7.4 + '@types/node': 22.8.6 anymatch: 3.1.3 fb-watchman: 2.0.2 graceful-fs: 4.2.11 @@ -2921,7 +2865,7 @@ snapshots: jest-message-util@29.7.0: dependencies: - '@babel/code-frame': 7.24.7 + '@babel/code-frame': 7.26.2 '@jest/types': 29.6.3 '@types/stack-utils': 2.0.3 chalk: 4.1.2 @@ -2934,7 +2878,7 @@ snapshots: jest-mock@29.7.0: dependencies: '@jest/types': 29.6.3 - '@types/node': 22.7.4 + '@types/node': 22.8.6 jest-util: 29.7.0 jest-pnp-resolver@1.2.3(jest-resolve@29.7.0): @@ -2969,7 +2913,7 @@ snapshots: '@jest/test-result': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 22.7.4 + '@types/node': 22.8.6 chalk: 4.1.2 emittery: 0.13.1 graceful-fs: 4.2.11 @@ -2997,7 +2941,7 @@ snapshots: '@jest/test-result': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 22.7.4 + '@types/node': 22.8.6 chalk: 4.1.2 cjs-module-lexer: 1.2.3 collect-v8-coverage: 1.0.2 @@ -3043,7 +2987,7 @@ snapshots: jest-util@29.7.0: dependencies: '@jest/types': 29.6.3 - '@types/node': 22.7.4 + '@types/node': 22.8.6 chalk: 4.1.2 ci-info: 3.9.0 graceful-fs: 4.2.11 @@ -3062,7 +3006,7 @@ snapshots: dependencies: '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 22.7.4 + '@types/node': 22.8.6 ansi-escapes: 4.3.2 chalk: 4.1.2 emittery: 0.13.1 @@ -3071,17 +3015,17 @@ snapshots: jest-worker@29.7.0: dependencies: - '@types/node': 22.7.4 + '@types/node': 22.8.6 jest-util: 29.7.0 merge-stream: 2.0.0 supports-color: 8.1.1 - jest@29.7.0(@types/node@22.7.4): + jest@29.7.0(@types/node@22.8.6): dependencies: '@jest/core': 29.7.0 '@jest/types': 29.6.3 import-local: 3.1.0 - jest-cli: 29.7.0(@types/node@22.7.4) + jest-cli: 29.7.0(@types/node@22.8.6) transitivePeerDependencies: - '@types/node' - babel-plugin-macros @@ -3117,7 +3061,7 @@ snapshots: kleur@3.0.3: {} - lemmy-js-client@0.20.0-alpha.11: {} + lemmy-js-client@0.20.0-private-community.9: {} leven@3.1.0: {} @@ -3237,7 +3181,7 @@ snapshots: parse-json@5.2.0: dependencies: - '@babel/code-frame': 7.24.7 + '@babel/code-frame': 7.26.2 error-ex: 1.3.2 json-parse-even-better-errors: 2.3.1 lines-and-columns: 1.2.4 @@ -3250,7 +3194,7 @@ snapshots: path-parse@1.0.7: {} - picocolors@1.1.0: {} + picocolors@1.1.1: {} picomatch@2.3.1: {} @@ -3363,10 +3307,6 @@ snapshots: strip-json-comments@3.1.1: {} - supports-color@5.5.0: - dependencies: - has-flag: 3.0.0 - supports-color@7.2.0: dependencies: has-flag: 4.0.0 @@ -3398,22 +3338,22 @@ snapshots: dependencies: is-number: 7.0.0 - ts-api-utils@1.3.0(typescript@5.6.2): + ts-api-utils@1.4.0(typescript@5.6.3): dependencies: - typescript: 5.6.2 + typescript: 5.6.3 - ts-jest@29.2.5(@babel/core@7.23.9)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(jest@29.7.0(@types/node@22.7.4))(typescript@5.6.2): + ts-jest@29.2.5(@babel/core@7.23.9)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(jest@29.7.0(@types/node@22.8.6))(typescript@5.6.3): dependencies: bs-logger: 0.2.6 ejs: 3.1.10 fast-json-stable-stringify: 2.1.0 - jest: 29.7.0(@types/node@22.7.4) + jest: 29.7.0(@types/node@22.8.6) jest-util: 29.7.0 json5: 2.2.3 lodash.memoize: 4.1.2 make-error: 1.3.6 semver: 7.6.3 - typescript: 5.6.2 + typescript: 5.6.3 yargs-parser: 21.1.1 optionalDependencies: '@babel/core': 7.23.9 @@ -3431,18 +3371,18 @@ snapshots: type-fest@0.21.3: {} - typescript-eslint@8.8.1(eslint@9.12.0)(typescript@5.6.2): + typescript-eslint@8.12.2(eslint@9.13.0)(typescript@5.6.3): dependencies: - '@typescript-eslint/eslint-plugin': 8.8.1(@typescript-eslint/parser@8.8.1(eslint@9.12.0)(typescript@5.6.2))(eslint@9.12.0)(typescript@5.6.2) - '@typescript-eslint/parser': 8.8.1(eslint@9.12.0)(typescript@5.6.2) - '@typescript-eslint/utils': 8.8.1(eslint@9.12.0)(typescript@5.6.2) + '@typescript-eslint/eslint-plugin': 8.12.2(@typescript-eslint/parser@8.12.2(eslint@9.13.0)(typescript@5.6.3))(eslint@9.13.0)(typescript@5.6.3) + '@typescript-eslint/parser': 8.12.2(eslint@9.13.0)(typescript@5.6.3) + '@typescript-eslint/utils': 8.12.2(eslint@9.13.0)(typescript@5.6.3) optionalDependencies: - typescript: 5.6.2 + typescript: 5.6.3 transitivePeerDependencies: - eslint - supports-color - typescript@5.6.2: {} + typescript@5.6.3: {} undici-types@6.19.8: {} @@ -3450,7 +3390,7 @@ snapshots: dependencies: browserslist: 4.22.3 escalade: 3.1.1 - picocolors: 1.1.0 + picocolors: 1.1.1 uri-js@4.4.1: dependencies: diff --git a/api_tests/src/comment.spec.ts b/api_tests/src/comment.spec.ts index 5f2059e4f..c3f4b3efe 100644 --- a/api_tests/src/comment.spec.ts +++ b/api_tests/src/comment.spec.ts @@ -158,16 +158,16 @@ test("Delete a comment", async () => { expect(deleteCommentRes.comment_view.comment.deleted).toBe(true); expect(deleteCommentRes.comment_view.comment.content).toBe(""); - // Make sure that comment is undefined on beta + // Make sure that comment is deleted on beta await waitUntil( - () => resolveComment(beta, commentRes.comment_view.comment).catch(e => e), - e => e.message == "not_found", + () => resolveComment(beta, commentRes.comment_view.comment), + c => c.comment?.comment.deleted === true, ); - // Make sure that comment is undefined on gamma after delete + // Make sure that comment is deleted on gamma after delete await waitUntil( - () => resolveComment(gamma, commentRes.comment_view.comment).catch(e => e), - e => e.message === "not_found", + () => resolveComment(gamma, commentRes.comment_view.comment), + c => c.comment?.comment.deleted === true, ); // Test undeleting the comment @@ -181,11 +181,10 @@ test("Delete a comment", async () => { // Make sure that comment is undeleted on beta let betaComment2 = ( await waitUntil( - () => resolveComment(beta, commentRes.comment_view.comment).catch(e => e), - e => e.message !== "not_found", + () => resolveComment(beta, commentRes.comment_view.comment), + c => c.comment?.comment.deleted === false, ) ).comment; - expect(betaComment2?.comment.deleted).toBe(false); assertCommentFederation(betaComment2, undeleteCommentRes.comment_view); }); @@ -861,7 +860,7 @@ test("Dont send a comment reply to a blocked community", async () => { /// Fetching a deeply nested comment can lead to stack overflow as all parent comments are also /// fetched recursively. Ensure that it works properly. -test("Fetch a deeply nested comment", async () => { +test.skip("Fetch a deeply nested comment", async () => { let lastComment; for (let i = 0; i < 50; i++) { let commentRes = await createComment( diff --git a/api_tests/src/private_community.spec.ts b/api_tests/src/private_community.spec.ts new file mode 100644 index 000000000..76faf800f --- /dev/null +++ b/api_tests/src/private_community.spec.ts @@ -0,0 +1,214 @@ +jest.setTimeout(120000); + +import { FollowCommunity } from "lemmy-js-client"; +import { + alpha, + setupLogins, + createCommunity, + unfollows, + registerUser, + listCommunityPendingFollows, + getCommunity, + getCommunityPendingFollowsCount, + approveCommunityPendingFollow, + randomString, + createPost, + createComment, + beta, + resolveCommunity, + betaUrl, + resolvePost, + resolveComment, + likeComment, + waitUntil, +} from "./shared"; + +beforeAll(setupLogins); +afterAll(unfollows); + +test("Follow a private community", async () => { + // create private community + const community = await createCommunity(alpha, randomString(10), "Private"); + expect(community.community_view.community.visibility).toBe("Private"); + const alphaCommunityId = community.community_view.community.id; + + // No pending follows yet + const pendingFollows0 = await listCommunityPendingFollows(alpha); + expect(pendingFollows0.items.length).toBe(0); + const pendingFollowsCount0 = await getCommunityPendingFollowsCount( + alpha, + alphaCommunityId, + ); + expect(pendingFollowsCount0.count).toBe(0); + + // follow as new user + const user = await registerUser(beta, betaUrl); + const betaCommunity = ( + await resolveCommunity(user, community.community_view.community.actor_id) + ).community; + expect(betaCommunity).toBeDefined(); + const betaCommunityId = betaCommunity!.community.id; + const follow_form: FollowCommunity = { + community_id: betaCommunityId, + follow: true, + }; + await user.followCommunity(follow_form); + + // Follow listed as pending + const follow1 = await getCommunity(user, betaCommunityId); + expect(follow1.community_view.subscribed).toBe("ApprovalRequired"); + + // Wait for follow to federate, shown as pending + let pendingFollows1 = await waitUntil( + () => listCommunityPendingFollows(alpha), + f => f.items.length == 1, + ); + expect(pendingFollows1.items[0].is_new_instance).toBe(true); + const pendingFollowsCount1 = await getCommunityPendingFollowsCount( + alpha, + alphaCommunityId, + ); + expect(pendingFollowsCount1.count).toBe(1); + + // user still sees approval required at this point + const betaCommunity2 = await getCommunity(user, betaCommunityId); + expect(betaCommunity2.community_view.subscribed).toBe("ApprovalRequired"); + + // Approve the follow + const approve = await approveCommunityPendingFollow( + alpha, + alphaCommunityId, + pendingFollows1.items[0].person.id, + ); + expect(approve.success).toBe(true); + + // Follow is confirmed + await waitUntil( + () => getCommunity(user, betaCommunityId), + c => c.community_view.subscribed == "Subscribed", + ); + const pendingFollows2 = await listCommunityPendingFollows(alpha); + expect(pendingFollows2.items.length).toBe(0); + const pendingFollowsCount2 = await getCommunityPendingFollowsCount( + alpha, + alphaCommunityId, + ); + expect(pendingFollowsCount2.count).toBe(0); + + // follow with another user from that instance, is_new_instance should be false now + const user2 = await registerUser(beta, betaUrl); + await user2.followCommunity(follow_form); + let pendingFollows3 = await waitUntil( + () => listCommunityPendingFollows(alpha), + f => f.items.length == 1, + ); + expect(pendingFollows3.items[0].is_new_instance).toBe(false); + + // cleanup pending follow + const approve2 = await approveCommunityPendingFollow( + alpha, + alphaCommunityId, + pendingFollows3.items[0].person.id, + ); + expect(approve2.success).toBe(true); +}); + +test("Only followers can view and interact with private community content", async () => { + // create private community + const community = await createCommunity(alpha, randomString(10), "Private"); + expect(community.community_view.community.visibility).toBe("Private"); + const alphaCommunityId = community.community_view.community.id; + + // create post and comment + const post0 = await createPost(alpha, alphaCommunityId); + const post_id = post0.post_view.post.id; + expect(post_id).toBeDefined(); + const comment = await createComment(alpha, post_id); + const comment_id = comment.comment_view.comment.id; + expect(comment_id).toBeDefined(); + + // user is not following the community and cannot view nor create posts + const user = await registerUser(beta, betaUrl); + const betaCommunity = ( + await resolveCommunity(user, community.community_view.community.actor_id) + ).community!.community; + await expect(resolvePost(user, post0.post_view.post)).rejects.toStrictEqual( + Error("not_found"), + ); + await expect( + resolveComment(user, comment.comment_view.comment), + ).rejects.toStrictEqual(Error("not_found")); + await expect(createPost(user, betaCommunity.id)).rejects.toStrictEqual( + Error("not_found"), + ); + + // follow the community and approve + const follow_form: FollowCommunity = { + community_id: betaCommunity.id, + follow: true, + }; + await user.followCommunity(follow_form); + const pendingFollows1 = await waitUntil( + () => listCommunityPendingFollows(alpha), + f => f.items.length == 1, + ); + const approve = await approveCommunityPendingFollow( + alpha, + alphaCommunityId, + pendingFollows1.items[0].person.id, + ); + expect(approve.success).toBe(true); + + // now user can fetch posts and comments in community (using signed fetch), and create posts + await waitUntil( + () => resolvePost(user, post0.post_view.post), + p => p?.post?.post.id != undefined, + ); + const resolvedComment = ( + await resolveComment(user, comment.comment_view.comment) + ).comment; + expect(resolvedComment?.comment.id).toBeDefined(); + + const post1 = await createPost(user, betaCommunity.id); + expect(post1.post_view).toBeDefined(); + const like = await likeComment(user, 1, resolvedComment!.comment); + expect(like.comment_view.my_vote).toBe(1); +}); + +test("Reject follower", async () => { + // create private community + const community = await createCommunity(alpha, randomString(10), "Private"); + expect(community.community_view.community.visibility).toBe("Private"); + const alphaCommunityId = community.community_view.community.id; + + // user is not following the community and cannot view nor create posts + const user = await registerUser(beta, betaUrl); + const betaCommunity1 = ( + await resolveCommunity(user, community.community_view.community.actor_id) + ).community!.community; + + // follow the community and reject + const follow_form: FollowCommunity = { + community_id: betaCommunity1.id, + follow: true, + }; + const follow = await user.followCommunity(follow_form); + expect(follow.community_view.subscribed).toBe("ApprovalRequired"); + + const pendingFollows1 = await waitUntil( + () => listCommunityPendingFollows(alpha), + f => f.items.length == 1, + ); + const approve = await approveCommunityPendingFollow( + alpha, + alphaCommunityId, + pendingFollows1.items[0].person.id, + false, + ); + expect(approve.success).toBe(true); + + await waitUntil( + () => getCommunity(user, betaCommunity1.id), + c => c.community_view.subscribed == "NotSubscribed", + ); +}); diff --git a/api_tests/src/shared.ts b/api_tests/src/shared.ts index 8ec4b29ed..95e916ef2 100644 --- a/api_tests/src/shared.ts +++ b/api_tests/src/shared.ts @@ -1,17 +1,24 @@ import { + ApproveCommunityPendingFollower, BlockCommunity, BlockCommunityResponse, BlockInstance, BlockInstanceResponse, CommunityId, + CommunityVisibility, CreatePrivateMessageReport, DeleteImage, EditCommunity, + GetCommunityPendingFollowsCount, + GetCommunityPendingFollowsCountResponse, GetReplies, GetRepliesResponse, GetUnreadCountResponse, InstanceId, LemmyHttp, + ListCommunityPendingFollows, + ListCommunityPendingFollowsResponse, + PersonId, PostView, PrivateMessageReportResponse, SuccessResponse, @@ -83,7 +90,7 @@ export const fetchFunction = fetch; export const imageFetchLimit = 50; export const sampleImage = "https://i.pinimg.com/originals/df/5f/5b/df5f5b1b174a2b4b6026cc6c8f9395c1.jpg"; -export const sampleSite = "https://yahoo.com"; +export const sampleSite = "https://w3.org"; export const alphaUrl = "http://127.0.0.1:8541"; export const betaUrl = "http://127.0.0.1:8551"; @@ -198,7 +205,7 @@ export async function setupLogins() { // only needed the first time so do in this try await delay(10_000); } catch { - console.log("Communities already exist"); + //console.log("Communities already exist"); } } @@ -554,12 +561,14 @@ export async function likeComment( export async function createCommunity( api: LemmyHttp, name_: string = randomString(10), + visibility: CommunityVisibility = "Public", ): Promise { let description = "a sample description"; let form: CreateCommunity = { name: name_, title: name_, description, + visibility, }; return api.createCommunity(form); } @@ -688,7 +697,6 @@ export async function saveUserSettingsBio( let form: SaveUserSettings = { show_nsfw: true, blur_nsfw: false, - auto_expand: true, theme: "darkly", default_post_sort_type: "Active", default_listing_type: "All", @@ -709,7 +717,6 @@ export async function saveUserSettingsFederated( let form: SaveUserSettings = { show_nsfw: false, blur_nsfw: true, - auto_expand: false, default_post_sort_type: "Hot", default_listing_type: "All", interface_language: "", @@ -872,6 +879,39 @@ export function blockCommunity( return api.blockCommunity(form); } +export function listCommunityPendingFollows( + api: LemmyHttp, +): Promise { + let form: ListCommunityPendingFollows = { + pending_only: true, + all_communities: false, + page: 1, + limit: 50, + }; + return api.listCommunityPendingFollows(form); +} + +export function getCommunityPendingFollowsCount( + api: LemmyHttp, + community_id: CommunityId, +): Promise { + return api.getCommunityPendingFollowsCount(community_id); +} + +export function approveCommunityPendingFollow( + api: LemmyHttp, + community_id: CommunityId, + follower_id: PersonId, + approve: boolean = true, +): Promise { + let form: ApproveCommunityPendingFollower = { + community_id, + follower_id, + approve, + }; + return api.approveCommunityPendingFollow(form); +} + export function delay(millis = 500) { return new Promise(resolve => setTimeout(resolve, millis)); } @@ -962,8 +1002,12 @@ export async function waitUntil( let retry = 0; let result; while (retry++ < retries) { - result = await fetcher(); - if (checker(result)) return result; + try { + result = await fetcher(); + if (checker(result)) return result; + } catch (error) { + //console.error(error); + } await delay( delaySeconds[Math.min(retry - 1, delaySeconds.length - 1)] * 1000, ); diff --git a/config/defaults.hjson b/config/defaults.hjson index f0b9d56df..96dc30b79 100644 --- a/config/defaults.hjson +++ b/config/defaults.hjson @@ -76,7 +76,7 @@ # Timeout for uploading images to pictrs (in seconds) upload_timeout: 30 # Resize post thumbnails to this maximum width/height. - max_thumbnail_size: 256 + max_thumbnail_size: 512 } # Email sending configuration. All options except login/password are mandatory email: { diff --git a/crates/api/src/comment/distinguish.rs b/crates/api/src/comment/distinguish.rs index a1b25ea44..17608a230 100644 --- a/crates/api/src/comment/distinguish.rs +++ b/crates/api/src/comment/distinguish.rs @@ -26,7 +26,7 @@ pub async fn distinguish_comment( check_community_user_action( &local_user_view.person, - orig_comment.community.id, + &orig_comment.community, &mut context.pool(), ) .await?; @@ -39,7 +39,7 @@ pub async fn distinguish_comment( // Verify that only a mod or admin can distinguish a comment check_community_mod_action( &local_user_view.person, - orig_comment.community.id, + &orig_comment.community, false, &mut context.pool(), ) diff --git a/crates/api/src/comment/like.rs b/crates/api/src/comment/like.rs index 749b90426..fbc720102 100644 --- a/crates/api/src/comment/like.rs +++ b/crates/api/src/comment/like.rs @@ -50,7 +50,7 @@ pub async fn like_comment( check_community_user_action( &local_user_view.person, - orig_comment.community.id, + &orig_comment.community, &mut context.pool(), ) .await?; @@ -67,7 +67,6 @@ pub async fn like_comment( let like_form = CommentLikeForm { comment_id: data.comment_id, - post_id: orig_comment.post.id, person_id: local_user_view.person.id, score: data.score, }; @@ -93,8 +92,7 @@ pub async fn like_comment( score: data.score, }, &context, - ) - .await?; + )?; Ok(Json( build_comment_response( diff --git a/crates/api/src/comment_report/create.rs b/crates/api/src/comment_report/create.rs index a0ff4be77..48066cfe6 100644 --- a/crates/api/src/comment_report/create.rs +++ b/crates/api/src/comment_report/create.rs @@ -44,7 +44,7 @@ pub async fn create_comment_report( check_community_user_action( &local_user_view.person, - comment_view.community.id, + &comment_view.community, &mut context.pool(), ) .await?; @@ -85,8 +85,7 @@ pub async fn create_comment_report( reason: data.reason.clone(), }, &context, - ) - .await?; + )?; Ok(Json(CommentReportResponse { comment_report_view, diff --git a/crates/api/src/comment_report/resolve.rs b/crates/api/src/comment_report/resolve.rs index a663fdf74..58d5041dc 100644 --- a/crates/api/src/comment_report/resolve.rs +++ b/crates/api/src/comment_report/resolve.rs @@ -22,7 +22,7 @@ pub async fn resolve_comment_report( let person_id = local_user_view.person.id; check_community_mod_action( &local_user_view.person, - report.community.id, + &report.community, true, &mut context.pool(), ) diff --git a/crates/api/src/community/add_mod.rs b/crates/api/src/community/add_mod.rs index 7d04f6bb0..9e85788ea 100644 --- a/crates/api/src/community/add_mod.rs +++ b/crates/api/src/community/add_mod.rs @@ -24,12 +24,11 @@ pub async fn add_mod_to_community( context: Data, local_user_view: LocalUserView, ) -> LemmyResult> { - let community_id = data.community_id; - + let community = Community::read(&mut context.pool(), data.community_id).await?; // Verify that only mods or admins can add mod check_community_mod_action( &local_user_view.person, - community_id, + &community, false, &mut context.pool(), ) @@ -39,15 +38,13 @@ pub async fn add_mod_to_community( if !data.added { LocalUser::is_higher_mod_or_admin_check( &mut context.pool(), - community_id, + community.id, local_user_view.person.id, vec![data.person_id], ) .await?; } - let community = Community::read(&mut context.pool(), community_id).await?; - // If user is admin and community is remote, explicitly check that he is a // moderator. This is necessary because otherwise the action would be rejected // by the community's home instance. @@ -98,8 +95,7 @@ pub async fn add_mod_to_community( added: data.added, }, &context, - ) - .await?; + )?; Ok(Json(AddModToCommunityResponse { moderators })) } diff --git a/crates/api/src/community/ban.rs b/crates/api/src/community/ban.rs index 64b1c7196..a0e57061b 100644 --- a/crates/api/src/community/ban.rs +++ b/crates/api/src/community/ban.rs @@ -13,6 +13,7 @@ use lemmy_api_common::{ use lemmy_db_schema::{ source::{ community::{ + Community, CommunityFollower, CommunityFollowerForm, CommunityPersonBan, @@ -38,11 +39,12 @@ pub async fn ban_from_community( ) -> LemmyResult> { let banned_person_id = data.person_id; let expires = check_expire_time(data.expires)?; + let community = Community::read(&mut context.pool(), data.community_id).await?; // Verify that only mods or admins can ban check_community_mod_action( &local_user_view.person, - data.community_id, + &community, false, &mut context.pool(), ) @@ -72,12 +74,7 @@ pub async fn ban_from_community( .with_lemmy_type(LemmyErrorType::CommunityUserAlreadyBanned)?; // Also unsubscribe them from the community, if they are subscribed - let community_follower_form = CommunityFollowerForm { - community_id: data.community_id, - person_id: banned_person_id, - pending: false, - }; - + let community_follower_form = CommunityFollowerForm::new(data.community_id, banned_person_id); CommunityFollower::unfollow(&mut context.pool(), &community_follower_form) .await .ok(); @@ -123,8 +120,7 @@ pub async fn ban_from_community( data: data.0.clone(), }, &context, - ) - .await?; + )?; Ok(Json(BanFromCommunityResponse { person_view, diff --git a/crates/api/src/community/block.rs b/crates/api/src/community/block.rs index 90931c762..a6a48e2e7 100644 --- a/crates/api/src/community/block.rs +++ b/crates/api/src/community/block.rs @@ -35,12 +35,7 @@ pub async fn block_community( .with_lemmy_type(LemmyErrorType::CommunityBlockAlreadyExists)?; // Also, unfollow the community, and send a federated unfollow - let community_follower_form = CommunityFollowerForm { - community_id: data.community_id, - person_id, - pending: false, - }; - + let community_follower_form = CommunityFollowerForm::new(data.community_id, person_id); CommunityFollower::unfollow(&mut context.pool(), &community_follower_form) .await .ok(); @@ -65,8 +60,7 @@ pub async fn block_community( false, ), &context, - ) - .await?; + )?; Ok(Json(BlockCommunityResponse { blocked: data.block, diff --git a/crates/api/src/community/follow.rs b/crates/api/src/community/follow.rs index d0f5bbf0d..d5cd3e5b1 100644 --- a/crates/api/src/community/follow.rs +++ b/crates/api/src/community/follow.rs @@ -4,17 +4,18 @@ use lemmy_api_common::{ community::{CommunityResponse, FollowCommunity}, context::LemmyContext, send_activity::{ActivityChannel, SendActivityData}, - utils::check_community_user_action, + utils::{check_community_deleted_removed, check_user_valid}, }; use lemmy_db_schema::{ source::{ actor_language::CommunityLanguage, - community::{Community, CommunityFollower, CommunityFollowerForm}, + community::{Community, CommunityFollower, CommunityFollowerForm, CommunityFollowerState}, }, traits::{Crud, Followable}, + CommunityVisibility, }; use lemmy_db_views::structs::LocalUserView; -use lemmy_db_views_actor::structs::CommunityView; +use lemmy_db_views_actor::structs::{CommunityPersonBanView, CommunityView}; use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult}; #[tracing::instrument(skip(context))] @@ -23,40 +24,52 @@ pub async fn follow_community( context: Data, local_user_view: LocalUserView, ) -> LemmyResult> { + check_user_valid(&local_user_view.person)?; let community = Community::read(&mut context.pool(), data.community_id).await?; - let mut community_follower_form = CommunityFollowerForm { - community_id: community.id, - person_id: local_user_view.person.id, - pending: false, - }; + let form = CommunityFollowerForm::new(community.id, local_user_view.person.id); if data.follow { + // Only run these checks for local community, in case of remote community the local + // state may be outdated. Can't use check_community_user_action() here as it only allows + // actions from existing followers for private community (so following would be impossible). if community.local { - check_community_user_action(&local_user_view.person, community.id, &mut context.pool()) + check_community_deleted_removed(&community)?; + CommunityPersonBanView::check(&mut context.pool(), local_user_view.person.id, community.id) .await?; - - CommunityFollower::follow(&mut context.pool(), &community_follower_form) - .await - .with_lemmy_type(LemmyErrorType::CommunityFollowerAlreadyExists)?; - } else { - // Mark as pending, the actual federation activity is sent via `SendActivity` handler - community_follower_form.pending = true; - CommunityFollower::follow(&mut context.pool(), &community_follower_form) - .await - .with_lemmy_type(LemmyErrorType::CommunityFollowerAlreadyExists)?; } + + let state = if community.local { + // Local follow is accepted immediately + Some(CommunityFollowerState::Accepted) + } else if community.visibility == CommunityVisibility::Private { + // Private communities require manual approval + Some(CommunityFollowerState::ApprovalRequired) + } else { + // remote follow needs to be federated first + Some(CommunityFollowerState::Pending) + }; + + let form = CommunityFollowerForm { + state, + ..CommunityFollowerForm::new(community.id, local_user_view.person.id) + }; + + // Write to db + CommunityFollower::follow(&mut context.pool(), &form) + .await + .with_lemmy_type(LemmyErrorType::CommunityFollowerAlreadyExists)?; } else { - CommunityFollower::unfollow(&mut context.pool(), &community_follower_form) + CommunityFollower::unfollow(&mut context.pool(), &form) .await .with_lemmy_type(LemmyErrorType::CommunityFollowerAlreadyExists)?; } + // Send the federated follow if !community.local { ActivityChannel::submit_activity( SendActivityData::FollowCommunity(community, local_user_view.person.clone(), data.follow), &context, - ) - .await?; + )?; } let community_id = data.community_id; diff --git a/crates/api/src/community/hide.rs b/crates/api/src/community/hide.rs index 997d88de3..077ed1c5e 100644 --- a/crates/api/src/community/hide.rs +++ b/crates/api/src/community/hide.rs @@ -48,8 +48,7 @@ pub async fn hide_community( ActivityChannel::submit_activity( SendActivityData::UpdateCommunity(local_user_view.person.clone(), community), &context, - ) - .await?; + )?; Ok(Json(SuccessResponse::default())) } diff --git a/crates/api/src/community/mod.rs b/crates/api/src/community/mod.rs index 54bdbef28..121e181c6 100644 --- a/crates/api/src/community/mod.rs +++ b/crates/api/src/community/mod.rs @@ -3,5 +3,6 @@ pub mod ban; pub mod block; pub mod follow; pub mod hide; +pub mod pending_follows; pub mod random; pub mod transfer; diff --git a/crates/api/src/community/pending_follows/approve.rs b/crates/api/src/community/pending_follows/approve.rs new file mode 100644 index 000000000..468e9d9d0 --- /dev/null +++ b/crates/api/src/community/pending_follows/approve.rs @@ -0,0 +1,46 @@ +use activitypub_federation::config::Data; +use actix_web::web::Json; +use lemmy_api_common::{ + community::ApproveCommunityPendingFollower, + context::LemmyContext, + send_activity::{ActivityChannel, SendActivityData}, + utils::is_mod_or_admin, + SuccessResponse, +}; +use lemmy_db_schema::{ + source::community::{CommunityFollower, CommunityFollowerForm}, + traits::Followable, +}; +use lemmy_db_views::structs::LocalUserView; +use lemmy_utils::error::LemmyResult; + +pub async fn post_pending_follows_approve( + data: Json, + context: Data, + local_user_view: LocalUserView, +) -> LemmyResult> { + is_mod_or_admin( + &mut context.pool(), + &local_user_view.person, + data.community_id, + ) + .await?; + + let activity_data = if data.approve { + CommunityFollower::approve( + &mut context.pool(), + data.community_id, + data.follower_id, + local_user_view.person.id, + ) + .await?; + SendActivityData::AcceptFollower(data.community_id, data.follower_id) + } else { + let form = CommunityFollowerForm::new(data.community_id, data.follower_id); + CommunityFollower::unfollow(&mut context.pool(), &form).await?; + SendActivityData::RejectFollower(data.community_id, data.follower_id) + }; + ActivityChannel::submit_activity(activity_data, &context)?; + + Ok(Json(SuccessResponse::default())) +} diff --git a/crates/api/src/community/pending_follows/count.rs b/crates/api/src/community/pending_follows/count.rs new file mode 100644 index 000000000..e8e333c84 --- /dev/null +++ b/crates/api/src/community/pending_follows/count.rs @@ -0,0 +1,25 @@ +use actix_web::web::{Data, Json, Query}; +use lemmy_api_common::{ + community::{GetCommunityPendingFollowsCount, GetCommunityPendingFollowsCountResponse}, + context::LemmyContext, + utils::is_mod_or_admin, +}; +use lemmy_db_views::structs::LocalUserView; +use lemmy_db_views_actor::structs::CommunityFollowerView; +use lemmy_utils::error::LemmyResult; + +pub async fn get_pending_follows_count( + data: Query, + context: Data, + local_user_view: LocalUserView, +) -> LemmyResult> { + is_mod_or_admin( + &mut context.pool(), + &local_user_view.person, + data.community_id, + ) + .await?; + let count = + CommunityFollowerView::count_approval_required(&mut context.pool(), data.community_id).await?; + Ok(Json(GetCommunityPendingFollowsCountResponse { count })) +} diff --git a/crates/api/src/community/pending_follows/list.rs b/crates/api/src/community/pending_follows/list.rs new file mode 100644 index 000000000..9f300a74f --- /dev/null +++ b/crates/api/src/community/pending_follows/list.rs @@ -0,0 +1,29 @@ +use actix_web::web::{Data, Json, Query}; +use lemmy_api_common::{ + community::{ListCommunityPendingFollows, ListCommunityPendingFollowsResponse}, + context::LemmyContext, + utils::check_community_mod_of_any_or_admin_action, +}; +use lemmy_db_views::structs::LocalUserView; +use lemmy_db_views_actor::structs::CommunityFollowerView; +use lemmy_utils::error::LemmyResult; + +pub async fn get_pending_follows_list( + data: Query, + context: Data, + local_user_view: LocalUserView, +) -> LemmyResult> { + check_community_mod_of_any_or_admin_action(&local_user_view, &mut context.pool()).await?; + let all_communities = + data.all_communities.unwrap_or_default() && local_user_view.local_user.admin; + let items = CommunityFollowerView::list_approval_required( + &mut context.pool(), + local_user_view.person.id, + all_communities, + data.pending_only.unwrap_or_default(), + data.page, + data.limit, + ) + .await?; + Ok(Json(ListCommunityPendingFollowsResponse { items })) +} diff --git a/crates/api/src/community/pending_follows/mod.rs b/crates/api/src/community/pending_follows/mod.rs new file mode 100644 index 000000000..dcc82e250 --- /dev/null +++ b/crates/api/src/community/pending_follows/mod.rs @@ -0,0 +1,3 @@ +pub mod approve; +pub mod count; +pub mod list; diff --git a/crates/api/src/community/transfer.rs b/crates/api/src/community/transfer.rs index 195adbd8d..a5255e5e1 100644 --- a/crates/api/src/community/transfer.rs +++ b/crates/api/src/community/transfer.rs @@ -7,7 +7,7 @@ use lemmy_api_common::{ }; use lemmy_db_schema::{ source::{ - community::{CommunityModerator, CommunityModeratorForm}, + community::{Community, CommunityModerator, CommunityModeratorForm}, moderator::{ModTransferCommunity, ModTransferCommunityForm}, }, traits::{Crud, Joinable}, @@ -27,11 +27,11 @@ pub async fn transfer_community( context: Data, local_user_view: LocalUserView, ) -> LemmyResult> { - let community_id = data.community_id; + let community = Community::read(&mut context.pool(), data.community_id).await?; let mut community_mods = - CommunityModeratorView::for_community(&mut context.pool(), community_id).await?; + CommunityModeratorView::for_community(&mut context.pool(), community.id).await?; - check_community_user_action(&local_user_view.person, community_id, &mut context.pool()).await?; + check_community_user_action(&local_user_view.person, &community, &mut context.pool()).await?; // Make sure transferrer is either the top community mod, or an admin if !(is_top_mod(&local_user_view, &community_mods).is_ok() || is_admin(&local_user_view).is_ok()) diff --git a/crates/api/src/lib.rs b/crates/api/src/lib.rs index 6ffa52f77..3ab2ba277 100644 --- a/crates/api/src/lib.rs +++ b/crates/api/src/lib.rs @@ -197,11 +197,7 @@ pub(crate) async fn ban_nonlocal_user_from_local_communities( .ok(); // Also unsubscribe them from the community, if they are subscribed - let community_follower_form = CommunityFollowerForm { - community_id, - person_id: target.id, - pending: false, - }; + let community_follower_form = CommunityFollowerForm::new(community_id, target.id); CommunityFollower::unfollow(&mut context.pool(), &community_follower_form) .await @@ -242,8 +238,7 @@ pub(crate) async fn ban_nonlocal_user_from_local_communities( data: ban_from_community, }, context, - ) - .await?; + )?; } } diff --git a/crates/api/src/local_user/ban_person.rs b/crates/api/src/local_user/ban_person.rs index 2ace7f031..9349cc632 100644 --- a/crates/api/src/local_user/ban_person.rs +++ b/crates/api/src/local_user/ban_person.rs @@ -111,8 +111,7 @@ pub async fn ban_from_site( expires: data.expires, }, &context, - ) - .await?; + )?; Ok(Json(BanPersonResponse { person_view, diff --git a/crates/api/src/local_user/save_settings.rs b/crates/api/src/local_user/save_settings.rs index 08820cadd..ac2e321a1 100644 --- a/crates/api/src/local_user/save_settings.rs +++ b/crates/api/src/local_user/save_settings.rs @@ -141,6 +141,7 @@ pub async fn save_user_settings( post_listing_mode: data.post_listing_mode, enable_keyboard_navigation: data.enable_keyboard_navigation, enable_animated_images: data.enable_animated_images, + enable_private_messages: data.enable_private_messages, collapse_bot_comments: data.collapse_bot_comments, ..Default::default() }; diff --git a/crates/api/src/post/feature.rs b/crates/api/src/post/feature.rs index cb6e6c144..6fc2f443c 100644 --- a/crates/api/src/post/feature.rs +++ b/crates/api/src/post/feature.rs @@ -9,6 +9,7 @@ use lemmy_api_common::{ }; use lemmy_db_schema::{ source::{ + community::Community, moderator::{ModFeaturePost, ModFeaturePostForm}, post::{Post, PostUpdateForm}, }, @@ -27,9 +28,10 @@ pub async fn feature_post( let post_id = data.post_id; let orig_post = Post::read(&mut context.pool(), post_id).await?; + let community = Community::read(&mut context.pool(), orig_post.community_id).await?; check_community_mod_action( &local_user_view.person, - orig_post.community_id, + &community, false, &mut context.pool(), ) @@ -67,8 +69,7 @@ pub async fn feature_post( ActivityChannel::submit_activity( SendActivityData::FeaturePost(post, local_user_view.person.clone(), data.featured), &context, - ) - .await?; + )?; build_post_response(&context, orig_post.community_id, local_user_view, post_id).await } diff --git a/crates/api/src/post/get_link_metadata.rs b/crates/api/src/post/get_link_metadata.rs index e469b51c7..a777cab17 100644 --- a/crates/api/src/post/get_link_metadata.rs +++ b/crates/api/src/post/get_link_metadata.rs @@ -5,10 +5,7 @@ use lemmy_api_common::{ request::fetch_link_metadata, }; use lemmy_db_views::structs::LocalUserView; -use lemmy_utils::{ - error::{LemmyErrorExt, LemmyResult}, - LemmyErrorType, -}; +use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult}; use url::Url; #[tracing::instrument(skip(context))] diff --git a/crates/api/src/post/like.rs b/crates/api/src/post/like.rs index c81d9630a..ec01e3e8c 100644 --- a/crates/api/src/post/like.rs +++ b/crates/api/src/post/like.rs @@ -15,13 +15,12 @@ use lemmy_api_common::{ }; use lemmy_db_schema::{ source::{ - community::Community, local_site::LocalSite, - post::{Post, PostLike, PostLikeForm}, + post::{PostLike, PostLikeForm}, }, - traits::{Crud, Likeable}, + traits::Likeable, }; -use lemmy_db_views::structs::LocalUserView; +use lemmy_db_views::structs::{LocalUserView, PostView}; use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult}; use std::ops::Deref; @@ -45,11 +44,11 @@ pub async fn like_post( check_bot_account(&local_user_view.person)?; // Check for a community ban - let post = Post::read(&mut context.pool(), post_id).await?; + let post = PostView::read(&mut context.pool(), post_id, None, false).await?; check_community_user_action( &local_user_view.person, - post.community_id, + &post.community, &mut context.pool(), ) .await?; @@ -75,18 +74,15 @@ pub async fn like_post( mark_post_as_read(person_id, post_id, &mut context.pool()).await?; - let community = Community::read(&mut context.pool(), post.community_id).await?; - ActivityChannel::submit_activity( SendActivityData::LikePostOrComment { - object_id: post.ap_id, + object_id: post.post.ap_id, actor: local_user_view.person.clone(), - community, + community: post.community.clone(), score: data.score, }, &context, - ) - .await?; + )?; - build_post_response(context.deref(), post.community_id, local_user_view, post_id).await + build_post_response(context.deref(), post.community.id, local_user_view, post_id).await } diff --git a/crates/api/src/post/lock.rs b/crates/api/src/post/lock.rs index 548947b78..011770c2e 100644 --- a/crates/api/src/post/lock.rs +++ b/crates/api/src/post/lock.rs @@ -14,7 +14,7 @@ use lemmy_db_schema::{ }, traits::Crud, }; -use lemmy_db_views::structs::LocalUserView; +use lemmy_db_views::structs::{LocalUserView, PostView}; use lemmy_utils::error::LemmyResult; #[tracing::instrument(skip(context))] @@ -24,11 +24,11 @@ pub async fn lock_post( local_user_view: LocalUserView, ) -> LemmyResult> { let post_id = data.post_id; - let orig_post = Post::read(&mut context.pool(), post_id).await?; + let orig_post = PostView::read(&mut context.pool(), post_id, None, false).await?; check_community_mod_action( &local_user_view.person, - orig_post.community_id, + &orig_post.community, false, &mut context.pool(), ) @@ -58,8 +58,7 @@ pub async fn lock_post( ActivityChannel::submit_activity( SendActivityData::LockPost(post, local_user_view.person.clone(), data.locked), &context, - ) - .await?; + )?; - build_post_response(&context, orig_post.community_id, local_user_view, post_id).await + build_post_response(&context, orig_post.community.id, local_user_view, post_id).await } diff --git a/crates/api/src/post_report/create.rs b/crates/api/src/post_report/create.rs index 590c9af40..b9edf35c5 100644 --- a/crates/api/src/post_report/create.rs +++ b/crates/api/src/post_report/create.rs @@ -39,7 +39,7 @@ pub async fn create_post_report( check_community_user_action( &local_user_view.person, - post_view.community.id, + &post_view.community, &mut context.pool(), ) .await?; @@ -80,8 +80,7 @@ pub async fn create_post_report( reason: data.reason.clone(), }, &context, - ) - .await?; + )?; Ok(Json(PostReportResponse { post_report_view })) } diff --git a/crates/api/src/post_report/resolve.rs b/crates/api/src/post_report/resolve.rs index a3cb85c6c..652327513 100644 --- a/crates/api/src/post_report/resolve.rs +++ b/crates/api/src/post_report/resolve.rs @@ -22,7 +22,7 @@ pub async fn resolve_post_report( let person_id = local_user_view.person.id; check_community_mod_action( &local_user_view.person, - report.community.id, + &report.community, true, &mut context.pool(), ) diff --git a/crates/api/src/site/purge/comment.rs b/crates/api/src/site/purge/comment.rs index b21ffbc80..ae79a835a 100644 --- a/crates/api/src/site/purge/comment.rs +++ b/crates/api/src/site/purge/comment.rs @@ -67,8 +67,7 @@ pub async fn purge_comment( reason: data.reason.clone(), }, &context, - ) - .await?; + )?; Ok(Json(SuccessResponse::default())) } diff --git a/crates/api/src/site/purge/community.rs b/crates/api/src/site/purge/community.rs index bf06bd529..f0252e303 100644 --- a/crates/api/src/site/purge/community.rs +++ b/crates/api/src/site/purge/community.rs @@ -75,8 +75,7 @@ pub async fn purge_community( removed: true, }, &context, - ) - .await?; + )?; Ok(Json(SuccessResponse::default())) } diff --git a/crates/api/src/site/purge/person.rs b/crates/api/src/site/purge/person.rs index 7ab573cbc..6dad4ce65 100644 --- a/crates/api/src/site/purge/person.rs +++ b/crates/api/src/site/purge/person.rs @@ -80,8 +80,7 @@ pub async fn purge_person( expires: None, }, &context, - ) - .await?; + )?; Ok(Json(SuccessResponse::default())) } diff --git a/crates/api/src/site/purge/post.rs b/crates/api/src/site/purge/post.rs index d2cacdae1..f808269e7 100644 --- a/crates/api/src/site/purge/post.rs +++ b/crates/api/src/site/purge/post.rs @@ -66,8 +66,7 @@ pub async fn purge_post( removed: true, }, &context, - ) - .await?; + )?; Ok(Json(SuccessResponse::default())) } diff --git a/crates/api/src/site/registration_applications/tests.rs b/crates/api/src/site/registration_applications/tests.rs index 022cbf236..bdfb1535e 100644 --- a/crates/api/src/site/registration_applications/tests.rs +++ b/crates/api/src/site/registration_applications/tests.rs @@ -31,7 +31,10 @@ use lemmy_db_schema::{ RegistrationMode, }; use lemmy_db_views::structs::LocalUserView; -use lemmy_utils::{error::LemmyResult, LemmyErrorType, CACHE_DURATION_API}; +use lemmy_utils::{ + error::{LemmyErrorType, LemmyResult}, + CACHE_DURATION_API, +}; use serial_test::serial; async fn create_test_site(context: &Data) -> LemmyResult<(Instance, LocalUserView)> { diff --git a/crates/api/src/sitemap.rs b/crates/api/src/sitemap.rs index c3c3c417c..4d3799b1b 100644 --- a/crates/api/src/sitemap.rs +++ b/crates/api/src/sitemap.rs @@ -9,14 +9,12 @@ use lemmy_utils::error::LemmyResult; use sitemap_rs::{url::Url, url_set::UrlSet}; use tracing::info; -async fn generate_urlset( - posts: Vec<(DbUrl, chrono::DateTime)>, -) -> LemmyResult { +fn generate_urlset(posts: Vec<(DbUrl, chrono::DateTime)>) -> LemmyResult { let urls = posts .into_iter() - .map_while(|post| { - Url::builder(post.0.to_string()) - .last_modified(post.1.into()) + .map_while(|(url, date_time)| { + Url::builder(url.to_string()) + .last_modified(date_time.into()) .build() .ok() }) @@ -31,7 +29,7 @@ pub async fn get_sitemap(context: Data) -> LemmyResult::new(); - generate_urlset(posts).await?.write(&mut buf)?; + generate_urlset(posts)?.write(&mut buf)?; Ok( HttpResponse::Ok() @@ -74,7 +72,7 @@ pub(crate) mod tests { ]; let mut buf = Vec::::new(); - generate_urlset(posts).await?.write(&mut buf)?; + generate_urlset(posts)?.write(&mut buf)?; let root = Element::from_reader(buf.as_slice())?; assert_eq!(root.tag().name(), "urlset"); diff --git a/crates/api_common/Cargo.toml b/crates/api_common/Cargo.toml index 4eabfe5f9..f939985e8 100644 --- a/crates/api_common/Cargo.toml +++ b/crates/api_common/Cargo.toml @@ -72,7 +72,7 @@ jsonwebtoken = { version = "9.3.0", optional = true } # necessary for wasmt compilation getrandom = { version = "0.2.15", features = ["js"] } -[package.metadata.cargo-machete] +[package.metadata.cargo-shear] ignored = ["getrandom"] [dev-dependencies] diff --git a/crates/api_common/src/claims.rs b/crates/api_common/src/claims.rs index 6476f855a..759673f4b 100644 --- a/crates/api_common/src/claims.rs +++ b/crates/api_common/src/claims.rs @@ -92,7 +92,7 @@ mod tests { #[tokio::test] #[serial] async fn test_should_not_validate_user_token_after_password_change() -> LemmyResult<()> { - let pool_ = build_db_pool_for_tests().await; + let pool_ = build_db_pool_for_tests(); let pool = &mut (&pool_).into(); let secret = Secret::init(pool).await?; let context = LemmyContext::create( diff --git a/crates/api_common/src/comment.rs b/crates/api_common/src/comment.rs index 48800cf8d..e08365789 100644 --- a/crates/api_common/src/comment.rs +++ b/crates/api_common/src/comment.rs @@ -17,7 +17,9 @@ use ts_rs::TS; pub struct CreateComment { pub content: String, pub post_id: PostId, + #[cfg_attr(feature = "full", ts(optional))] pub parent_id: Option, + #[cfg_attr(feature = "full", ts(optional))] pub language_id: Option, } @@ -37,7 +39,9 @@ pub struct GetComment { /// Edit a comment. pub struct EditComment { pub comment_id: CommentId, + #[cfg_attr(feature = "full", ts(optional))] pub content: Option, + #[cfg_attr(feature = "full", ts(optional))] pub language_id: Option, } @@ -69,6 +73,7 @@ pub struct DeleteComment { pub struct RemoveComment { pub comment_id: CommentId, pub removed: bool, + #[cfg_attr(feature = "full", ts(optional))] pub reason: Option, } @@ -107,17 +112,29 @@ pub struct CreateCommentLike { #[cfg_attr(feature = "full", ts(export))] /// Get a list of comments. pub struct GetComments { + #[cfg_attr(feature = "full", ts(optional))] pub type_: Option, + #[cfg_attr(feature = "full", ts(optional))] pub sort: Option, + #[cfg_attr(feature = "full", ts(optional))] pub max_depth: Option, + #[cfg_attr(feature = "full", ts(optional))] pub page: Option, + #[cfg_attr(feature = "full", ts(optional))] pub limit: Option, + #[cfg_attr(feature = "full", ts(optional))] pub community_id: Option, + #[cfg_attr(feature = "full", ts(optional))] pub community_name: Option, + #[cfg_attr(feature = "full", ts(optional))] pub post_id: Option, + #[cfg_attr(feature = "full", ts(optional))] pub parent_id: Option, + #[cfg_attr(feature = "full", ts(optional))] pub saved_only: Option, + #[cfg_attr(feature = "full", ts(optional))] pub liked_only: Option, + #[cfg_attr(feature = "full", ts(optional))] pub disliked_only: Option, } @@ -161,12 +178,17 @@ pub struct ResolveCommentReport { #[cfg_attr(feature = "full", ts(export))] /// List comment reports. pub struct ListCommentReports { + #[cfg_attr(feature = "full", ts(optional))] pub comment_id: Option, + #[cfg_attr(feature = "full", ts(optional))] pub page: Option, + #[cfg_attr(feature = "full", ts(optional))] pub limit: Option, /// Only shows the unresolved reports + #[cfg_attr(feature = "full", ts(optional))] pub unresolved_only: Option, /// if no community is given, it returns reports for all communities moderated by the auth user + #[cfg_attr(feature = "full", ts(optional))] pub community_id: Option, } @@ -185,7 +207,9 @@ pub struct ListCommentReportsResponse { /// List comment likes. Admins-only. pub struct ListCommentLikes { pub comment_id: CommentId, + #[cfg_attr(feature = "full", ts(optional))] pub page: Option, + #[cfg_attr(feature = "full", ts(optional))] pub limit: Option, } diff --git a/crates/api_common/src/community.rs b/crates/api_common/src/community.rs index f8e741a58..898767b34 100644 --- a/crates/api_common/src/community.rs +++ b/crates/api_common/src/community.rs @@ -8,6 +8,7 @@ use lemmy_db_views_actor::structs::{ CommunityModeratorView, CommunitySortType, CommunityView, + PendingFollow, PersonView, }; use serde::{Deserialize, Serialize}; @@ -19,10 +20,13 @@ use ts_rs::TS; #[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq, Hash)] #[cfg_attr(feature = "full", derive(TS))] #[cfg_attr(feature = "full", ts(export))] +// TODO make this into a tagged enum /// Get a community. Must provide either an id, or a name. pub struct GetCommunity { + #[cfg_attr(feature = "full", ts(optional))] pub id: Option, /// Example: star_trek , or star_trek@xyz.tld + #[cfg_attr(feature = "full", ts(optional))] pub name: Option, } @@ -33,6 +37,7 @@ pub struct GetCommunity { /// The community response. pub struct GetCommunityResponse { pub community_view: CommunityView, + #[cfg_attr(feature = "full", ts(optional))] pub site: Option, pub moderators: Vec, pub discussion_languages: Vec, @@ -48,17 +53,27 @@ pub struct CreateCommunity { pub name: String, /// A longer title. pub title: String, - /// A longer sidebar, or description of your community, in markdown. + /// A sidebar for the community in markdown. + #[cfg_attr(feature = "full", ts(optional))] + pub sidebar: Option, + /// A shorter, one line description of your community. + #[cfg_attr(feature = "full", ts(optional))] pub description: Option, /// An icon URL. + #[cfg_attr(feature = "full", ts(optional))] pub icon: Option, /// A banner URL. + #[cfg_attr(feature = "full", ts(optional))] pub banner: Option, /// Whether its an NSFW community. + #[cfg_attr(feature = "full", ts(optional))] pub nsfw: Option, /// Whether to restrict posting only to moderators. + #[cfg_attr(feature = "full", ts(optional))] pub posting_restricted_to_mods: Option, + #[cfg_attr(feature = "full", ts(optional))] pub discussion_languages: Option>, + #[cfg_attr(feature = "full", ts(optional))] pub visibility: Option, } @@ -77,10 +92,15 @@ pub struct CommunityResponse { #[cfg_attr(feature = "full", ts(export))] /// Fetches a list of communities. pub struct ListCommunities { + #[cfg_attr(feature = "full", ts(optional))] pub type_: Option, + #[cfg_attr(feature = "full", ts(optional))] pub sort: Option, + #[cfg_attr(feature = "full", ts(optional))] pub show_nsfw: Option, + #[cfg_attr(feature = "full", ts(optional))] pub page: Option, + #[cfg_attr(feature = "full", ts(optional))] pub limit: Option, } @@ -103,11 +123,14 @@ pub struct BanFromCommunity { pub ban: bool, /// Optionally remove or restore all their data. Useful for new troll accounts. /// If ban is true, then this means remove. If ban is false, it means restore. + #[cfg_attr(feature = "full", ts(optional))] pub remove_or_restore_data: Option, + #[cfg_attr(feature = "full", ts(optional))] pub reason: Option, /// A time that the ban will expire, in unix epoch seconds. /// /// An i64 unix timestamp is used for a simpler API client implementation. + #[cfg_attr(feature = "full", ts(optional))] pub expires: Option, } @@ -146,18 +169,29 @@ pub struct AddModToCommunityResponse { pub struct EditCommunity { pub community_id: CommunityId, /// A longer title. + #[cfg_attr(feature = "full", ts(optional))] pub title: Option, - /// A longer sidebar, or description of your community, in markdown. + /// A sidebar for the community in markdown. + #[cfg_attr(feature = "full", ts(optional))] + pub sidebar: Option, + /// A shorter, one line description of your community. + #[cfg_attr(feature = "full", ts(optional))] pub description: Option, /// An icon URL. + #[cfg_attr(feature = "full", ts(optional))] pub icon: Option, /// A banner URL. + #[cfg_attr(feature = "full", ts(optional))] pub banner: Option, /// Whether its an NSFW community. + #[cfg_attr(feature = "full", ts(optional))] pub nsfw: Option, /// Whether to restrict posting only to moderators. + #[cfg_attr(feature = "full", ts(optional))] pub posting_restricted_to_mods: Option, + #[cfg_attr(feature = "full", ts(optional))] pub discussion_languages: Option>, + #[cfg_attr(feature = "full", ts(optional))] pub visibility: Option, } @@ -169,6 +203,7 @@ pub struct EditCommunity { pub struct HideCommunity { pub community_id: CommunityId, pub hidden: bool, + #[cfg_attr(feature = "full", ts(optional))] pub reason: Option, } @@ -190,6 +225,7 @@ pub struct DeleteCommunity { pub struct RemoveCommunity { pub community_id: CommunityId, pub removed: bool, + #[cfg_attr(feature = "full", ts(optional))] pub reason: Option, } @@ -236,5 +272,53 @@ pub struct TransferCommunity { #[cfg_attr(feature = "full", ts(export))] /// Fetches a random community pub struct GetRandomCommunity { + #[cfg_attr(feature = "full", ts(optional))] pub type_: Option, } + +#[skip_serializing_none] +#[derive(Debug, Serialize, Deserialize, Clone)] +#[cfg_attr(feature = "full", derive(TS))] +#[cfg_attr(feature = "full", ts(export))] +pub struct ListCommunityPendingFollows { + /// Only shows the unapproved applications + #[cfg_attr(feature = "full", ts(optional))] + pub pending_only: Option, + // Only for admins, show pending follows for communities which you dont moderate + #[cfg_attr(feature = "full", ts(optional))] + pub all_communities: Option, + #[cfg_attr(feature = "full", ts(optional))] + pub page: Option, + #[cfg_attr(feature = "full", ts(optional))] + pub limit: Option, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +#[cfg_attr(feature = "full", derive(TS))] +#[cfg_attr(feature = "full", ts(export))] +pub struct GetCommunityPendingFollowsCount { + pub community_id: CommunityId, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +#[cfg_attr(feature = "full", derive(TS))] +#[cfg_attr(feature = "full", ts(export))] +pub struct GetCommunityPendingFollowsCountResponse { + pub count: i64, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +#[cfg_attr(feature = "full", derive(TS))] +#[cfg_attr(feature = "full", ts(export))] +pub struct ListCommunityPendingFollowsResponse { + pub items: Vec, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +#[cfg_attr(feature = "full", derive(TS))] +#[cfg_attr(feature = "full", ts(export))] +pub struct ApproveCommunityPendingFollower { + pub community_id: CommunityId, + pub follower_id: PersonId, + pub approve: bool, +} diff --git a/crates/api_common/src/context.rs b/crates/api_common/src/context.rs index 334983b20..c6ab23bfc 100644 --- a/crates/api_common/src/context.rs +++ b/crates/api_common/src/context.rs @@ -57,7 +57,7 @@ impl LemmyContext { /// Do not use this in production code. pub async fn init_test_federation_config() -> FederationConfig { // call this to run migrations - let pool = build_db_pool_for_tests().await; + let pool = build_db_pool_for_tests(); let client = client_builder(&SETTINGS).build().expect("build client"); diff --git a/crates/api_common/src/custom_emoji.rs b/crates/api_common/src/custom_emoji.rs index 3804b71af..76bc9c9e2 100644 --- a/crates/api_common/src/custom_emoji.rs +++ b/crates/api_common/src/custom_emoji.rs @@ -62,8 +62,12 @@ pub struct ListCustomEmojisResponse { #[cfg_attr(feature = "full", ts(export))] /// Fetches a list of custom emojis. pub struct ListCustomEmojis { + #[cfg_attr(feature = "full", ts(optional))] pub page: Option, + #[cfg_attr(feature = "full", ts(optional))] pub limit: Option, + #[cfg_attr(feature = "full", ts(optional))] pub category: Option, + #[cfg_attr(feature = "full", ts(optional))] pub ignore_page_limits: Option, } diff --git a/crates/api_common/src/lib.rs b/crates/api_common/src/lib.rs index 68eeadecc..6e09d904d 100644 --- a/crates/api_common/src/lib.rs +++ b/crates/api_common/src/lib.rs @@ -26,7 +26,7 @@ pub extern crate lemmy_db_views_actor; pub extern crate lemmy_db_views_moderator; pub extern crate lemmy_utils; -pub use lemmy_utils::LemmyErrorType; +pub use lemmy_utils::error::LemmyErrorType; use serde::{Deserialize, Serialize}; use std::{cmp::min, time::Duration}; diff --git a/crates/api_common/src/oauth_provider.rs b/crates/api_common/src/oauth_provider.rs index 14847edf1..36fef3b18 100644 --- a/crates/api_common/src/oauth_provider.rs +++ b/crates/api_common/src/oauth_provider.rs @@ -20,8 +20,11 @@ pub struct CreateOAuthProvider { pub client_id: String, pub client_secret: String, pub scopes: String, + #[cfg_attr(feature = "full", ts(optional))] pub auto_verify_email: Option, + #[cfg_attr(feature = "full", ts(optional))] pub account_linking_enabled: Option, + #[cfg_attr(feature = "full", ts(optional))] pub enabled: Option, } @@ -32,15 +35,25 @@ pub struct CreateOAuthProvider { /// Edit an external auth method. pub struct EditOAuthProvider { pub id: OAuthProviderId, + #[cfg_attr(feature = "full", ts(optional))] pub display_name: Option, + #[cfg_attr(feature = "full", ts(optional))] pub authorization_endpoint: Option, + #[cfg_attr(feature = "full", ts(optional))] pub token_endpoint: Option, + #[cfg_attr(feature = "full", ts(optional))] pub userinfo_endpoint: Option, + #[cfg_attr(feature = "full", ts(optional))] pub id_claim: Option, + #[cfg_attr(feature = "full", ts(optional))] pub client_secret: Option, + #[cfg_attr(feature = "full", ts(optional))] pub scopes: Option, + #[cfg_attr(feature = "full", ts(optional))] pub auto_verify_email: Option, + #[cfg_attr(feature = "full", ts(optional))] pub account_linking_enabled: Option, + #[cfg_attr(feature = "full", ts(optional))] pub enabled: Option, } @@ -59,13 +72,14 @@ pub struct DeleteOAuthProvider { /// Logging in with an OAuth 2.0 authorization pub struct AuthenticateWithOauth { pub code: String, - #[cfg_attr(feature = "full", ts(type = "string"))] pub oauth_provider_id: OAuthProviderId, - #[cfg_attr(feature = "full", ts(type = "string"))] pub redirect_uri: Url, + #[cfg_attr(feature = "full", ts(optional))] pub show_nsfw: Option, /// Username is mandatory at registration time + #[cfg_attr(feature = "full", ts(optional))] pub username: Option, /// An answer is mandatory if require application is enabled on the server + #[cfg_attr(feature = "full", ts(optional))] pub answer: Option, } diff --git a/crates/api_common/src/person.rs b/crates/api_common/src/person.rs index 6f1ddfe43..742dc88db 100644 --- a/crates/api_common/src/person.rs +++ b/crates/api_common/src/person.rs @@ -28,6 +28,7 @@ pub struct Login { pub username_or_email: SensitiveString, pub password: SensitiveString, /// May be required, if totp is enabled for their account. + #[cfg_attr(feature = "full", ts(optional))] pub totp_2fa_token: Option, } @@ -40,16 +41,22 @@ pub struct Register { pub username: String, pub password: SensitiveString, pub password_verify: SensitiveString, + #[cfg_attr(feature = "full", ts(optional))] pub show_nsfw: Option, /// email is mandatory if email verification is enabled on the server + #[cfg_attr(feature = "full", ts(optional))] pub email: Option, /// The UUID of the captcha item. + #[cfg_attr(feature = "full", ts(optional))] pub captcha_uuid: Option, /// Your captcha answer. + #[cfg_attr(feature = "full", ts(optional))] pub captcha_answer: Option, /// A form field to trick signup bots. Should be None. + #[cfg_attr(feature = "full", ts(optional))] pub honeypot: Option, /// An answer is mandatory if require application is enabled on the server + #[cfg_attr(feature = "full", ts(optional))] pub answer: Option, } @@ -60,6 +67,7 @@ pub struct Register { /// A wrapper for the captcha response. pub struct GetCaptchaResponse { /// Will be None if captchas are disabled. + #[cfg_attr(feature = "full", ts(optional))] pub ok: Option, } @@ -83,60 +91,92 @@ pub struct CaptchaResponse { /// Saves settings for your user. pub struct SaveUserSettings { /// Show nsfw posts. + #[cfg_attr(feature = "full", ts(optional))] pub show_nsfw: Option, /// Blur nsfw posts. + #[cfg_attr(feature = "full", ts(optional))] pub blur_nsfw: Option, /// Your user's theme. + #[cfg_attr(feature = "full", ts(optional))] pub theme: Option, /// The default post listing type, usually "local" + #[cfg_attr(feature = "full", ts(optional))] pub default_listing_type: Option, /// A post-view mode that changes how multiple post listings look. + #[cfg_attr(feature = "full", ts(optional))] pub post_listing_mode: Option, /// The default post sort, usually "active" + #[cfg_attr(feature = "full", ts(optional))] pub default_post_sort_type: Option, /// The default comment sort, usually "hot" + #[cfg_attr(feature = "full", ts(optional))] pub default_comment_sort_type: Option, /// The language of the lemmy interface + #[cfg_attr(feature = "full", ts(optional))] pub interface_language: Option, /// A URL for your avatar. + #[cfg_attr(feature = "full", ts(optional))] pub avatar: Option, /// A URL for your banner. + #[cfg_attr(feature = "full", ts(optional))] pub banner: Option, /// Your display name, which can contain strange characters, and does not need to be unique. + #[cfg_attr(feature = "full", ts(optional))] pub display_name: Option, /// Your email. + #[cfg_attr(feature = "full", ts(optional))] pub email: Option, /// Your bio / info, in markdown. + #[cfg_attr(feature = "full", ts(optional))] pub bio: Option, /// Your matrix user id. Ex: @my_user:matrix.org + #[cfg_attr(feature = "full", ts(optional))] pub matrix_user_id: Option, /// Whether to show or hide avatars. + #[cfg_attr(feature = "full", ts(optional))] pub show_avatars: Option, /// Sends notifications to your email. + #[cfg_attr(feature = "full", ts(optional))] pub send_notifications_to_email: Option, /// Whether this account is a bot account. Users can hide these accounts easily if they wish. + #[cfg_attr(feature = "full", ts(optional))] pub bot_account: Option, /// Whether to show bot accounts. + #[cfg_attr(feature = "full", ts(optional))] pub show_bot_accounts: Option, /// Whether to show read posts. + #[cfg_attr(feature = "full", ts(optional))] pub show_read_posts: Option, /// A list of languages you are able to see discussion in. + #[cfg_attr(feature = "full", ts(optional))] pub discussion_languages: Option>, /// Open links in a new tab + #[cfg_attr(feature = "full", ts(optional))] pub open_links_in_new_tab: Option, /// Enable infinite scroll + #[cfg_attr(feature = "full", ts(optional))] pub infinite_scroll_enabled: Option, /// Whether to allow keyboard navigation (for browsing and interacting with posts and comments). + #[cfg_attr(feature = "full", ts(optional))] pub enable_keyboard_navigation: Option, /// Whether user avatars or inline images in the UI that are gifs should be allowed to play or /// should be paused + #[cfg_attr(feature = "full", ts(optional))] pub enable_animated_images: Option, + /// Whether a user can send / receive private messages + #[cfg_attr(feature = "full", ts(optional))] + pub enable_private_messages: Option, /// Whether to auto-collapse bot comments. + #[cfg_attr(feature = "full", ts(optional))] pub collapse_bot_comments: Option, /// Some vote display mode settings + #[cfg_attr(feature = "full", ts(optional))] pub show_scores: Option, + #[cfg_attr(feature = "full", ts(optional))] pub show_upvotes: Option, + #[cfg_attr(feature = "full", ts(optional))] pub show_downvotes: Option, + #[cfg_attr(feature = "full", ts(optional))] pub show_upvote_percentage: Option, } @@ -158,6 +198,7 @@ pub struct ChangePassword { pub struct LoginResponse { /// This is None in response to `Register` if email verification is enabled, or the server /// requires registration applications. + #[cfg_attr(feature = "full", ts(optional))] pub jwt: Option, /// If registration applications are required, this will return true for a signup response. pub registration_created: bool, @@ -173,13 +214,20 @@ pub struct LoginResponse { /// /// Either person_id, or username are required. pub struct GetPersonDetails { + #[cfg_attr(feature = "full", ts(optional))] pub person_id: Option, /// Example: dessalines , or dessalines@xyz.tld + #[cfg_attr(feature = "full", ts(optional))] pub username: Option, + #[cfg_attr(feature = "full", ts(optional))] pub sort: Option, + #[cfg_attr(feature = "full", ts(optional))] pub page: Option, + #[cfg_attr(feature = "full", ts(optional))] pub limit: Option, + #[cfg_attr(feature = "full", ts(optional))] pub community_id: Option, + #[cfg_attr(feature = "full", ts(optional))] pub saved_only: Option, } @@ -190,6 +238,7 @@ pub struct GetPersonDetails { /// A person's details response. pub struct GetPersonDetailsResponse { pub person_view: PersonView, + #[cfg_attr(feature = "full", ts(optional))] pub site: Option, pub comments: Vec, pub posts: Vec, @@ -223,11 +272,14 @@ pub struct BanPerson { pub ban: bool, /// Optionally remove or restore all their data. Useful for new troll accounts. /// If ban is true, then this means remove. If ban is false, it means restore. + #[cfg_attr(feature = "full", ts(optional))] pub remove_or_restore_data: Option, + #[cfg_attr(feature = "full", ts(optional))] pub reason: Option, /// A time that the ban will expire, in unix epoch seconds. /// /// An i64 unix timestamp is used for a simpler API client implementation. + #[cfg_attr(feature = "full", ts(optional))] pub expires: Option, } @@ -273,9 +325,13 @@ pub struct BlockPersonResponse { #[cfg_attr(feature = "full", ts(export))] /// Get comment replies. pub struct GetReplies { + #[cfg_attr(feature = "full", ts(optional))] pub sort: Option, + #[cfg_attr(feature = "full", ts(optional))] pub page: Option, + #[cfg_attr(feature = "full", ts(optional))] pub limit: Option, + #[cfg_attr(feature = "full", ts(optional))] pub unread_only: Option, } @@ -294,9 +350,13 @@ pub struct GetRepliesResponse { #[cfg_attr(feature = "full", ts(export))] /// Get mentions for your user. pub struct GetPersonMentions { + #[cfg_attr(feature = "full", ts(optional))] pub sort: Option, + #[cfg_attr(feature = "full", ts(optional))] pub page: Option, + #[cfg_attr(feature = "full", ts(optional))] pub limit: Option, + #[cfg_attr(feature = "full", ts(optional))] pub unread_only: Option, } @@ -375,6 +435,7 @@ pub struct PasswordChangeAfterReset { #[cfg_attr(feature = "full", ts(export))] /// Get a count of the number of reports. pub struct GetReportCount { + #[cfg_attr(feature = "full", ts(optional))] pub community_id: Option, } @@ -384,9 +445,11 @@ pub struct GetReportCount { #[cfg_attr(feature = "full", ts(export))] /// A response for the number of reports. pub struct GetReportCountResponse { + #[cfg_attr(feature = "full", ts(optional))] pub community_id: Option, pub comment_reports: i64, pub post_reports: i64, + #[cfg_attr(feature = "full", ts(optional))] pub private_message_reports: Option, } @@ -436,7 +499,9 @@ pub struct UpdateTotpResponse { #[cfg_attr(feature = "full", ts(export))] /// Get your user's image / media uploads. pub struct ListMedia { + #[cfg_attr(feature = "full", ts(optional))] pub page: Option, + #[cfg_attr(feature = "full", ts(optional))] pub limit: Option, } diff --git a/crates/api_common/src/post.rs b/crates/api_common/src/post.rs index fa45459e2..ca4f53e9d 100644 --- a/crates/api_common/src/post.rs +++ b/crates/api_common/src/post.rs @@ -19,18 +19,26 @@ use ts_rs::TS; pub struct CreatePost { pub name: String, pub community_id: CommunityId, + #[cfg_attr(feature = "full", ts(optional))] pub url: Option, /// An optional body for the post in markdown. + #[cfg_attr(feature = "full", ts(optional))] pub body: Option, /// An optional alt_text, usable for image posts. + #[cfg_attr(feature = "full", ts(optional))] pub alt_text: Option, /// A honeypot to catch bots. Should be None. + #[cfg_attr(feature = "full", ts(optional))] pub honeypot: Option, + #[cfg_attr(feature = "full", ts(optional))] pub nsfw: Option, + #[cfg_attr(feature = "full", ts(optional))] pub language_id: Option, /// Instead of fetching a thumbnail, use a custom one. + #[cfg_attr(feature = "full", ts(optional))] pub custom_thumbnail: Option, /// Time when this post should be scheduled. Null means publish immediately. + #[cfg_attr(feature = "full", ts(optional))] pub scheduled_publish_time: Option, } @@ -45,9 +53,12 @@ pub struct PostResponse { #[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Hash)] #[cfg_attr(feature = "full", derive(TS))] #[cfg_attr(feature = "full", ts(export))] +// TODO this should be made into a tagged enum /// Get a post. Needs either the post id, or comment_id. pub struct GetPost { + #[cfg_attr(feature = "full", ts(optional))] pub id: Option, + #[cfg_attr(feature = "full", ts(optional))] pub comment_id: Option, } @@ -70,21 +81,37 @@ pub struct GetPostResponse { #[cfg_attr(feature = "full", ts(export))] /// Get a list of posts. pub struct GetPosts { + #[cfg_attr(feature = "full", ts(optional))] pub type_: Option, + #[cfg_attr(feature = "full", ts(optional))] pub sort: Option, /// DEPRECATED, use page_cursor + #[cfg_attr(feature = "full", ts(optional))] pub page: Option, + #[cfg_attr(feature = "full", ts(optional))] pub limit: Option, + #[cfg_attr(feature = "full", ts(optional))] pub community_id: Option, + #[cfg_attr(feature = "full", ts(optional))] pub community_name: Option, + #[cfg_attr(feature = "full", ts(optional))] pub saved_only: Option, + #[cfg_attr(feature = "full", ts(optional))] pub liked_only: Option, + #[cfg_attr(feature = "full", ts(optional))] pub disliked_only: Option, + #[cfg_attr(feature = "full", ts(optional))] pub show_hidden: Option, /// If true, then show the read posts (even if your user setting is to hide them) + #[cfg_attr(feature = "full", ts(optional))] pub show_read: Option, /// If true, then show the nsfw posts (even if your user setting is to hide them) + #[cfg_attr(feature = "full", ts(optional))] pub show_nsfw: Option, + #[cfg_attr(feature = "full", ts(optional))] + /// If true, then only show posts with no comments + pub no_comments_only: Option, + #[cfg_attr(feature = "full", ts(optional))] pub page_cursor: Option, } @@ -96,6 +123,7 @@ pub struct GetPosts { pub struct GetPostsResponse { pub posts: Vec, /// the pagination cursor to use to fetch the next page + #[cfg_attr(feature = "full", ts(optional))] pub next_page: Option, } @@ -116,17 +144,25 @@ pub struct CreatePostLike { /// Edit a post. pub struct EditPost { pub post_id: PostId, + #[cfg_attr(feature = "full", ts(optional))] pub name: Option, + #[cfg_attr(feature = "full", ts(optional))] pub url: Option, /// An optional body for the post in markdown. + #[cfg_attr(feature = "full", ts(optional))] pub body: Option, /// An optional alt_text, usable for image posts. + #[cfg_attr(feature = "full", ts(optional))] pub alt_text: Option, + #[cfg_attr(feature = "full", ts(optional))] pub nsfw: Option, + #[cfg_attr(feature = "full", ts(optional))] pub language_id: Option, /// Instead of fetching a thumbnail, use a custom one. + #[cfg_attr(feature = "full", ts(optional))] pub custom_thumbnail: Option, /// Time when this post should be scheduled. Null means publish immediately. + #[cfg_attr(feature = "full", ts(optional))] pub scheduled_publish_time: Option, } @@ -147,6 +183,7 @@ pub struct DeletePost { pub struct RemovePost { pub post_id: PostId, pub removed: bool, + #[cfg_attr(feature = "full", ts(optional))] pub reason: Option, } @@ -230,12 +267,18 @@ pub struct ResolvePostReport { #[cfg_attr(feature = "full", ts(export))] /// List post reports. pub struct ListPostReports { + #[cfg_attr(feature = "full", ts(optional))] pub page: Option, + #[cfg_attr(feature = "full", ts(optional))] pub limit: Option, /// Only shows the unresolved reports + #[cfg_attr(feature = "full", ts(optional))] pub unresolved_only: Option, + // TODO make into tagged enum at some point /// if no community is given, it returns reports for all communities moderated by the auth user + #[cfg_attr(feature = "full", ts(optional))] pub community_id: Option, + #[cfg_attr(feature = "full", ts(optional))] pub post_id: Option, } @@ -271,6 +314,7 @@ pub struct GetSiteMetadataResponse { pub struct LinkMetadata { #[serde(flatten)] pub opengraph_data: OpenGraphData, + #[cfg_attr(feature = "full", ts(optional))] pub content_type: Option, } @@ -280,9 +324,13 @@ pub struct LinkMetadata { #[cfg_attr(feature = "full", ts(export))] /// Site metadata, from its opengraph tags. pub struct OpenGraphData { + #[cfg_attr(feature = "full", ts(optional))] pub title: Option, + #[cfg_attr(feature = "full", ts(optional))] pub description: Option, + #[cfg_attr(feature = "full", ts(optional))] pub(crate) image: Option, + #[cfg_attr(feature = "full", ts(optional))] pub embed_video_url: Option, } @@ -293,7 +341,9 @@ pub struct OpenGraphData { /// List post likes. Admins-only. pub struct ListPostLikes { pub post_id: PostId, + #[cfg_attr(feature = "full", ts(optional))] pub page: Option, + #[cfg_attr(feature = "full", ts(optional))] pub limit: Option, } diff --git a/crates/api_common/src/private_message.rs b/crates/api_common/src/private_message.rs index 429d68643..666fe3865 100644 --- a/crates/api_common/src/private_message.rs +++ b/crates/api_common/src/private_message.rs @@ -47,9 +47,13 @@ pub struct MarkPrivateMessageAsRead { #[cfg_attr(feature = "full", ts(export))] /// Get your private messages. pub struct GetPrivateMessages { + #[cfg_attr(feature = "full", ts(optional))] pub unread_only: Option, + #[cfg_attr(feature = "full", ts(optional))] pub page: Option, + #[cfg_attr(feature = "full", ts(optional))] pub limit: Option, + #[cfg_attr(feature = "full", ts(optional))] pub creator_id: Option, } @@ -102,9 +106,12 @@ pub struct ResolvePrivateMessageReport { /// List private message reports. // TODO , perhaps GetReports should be a tagged enum list too. pub struct ListPrivateMessageReports { + #[cfg_attr(feature = "full", ts(optional))] pub page: Option, + #[cfg_attr(feature = "full", ts(optional))] pub limit: Option, /// Only shows the unresolved reports + #[cfg_attr(feature = "full", ts(optional))] pub unresolved_only: Option, } diff --git a/crates/api_common/src/request.rs b/crates/api_common/src/request.rs index b0da6cf4d..96d64d0e5 100644 --- a/crates/api_common/src/request.rs +++ b/crates/api_common/src/request.rs @@ -175,7 +175,7 @@ pub async fn generate_post_link_metadata( }; let updated_post = Post::update(&mut context.pool(), post.id, &form).await?; if let Some(send_activity) = send_activity(updated_post) { - ActivityChannel::submit_activity(send_activity, &context).await?; + ActivityChannel::submit_activity(send_activity, &context)?; } Ok(()) } diff --git a/crates/api_common/src/send_activity.rs b/crates/api_common/src/send_activity.rs index 465e074f4..b606c9a90 100644 --- a/crates/api_common/src/send_activity.rs +++ b/crates/api_common/src/send_activity.rs @@ -59,6 +59,8 @@ pub enum SendActivityData { score: i16, }, FollowCommunity(Community, Person, bool), + AcceptFollower(CommunityId, PersonId), + RejectFollower(CommunityId, PersonId), UpdateCommunity(Person, Community), DeleteCommunity(Person, Community, bool), RemoveCommunity { @@ -123,10 +125,7 @@ impl ActivityChannel { lock.recv().await } - pub async fn submit_activity( - data: SendActivityData, - _context: &Data, - ) -> LemmyResult<()> { + pub fn submit_activity(data: SendActivityData, _context: &Data) -> LemmyResult<()> { // could do `ACTIVITY_CHANNEL.keepalive_sender.lock()` instead and get rid of weak_sender, // not sure which way is more efficient if let Some(sender) = ACTIVITY_CHANNEL.weak_sender.upgrade() { diff --git a/crates/api_common/src/site.rs b/crates/api_common/src/site.rs index 8316b30ee..40a5cc42d 100644 --- a/crates/api_common/src/site.rs +++ b/crates/api_common/src/site.rs @@ -71,18 +71,31 @@ use ts_rs::TS; /// Searches the site, given a query string, and some optional filters. pub struct Search { pub q: String, + #[cfg_attr(feature = "full", ts(optional))] pub community_id: Option, + #[cfg_attr(feature = "full", ts(optional))] pub community_name: Option, + #[cfg_attr(feature = "full", ts(optional))] pub creator_id: Option, + #[cfg_attr(feature = "full", ts(optional))] pub type_: Option, + #[cfg_attr(feature = "full", ts(optional))] pub sort: Option, + #[cfg_attr(feature = "full", ts(optional))] pub listing_type: Option, + #[cfg_attr(feature = "full", ts(optional))] pub page: Option, + #[cfg_attr(feature = "full", ts(optional))] pub limit: Option, + #[cfg_attr(feature = "full", ts(optional))] pub title_only: Option, + #[cfg_attr(feature = "full", ts(optional))] pub post_url_only: Option, + #[cfg_attr(feature = "full", ts(optional))] pub saved_only: Option, + #[cfg_attr(feature = "full", ts(optional))] pub liked_only: Option, + #[cfg_attr(feature = "full", ts(optional))] pub disliked_only: Option, } @@ -115,9 +128,13 @@ pub struct ResolveObject { // TODO Change this to an enum /// The response of an apub object fetch. pub struct ResolveObjectResponse { + #[cfg_attr(feature = "full", ts(optional))] pub comment: Option, + #[cfg_attr(feature = "full", ts(optional))] pub post: Option, + #[cfg_attr(feature = "full", ts(optional))] pub community: Option, + #[cfg_attr(feature = "full", ts(optional))] pub person: Option, } @@ -127,13 +144,21 @@ pub struct ResolveObjectResponse { #[cfg_attr(feature = "full", ts(export))] /// Fetches the modlog. pub struct GetModlog { + #[cfg_attr(feature = "full", ts(optional))] pub mod_person_id: Option, + #[cfg_attr(feature = "full", ts(optional))] pub community_id: Option, + #[cfg_attr(feature = "full", ts(optional))] pub page: Option, + #[cfg_attr(feature = "full", ts(optional))] pub limit: Option, + #[cfg_attr(feature = "full", ts(optional))] pub type_: Option, + #[cfg_attr(feature = "full", ts(optional))] pub other_person_id: Option, + #[cfg_attr(feature = "full", ts(optional))] pub post_id: Option, + #[cfg_attr(feature = "full", ts(optional))] pub comment_id: Option, } @@ -167,50 +192,95 @@ pub struct GetModlogResponse { /// Creates a site. Should be done after first running lemmy. pub struct CreateSite { pub name: String, + #[cfg_attr(feature = "full", ts(optional))] pub sidebar: Option, + #[cfg_attr(feature = "full", ts(optional))] pub description: Option, + #[cfg_attr(feature = "full", ts(optional))] pub icon: Option, + #[cfg_attr(feature = "full", ts(optional))] pub banner: Option, + #[cfg_attr(feature = "full", ts(optional))] pub enable_nsfw: Option, + #[cfg_attr(feature = "full", ts(optional))] pub community_creation_admin_only: Option, + #[cfg_attr(feature = "full", ts(optional))] pub require_email_verification: Option, + #[cfg_attr(feature = "full", ts(optional))] pub application_question: Option, + #[cfg_attr(feature = "full", ts(optional))] pub private_instance: Option, + #[cfg_attr(feature = "full", ts(optional))] pub default_theme: Option, + #[cfg_attr(feature = "full", ts(optional))] pub default_post_listing_type: Option, + #[cfg_attr(feature = "full", ts(optional))] pub default_post_listing_mode: Option, + #[cfg_attr(feature = "full", ts(optional))] pub default_post_sort_type: Option, + #[cfg_attr(feature = "full", ts(optional))] pub default_comment_sort_type: Option, + #[cfg_attr(feature = "full", ts(optional))] pub legal_information: Option, + #[cfg_attr(feature = "full", ts(optional))] pub application_email_admins: Option, + #[cfg_attr(feature = "full", ts(optional))] pub hide_modlog_mod_names: Option, + #[cfg_attr(feature = "full", ts(optional))] pub discussion_languages: Option>, + #[cfg_attr(feature = "full", ts(optional))] pub slur_filter_regex: Option, + #[cfg_attr(feature = "full", ts(optional))] pub actor_name_max_length: Option, + #[cfg_attr(feature = "full", ts(optional))] pub rate_limit_message: Option, + #[cfg_attr(feature = "full", ts(optional))] pub rate_limit_message_per_second: Option, + #[cfg_attr(feature = "full", ts(optional))] pub rate_limit_post: Option, + #[cfg_attr(feature = "full", ts(optional))] pub rate_limit_post_per_second: Option, + #[cfg_attr(feature = "full", ts(optional))] pub rate_limit_register: Option, + #[cfg_attr(feature = "full", ts(optional))] pub rate_limit_register_per_second: Option, + #[cfg_attr(feature = "full", ts(optional))] pub rate_limit_image: Option, + #[cfg_attr(feature = "full", ts(optional))] pub rate_limit_image_per_second: Option, + #[cfg_attr(feature = "full", ts(optional))] pub rate_limit_comment: Option, + #[cfg_attr(feature = "full", ts(optional))] pub rate_limit_comment_per_second: Option, + #[cfg_attr(feature = "full", ts(optional))] pub rate_limit_search: Option, + #[cfg_attr(feature = "full", ts(optional))] pub rate_limit_search_per_second: Option, + #[cfg_attr(feature = "full", ts(optional))] pub federation_enabled: Option, + #[cfg_attr(feature = "full", ts(optional))] pub federation_debug: Option, + #[cfg_attr(feature = "full", ts(optional))] pub captcha_enabled: Option, + #[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, + #[cfg_attr(feature = "full", ts(optional))] pub content_warning: Option, + #[cfg_attr(feature = "full", ts(optional))] pub post_upvotes: Option, + #[cfg_attr(feature = "full", ts(optional))] pub post_downvotes: Option, + #[cfg_attr(feature = "full", ts(optional))] pub comment_upvotes: Option, + #[cfg_attr(feature = "full", ts(optional))] pub comment_downvotes: Option, } @@ -220,93 +290,142 @@ pub struct CreateSite { #[cfg_attr(feature = "full", ts(export))] /// Edits a site. pub struct EditSite { + #[cfg_attr(feature = "full", ts(optional))] pub name: Option, + /// A sidebar for the site, in markdown. + #[cfg_attr(feature = "full", ts(optional))] pub sidebar: Option, /// A shorter, one line description of your site. + #[cfg_attr(feature = "full", ts(optional))] pub description: Option, /// A url for your site's icon. + #[cfg_attr(feature = "full", ts(optional))] pub icon: Option, /// A url for your site's banner. + #[cfg_attr(feature = "full", ts(optional))] pub banner: Option, /// Whether to enable NSFW. + #[cfg_attr(feature = "full", ts(optional))] pub enable_nsfw: Option, /// Limits community creation to admins only. + #[cfg_attr(feature = "full", ts(optional))] pub community_creation_admin_only: Option, /// Whether to require email verification. + #[cfg_attr(feature = "full", ts(optional))] pub require_email_verification: Option, /// Your application question form. This is in markdown, and can be many questions. + #[cfg_attr(feature = "full", ts(optional))] pub application_question: Option, /// Whether your instance is public, or private. + #[cfg_attr(feature = "full", ts(optional))] pub private_instance: Option, /// The default theme. Usually "browser" + #[cfg_attr(feature = "full", ts(optional))] pub default_theme: Option, /// The default post listing type, usually "local" + #[cfg_attr(feature = "full", ts(optional))] pub default_post_listing_type: Option, /// Default value for listing mode, usually "list" + #[cfg_attr(feature = "full", ts(optional))] pub default_post_listing_mode: Option, /// The default post sort, usually "active" + #[cfg_attr(feature = "full", ts(optional))] pub default_post_sort_type: Option, /// The default comment sort, usually "hot" + #[cfg_attr(feature = "full", ts(optional))] pub default_comment_sort_type: Option, /// An optional page of legal information + #[cfg_attr(feature = "full", ts(optional))] pub legal_information: Option, /// Whether to email admins when receiving a new application. + #[cfg_attr(feature = "full", ts(optional))] pub application_email_admins: Option, /// Whether to hide moderator names from the modlog. + #[cfg_attr(feature = "full", ts(optional))] pub hide_modlog_mod_names: Option, /// A list of allowed discussion languages. + #[cfg_attr(feature = "full", ts(optional))] pub discussion_languages: Option>, /// A regex string of items to filter. + #[cfg_attr(feature = "full", ts(optional))] pub slur_filter_regex: Option, /// The max length of actor names. + #[cfg_attr(feature = "full", ts(optional))] pub actor_name_max_length: Option, /// The number of messages allowed in a given time frame. + #[cfg_attr(feature = "full", ts(optional))] pub rate_limit_message: Option, + #[cfg_attr(feature = "full", ts(optional))] pub rate_limit_message_per_second: Option, /// The number of posts allowed in a given time frame. + #[cfg_attr(feature = "full", ts(optional))] pub rate_limit_post: Option, + #[cfg_attr(feature = "full", ts(optional))] pub rate_limit_post_per_second: Option, /// The number of registrations allowed in a given time frame. + #[cfg_attr(feature = "full", ts(optional))] pub rate_limit_register: Option, + #[cfg_attr(feature = "full", ts(optional))] pub rate_limit_register_per_second: Option, /// The number of image uploads allowed in a given time frame. + #[cfg_attr(feature = "full", ts(optional))] pub rate_limit_image: Option, + #[cfg_attr(feature = "full", ts(optional))] pub rate_limit_image_per_second: Option, /// The number of comments allowed in a given time frame. + #[cfg_attr(feature = "full", ts(optional))] pub rate_limit_comment: Option, + #[cfg_attr(feature = "full", ts(optional))] pub rate_limit_comment_per_second: Option, /// The number of searches allowed in a given time frame. + #[cfg_attr(feature = "full", ts(optional))] pub rate_limit_search: Option, + #[cfg_attr(feature = "full", ts(optional))] pub rate_limit_search_per_second: Option, /// Whether to enable federation. + #[cfg_attr(feature = "full", ts(optional))] pub federation_enabled: Option, /// Enables federation debugging. + #[cfg_attr(feature = "full", ts(optional))] pub federation_debug: Option, /// Whether to enable captchas for signups. + #[cfg_attr(feature = "full", ts(optional))] pub captcha_enabled: Option, /// 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>, + #[cfg_attr(feature = "full", ts(optional))] pub registration_mode: Option, /// Whether to email admins for new reports. + #[cfg_attr(feature = "full", ts(optional))] pub reports_email_admins: Option, /// If present, nsfw content is visible by default. Should be displayed by frontends/clients /// when the site is first opened by a user. + #[cfg_attr(feature = "full", ts(optional))] pub content_warning: Option, /// Whether or not external auth methods can auto-register users. + #[cfg_attr(feature = "full", ts(optional))] pub oauth_registration: Option, /// What kind of post upvotes your site allows. + #[cfg_attr(feature = "full", ts(optional))] pub post_upvotes: Option, /// What kind of post downvotes your site allows. + #[cfg_attr(feature = "full", ts(optional))] pub post_downvotes: Option, /// What kind of comment upvotes your site allows. + #[cfg_attr(feature = "full", ts(optional))] pub comment_upvotes: Option, /// What kind of comment downvotes your site allows. + #[cfg_attr(feature = "full", ts(optional))] pub comment_downvotes: Option, } @@ -329,6 +448,7 @@ pub struct GetSiteResponse { pub site_view: SiteView, pub admins: Vec, pub version: String, + #[cfg_attr(feature = "full", ts(optional))] pub my_user: Option, pub all_languages: Vec, pub discussion_languages: Vec, @@ -337,9 +457,12 @@ pub struct GetSiteResponse { /// 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, /// A list of external auth methods your site supports. + #[cfg_attr(feature = "full", ts(optional))] pub oauth_providers: Option>, + #[cfg_attr(feature = "full", ts(optional))] pub admin_oauth_providers: Option>, pub blocked_urls: Vec, } @@ -351,6 +474,7 @@ pub struct GetSiteResponse { /// A response of federated instances. pub struct GetFederatedInstancesResponse { /// Optional, because federation may be disabled. + #[cfg_attr(feature = "full", ts(optional))] pub federated_instances: Option, } @@ -386,6 +510,7 @@ pub struct ReadableFederationState { #[serde(flatten)] internal_state: FederationQueueState, /// timestamp of the next retry attempt (null if fail count is 0) + #[cfg_attr(feature = "full", ts(optional))] next_retry: Option>, } @@ -410,6 +535,7 @@ pub struct InstanceWithFederationState { pub instance: Instance, /// if federation to this instance is or was active, show state of outgoing federation to this /// instance + #[cfg_attr(feature = "full", ts(optional))] pub federation_state: Option, } @@ -420,6 +546,7 @@ pub struct InstanceWithFederationState { /// Purges a person from the database. This will delete all content attached to that person. pub struct PurgePerson { pub person_id: PersonId, + #[cfg_attr(feature = "full", ts(optional))] pub reason: Option, } @@ -430,6 +557,7 @@ pub struct PurgePerson { /// Purges a community from the database. This will delete all content attached to that community. pub struct PurgeCommunity { pub community_id: CommunityId, + #[cfg_attr(feature = "full", ts(optional))] pub reason: Option, } @@ -440,6 +568,7 @@ pub struct PurgeCommunity { /// Purges a post from the database. This will delete all content attached to that post. pub struct PurgePost { pub post_id: PostId, + #[cfg_attr(feature = "full", ts(optional))] pub reason: Option, } @@ -450,6 +579,7 @@ pub struct PurgePost { /// Purges a comment from the database. This will delete all content attached to that comment. pub struct PurgeComment { pub comment_id: CommentId, + #[cfg_attr(feature = "full", ts(optional))] pub reason: Option, } @@ -460,8 +590,11 @@ pub struct PurgeComment { /// Fetches a list of registration applications. pub struct ListRegistrationApplications { /// Only shows the unread applications (IE those without an admin actor) + #[cfg_attr(feature = "full", ts(optional))] pub unread_only: Option, + #[cfg_attr(feature = "full", ts(optional))] pub page: Option, + #[cfg_attr(feature = "full", ts(optional))] pub limit: Option, } @@ -490,6 +623,7 @@ pub struct GetRegistrationApplication { pub struct ApproveRegistrationApplication { pub id: RegistrationApplicationId, pub approve: bool, + #[cfg_attr(feature = "full", ts(optional))] pub deny_reason: Option, } diff --git a/crates/api_common/src/tagline.rs b/crates/api_common/src/tagline.rs index 3090a2678..528d37947 100644 --- a/crates/api_common/src/tagline.rs +++ b/crates/api_common/src/tagline.rs @@ -50,6 +50,8 @@ pub struct ListTaglinesResponse { #[cfg_attr(feature = "full", ts(export))] /// Fetches a list of taglines. pub struct ListTaglines { + #[cfg_attr(feature = "full", ts(optional))] pub page: Option, + #[cfg_attr(feature = "full", ts(optional))] pub limit: Option, } diff --git a/crates/api_common/src/utils.rs b/crates/api_common/src/utils.rs index e255e36c2..f2c03509d 100644 --- a/crates/api_common/src/utils.rs +++ b/crates/api_common/src/utils.rs @@ -42,6 +42,7 @@ use lemmy_db_views::{ structs::{LocalImageView, LocalUserView, SiteView}, }; use lemmy_db_views_actor::structs::{ + CommunityFollowerView, CommunityModeratorView, CommunityPersonBanView, CommunityView, @@ -50,7 +51,10 @@ use lemmy_utils::{ email::{send_email, translations::Lang}, error::{LemmyError, LemmyErrorExt, LemmyErrorType, LemmyResult}, rate_limit::{ActionType, BucketConfig}, - settings::structs::{PictrsImageMode, Settings}, + settings::{ + structs::{PictrsImageMode, Settings}, + SETTINGS, + }, utils::{ markdown::{image_links::markdown_rewrite_image_links, markdown_check_for_blocked_urls}, slurs::{build_slur_regex, remove_slurs}, @@ -162,7 +166,6 @@ pub async fn update_read_comments( person_id, post_id, read_comments, - ..PersonPostAggregatesForm::default() }; PersonPostAggregates::upsert(pool, &person_post_agg_form).await?; @@ -213,7 +216,9 @@ pub async fn check_registration_application( let local_user_id = local_user_view.local_user.id; let registration = RegistrationApplication::find_by_local_user_id(pool, local_user_id).await?; if registration.admin_id.is_some() { - Err(LemmyErrorType::RegistrationDenied(registration.deny_reason))? + Err(LemmyErrorType::RegistrationDenied { + reason: registration.deny_reason, + })? } else { Err(LemmyErrorType::RegistrationApplicationIsPending)? } @@ -227,20 +232,17 @@ pub async fn check_registration_application( /// the user isn't banned. pub async fn check_community_user_action( person: &Person, - community_id: CommunityId, + community: &Community, pool: &mut DbPool<'_>, ) -> LemmyResult<()> { check_user_valid(person)?; - check_community_deleted_removed(community_id, pool).await?; - CommunityPersonBanView::check(pool, person.id, community_id).await?; + check_community_deleted_removed(community)?; + CommunityPersonBanView::check(pool, person.id, community.id).await?; + CommunityFollowerView::check_private_community_action(pool, person.id, community).await?; Ok(()) } -async fn check_community_deleted_removed( - community_id: CommunityId, - pool: &mut DbPool<'_>, -) -> LemmyResult<()> { - let community = Community::read(pool, community_id).await?; +pub fn check_community_deleted_removed(community: &Community) -> LemmyResult<()> { if community.deleted || community.removed { Err(LemmyErrorType::Deleted)? } @@ -253,16 +255,16 @@ async fn check_community_deleted_removed( /// removed/deleted. pub async fn check_community_mod_action( person: &Person, - community_id: CommunityId, + community: &Community, allow_deleted: bool, pool: &mut DbPool<'_>, ) -> LemmyResult<()> { - is_mod_or_admin(pool, person, community_id).await?; - CommunityPersonBanView::check(pool, person.id, community_id).await?; + is_mod_or_admin(pool, person, community.id).await?; + CommunityPersonBanView::check(pool, person.id, community.id).await?; // it must be possible to restore deleted community if !allow_deleted { - check_community_deleted_removed(community_id, pool).await?; + check_community_deleted_removed(community)?; } Ok(()) } @@ -352,6 +354,16 @@ pub fn check_private_instance( } } +/// If private messages are disabled, dont allow them to be sent / received +#[tracing::instrument(skip_all)] +pub fn check_private_messages_enabled(local_user_view: &LocalUserView) -> Result<(), LemmyError> { + if !local_user_view.local_user.enable_private_messages { + Err(LemmyErrorType::CouldntCreatePrivateMessage)? + } else { + Ok(()) + } +} + #[tracing::instrument(skip_all)] pub async fn build_federated_instances( local_site: &LocalSite, @@ -973,12 +985,8 @@ pub fn generate_followers_url(actor_id: &DbUrl) -> Result { Ok(Url::parse(&format!("{actor_id}/followers"))?.into()) } -pub fn generate_inbox_url(actor_id: &DbUrl) -> Result { - Ok(Url::parse(&format!("{actor_id}/inbox"))?.into()) -} - -pub fn generate_shared_inbox_url(settings: &Settings) -> LemmyResult { - let url = format!("{}/inbox", settings.get_protocol_and_hostname()); +pub fn generate_inbox_url() -> LemmyResult { + let url = format!("{}/inbox", SETTINGS.get_protocol_and_hostname()); Ok(Url::parse(&url)?.into()) } @@ -1111,7 +1119,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 /// if image_proxy setting is enabled. -pub(crate) async fn proxy_image_link(link: Url, context: &LemmyContext) -> LemmyResult { +pub async fn proxy_image_link(link: Url, context: &LemmyContext) -> LemmyResult { proxy_image_link_internal( link, context.settings().pictrs_config()?.image_mode(), diff --git a/crates/api_crud/Cargo.toml b/crates/api_crud/Cargo.toml index 2793beac3..723864705 100644 --- a/crates/api_crud/Cargo.toml +++ b/crates/api_crud/Cargo.toml @@ -34,5 +34,5 @@ serde_json = { workspace = true } serde = { workspace = true } serde_with = { workspace = true } -[package.metadata.cargo-machete] +[package.metadata.cargo-shear] ignored = ["futures"] diff --git a/crates/api_crud/src/comment/create.rs b/crates/api_crud/src/comment/create.rs index 713292314..edcf7db30 100644 --- a/crates/api_crud/src/comment/create.rs +++ b/crates/api_crud/src/comment/create.rs @@ -60,7 +60,12 @@ pub async fn create_comment( let post = post_view.post; let community_id = post_view.community.id; - check_community_user_action(&local_user_view.person, community_id, &mut context.pool()).await?; + check_community_user_action( + &local_user_view.person, + &post_view.community, + &mut context.pool(), + ) + .await?; check_post_deleted_or_removed(&post)?; // Check if post is locked, no new comments @@ -123,7 +128,6 @@ pub async fn create_comment( // You like your own comment by default let like_form = CommentLikeForm { comment_id: inserted_comment.id, - post_id: post.id, person_id: local_user_view.person.id, score: 1, }; @@ -135,8 +139,7 @@ pub async fn create_comment( ActivityChannel::submit_activity( SendActivityData::CreateComment(inserted_comment.clone()), &context, - ) - .await?; + )?; // Update the read comments, so your own new comment doesn't appear as a +1 unread update_read_comments( diff --git a/crates/api_crud/src/comment/delete.rs b/crates/api_crud/src/comment/delete.rs index 2b5f35827..60a22a2ef 100644 --- a/crates/api_crud/src/comment/delete.rs +++ b/crates/api_crud/src/comment/delete.rs @@ -35,7 +35,7 @@ pub async fn delete_comment( check_community_user_action( &local_user_view.person, - orig_comment.community.id, + &orig_comment.community, &mut context.pool(), ) .await?; @@ -76,8 +76,7 @@ pub async fn delete_comment( orig_comment.community, ), &context, - ) - .await?; + )?; Ok(Json( build_comment_response( diff --git a/crates/api_crud/src/comment/remove.rs b/crates/api_crud/src/comment/remove.rs index 3c137a984..4e8a1871a 100644 --- a/crates/api_crud/src/comment/remove.rs +++ b/crates/api_crud/src/comment/remove.rs @@ -35,7 +35,7 @@ pub async fn remove_comment( check_community_mod_action( &local_user_view.person, - orig_comment.community.id, + &orig_comment.community, false, &mut context.pool(), ) @@ -99,8 +99,7 @@ pub async fn remove_comment( reason: data.reason.clone(), }, &context, - ) - .await?; + )?; Ok(Json( build_comment_response( diff --git a/crates/api_crud/src/comment/update.rs b/crates/api_crud/src/comment/update.rs index 0afe3390d..85ee9bff0 100644 --- a/crates/api_crud/src/comment/update.rs +++ b/crates/api_crud/src/comment/update.rs @@ -45,7 +45,7 @@ pub async fn update_comment( check_community_user_action( &local_user_view.person, - orig_comment.community.id, + &orig_comment.community, &mut context.pool(), ) .await?; @@ -97,8 +97,7 @@ pub async fn update_comment( ActivityChannel::submit_activity( SendActivityData::UpdateComment(updated_comment.clone()), &context, - ) - .await?; + )?; Ok(Json( build_comment_response( diff --git a/crates/api_crud/src/community/create.rs b/crates/api_crud/src/community/create.rs index 4e3e356e0..c81157950 100644 --- a/crates/api_crud/src/community/create.rs +++ b/crates/api_crud/src/community/create.rs @@ -1,3 +1,4 @@ +use super::check_community_visibility_allowed; use activitypub_federation::{config::Data, http_signatures::generate_actor_keypair}; use actix_web::web::Json; use lemmy_api_common::{ @@ -8,7 +9,6 @@ use lemmy_api_common::{ generate_followers_url, generate_inbox_url, generate_local_apub_endpoint, - generate_shared_inbox_url, get_url_blocklist, is_admin, local_site_to_slur_regex, @@ -24,6 +24,7 @@ use lemmy_db_schema::{ Community, CommunityFollower, CommunityFollowerForm, + CommunityFollowerState, CommunityInsertForm, CommunityModerator, CommunityModeratorForm, @@ -37,7 +38,11 @@ use lemmy_utils::{ error::{LemmyErrorExt, LemmyErrorType, LemmyResult}, utils::{ slurs::check_slurs, - validation::{is_valid_actor_name, is_valid_body_field}, + validation::{ + is_valid_actor_name, + is_valid_body_field, + site_or_community_description_length_check, + }, }, }; @@ -58,8 +63,18 @@ pub async fn create_community( let url_blocklist = get_url_blocklist(&context).await?; check_slurs(&data.name, &slur_regex)?; check_slurs(&data.title, &slur_regex)?; - let description = - process_markdown_opt(&data.description, &slur_regex, &url_blocklist, &context).await?; + let sidebar = process_markdown_opt(&data.sidebar, &slur_regex, &url_blocklist, &context).await?; + + // Ensure that the sidebar has fewer than the max num characters... + if let Some(sidebar) = &sidebar { + is_valid_body_field(sidebar, false)?; + } + + let description = data.description.clone(); + if let Some(desc) = &description { + site_or_community_description_length_check(desc)?; + check_slurs(desc, &slur_regex)?; + } let icon = diesel_url_create(data.icon.as_deref())?; let icon = proxy_image_link_api(icon, &context).await?; @@ -73,6 +88,8 @@ pub async fn create_community( is_valid_body_field(desc, false)?; } + check_community_visibility_allowed(data.visibility, &local_user_view)?; + // Double check for duplicate community actor_ids let community_actor_id = generate_local_apub_endpoint( EndpointType::Community, @@ -89,6 +106,7 @@ pub async fn create_community( let keypair = generate_actor_keypair()?; let community_form = CommunityInsertForm { + sidebar, description, icon, banner, @@ -96,8 +114,7 @@ pub async fn create_community( actor_id: Some(community_actor_id.clone()), private_key: Some(keypair.private_key), followers_url: Some(generate_followers_url(&community_actor_id)?), - inbox_url: Some(generate_inbox_url(&community_actor_id)?), - shared_inbox_url: Some(generate_shared_inbox_url(context.settings())?), + inbox_url: Some(generate_inbox_url()?), posting_restricted_to_mods: data.posting_restricted_to_mods, visibility: data.visibility, ..CommunityInsertForm::new( @@ -126,7 +143,8 @@ pub async fn create_community( let community_follower_form = CommunityFollowerForm { community_id: inserted_community.id, person_id: local_user_view.person.id, - pending: false, + state: Some(CommunityFollowerState::Accepted), + approver_id: None, }; CommunityFollower::follow(&mut context.pool(), &community_follower_form) diff --git a/crates/api_crud/src/community/delete.rs b/crates/api_crud/src/community/delete.rs index a2ceaff50..7f9d04933 100644 --- a/crates/api_crud/src/community/delete.rs +++ b/crates/api_crud/src/community/delete.rs @@ -22,13 +22,13 @@ pub async fn delete_community( local_user_view: LocalUserView, ) -> LemmyResult> { // Fetch the community mods - let community_id = data.community_id; let community_mods = - CommunityModeratorView::for_community(&mut context.pool(), community_id).await?; + CommunityModeratorView::for_community(&mut context.pool(), data.community_id).await?; + let community = Community::read(&mut context.pool(), data.community_id).await?; check_community_mod_action( &local_user_view.person, - community_id, + &community, true, &mut context.pool(), ) @@ -54,8 +54,7 @@ pub async fn delete_community( ActivityChannel::submit_activity( SendActivityData::DeleteCommunity(local_user_view.person.clone(), community, data.deleted), &context, - ) - .await?; + )?; build_community_response(&context, local_user_view, community_id).await } diff --git a/crates/api_crud/src/community/mod.rs b/crates/api_crud/src/community/mod.rs index 4bd028482..0c9a507f1 100644 --- a/crates/api_crud/src/community/mod.rs +++ b/crates/api_crud/src/community/mod.rs @@ -1,5 +1,22 @@ +use lemmy_api_common::utils::is_admin; +use lemmy_db_schema::CommunityVisibility; +use lemmy_db_views::structs::LocalUserView; +use lemmy_utils::error::LemmyResult; + pub mod create; pub mod delete; pub mod list; pub mod remove; pub mod update; + +/// For now only admins can make communities private, in order to prevent abuse. +/// Need to implement admin approval for new communities to get rid of this. +fn check_community_visibility_allowed( + visibility: Option, + local_user_view: &LocalUserView, +) -> LemmyResult<()> { + if visibility == Some(lemmy_db_schema::CommunityVisibility::Private) { + is_admin(local_user_view)?; + } + Ok(()) +} diff --git a/crates/api_crud/src/community/remove.rs b/crates/api_crud/src/community/remove.rs index f4271565d..c506bde1b 100644 --- a/crates/api_crud/src/community/remove.rs +++ b/crates/api_crud/src/community/remove.rs @@ -23,9 +23,10 @@ pub async fn remove_community( context: Data, local_user_view: LocalUserView, ) -> LemmyResult> { + let community = Community::read(&mut context.pool(), data.community_id).await?; check_community_mod_action( &local_user_view.person, - data.community_id, + &community, true, &mut context.pool(), ) @@ -65,8 +66,7 @@ pub async fn remove_community( removed: data.removed, }, &context, - ) - .await?; + )?; build_community_response(&context, local_user_view, community_id).await } diff --git a/crates/api_crud/src/community/update.rs b/crates/api_crud/src/community/update.rs index 798f4f4c1..3dca7d892 100644 --- a/crates/api_crud/src/community/update.rs +++ b/crates/api_crud/src/community/update.rs @@ -1,3 +1,4 @@ +use super::check_community_visibility_allowed; use activitypub_federation::config::Data; use actix_web::web::Json; use lemmy_api_common::{ @@ -41,16 +42,19 @@ pub async fn update_community( let url_blocklist = get_url_blocklist(&context).await?; check_slurs_opt(&data.title, &slur_regex)?; - let description = diesel_string_update( - process_markdown_opt(&data.description, &slur_regex, &url_blocklist, &context) + let sidebar = diesel_string_update( + process_markdown_opt(&data.sidebar, &slur_regex, &url_blocklist, &context) .await? .as_deref(), ); - if let Some(Some(desc)) = &description { - is_valid_body_field(desc, false)?; + if let Some(Some(sidebar)) = &sidebar { + is_valid_body_field(sidebar, false)?; } + check_community_visibility_allowed(data.visibility, &local_user_view)?; + let description = diesel_string_update(data.description.as_deref()); + let old_community = Community::read(&mut context.pool(), data.community_id).await?; let icon = diesel_url_update(data.icon.as_deref())?; @@ -64,7 +68,7 @@ pub async fn update_community( // Verify its a mod (only mods can edit it) check_community_mod_action( &local_user_view.person, - data.community_id, + &old_community, false, &mut context.pool(), ) @@ -84,6 +88,7 @@ pub async fn update_community( let community_form = CommunityUpdateForm { title: data.title.clone(), + sidebar, description, icon, banner, @@ -102,8 +107,7 @@ pub async fn update_community( ActivityChannel::submit_activity( SendActivityData::UpdateCommunity(local_user_view.person.clone(), community), &context, - ) - .await?; + )?; build_community_response(&context, local_user_view, community_id).await } diff --git a/crates/api_crud/src/post/create.rs b/crates/api_crud/src/post/create.rs index 1ca6508be..5dee5e21c 100644 --- a/crates/api_crud/src/post/create.rs +++ b/crates/api_crud/src/post/create.rs @@ -84,15 +84,9 @@ pub async fn create_post( is_valid_body_field(body, true)?; } - check_community_user_action( - &local_user_view.person, - data.community_id, - &mut context.pool(), - ) - .await?; + let community = Community::read(&mut context.pool(), data.community_id).await?; + check_community_user_action(&local_user_view.person, &community, &mut context.pool()).await?; - let community_id = data.community_id; - let community = Community::read(&mut context.pool(), community_id).await?; if community.posting_restricted_to_mods { let community_id = data.community_id; CommunityModeratorView::check_is_community_moderator( @@ -106,7 +100,7 @@ pub async fn create_post( let language_id = validate_post_language( &mut context.pool(), data.language_id, - community_id, + data.community_id, local_user_view.local_user.id, ) .await?; @@ -131,6 +125,7 @@ pub async fn create_post( .await .with_lemmy_type(LemmyErrorType::CouldntCreatePost)?; + let community_id = community.id; let federate_post = if scheduled_publish_time.is_none() { send_webmention(inserted_post.clone(), community); |post| Some(SendActivityData::CreatePost(post)) diff --git a/crates/api_crud/src/post/delete.rs b/crates/api_crud/src/post/delete.rs index be31759d5..e54086911 100644 --- a/crates/api_crud/src/post/delete.rs +++ b/crates/api_crud/src/post/delete.rs @@ -8,7 +8,10 @@ use lemmy_api_common::{ utils::check_community_user_action, }; use lemmy_db_schema::{ - source::post::{Post, PostUpdateForm}, + source::{ + community::Community, + post::{Post, PostUpdateForm}, + }, traits::Crud, }; use lemmy_db_views::structs::LocalUserView; @@ -28,12 +31,8 @@ pub async fn delete_post( Err(LemmyErrorType::CouldntUpdatePost)? } - check_community_user_action( - &local_user_view.person, - orig_post.community_id, - &mut context.pool(), - ) - .await?; + let community = Community::read(&mut context.pool(), orig_post.community_id).await?; + check_community_user_action(&local_user_view.person, &community, &mut context.pool()).await?; // Verify that only the creator can delete if !Post::is_post_creator(local_user_view.person.id, orig_post.creator_id) { @@ -54,8 +53,7 @@ pub async fn delete_post( ActivityChannel::submit_activity( SendActivityData::DeletePost(post, local_user_view.person.clone(), data.0), &context, - ) - .await?; + )?; build_post_response( &context, diff --git a/crates/api_crud/src/post/mod.rs b/crates/api_crud/src/post/mod.rs index 95df9663c..5db0ad5d0 100644 --- a/crates/api_crud/src/post/mod.rs +++ b/crates/api_crud/src/post/mod.rs @@ -2,7 +2,7 @@ use chrono::{DateTime, TimeZone, Utc}; use lemmy_api_common::context::LemmyContext; use lemmy_db_schema::source::post::Post; use lemmy_db_views::structs::LocalUserView; -use lemmy_utils::{error::LemmyResult, LemmyErrorType}; +use lemmy_utils::error::{LemmyErrorType, LemmyResult}; pub mod create; pub mod delete; diff --git a/crates/api_crud/src/post/remove.rs b/crates/api_crud/src/post/remove.rs index c53a4552c..7e3261e6f 100644 --- a/crates/api_crud/src/post/remove.rs +++ b/crates/api_crud/src/post/remove.rs @@ -9,6 +9,7 @@ use lemmy_api_common::{ }; use lemmy_db_schema::{ source::{ + community::Community, local_user::LocalUser, moderator::{ModRemovePost, ModRemovePostForm}, post::{Post, PostUpdateForm}, @@ -26,11 +27,16 @@ pub async fn remove_post( local_user_view: LocalUserView, ) -> LemmyResult> { let post_id = data.post_id; + + // We cannot use PostView to avoid a database read here, as it doesn't return removed items + // by default. So we would have to pass in `is_mod_or_admin`, but that is impossible without + // knowing which community the post belongs to. let orig_post = Post::read(&mut context.pool(), post_id).await?; + let community = Community::read(&mut context.pool(), orig_post.community_id).await?; check_community_mod_action( &local_user_view.person, - orig_post.community_id, + &community, false, &mut context.pool(), ) @@ -77,8 +83,7 @@ pub async fn remove_post( removed: data.removed, }, &context, - ) - .await?; + )?; build_post_response(&context, orig_post.community_id, local_user_view, post_id).await } diff --git a/crates/api_crud/src/post/update.rs b/crates/api_crud/src/post/update.rs index 645774b53..217a6cc24 100644 --- a/crates/api_crud/src/post/update.rs +++ b/crates/api_crud/src/post/update.rs @@ -24,7 +24,7 @@ use lemmy_db_schema::{ traits::Crud, utils::{diesel_string_update, diesel_url_update, naive_now}, }; -use lemmy_db_views::structs::LocalUserView; +use lemmy_db_views::structs::{LocalUserView, PostView}; use lemmy_utils::{ error::{LemmyErrorExt, LemmyErrorType, LemmyResult}, utils::{ @@ -87,31 +87,31 @@ pub async fn update_post( } let post_id = data.post_id; - let orig_post = Post::read(&mut context.pool(), post_id).await?; + let orig_post = PostView::read(&mut context.pool(), post_id, None, false).await?; check_community_user_action( &local_user_view.person, - orig_post.community_id, + &orig_post.community, &mut context.pool(), ) .await?; // Verify that only the creator can edit - if !Post::is_post_creator(local_user_view.person.id, orig_post.creator_id) { + if !Post::is_post_creator(local_user_view.person.id, orig_post.post.creator_id) { Err(LemmyErrorType::NoPostEditAllowed)? } let language_id = validate_post_language( &mut context.pool(), data.language_id, - orig_post.community_id, + orig_post.post.community_id, local_user_view.local_user.id, ) .await?; // handle changes to scheduled_publish_time let scheduled_publish_time = match ( - orig_post.scheduled_publish_time, + orig_post.post.scheduled_publish_time, data.scheduled_publish_time, ) { // schedule time can be changed if post is still scheduled (and not published yet) @@ -143,12 +143,12 @@ pub async fn update_post( // send out federation/webmention if necessary match ( - orig_post.scheduled_publish_time, + orig_post.post.scheduled_publish_time, data.scheduled_publish_time, ) { // schedule was removed, send create activity and webmention (Some(_), None) => { - let community = Community::read(&mut context.pool(), orig_post.community_id).await?; + let community = Community::read(&mut context.pool(), orig_post.community.id).await?; send_webmention(updated_post.clone(), community); generate_post_link_metadata( updated_post.clone(), @@ -174,7 +174,7 @@ pub async fn update_post( build_post_response( context.deref(), - orig_post.community_id, + orig_post.community.id, local_user_view, post_id, ) diff --git a/crates/api_crud/src/private_message/create.rs b/crates/api_crud/src/private_message/create.rs index 2a49e4ac0..1a6a78d00 100644 --- a/crates/api_crud/src/private_message/create.rs +++ b/crates/api_crud/src/private_message/create.rs @@ -5,6 +5,7 @@ use lemmy_api_common::{ private_message::{CreatePrivateMessage, PrivateMessageResponse}, send_activity::{ActivityChannel, SendActivityData}, utils::{ + check_private_messages_enabled, get_interface_language, get_url_blocklist, local_site_to_slur_regex, @@ -46,6 +47,16 @@ pub async fn create_private_message( ) .await?; + check_private_messages_enabled(&local_user_view)?; + + // Don't allow local sends to people who have private messages disabled + let recipient_local_user_opt = LocalUserView::read_person(&mut context.pool(), data.recipient_id) + .await + .ok(); + if let Some(recipient_local_user) = recipient_local_user_opt { + check_private_messages_enabled(&recipient_local_user)?; + } + let private_message_form = PrivateMessageInsertForm::new( local_user_view.person.id, data.recipient_id, @@ -78,8 +89,7 @@ pub async fn create_private_message( ActivityChannel::submit_activity( SendActivityData::CreatePrivateMessage(view.clone()), &context, - ) - .await?; + )?; Ok(Json(PrivateMessageResponse { private_message_view: view, diff --git a/crates/api_crud/src/private_message/delete.rs b/crates/api_crud/src/private_message/delete.rs index 936ff57b8..30efc020c 100644 --- a/crates/api_crud/src/private_message/delete.rs +++ b/crates/api_crud/src/private_message/delete.rs @@ -42,8 +42,7 @@ pub async fn delete_private_message( ActivityChannel::submit_activity( SendActivityData::DeletePrivateMessage(local_user_view.person, private_message, data.deleted), &context, - ) - .await?; + )?; let view = PrivateMessageView::read(&mut context.pool(), private_message_id).await?; Ok(Json(PrivateMessageResponse { diff --git a/crates/api_crud/src/private_message/update.rs b/crates/api_crud/src/private_message/update.rs index 20eaadb36..aa562c626 100644 --- a/crates/api_crud/src/private_message/update.rs +++ b/crates/api_crud/src/private_message/update.rs @@ -59,8 +59,7 @@ pub async fn update_private_message( ActivityChannel::submit_activity( SendActivityData::UpdatePrivateMessage(view.clone()), &context, - ) - .await?; + )?; Ok(Json(PrivateMessageResponse { private_message_view: view, diff --git a/crates/api_crud/src/site/create.rs b/crates/api_crud/src/site/create.rs index 07efa72b9..e1ea1d992 100644 --- a/crates/api_crud/src/site/create.rs +++ b/crates/api_crud/src/site/create.rs @@ -6,7 +6,7 @@ use lemmy_api_common::{ context::LemmyContext, site::{CreateSite, SiteResponse}, utils::{ - generate_shared_inbox_url, + generate_inbox_url, get_url_blocklist, is_admin, local_site_rate_limit_to_rate_limit_config, @@ -29,13 +29,13 @@ use lemmy_db_views::structs::{LocalUserView, SiteView}; use lemmy_utils::{ error::{LemmyErrorType, LemmyResult}, utils::{ - slurs::{check_slurs, check_slurs_opt}, + slurs::check_slurs, validation::{ build_and_check_regex, check_site_visibility_valid, is_valid_body_field, - site_description_length_check, site_name_length_check, + site_or_community_description_length_check, }, }, }; @@ -55,7 +55,7 @@ pub async fn create_site( validate_create_payload(&local_site, &data)?; let actor_id: DbUrl = Url::parse(&context.settings().get_protocol_and_hostname())?.into(); - let inbox_url = Some(generate_shared_inbox_url(context.settings())?); + let inbox_url = Some(generate_inbox_url()?); let keypair = generate_actor_keypair()?; let slur_regex = local_site_to_slur_regex(&local_site); @@ -167,8 +167,8 @@ fn validate_create_payload(local_site: &LocalSite, create_site: &CreateSite) -> check_slurs(&create_site.name, &slur_regex)?; if let Some(desc) = &create_site.description { - site_description_length_check(desc)?; - check_slurs_opt(&create_site.description, &slur_regex)?; + site_or_community_description_length_check(desc)?; + check_slurs(desc, &slur_regex)?; } site_default_post_listing_type_check(&create_site.default_post_listing_type)?; diff --git a/crates/api_crud/src/site/update.rs b/crates/api_crud/src/site/update.rs index cce428cc1..085ed69d1 100644 --- a/crates/api_crud/src/site/update.rs +++ b/crates/api_crud/src/site/update.rs @@ -40,8 +40,8 @@ use lemmy_utils::{ check_site_visibility_valid, check_urls_are_valid, is_valid_body_field, - site_description_length_check, site_name_length_check, + site_or_community_description_length_check, }, }, }; @@ -219,7 +219,7 @@ fn validate_update_payload(local_site: &LocalSite, edit_site: &EditSite) -> Lemm } if let Some(desc) = &edit_site.description { - site_description_length_check(desc)?; + site_or_community_description_length_check(desc)?; check_slurs_opt(&edit_site.description, &slur_regex)?; } diff --git a/crates/api_crud/src/user/create.rs b/crates/api_crud/src/user/create.rs index bf17d6f8e..ed560e3d6 100644 --- a/crates/api_crud/src/user/create.rs +++ b/crates/api_crud/src/user/create.rs @@ -11,7 +11,6 @@ use lemmy_api_common::{ check_user_valid, generate_inbox_url, generate_local_apub_endpoint, - generate_shared_inbox_url, honeypot_check, local_site_to_slur_regex, password_length_check, @@ -418,8 +417,7 @@ async fn create_person( // Register the new person let person_form = PersonInsertForm { actor_id: Some(actor_id.clone()), - inbox_url: Some(generate_inbox_url(&actor_id)?), - shared_inbox_url: Some(generate_shared_inbox_url(context.settings())?), + inbox_url: Some(generate_inbox_url()?), private_key: Some(actor_keypair.private_key), ..PersonInsertForm::new(username.clone(), actor_keypair.public_key, instance_id) }; diff --git a/crates/api_crud/src/user/delete.rs b/crates/api_crud/src/user/delete.rs index d1825425c..39598265a 100644 --- a/crates/api_crud/src/user/delete.rs +++ b/crates/api_crud/src/user/delete.rs @@ -45,8 +45,7 @@ pub async fn delete_account( ActivityChannel::submit_activity( SendActivityData::DeleteUser(local_user_view.person, data.delete_content), &context, - ) - .await?; + )?; Ok(Json(SuccessResponse::default())) } diff --git a/crates/apub/Cargo.toml b/crates/apub/Cargo.toml index 660489a68..55eadeaf9 100644 --- a/crates/apub/Cargo.toml +++ b/crates/apub/Cargo.toml @@ -33,7 +33,6 @@ tokio = { workspace = true } tracing = { workspace = true } strum = { workspace = true } url = { workspace = true } -http = { workspace = true } futures = { workspace = true } itertools = { workspace = true } uuid = { workspace = true } diff --git a/crates/apub/assets/lemmy/objects/group.json b/crates/apub/assets/lemmy/objects/group.json index bd6e44065..226f50c34 100644 --- a/crates/apub/assets/lemmy/objects/group.json +++ b/crates/apub/assets/lemmy/objects/group.json @@ -3,11 +3,13 @@ "type": "Group", "preferredUsername": "tenforward", "name": "Ten Forward", - "summary": "

Lounge and recreation facility

\n
\n

Welcome to the Enterprise!.

\n", + "summary": "A description of ten forward.", + "content": "

Lounge and recreation facility

\n
\n

Welcome to the Enterprise!.

\n", "source": { "content": "Lounge and recreation facility\n\n---\n\nWelcome to the Enterprise!", "mediaType": "text/markdown" }, + "mediaType": "text/html", "sensitive": false, "icon": { "type": "Image", diff --git a/crates/apub/assets/mastodon/objects/note.json b/crates/apub/assets/mastodon/objects/note_1.json similarity index 100% rename from crates/apub/assets/mastodon/objects/note.json rename to crates/apub/assets/mastodon/objects/note_1.json diff --git a/crates/apub/assets/mastodon/objects/note_2.json b/crates/apub/assets/mastodon/objects/note_2.json new file mode 100644 index 000000000..b8c22b976 --- /dev/null +++ b/crates/apub/assets/mastodon/objects/note_2.json @@ -0,0 +1,79 @@ +{ + "@context": [ + "https://www.w3.org/ns/activitystreams", + { + "ostatus": "http://ostatus.org#", + "atomUri": "ostatus:atomUri", + "inReplyToAtomUri": "ostatus:inReplyToAtomUri", + "conversation": "ostatus:conversation", + "sensitive": "as:sensitive", + "toot": "http://joinmastodon.org/ns#", + "votersCount": "toot:votersCount", + "blurhash": "toot:blurhash", + "focalPoint": { + "@container": "@list", + "@id": "toot:focalPoint" + } + } + ], + "id": "https://floss.social/users/kde/statuses/113306831140126616", + "type": "Note", + "summary": null, + "inReplyTo": "https://floss.social/users/kde/statuses/113306824627995724", + "published": "2024-10-14T16:57:15Z", + "url": "https://floss.social/@kde/113306831140126616", + "attributedTo": "https://floss.social/users/kde", + "to": ["https://www.w3.org/ns/activitystreams#Public"], + "cc": [ + "https://floss.social/users/kde/followers", + "https://lemmy.kde.social/c/kde", + "https://lemmy.kde.social/c/kde/followers" + ], + "sensitive": false, + "atomUri": "https://floss.social/users/kde/statuses/113306831140126616", + "inReplyToAtomUri": "https://floss.social/users/kde/statuses/113306824627995724", + "conversation": "tag:floss.social,2024-10-14:objectId=71424279:objectType=Conversation", + "content": "

@kde@lemmy.kde.social

We also need funding 💶 to keep the gears turning! Please support us with a donation:

https://kde.org/donate/

[3/3]

", + "contentMap": { + "en": "

@kde@lemmy.kde.social

We also need funding 💶 to keep the gears turning! Please support us with a donation:

https://kde.org/donate/

[3/3]

" + }, + "attachment": [ + { + "type": "Document", + "mediaType": "image/jpeg", + "url": "https://cdn.masto.host/floss/media_attachments/files/113/306/826/682/985/891/original/c8d906a2f2ab2334.jpg", + "name": "The KDE dragons Katie and Konqi stand on either side of a pot filling up with gold coins. Donate!", + "blurhash": "USQv:h-W-qI-^,W;RPs=^-R%NZxbo#sDobSc", + "focalPoint": [0.0, 0.0], + "width": 1500, + "height": 1095 + } + ], + "tag": [ + { + "type": "Mention", + "href": "https://lemmy.kde.social/c/kde", + "name": "@kde@lemmy.kde.social" + } + ], + "replies": { + "id": "https://floss.social/users/kde/statuses/113306831140126616/replies", + "type": "Collection", + "first": { + "type": "CollectionPage", + "next": "https://floss.social/users/kde/statuses/113306831140126616/replies?only_other_accounts=true&page=true", + "partOf": "https://floss.social/users/kde/statuses/113306831140126616/replies", + "items": [] + } + }, + "likes": { + "id": "https://floss.social/users/kde/statuses/113306831140126616/likes", + "type": "Collection", + "totalItems": 39 + }, + "shares": { + "id": "https://floss.social/users/kde/statuses/113306831140126616/shares", + "type": "Collection", + "totalItems": 24 + } +} diff --git a/crates/apub/src/activities/block/block_user.rs b/crates/apub/src/activities/block/block_user.rs index e291cc4a4..866e1cc6c 100644 --- a/crates/apub/src/activities/block/block_user.rs +++ b/crates/apub/src/activities/block/block_user.rs @@ -1,3 +1,4 @@ +use super::to_and_audience; use crate::{ activities::{ block::{generate_cc, SiteOrCommunity}, @@ -7,6 +8,7 @@ use crate::{ verify_is_public, verify_mod_action, verify_person_in_community, + verify_visibility, }, activity_lists::AnnouncableActivities, insert_received_activity, @@ -15,7 +17,7 @@ use crate::{ }; use activitypub_federation::{ config::Data, - kinds::{activity::BlockType, public}, + kinds::activity::BlockType, protocol::verification::verify_domains_match, traits::{ActivityHandler, Actor}, }; @@ -39,10 +41,7 @@ use lemmy_db_schema::{ }, traits::{Bannable, Crud, Followable}, }; -use lemmy_utils::{ - error::{LemmyError, LemmyResult}, - LemmyErrorType, -}; +use lemmy_utils::error::{FederationError, LemmyError, LemmyResult}; use url::Url; impl BlockUser { @@ -55,14 +54,10 @@ impl BlockUser { expires: Option>, context: &Data, ) -> LemmyResult { - let audience = if let SiteOrCommunity::Community(c) = target { - Some(c.id().into()) - } else { - None - }; + let (to, audience) = to_and_audience(target)?; Ok(BlockUser { actor: mod_.id().into(), - to: vec![public()], + to, object: user.id().into(), cc: generate_cc(target, &mut context.pool()).await?, target: target.id(), @@ -128,14 +123,14 @@ impl ActivityHandler for BlockUser { #[tracing::instrument(skip_all)] async fn verify(&self, context: &Data) -> LemmyResult<()> { - verify_is_public(&self.to, &self.cc)?; match self.target.dereference(context).await? { SiteOrCommunity::Site(site) => { + verify_is_public(&self.to, &self.cc)?; let domain = self .object .inner() .domain() - .ok_or(LemmyErrorType::UrlWithoutDomain)?; + .ok_or(FederationError::UrlWithoutDomain)?; if context.settings().hostname == domain { return Err( anyhow!("Site bans from remote instance can't affect user's home instance").into(), @@ -146,6 +141,7 @@ impl ActivityHandler for BlockUser { verify_domains_match(&site.id(), self.object.inner())?; } SiteOrCommunity::Community(community) => { + verify_visibility(&self.to, &self.cc, &community)?; verify_person_in_community(&self.actor, &community, context).await?; verify_mod_action(&self.actor, &community, context).await?; } @@ -197,11 +193,7 @@ impl ActivityHandler for BlockUser { CommunityPersonBan::ban(&mut context.pool(), &community_user_ban_form).await?; // Also unsubscribe them from the community, if they are subscribed - let community_follower_form = CommunityFollowerForm { - community_id: community.id, - person_id: blocked_person.id, - pending: false, - }; + let community_follower_form = CommunityFollowerForm::new(community.id, blocked_person.id); CommunityFollower::unfollow(&mut context.pool(), &community_follower_form) .await .ok(); diff --git a/crates/apub/src/activities/block/mod.rs b/crates/apub/src/activities/block/mod.rs index c8323fcb4..550d98183 100644 --- a/crates/apub/src/activities/block/mod.rs +++ b/crates/apub/src/activities/block/mod.rs @@ -1,3 +1,4 @@ +use super::generate_to; use crate::{ objects::{community::ApubCommunity, instance::ApubSite}, protocol::{ @@ -8,6 +9,7 @@ use crate::{ use activitypub_federation::{ config::Data, fetch::object_id::ObjectId, + kinds::public, traits::{Actor, Object}, }; use chrono::{DateTime, Utc}; @@ -205,3 +207,13 @@ pub(crate) async fn send_ban_from_community( .await } } + +fn to_and_audience( + target: &SiteOrCommunity, +) -> LemmyResult<(Vec, Option>)> { + Ok(if let SiteOrCommunity::Community(c) = target { + (vec![generate_to(c)?], Some(c.id().into())) + } else { + (vec![public()], None) + }) +} diff --git a/crates/apub/src/activities/block/undo_block_user.rs b/crates/apub/src/activities/block/undo_block_user.rs index f9f6890b6..29fc22f0c 100644 --- a/crates/apub/src/activities/block/undo_block_user.rs +++ b/crates/apub/src/activities/block/undo_block_user.rs @@ -1,3 +1,4 @@ +use super::to_and_audience; use crate::{ activities::{ block::{generate_cc, SiteOrCommunity}, @@ -5,6 +6,7 @@ use crate::{ generate_activity_id, send_lemmy_activity, verify_is_public, + verify_visibility, }, activity_lists::AnnouncableActivities, insert_received_activity, @@ -13,7 +15,7 @@ use crate::{ }; use activitypub_federation::{ config::Data, - kinds::{activity::UndoType, public}, + kinds::activity::UndoType, protocol::verification::verify_domains_match, traits::{ActivityHandler, Actor}, }; @@ -44,11 +46,7 @@ impl UndoBlockUser { context: &Data, ) -> LemmyResult<()> { let block = BlockUser::new(target, user, mod_, None, reason, None, context).await?; - let audience = if let SiteOrCommunity::Community(c) = target { - Some(c.id().into()) - } else { - None - }; + let (to, audience) = to_and_audience(target)?; let id = generate_activity_id( UndoType::Undo, @@ -56,7 +54,7 @@ impl UndoBlockUser { )?; let undo = UndoBlockUser { actor: mod_.id().into(), - to: vec![public()], + to, object: block, cc: generate_cc(target, &mut context.pool()).await?, kind: UndoType::Undo, @@ -94,7 +92,6 @@ impl ActivityHandler for UndoBlockUser { #[tracing::instrument(skip_all)] async fn verify(&self, context: &Data) -> LemmyResult<()> { - verify_is_public(&self.to, &self.cc)?; verify_domains_match(self.actor.inner(), self.object.actor.inner())?; self.object.verify(context).await?; Ok(()) @@ -108,6 +105,7 @@ impl ActivityHandler for UndoBlockUser { let blocked_person = self.object.object.dereference(context).await?; match self.object.target.dereference(context).await? { SiteOrCommunity::Site(_site) => { + verify_is_public(&self.to, &self.cc)?; let blocked_person = Person::update( &mut context.pool(), blocked_person.id, @@ -135,6 +133,7 @@ impl ActivityHandler for UndoBlockUser { ModBan::create(&mut context.pool(), &form).await?; } SiteOrCommunity::Community(community) => { + verify_visibility(&self.to, &self.cc, &community)?; let community_user_ban_form = CommunityPersonBanForm { community_id: community.id, person_id: blocked_person.id, diff --git a/crates/apub/src/activities/community/announce.rs b/crates/apub/src/activities/community/announce.rs index 0a506a791..d32b9d76e 100644 --- a/crates/apub/src/activities/community/announce.rs +++ b/crates/apub/src/activities/community/announce.rs @@ -2,9 +2,10 @@ use crate::{ activities::{ generate_activity_id, generate_announce_activity_id, + generate_to, send_lemmy_activity, - verify_is_public, verify_person_in_community, + verify_visibility, }, activity_lists::AnnouncableActivities, insert_received_activity, @@ -18,7 +19,7 @@ use crate::{ }; use activitypub_federation::{ config::Data, - kinds::{activity::AnnounceType, public}, + kinds::activity::AnnounceType, traits::{ActivityHandler, Actor}, }; use lemmy_api_common::context::LemmyContext; @@ -26,7 +27,7 @@ use lemmy_db_schema::{ source::{activity::ActivitySendTargets, community::CommunityFollower}, CommunityVisibility, }; -use lemmy_utils::error::{LemmyError, LemmyErrorType, LemmyResult}; +use lemmy_utils::error::{FederationError, LemmyError, LemmyErrorType, LemmyResult}; use serde_json::Value; use url::Url; @@ -54,7 +55,7 @@ impl ActivityHandler for RawAnnouncableActivities { // This is only for sending, not receiving so we reject it. if let AnnouncableActivities::Page(_) = activity { - Err(LemmyErrorType::CannotReceivePage)? + Err(FederationError::CannotReceivePage)? } // Need to treat community as optional here because `Delete/PrivateMessage` gets routed through @@ -92,7 +93,7 @@ impl AnnounceActivity { generate_announce_activity_id(inner_kind, &context.settings().get_protocol_and_hostname())?; Ok(AnnounceActivity { actor: community.id().into(), - to: vec![public()], + to: vec![generate_to(community)?], object: IdOrNestedObject::NestedObject(object), cc: community .followers_url @@ -154,7 +155,6 @@ impl ActivityHandler for AnnounceActivity { #[tracing::instrument(skip_all)] async fn verify(&self, _context: &Data) -> LemmyResult<()> { - verify_is_public(&self.to, &self.cc)?; Ok(()) } @@ -165,10 +165,11 @@ impl ActivityHandler for AnnounceActivity { // This is only for sending, not receiving so we reject it. if let AnnouncableActivities::Page(_) = object { - Err(LemmyErrorType::CannotReceivePage)? + Err(FederationError::CannotReceivePage)? } let community = object.community(context).await?; + verify_visibility(&self.to, &self.cc, &community)?; can_accept_activity_in_community(&Some(community), context).await?; // verify here in order to avoid fetching the object twice over http diff --git a/crates/apub/src/activities/community/collection_add.rs b/crates/apub/src/activities/community/collection_add.rs index 5ab754d35..ae508c2c5 100644 --- a/crates/apub/src/activities/community/collection_add.rs +++ b/crates/apub/src/activities/community/collection_add.rs @@ -2,9 +2,10 @@ use crate::{ activities::{ community::send_activity_in_community, generate_activity_id, - verify_is_public, + generate_to, verify_mod_action, verify_person_in_community, + verify_visibility, }, activity_lists::AnnouncableActivities, insert_received_activity, @@ -17,7 +18,7 @@ use crate::{ use activitypub_federation::{ config::Data, fetch::object_id::ObjectId, - kinds::{activity::AddType, public}, + kinds::activity::AddType, traits::{ActivityHandler, Actor}, }; use lemmy_api_common::{ @@ -53,7 +54,7 @@ impl CollectionAdd { )?; let add = CollectionAdd { actor: actor.id().into(), - to: vec![public()], + to: vec![generate_to(community)?], object: added_mod.id(), target: generate_moderators_url(&community.actor_id)?.into(), cc: vec![community.id()], @@ -79,7 +80,7 @@ impl CollectionAdd { )?; let add = CollectionAdd { actor: actor.id().into(), - to: vec![public()], + to: vec![generate_to(community)?], object: featured_post.ap_id.clone().into(), target: generate_featured_url(&community.actor_id)?.into(), cc: vec![community.id()], @@ -115,8 +116,8 @@ impl ActivityHandler for CollectionAdd { #[tracing::instrument(skip_all)] async fn verify(&self, context: &Data) -> LemmyResult<()> { - verify_is_public(&self.to, &self.cc)?; let community = self.community(context).await?; + verify_visibility(&self.to, &self.cc, &community)?; verify_person_in_community(&self.actor, &community, context).await?; verify_mod_action(&self.actor, &community, context).await?; Ok(()) diff --git a/crates/apub/src/activities/community/collection_remove.rs b/crates/apub/src/activities/community/collection_remove.rs index 90df1fd14..6c08735ed 100644 --- a/crates/apub/src/activities/community/collection_remove.rs +++ b/crates/apub/src/activities/community/collection_remove.rs @@ -2,9 +2,10 @@ use crate::{ activities::{ community::send_activity_in_community, generate_activity_id, - verify_is_public, + generate_to, verify_mod_action, verify_person_in_community, + verify_visibility, }, activity_lists::AnnouncableActivities, insert_received_activity, @@ -14,7 +15,7 @@ use crate::{ use activitypub_federation::{ config::Data, fetch::object_id::ObjectId, - kinds::{activity::RemoveType, public}, + kinds::activity::RemoveType, traits::{ActivityHandler, Actor}, }; use lemmy_api_common::{ @@ -48,7 +49,7 @@ impl CollectionRemove { )?; let remove = CollectionRemove { actor: actor.id().into(), - to: vec![public()], + to: vec![generate_to(community)?], object: removed_mod.id(), target: generate_moderators_url(&community.actor_id)?.into(), id: id.clone(), @@ -74,7 +75,7 @@ impl CollectionRemove { )?; let remove = CollectionRemove { actor: actor.id().into(), - to: vec![public()], + to: vec![generate_to(community)?], object: featured_post.ap_id.clone().into(), target: generate_featured_url(&community.actor_id)?.into(), cc: vec![community.id()], @@ -110,8 +111,8 @@ impl ActivityHandler for CollectionRemove { #[tracing::instrument(skip_all)] async fn verify(&self, context: &Data) -> LemmyResult<()> { - verify_is_public(&self.to, &self.cc)?; let community = self.community(context).await?; + verify_visibility(&self.to, &self.cc, &community)?; verify_person_in_community(&self.actor, &community, context).await?; verify_mod_action(&self.actor, &community, context).await?; Ok(()) diff --git a/crates/apub/src/activities/community/lock_page.rs b/crates/apub/src/activities/community/lock_page.rs index 0d90b5bb0..a9bacea8a 100644 --- a/crates/apub/src/activities/community/lock_page.rs +++ b/crates/apub/src/activities/community/lock_page.rs @@ -3,9 +3,10 @@ use crate::{ check_community_deleted_or_removed, community::send_activity_in_community, generate_activity_id, - verify_is_public, + generate_to, verify_mod_action, verify_person_in_community, + verify_visibility, }, activity_lists::AnnouncableActivities, insert_received_activity, @@ -18,7 +19,7 @@ use crate::{ use activitypub_federation::{ config::Data, fetch::object_id::ObjectId, - kinds::{activity::UndoType, public}, + kinds::activity::UndoType, traits::ActivityHandler, }; use lemmy_api_common::context::LemmyContext; @@ -49,8 +50,8 @@ impl ActivityHandler for LockPage { } async fn verify(&self, context: &Data) -> Result<(), Self::Error> { - verify_is_public(&self.to, &self.cc)?; let community = self.community(context).await?; + verify_visibility(&self.to, &self.cc, &community)?; verify_person_in_community(&self.actor, &community, context).await?; check_community_deleted_or_removed(&community)?; verify_mod_action(&self.actor, &community, context).await?; @@ -92,8 +93,8 @@ impl ActivityHandler for UndoLockPage { } async fn verify(&self, context: &Data) -> Result<(), Self::Error> { - verify_is_public(&self.to, &self.cc)?; let community = self.community(context).await?; + verify_visibility(&self.to, &self.cc, &community)?; verify_person_in_community(&self.actor, &community, context).await?; check_community_deleted_or_removed(&community)?; verify_mod_action(&self.actor, &community, context).await?; @@ -137,7 +138,7 @@ pub(crate) async fn send_lock_post( let community_id = community.actor_id.inner().clone(); let lock = LockPage { actor: actor.actor_id.clone().into(), - to: vec![public()], + to: vec![generate_to(&community)?], object: ObjectId::from(post.ap_id), cc: vec![community_id.clone()], kind: LockType::Lock, @@ -153,7 +154,7 @@ pub(crate) async fn send_lock_post( )?; let undo = UndoLockPage { actor: lock.actor.clone(), - to: vec![public()], + to: vec![generate_to(&community)?], cc: lock.cc.clone(), kind: UndoType::Undo, id, diff --git a/crates/apub/src/activities/community/update.rs b/crates/apub/src/activities/community/update.rs index f507b3425..85be94246 100644 --- a/crates/apub/src/activities/community/update.rs +++ b/crates/apub/src/activities/community/update.rs @@ -2,9 +2,10 @@ use crate::{ activities::{ community::send_activity_in_community, generate_activity_id, - verify_is_public, + generate_to, verify_mod_action, verify_person_in_community, + verify_visibility, }, activity_lists::AnnouncableActivities, insert_received_activity, @@ -13,7 +14,7 @@ use crate::{ }; use activitypub_federation::{ config::Data, - kinds::{activity::UpdateType, public}, + kinds::activity::UpdateType, traits::{ActivityHandler, Actor, Object}, }; use lemmy_api_common::context::LemmyContext; @@ -42,7 +43,7 @@ pub(crate) async fn send_update_community( )?; let update = UpdateCommunity { actor: actor.id().into(), - to: vec![public()], + to: vec![generate_to(&community)?], object: Box::new(community.clone().into_json(&context).await?), cc: vec![community.id()], kind: UpdateType::Update, @@ -77,8 +78,8 @@ impl ActivityHandler for UpdateCommunity { #[tracing::instrument(skip_all)] async fn verify(&self, context: &Data) -> LemmyResult<()> { - verify_is_public(&self.to, &self.cc)?; let community = self.community(context).await?; + verify_visibility(&self.to, &self.cc, &community)?; verify_person_in_community(&self.actor, &community, context).await?; verify_mod_action(&self.actor, &community, context).await?; ApubCommunity::verify(&self.object, &community.actor_id.clone().into(), context).await?; @@ -106,8 +107,14 @@ impl ActivityHandler for UpdateCommunity { icon: Some(self.object.icon.map(|i| i.url.into())), banner: Some(self.object.image.map(|i| i.url.into())), followers_url: self.object.followers.map(Into::into), - inbox_url: Some(self.object.inbox.into()), - shared_inbox_url: Some(self.object.endpoints.map(|e| e.shared_inbox.into())), + inbox_url: Some( + self + .object + .endpoints + .map(|e| e.shared_inbox) + .unwrap_or(self.object.inbox) + .into(), + ), moderators_url: self.object.attributed_to.map(Into::into), posting_restricted_to_mods: self.object.posting_restricted_to_mods, featured_url: self.object.featured.map(Into::into), diff --git a/crates/apub/src/activities/create_or_update/comment.rs b/crates/apub/src/activities/create_or_update/comment.rs index b2c436049..90ab0153f 100644 --- a/crates/apub/src/activities/create_or_update/comment.rs +++ b/crates/apub/src/activities/create_or_update/comment.rs @@ -3,8 +3,9 @@ use crate::{ check_community_deleted_or_removed, community::send_activity_in_community, generate_activity_id, - verify_is_public, + generate_to, verify_person_in_community, + verify_visibility, }, activity_lists::AnnouncableActivities, insert_received_activity, @@ -18,7 +19,6 @@ use crate::{ use activitypub_federation::{ config::Data, fetch::object_id::ObjectId, - kinds::public, protocol::verification::{verify_domains_match, verify_urls_match}, traits::{ActivityHandler, Actor, Object}, }; @@ -70,7 +70,7 @@ impl CreateOrUpdateNote { let create_or_update = CreateOrUpdateNote { actor: person.id().into(), - to: vec![public()], + to: vec![generate_to(&community)?], cc: note.cc.clone(), tag: note.tag.clone(), object: note, @@ -118,9 +118,9 @@ impl ActivityHandler for CreateOrUpdateNote { #[tracing::instrument(skip_all)] async fn verify(&self, context: &Data) -> LemmyResult<()> { - verify_is_public(&self.to, &self.cc)?; let post = self.object.get_parents(context).await?.0; let community = self.community(context).await?; + verify_visibility(&self.to, &self.cc, &community)?; verify_person_in_community(&self.actor, &community, context).await?; verify_domains_match(self.actor.inner(), self.object.id.inner())?; @@ -153,7 +153,6 @@ impl ActivityHandler for CreateOrUpdateNote { // author likes their own comment by default let like_form = CommentLikeForm { comment_id: comment.id, - post_id: comment.post_id, person_id: comment.creator_id, score: 1, }; diff --git a/crates/apub/src/activities/create_or_update/post.rs b/crates/apub/src/activities/create_or_update/post.rs index fb53100f6..d0cf17a51 100644 --- a/crates/apub/src/activities/create_or_update/post.rs +++ b/crates/apub/src/activities/create_or_update/post.rs @@ -3,8 +3,9 @@ use crate::{ check_community_deleted_or_removed, community::send_activity_in_community, generate_activity_id, - verify_is_public, + generate_to, verify_person_in_community, + verify_visibility, }, activity_lists::AnnouncableActivities, insert_received_activity, @@ -16,7 +17,6 @@ use crate::{ }; use activitypub_federation::{ config::Data, - kinds::public, protocol::verification::{verify_domains_match, verify_urls_match}, traits::{ActivityHandler, Actor, Object}, }; @@ -49,7 +49,7 @@ impl CreateOrUpdatePage { )?; Ok(CreateOrUpdatePage { actor: actor.id().into(), - to: vec![public()], + to: vec![generate_to(community)?], object: post.into_json(context).await?, cc: vec![community.id()], kind, @@ -102,8 +102,8 @@ impl ActivityHandler for CreateOrUpdatePage { #[tracing::instrument(skip_all)] async fn verify(&self, context: &Data) -> LemmyResult<()> { - verify_is_public(&self.to, &self.cc)?; let community = self.community(context).await?; + verify_visibility(&self.to, &self.cc, &community)?; verify_person_in_community(&self.actor, &community, context).await?; check_community_deleted_or_removed(&community)?; verify_domains_match(self.actor.inner(), self.object.id.inner())?; diff --git a/crates/apub/src/activities/deletion/delete.rs b/crates/apub/src/activities/deletion/delete.rs index ec2a0de95..064f0bc82 100644 --- a/crates/apub/src/activities/deletion/delete.rs +++ b/crates/apub/src/activities/deletion/delete.rs @@ -27,7 +27,7 @@ use lemmy_db_schema::{ }, traits::{Crud, Reportable}, }; -use lemmy_utils::error::{LemmyError, LemmyErrorType, LemmyResult}; +use lemmy_utils::error::{FederationError, LemmyError, LemmyErrorType, LemmyResult}; use url::Url; #[async_trait::async_trait] @@ -84,7 +84,7 @@ impl Delete { pub(in crate::activities::deletion) fn new( actor: &ApubPerson, object: DeletableObjects, - to: Url, + to: Vec, community: Option<&Community>, summary: Option, context: &Data, @@ -96,7 +96,7 @@ impl Delete { let cc: Option = community.map(|c| c.actor_id.clone().into()); Ok(Delete { actor: actor.actor_id.clone().into(), - to: vec![to], + to, object: IdOrNestedObject::Id(object.id()), cc: cc.into_iter().collect(), kind: DeleteType::Delete, @@ -118,7 +118,7 @@ pub(in crate::activities) async fn receive_remove_action( match DeletableObjects::read_from_db(object, context).await? { DeletableObjects::Community(community) => { if community.local { - Err(LemmyErrorType::OnlyLocalAdminCanRemoveCommunity)? + Err(FederationError::OnlyLocalAdminCanRemoveCommunity)? } let form = ModRemoveCommunityForm { mod_person_id: actor.id, diff --git a/crates/apub/src/activities/deletion/mod.rs b/crates/apub/src/activities/deletion/mod.rs index b12532087..15118a476 100644 --- a/crates/apub/src/activities/deletion/mod.rs +++ b/crates/apub/src/activities/deletion/mod.rs @@ -1,11 +1,12 @@ +use super::{generate_to, verify_is_public}; use crate::{ activities::{ community::send_activity_in_community, send_lemmy_activity, - verify_is_public, verify_mod_action, verify_person, verify_person_in_community, + verify_visibility, }, activity_lists::AnnouncableActivities, objects::{ @@ -59,11 +60,12 @@ pub(crate) async fn send_apub_delete_in_community( ) -> LemmyResult<()> { let actor = ApubPerson::from(actor); let is_mod_action = reason.is_some(); + let to = vec![generate_to(&community)?]; let activity = if deleted { - let delete = Delete::new(&actor, object, public(), Some(&community), reason, context)?; + let delete = Delete::new(&actor, object, to, Some(&community), reason, context)?; AnnouncableActivities::Delete(delete) } else { - let undo = UndoDelete::new(&actor, object, public(), Some(&community), reason, context)?; + let undo = UndoDelete::new(&actor, object, to, Some(&community), reason, context)?; AnnouncableActivities::UndoDelete(undo) }; send_activity_in_community( @@ -92,10 +94,10 @@ pub(crate) async fn send_apub_delete_private_message( let deletable = DeletableObjects::PrivateMessage(pm.into()); let inbox = ActivitySendTargets::to_inbox(recipient.shared_inbox_or_inbox()); if deleted { - let delete: Delete = Delete::new(actor, deletable, recipient.id(), None, None, &context)?; + let delete: Delete = Delete::new(actor, deletable, vec![recipient.id()], None, None, &context)?; send_lemmy_activity(&context, delete, actor, inbox, true).await?; } else { - let undo = UndoDelete::new(actor, deletable, recipient.id(), None, None, &context)?; + let undo = UndoDelete::new(actor, deletable, vec![recipient.id()], None, None, &context)?; send_lemmy_activity(&context, undo, actor, inbox, true).await?; }; Ok(()) @@ -109,7 +111,7 @@ pub async fn send_apub_delete_user( let person: ApubPerson = person.into(); let deletable = DeletableObjects::Person(person.clone()); - let mut delete: Delete = Delete::new(&person, deletable, public(), None, None, &context)?; + let mut delete: Delete = Delete::new(&person, deletable, vec![public()], None, None, &context)?; delete.remove_data = Some(remove_data); let inboxes = ActivitySendTargets::to_all_instances(); @@ -170,7 +172,7 @@ pub(in crate::activities) async fn verify_delete_activity( let object = DeletableObjects::read_from_db(activity.object.id(), context).await?; match object { DeletableObjects::Community(community) => { - verify_is_public(&activity.to, &[])?; + verify_visibility(&activity.to, &[], &community)?; if community.local { // can only do this check for local community, in remote case it would try to fetch the // deleted community (which fails) @@ -185,22 +187,24 @@ pub(in crate::activities) async fn verify_delete_activity( verify_urls_match(person.actor_id.inner(), activity.object.id())?; } DeletableObjects::Post(p) => { - verify_is_public(&activity.to, &[])?; + let community = activity.community(context).await?; + verify_visibility(&activity.to, &[], &community)?; verify_delete_post_or_comment( &activity.actor, &p.ap_id.clone().into(), - &activity.community(context).await?, + &community, is_mod_action, context, ) .await?; } DeletableObjects::Comment(c) => { - verify_is_public(&activity.to, &[])?; + let community = activity.community(context).await?; + verify_visibility(&activity.to, &[], &community)?; verify_delete_post_or_comment( &activity.actor, &c.ap_id.clone().into(), - &activity.community(context).await?, + &community, is_mod_action, context, ) diff --git a/crates/apub/src/activities/deletion/undo_delete.rs b/crates/apub/src/activities/deletion/undo_delete.rs index 102c72678..f4a7bb9b9 100644 --- a/crates/apub/src/activities/deletion/undo_delete.rs +++ b/crates/apub/src/activities/deletion/undo_delete.rs @@ -25,7 +25,7 @@ use lemmy_db_schema::{ }, traits::Crud, }; -use lemmy_utils::error::{LemmyError, LemmyErrorType, LemmyResult}; +use lemmy_utils::error::{FederationError, LemmyError, LemmyErrorType, LemmyResult}; use url::Url; #[async_trait::async_trait] @@ -68,7 +68,7 @@ impl UndoDelete { pub(in crate::activities::deletion) fn new( actor: &ApubPerson, object: DeletableObjects, - to: Url, + to: Vec, community: Option<&Community>, summary: Option, context: &Data, @@ -82,7 +82,7 @@ impl UndoDelete { let cc: Option = community.map(|c| c.actor_id.clone().into()); Ok(UndoDelete { actor: actor.actor_id.clone().into(), - to: vec![to], + to, object, cc: cc.into_iter().collect(), kind: UndoType::Undo, @@ -100,7 +100,7 @@ impl UndoDelete { match DeletableObjects::read_from_db(object, context).await? { DeletableObjects::Community(community) => { if community.local { - Err(LemmyErrorType::OnlyLocalAdminCanRestoreCommunity)? + Err(FederationError::OnlyLocalAdminCanRestoreCommunity)? } let form = ModRemoveCommunityForm { mod_person_id: actor.id, diff --git a/crates/apub/src/activities/following/follow.rs b/crates/apub/src/activities/following/follow.rs index 02f29a1a9..befa2e00c 100644 --- a/crates/apub/src/activities/following/follow.rs +++ b/crates/apub/src/activities/following/follow.rs @@ -20,7 +20,7 @@ use lemmy_api_common::context::LemmyContext; use lemmy_db_schema::{ source::{ activity::ActivitySendTargets, - community::{CommunityFollower, CommunityFollowerForm}, + community::{CommunityFollower, CommunityFollowerForm, CommunityFollowerState}, person::{PersonFollower, PersonFollowerForm}, }, traits::Followable, @@ -102,21 +102,25 @@ impl ActivityHandler for Follow { pending: false, }; PersonFollower::follow(&mut context.pool(), &form).await?; + AcceptFollow::send(self, context).await?; } UserOrCommunity::Community(c) => { - // Dont allow following local-only community via federation. - if c.visibility != CommunityVisibility::Public { - return Err(LemmyErrorType::NotFound.into()); - } + let state = Some(match c.visibility { + CommunityVisibility::Public => CommunityFollowerState::Accepted, + CommunityVisibility::Private => CommunityFollowerState::ApprovalRequired, + // Dont allow following local-only community via federation. + CommunityVisibility::LocalOnly => return Err(LemmyErrorType::NotFound.into()), + }); let form = CommunityFollowerForm { - community_id: c.id, - person_id: actor.id, - pending: false, + state, + ..CommunityFollowerForm::new(c.id, actor.id) }; CommunityFollower::follow(&mut context.pool(), &form).await?; + if c.visibility == CommunityVisibility::Public { + AcceptFollow::send(self, context).await?; + } } } - - AcceptFollow::send(self, context).await + Ok(()) } } diff --git a/crates/apub/src/activities/following/mod.rs b/crates/apub/src/activities/following/mod.rs index 7c7163f12..83cdc841c 100644 --- a/crates/apub/src/activities/following/mod.rs +++ b/crates/apub/src/activities/following/mod.rs @@ -1,15 +1,26 @@ +use super::generate_activity_id; use crate::{ objects::{community::ApubCommunity, person::ApubPerson}, - protocol::activities::following::{follow::Follow, undo_follow::UndoFollow}, + protocol::activities::following::{ + accept::AcceptFollow, + follow::Follow, + reject::RejectFollow, + undo_follow::UndoFollow, + }, }; -use activitypub_federation::config::Data; +use activitypub_federation::{config::Data, kinds::activity::FollowType}; use lemmy_api_common::context::LemmyContext; -use lemmy_db_schema::source::{community::Community, person::Person}; +use lemmy_db_schema::{ + newtypes::{CommunityId, PersonId}, + source::{community::Community, person::Person}, + traits::Crud, +}; use lemmy_utils::error::LemmyResult; -pub mod accept; -pub mod follow; -pub mod undo_follow; +pub(crate) mod accept; +pub(crate) mod follow; +pub(crate) mod reject; +pub(crate) mod undo_follow; pub async fn send_follow_community( community: Community, @@ -25,3 +36,29 @@ pub async fn send_follow_community( UndoFollow::send(&actor, &community, context).await } } + +pub async fn send_accept_or_reject_follow( + community_id: CommunityId, + person_id: PersonId, + accepted: bool, + context: &Data, +) -> LemmyResult<()> { + let community = Community::read(&mut context.pool(), community_id).await?; + let person = Person::read(&mut context.pool(), person_id).await?; + + let follow = Follow { + actor: person.actor_id.into(), + to: Some([community.actor_id.clone().into()]), + object: community.actor_id.into(), + kind: FollowType::Follow, + id: generate_activity_id( + FollowType::Follow, + &context.settings().get_protocol_and_hostname(), + )?, + }; + if accepted { + AcceptFollow::send(follow, context).await + } else { + RejectFollow::send(follow, context).await + } +} diff --git a/crates/apub/src/activities/following/reject.rs b/crates/apub/src/activities/following/reject.rs new file mode 100644 index 000000000..8f1623d20 --- /dev/null +++ b/crates/apub/src/activities/following/reject.rs @@ -0,0 +1,79 @@ +use crate::{ + activities::{generate_activity_id, send_lemmy_activity}, + insert_received_activity, + protocol::activities::following::{follow::Follow, reject::RejectFollow}, +}; +use activitypub_federation::{ + config::Data, + kinds::activity::RejectType, + protocol::verification::verify_urls_match, + traits::{ActivityHandler, Actor}, +}; +use lemmy_api_common::context::LemmyContext; +use lemmy_db_schema::{ + source::{ + activity::ActivitySendTargets, + community::{CommunityFollower, CommunityFollowerForm}, + }, + traits::Followable, +}; +use lemmy_utils::error::{LemmyError, LemmyResult}; +use url::Url; + +impl RejectFollow { + #[tracing::instrument(skip_all)] + pub async fn send(follow: Follow, context: &Data) -> LemmyResult<()> { + let user_or_community = follow.object.dereference_local(context).await?; + let person = follow.actor.clone().dereference(context).await?; + let reject = RejectFollow { + actor: user_or_community.id().into(), + to: Some([person.id().into()]), + object: follow, + kind: RejectType::Reject, + id: generate_activity_id( + RejectType::Reject, + &context.settings().get_protocol_and_hostname(), + )?, + }; + let inbox = ActivitySendTargets::to_inbox(person.shared_inbox_or_inbox()); + send_lemmy_activity(context, reject, &user_or_community, inbox, true).await + } +} + +/// Handle rejected follows +#[async_trait::async_trait] +impl ActivityHandler for RejectFollow { + type DataType = LemmyContext; + type Error = LemmyError; + + fn id(&self) -> &Url { + &self.id + } + + fn actor(&self) -> &Url { + self.actor.inner() + } + + #[tracing::instrument(skip_all)] + async fn verify(&self, context: &Data) -> LemmyResult<()> { + verify_urls_match(self.actor.inner(), self.object.object.inner())?; + self.object.verify(context).await?; + if let Some(to) = &self.to { + verify_urls_match(to[0].inner(), self.object.actor.inner())?; + } + Ok(()) + } + + #[tracing::instrument(skip_all)] + async fn receive(self, context: &Data) -> LemmyResult<()> { + insert_received_activity(&self.id, context).await?; + let community = self.actor.dereference(context).await?; + let person = self.object.actor.dereference(context).await?; + + // remove the follow + let form = CommunityFollowerForm::new(community.id, person.id); + CommunityFollower::unfollow(&mut context.pool(), &form).await?; + + Ok(()) + } +} diff --git a/crates/apub/src/activities/following/undo_follow.rs b/crates/apub/src/activities/following/undo_follow.rs index ba6253946..1aa6bb7fc 100644 --- a/crates/apub/src/activities/following/undo_follow.rs +++ b/crates/apub/src/activities/following/undo_follow.rs @@ -90,11 +90,7 @@ impl ActivityHandler for UndoFollow { PersonFollower::unfollow(&mut context.pool(), &form).await?; } UserOrCommunity::Community(c) => { - let form = CommunityFollowerForm { - community_id: c.id, - person_id: person.id, - pending: false, - }; + let form = CommunityFollowerForm::new(c.id, person.id); CommunityFollower::unfollow(&mut context.pool(), &form).await?; } } diff --git a/crates/apub/src/activities/mod.rs b/crates/apub/src/activities/mod.rs index 2c371f71c..ffb6a662e 100644 --- a/crates/apub/src/activities/mod.rs +++ b/crates/apub/src/activities/mod.rs @@ -30,6 +30,7 @@ use activitypub_federation::{ traits::{ActivityHandler, Actor}, }; use anyhow::anyhow; +use following::send_accept_or_reject_follow; use lemmy_api_common::{ context::LemmyContext, send_activity::{ActivityChannel, SendActivityData}, @@ -40,9 +41,10 @@ use lemmy_db_schema::{ community::Community, }, traits::Crud, + CommunityVisibility, }; use lemmy_db_views_actor::structs::{CommunityPersonBanView, CommunityView}; -use lemmy_utils::error::{LemmyError, LemmyErrorExt, LemmyErrorType, LemmyResult}; +use lemmy_utils::error::{FederationError, LemmyError, LemmyErrorExt, LemmyErrorType, LemmyResult}; use serde::Serialize; use tracing::info; use url::{ParseError, Url}; @@ -81,7 +83,7 @@ pub(crate) async fn verify_person_in_community( ) -> LemmyResult<()> { let person = person_id.dereference(context).await?; if person.banned { - Err(LemmyErrorType::PersonIsBannedFromSite( + Err(FederationError::PersonIsBannedFromSite( person.actor_id.to_string(), ))? } @@ -114,19 +116,41 @@ pub(crate) async fn verify_mod_action( pub(crate) fn verify_is_public(to: &[Url], cc: &[Url]) -> LemmyResult<()> { if ![to, cc].iter().any(|set| set.contains(&public())) { - Err(LemmyErrorType::ObjectIsNotPublic)? + Err(FederationError::ObjectIsNotPublic)? } else { Ok(()) } } +/// Returns an error if object visibility doesnt match community visibility +/// (ie content in private community must also be private). +pub(crate) fn verify_visibility(to: &[Url], cc: &[Url], community: &Community) -> LemmyResult<()> { + use CommunityVisibility::*; + let object_is_public = [to, cc].iter().any(|set| set.contains(&public())); + match community.visibility { + Public if !object_is_public => Err(FederationError::ObjectIsNotPublic)?, + Private if object_is_public => Err(FederationError::ObjectIsNotPrivate)?, + LocalOnly => Err(LemmyErrorType::NotFound.into()), + _ => Ok(()), + } +} + +/// Marks object as public only if the community is public +pub(crate) fn generate_to(community: &Community) -> LemmyResult { + if community.visibility == CommunityVisibility::Public { + Ok(public()) + } else { + Ok(Url::parse(&format!("{}/followers", community.actor_id))?) + } +} + pub(crate) fn verify_community_matches(a: &ObjectId, b: T) -> LemmyResult<()> where T: Into>, { let b: ObjectId = b.into(); if a != &b { - Err(LemmyErrorType::InvalidCommunity)? + Err(FederationError::InvalidCommunity)? } else { Ok(()) } @@ -134,7 +158,7 @@ where pub(crate) fn check_community_deleted_or_removed(community: &Community) -> LemmyResult<()> { if community.deleted || community.removed { - Err(LemmyErrorType::CannotCreatePostOrCommentInDeletedOrRemovedCommunity)? + Err(FederationError::CannotCreatePostOrCommentInDeletedOrRemovedCommunity)? } else { Ok(()) } @@ -367,6 +391,12 @@ pub async fn match_outgoing_activities( community, reason, } => Report::send(ObjectId::from(object_id), actor, community, reason, context).await, + AcceptFollower(community_id, person_id) => { + send_accept_or_reject_follow(community_id, person_id, true, &context).await + } + RejectFollower(community_id, person_id) => { + send_accept_or_reject_follow(community_id, person_id, false, &context).await + } } }; fed_task.await?; diff --git a/crates/apub/src/activities/voting/mod.rs b/crates/apub/src/activities/voting/mod.rs index 3e59cb7d0..7c39b2246 100644 --- a/crates/apub/src/activities/voting/mod.rs +++ b/crates/apub/src/activities/voting/mod.rs @@ -62,7 +62,6 @@ async fn vote_comment( let comment_id = comment.id; let like_form = CommentLikeForm { comment_id, - post_id: comment.post_id, person_id: actor.id, score: vote_type.into(), }; diff --git a/crates/apub/src/activity_lists.rs b/crates/apub/src/activity_lists.rs index 9262236d8..7ed1d8baf 100644 --- a/crates/apub/src/activity_lists.rs +++ b/crates/apub/src/activity_lists.rs @@ -17,7 +17,12 @@ use crate::{ page::CreateOrUpdatePage, }, deletion::{delete::Delete, undo_delete::UndoDelete}, - following::{accept::AcceptFollow, follow::Follow, undo_follow::UndoFollow}, + following::{ + accept::AcceptFollow, + follow::Follow, + reject::RejectFollow, + undo_follow::UndoFollow, + }, voting::{undo_vote::UndoVote, vote::Vote}, }, objects::page::Page, @@ -26,7 +31,7 @@ use crate::{ }; use activitypub_federation::{config::Data, traits::ActivityHandler}; use lemmy_api_common::context::LemmyContext; -use lemmy_utils::{error::LemmyResult, LemmyErrorType}; +use lemmy_utils::error::{LemmyErrorType, LemmyResult}; use serde::{Deserialize, Serialize}; use url::Url; @@ -41,6 +46,7 @@ use url::Url; pub enum SharedInboxActivities { Follow(Follow), AcceptFollow(AcceptFollow), + RejectFollow(RejectFollow), UndoFollow(UndoFollow), CreateOrUpdatePrivateMessage(CreateOrUpdateChatMessage), Report(Report), @@ -68,6 +74,7 @@ pub enum GroupInboxActivities { pub enum PersonInboxActivities { Follow(Follow), AcceptFollow(AcceptFollow), + RejectFollow(RejectFollow), UndoFollow(UndoFollow), CreateOrUpdatePrivateMessage(CreateOrUpdateChatMessage), Delete(Delete), diff --git a/crates/apub/src/api/list_posts.rs b/crates/apub/src/api/list_posts.rs index d75a82d3b..cdf24dbaa 100644 --- a/crates/apub/src/api/list_posts.rs +++ b/crates/apub/src/api/list_posts.rs @@ -42,6 +42,7 @@ pub async fn list_posts( let show_hidden = data.show_hidden; let show_read = data.show_read; let show_nsfw = data.show_nsfw; + let no_comments_only = data.no_comments_only; let liked_only = data.liked_only; let disliked_only = data.disliked_only; @@ -82,6 +83,7 @@ pub async fn list_posts( show_hidden, show_read, show_nsfw, + no_comments_only, ..Default::default() } .list(&local_site.site, &mut context.pool()) diff --git a/crates/apub/src/api/resolve_object.rs b/crates/apub/src/api/resolve_object.rs index 81329a97b..04d489592 100644 --- a/crates/apub/src/api/resolve_object.rs +++ b/crates/apub/src/api/resolve_object.rs @@ -5,7 +5,6 @@ use crate::fetcher::{ }; use activitypub_federation::config::Data; use actix_web::web::{Json, Query}; -use diesel::NotFound; use lemmy_api_common::{ context::LemmyContext, site::{ResolveObject, ResolveObjectResponse}, @@ -47,36 +46,145 @@ async fn convert_response( local_user_view: Option, pool: &mut DbPool<'_>, ) -> LemmyResult> { - let removed_or_deleted; let mut res = ResolveObjectResponse::default(); let local_user = local_user_view.map(|l| l.local_user); + let is_admin = local_user.clone().map(|l| l.admin).unwrap_or_default(); match object { SearchableObjects::PostOrComment(pc) => match *pc { PostOrComment::Post(p) => { - removed_or_deleted = p.deleted || p.removed; - res.post = Some(PostView::read(pool, p.id, local_user.as_ref(), false).await?) + res.post = Some(PostView::read(pool, p.id, local_user.as_ref(), is_admin).await?) } PostOrComment::Comment(c) => { - removed_or_deleted = c.deleted || c.removed; res.comment = Some(CommentView::read(pool, c.id, local_user.as_ref()).await?) } }, SearchableObjects::PersonOrCommunity(pc) => match *pc { - UserOrCommunity::User(u) => { - removed_or_deleted = u.deleted; - res.person = Some(PersonView::read(pool, u.id).await?) - } + UserOrCommunity::User(u) => res.person = Some(PersonView::read(pool, u.id).await?), UserOrCommunity::Community(c) => { - removed_or_deleted = c.deleted || c.removed; - res.community = Some(CommunityView::read(pool, c.id, local_user.as_ref(), false).await?) + res.community = Some(CommunityView::read(pool, c.id, local_user.as_ref(), is_admin).await?) } }, }; - // if the object was deleted from database, dont return it - if removed_or_deleted { - Err(NotFound {}.into()) - } else { - Ok(Json(res)) + + Ok(Json(res)) +} + +#[cfg(test)] +mod tests { + use crate::api::resolve_object::resolve_object; + use actix_web::web::Query; + use lemmy_api_common::{context::LemmyContext, site::ResolveObject}; + use lemmy_db_schema::{ + source::{ + community::{Community, CommunityInsertForm}, + instance::Instance, + local_site::{LocalSite, LocalSiteInsertForm}, + post::{Post, PostInsertForm, PostUpdateForm}, + site::{Site, SiteInsertForm}, + }, + traits::Crud, + }; + use lemmy_db_views::structs::LocalUserView; + use lemmy_utils::error::{LemmyErrorType, LemmyResult}; + use serial_test::serial; + + #[tokio::test] + #[serial] + #[expect(clippy::unwrap_used)] + async fn test_object_visibility() -> LemmyResult<()> { + let context = LemmyContext::init_test_context().await; + let pool = &mut context.pool(); + + let name = "test_local_user_name"; + let bio = "test_local_user_bio"; + + let creator = LocalUserView::create_test_user(pool, name, bio, false).await?; + let regular_user = LocalUserView::create_test_user(pool, name, bio, false).await?; + let admin_user = LocalUserView::create_test_user(pool, name, bio, true).await?; + + let instance_id = creator.person.instance_id; + let site_form = SiteInsertForm::new("test site".to_string(), instance_id); + let site = Site::create(pool, &site_form).await?; + + let local_site_form = LocalSiteInsertForm { + site_setup: Some(true), + private_instance: Some(false), + ..LocalSiteInsertForm::new(site.id) + }; + LocalSite::create(pool, &local_site_form).await?; + + let community = Community::create( + pool, + &CommunityInsertForm::new( + instance_id, + "test".to_string(), + "test".to_string(), + "pubkey".to_string(), + ), + ) + .await?; + + let post_insert_form = PostInsertForm::new("Test".to_string(), creator.person.id, community.id); + let post = Post::create(pool, &post_insert_form).await?; + + let query = format!("q={}", post.ap_id).to_string(); + let query: Query = Query::from_query(&query)?; + + // Objects should be resolvable without authentication + let res = resolve_object(query.clone(), context.reset_request_count(), None).await?; + assert_eq!(res.post.as_ref().unwrap().post.ap_id, post.ap_id); + // Objects should be resolvable by regular users + let res = resolve_object( + query.clone(), + context.reset_request_count(), + Some(regular_user.clone()), + ) + .await?; + assert_eq!(res.post.as_ref().unwrap().post.ap_id, post.ap_id); + // Objects should be resolvable by admins + let res = resolve_object( + query.clone(), + context.reset_request_count(), + Some(admin_user.clone()), + ) + .await?; + assert_eq!(res.post.as_ref().unwrap().post.ap_id, post.ap_id); + + Post::update( + pool, + post.id, + &PostUpdateForm { + deleted: Some(true), + ..Default::default() + }, + ) + .await?; + + // Deleted objects should not be resolvable without authentication + let res = resolve_object(query.clone(), context.reset_request_count(), None).await; + assert!(res.is_err_and(|e| e.error_type == LemmyErrorType::NotFound)); + // Deleted objects should not be resolvable by regular users + let res = resolve_object( + query.clone(), + context.reset_request_count(), + Some(regular_user.clone()), + ) + .await; + assert!(res.is_err_and(|e| e.error_type == LemmyErrorType::NotFound)); + // Deleted objects should be resolvable by admins + let res = resolve_object( + query.clone(), + context.reset_request_count(), + Some(admin_user.clone()), + ) + .await?; + assert_eq!(res.post.as_ref().unwrap().post.ap_id, post.ap_id); + + LocalSite::delete(pool).await?; + Site::delete(pool, site.id).await?; + Instance::delete(pool, instance_id).await?; + + Ok(()) } } diff --git a/crates/apub/src/api/user_settings_backup.rs b/crates/apub/src/api/user_settings_backup.rs index 36a6907a0..601ba8664 100644 --- a/crates/apub/src/api/user_settings_backup.rs +++ b/crates/apub/src/api/user_settings_backup.rs @@ -13,7 +13,7 @@ use lemmy_db_schema::{ newtypes::DbUrl, source::{ comment::{CommentSaved, CommentSavedForm}, - community::{CommunityFollower, CommunityFollowerForm}, + community::{CommunityFollower, CommunityFollowerForm, CommunityFollowerState}, community_block::{CommunityBlock, CommunityBlockForm}, instance::Instance, instance_block::{InstanceBlock, InstanceBlockForm}, @@ -186,9 +186,8 @@ pub async fn import_settings( |(followed, context)| async move { let community = followed.dereference(&context).await?; let form = CommunityFollowerForm { - person_id, - community_id: community.id, - pending: true, + state: Some(CommunityFollowerState::Pending), + ..CommunityFollowerForm::new(community.id, person_id) }; CommunityFollower::follow(&mut context.pool(), &form).await?; LemmyResult::Ok(()) @@ -314,54 +313,36 @@ where #[cfg(test)] #[expect(clippy::indexing_slicing)] pub(crate) mod tests { - use crate::api::user_settings_backup::{export_settings, import_settings}; - use activitypub_federation::config::Data; use actix_web::web::Json; use lemmy_api_common::context::LemmyContext; use lemmy_db_schema::{ source::{ - community::{Community, CommunityFollower, CommunityFollowerForm, CommunityInsertForm}, - instance::Instance, - local_user::{LocalUser, LocalUserInsertForm}, - person::{Person, PersonInsertForm}, + community::{ + Community, + CommunityFollower, + CommunityFollowerForm, + CommunityFollowerState, + CommunityInsertForm, + }, + local_user::LocalUser, }, traits::{Crud, Followable}, }; use lemmy_db_views::structs::LocalUserView; use lemmy_db_views_actor::structs::CommunityFollowerView; use lemmy_utils::error::{LemmyErrorType, LemmyResult}; - use pretty_assertions::assert_eq; use serial_test::serial; use std::time::Duration; use tokio::time::sleep; - pub(crate) async fn create_user( - name: String, - bio: Option, - context: &Data, - ) -> LemmyResult { - let instance = Instance::read_or_create(&mut context.pool(), "example.com".to_string()).await?; - let person_form = PersonInsertForm { - display_name: Some(name.clone()), - bio, - ..PersonInsertForm::test_form(instance.id, &name) - }; - let person = Person::create(&mut context.pool(), &person_form).await?; - - let user_form = LocalUserInsertForm::test_form(person.id); - let local_user = LocalUser::create(&mut context.pool(), &user_form, vec![]).await?; - - Ok(LocalUserView::read(&mut context.pool(), local_user.id).await?) - } - #[tokio::test] #[serial] async fn test_settings_export_import() -> LemmyResult<()> { let context = LemmyContext::init_test_context().await; + let pool = &mut context.pool(); - let export_user = - create_user("hanna".to_string(), Some("my bio".to_string()), &context).await?; + let export_user = LocalUserView::create_test_user(pool, "hanna", "my bio", false).await?; let community_form = CommunityInsertForm::new( export_user.person.instance_id, @@ -369,25 +350,24 @@ pub(crate) mod tests { "testcom".to_string(), "pubkey".to_string(), ); - let community = Community::create(&mut context.pool(), &community_form).await?; + let community = Community::create(pool, &community_form).await?; let follower_form = CommunityFollowerForm { - community_id: community.id, - person_id: export_user.person.id, - pending: false, + state: Some(CommunityFollowerState::Accepted), + ..CommunityFollowerForm::new(community.id, export_user.person.id) }; - CommunityFollower::follow(&mut context.pool(), &follower_form).await?; + CommunityFollower::follow(pool, &follower_form).await?; let backup = export_settings(export_user.clone(), context.reset_request_count()).await?; - let import_user = create_user("charles".to_string(), None, &context).await?; + let import_user = + LocalUserView::create_test_user(pool, "charles", "charles bio", false).await?; import_settings(backup, import_user.clone(), context.reset_request_count()).await?; // wait for background task to finish sleep(Duration::from_millis(1000)).await; - let import_user_updated = - LocalUserView::read(&mut context.pool(), import_user.local_user.id).await?; + let import_user_updated = LocalUserView::read(pool, import_user.local_user.id).await?; assert_eq!( export_user.person.display_name, @@ -395,13 +375,12 @@ pub(crate) mod tests { ); assert_eq!(export_user.person.bio, import_user_updated.person.bio); - let follows = - CommunityFollowerView::for_person(&mut context.pool(), import_user.person.id).await?; + let follows = CommunityFollowerView::for_person(pool, import_user.person.id).await?; assert_eq!(follows.len(), 1); assert_eq!(follows[0].community.actor_id, community.actor_id); - LocalUser::delete(&mut context.pool(), export_user.local_user.id).await?; - LocalUser::delete(&mut context.pool(), import_user.local_user.id).await?; + LocalUser::delete(pool, export_user.local_user.id).await?; + LocalUser::delete(pool, import_user.local_user.id).await?; Ok(()) } @@ -409,9 +388,9 @@ pub(crate) mod tests { #[serial] async fn disallow_large_backup() -> LemmyResult<()> { let context = LemmyContext::init_test_context().await; + let pool = &mut context.pool(); - let export_user = - create_user("hanna".to_string(), Some("my bio".to_string()), &context).await?; + let export_user = LocalUserView::create_test_user(pool, "harry", "harry bio", false).await?; let mut backup = export_settings(export_user.clone(), context.reset_request_count()).await?; @@ -426,7 +405,7 @@ pub(crate) mod tests { backup.saved_comments.push("http://example4.com".parse()?); } - let import_user = create_user("charles".to_string(), None, &context).await?; + let import_user = LocalUserView::create_test_user(pool, "sally", "sally bio", false).await?; let imported = import_settings(backup, import_user.clone(), context.reset_request_count()).await; @@ -436,8 +415,8 @@ pub(crate) mod tests { Some(LemmyErrorType::TooManyItems) ); - LocalUser::delete(&mut context.pool(), export_user.local_user.id).await?; - LocalUser::delete(&mut context.pool(), import_user.local_user.id).await?; + LocalUser::delete(pool, export_user.local_user.id).await?; + LocalUser::delete(pool, import_user.local_user.id).await?; Ok(()) } @@ -445,9 +424,9 @@ pub(crate) mod tests { #[serial] async fn import_partial_backup() -> LemmyResult<()> { let context = LemmyContext::init_test_context().await; + let pool = &mut context.pool(); - let import_user = - create_user("hanna".to_string(), Some("my bio".to_string()), &context).await?; + let import_user = LocalUserView::create_test_user(pool, "larry", "larry bio", false).await?; let backup = serde_json::from_str("{\"bot_account\": true, \"settings\": {\"theme\": \"my_theme\"}}")?; @@ -458,8 +437,7 @@ pub(crate) mod tests { ) .await?; - let import_user_updated = - LocalUserView::read(&mut context.pool(), import_user.local_user.id).await?; + let import_user_updated = LocalUserView::read(pool, import_user.local_user.id).await?; // mark as bot account assert!(import_user_updated.person.bot_account); // dont remove existing bio diff --git a/crates/apub/src/fetcher/markdown_links.rs b/crates/apub/src/fetcher/markdown_links.rs index 168958589..d83aae515 100644 --- a/crates/apub/src/fetcher/markdown_links.rs +++ b/crates/apub/src/fetcher/markdown_links.rs @@ -104,7 +104,6 @@ async fn format_actor_url( #[cfg(test)] mod tests { use super::*; - use crate::api::user_settings_backup::tests::create_user; use lemmy_db_schema::{ source::{ community::{Community, CommunityInsertForm}, @@ -112,6 +111,7 @@ mod tests { }, traits::Crud, }; + use lemmy_db_views::structs::LocalUserView; use pretty_assertions::assert_eq; use serial_test::serial; @@ -130,7 +130,8 @@ mod tests { ), ) .await?; - let user = create_user("john".to_string(), None, &context).await?; + let user = + LocalUserView::create_test_user(&mut context.pool(), "garda", "garda bio", false).await?; // insert a remote post which is already fetched let post_form = PostInsertForm { diff --git a/crates/apub/src/fetcher/post_or_comment.rs b/crates/apub/src/fetcher/post_or_comment.rs index 8502ffb1a..be48e8ebd 100644 --- a/crates/apub/src/fetcher/post_or_comment.rs +++ b/crates/apub/src/fetcher/post_or_comment.rs @@ -26,7 +26,7 @@ pub enum PostOrComment { #[serde(untagged)] pub enum PageOrNote { Page(Box), - Note(Note), + Note(Box), } #[async_trait::async_trait] @@ -61,7 +61,7 @@ impl Object for PostOrComment { async fn into_json(self, data: &Data) -> LemmyResult { Ok(match self { PostOrComment::Post(p) => PageOrNote::Page(Box::new(p.into_json(data).await?)), - PostOrComment::Comment(c) => PageOrNote::Note(c.into_json(data).await?), + PostOrComment::Comment(c) => PageOrNote::Note(Box::new(c.into_json(data).await?)), }) } @@ -81,7 +81,7 @@ impl Object for PostOrComment { async fn from_json(apub: PageOrNote, context: &Data) -> LemmyResult { Ok(match apub { PageOrNote::Page(p) => PostOrComment::Post(ApubPost::from_json(*p, context).await?), - PageOrNote::Note(n) => PostOrComment::Comment(ApubComment::from_json(n, context).await?), + PageOrNote::Note(n) => PostOrComment::Comment(ApubComment::from_json(*n, context).await?), }) } } diff --git a/crates/apub/src/fetcher/site_or_community_or_user.rs b/crates/apub/src/fetcher/site_or_community_or_user.rs index c6a1bb17e..79d7978ae 100644 --- a/crates/apub/src/fetcher/site_or_community_or_user.rs +++ b/crates/apub/src/fetcher/site_or_community_or_user.rs @@ -9,6 +9,7 @@ use activitypub_federation::{ }; use chrono::{DateTime, Utc}; use lemmy_api_common::context::LemmyContext; +use lemmy_db_schema::newtypes::InstanceId; use lemmy_utils::error::{LemmyError, LemmyResult}; use reqwest::Url; use serde::{Deserialize, Serialize}; @@ -127,3 +128,13 @@ impl Actor for SiteOrCommunityOrUser { } } } + +impl SiteOrCommunityOrUser { + pub fn instance_id(&self) -> InstanceId { + match self { + SiteOrCommunityOrUser::Site(s) => s.instance_id, + SiteOrCommunityOrUser::UserOrCommunity(UserOrCommunity::User(u)) => u.instance_id, + SiteOrCommunityOrUser::UserOrCommunity(UserOrCommunity::Community(c)) => c.instance_id, + } + } +} diff --git a/crates/apub/src/http/comment.rs b/crates/apub/src/http/comment.rs index d6b3c818d..41160234f 100644 --- a/crates/apub/src/http/comment.rs +++ b/crates/apub/src/http/comment.rs @@ -1,14 +1,10 @@ +use super::check_community_content_fetchable; use crate::{ - http::{ - check_community_public, - create_apub_response, - create_apub_tombstone_response, - redirect_remote_object, - }, + http::{create_apub_response, create_apub_tombstone_response, redirect_remote_object}, objects::comment::ApubComment, }; use activitypub_federation::{config::Data, traits::Object}; -use actix_web::{web::Path, HttpResponse}; +use actix_web::{web::Path, HttpRequest, HttpResponse}; use lemmy_api_common::context::LemmyContext; use lemmy_db_schema::{ newtypes::CommentId, @@ -28,13 +24,14 @@ pub(crate) struct CommentQuery { pub(crate) async fn get_apub_comment( info: Path, context: Data, + request: HttpRequest, ) -> LemmyResult { let id = CommentId(info.comment_id.parse::()?); // Can't use CommentView here because it excludes deleted/removed/local-only items let comment: ApubComment = Comment::read(&mut context.pool(), id).await?.into(); let post = Post::read(&mut context.pool(), comment.post_id).await?; let community = Community::read(&mut context.pool(), post.community_id).await?; - check_community_public(&community)?; + check_community_content_fetchable(&community, &request, &context).await?; if !comment.local { Ok(redirect_remote_object(&comment.ap_id)) diff --git a/crates/apub/src/http/community.rs b/crates/apub/src/http/community.rs index ca4d2eea5..96a917d91 100644 --- a/crates/apub/src/http/community.rs +++ b/crates/apub/src/http/community.rs @@ -1,24 +1,22 @@ +use super::check_community_content_fetchable; use crate::{ - activity_lists::GroupInboxActivities, collections::{ community_featured::ApubCommunityFeatured, community_follower::ApubCommunityFollower, community_moderators::ApubCommunityModerators, community_outbox::ApubCommunityOutbox, }, - http::{check_community_public, create_apub_response, create_apub_tombstone_response}, - objects::{community::ApubCommunity, person::ApubPerson}, + http::{check_community_fetchable, create_apub_response, create_apub_tombstone_response}, + objects::community::ApubCommunity, }; use activitypub_federation::{ - actix_web::inbox::receive_activity, config::Data, - protocol::context::WithContext, traits::{Collection, Object}, }; -use actix_web::{web, web::Bytes, HttpRequest, HttpResponse}; +use actix_web::{web, HttpRequest, HttpResponse}; use lemmy_api_common::context::LemmyContext; use lemmy_db_schema::{source::community::Community, traits::ApubActor}; -use lemmy_utils::{error::LemmyResult, LemmyErrorType}; +use lemmy_utils::error::{LemmyErrorType, LemmyResult}; use serde::Deserialize; #[derive(Deserialize, Clone)] @@ -41,25 +39,12 @@ pub(crate) async fn get_apub_community_http( if community.deleted || community.removed { return create_apub_tombstone_response(community.actor_id.clone()); } - check_community_public(&community)?; + check_community_fetchable(&community)?; let apub = community.into_json(&context).await?; create_apub_response(&apub) } -/// Handler for all incoming receive to community inboxes. -#[tracing::instrument(skip_all)] -pub async fn community_inbox( - request: HttpRequest, - body: Bytes, - data: Data, -) -> LemmyResult { - receive_activity::, ApubPerson, LemmyContext>( - request, body, &data, - ) - .await -} - /// Returns an empty followers collection, only populating the size (for privacy). pub(crate) async fn get_apub_community_followers( info: web::Path, @@ -68,7 +53,7 @@ pub(crate) async fn get_apub_community_followers( let community = Community::read_from_name(&mut context.pool(), &info.community_name, false) .await? .ok_or(LemmyErrorType::NotFound)?; - check_community_public(&community)?; + check_community_fetchable(&community)?; let followers = ApubCommunityFollower::read_local(&community.into(), &context).await?; create_apub_response(&followers) } @@ -78,13 +63,14 @@ pub(crate) async fn get_apub_community_followers( pub(crate) async fn get_apub_community_outbox( info: web::Path, context: Data, + request: HttpRequest, ) -> LemmyResult { let community: ApubCommunity = Community::read_from_name(&mut context.pool(), &info.community_name, false) .await? .ok_or(LemmyErrorType::NotFound)? .into(); - check_community_public(&community)?; + check_community_content_fetchable(&community, &request, &context).await?; let outbox = ApubCommunityOutbox::read_local(&community, &context).await?; create_apub_response(&outbox) } @@ -99,7 +85,7 @@ pub(crate) async fn get_apub_community_moderators( .await? .ok_or(LemmyErrorType::NotFound)? .into(); - check_community_public(&community)?; + check_community_fetchable(&community)?; let moderators = ApubCommunityModerators::read_local(&community, &context).await?; create_apub_response(&moderators) } @@ -108,13 +94,14 @@ pub(crate) async fn get_apub_community_moderators( pub(crate) async fn get_apub_community_featured( info: web::Path, context: Data, + request: HttpRequest, ) -> LemmyResult { let community: ApubCommunity = Community::read_from_name(&mut context.pool(), &info.community_name, false) .await? .ok_or(LemmyErrorType::NotFound)? .into(); - check_community_public(&community)?; + check_community_content_fetchable(&community, &request, &context).await?; let featured = ApubCommunityFeatured::read_local(&community, &context).await?; create_apub_response(&featured) } @@ -124,7 +111,7 @@ pub(crate) mod tests { use super::*; use crate::protocol::objects::{group::Group, tombstone::Tombstone}; - use actix_web::body::to_bytes; + use actix_web::{body::to_bytes, test::TestRequest}; use lemmy_db_schema::{ newtypes::InstanceId, source::{ @@ -191,6 +178,7 @@ pub(crate) mod tests { async fn test_get_community() -> LemmyResult<()> { let context = LemmyContext::init_test_context().await; let (instance, community) = init(false, CommunityVisibility::Public, &context).await?; + let request = TestRequest::default().to_http_request(); // fetch invalid community let query = CommunityQuery { @@ -210,8 +198,12 @@ pub(crate) mod tests { let group = community.clone().into_json(&context).await?; assert_eq!(group, res_group); - let res = - get_apub_community_featured(query.clone().into(), context.reset_request_count()).await?; + let res = get_apub_community_featured( + query.clone().into(), + context.reset_request_count(), + request.clone(), + ) + .await?; assert_eq!(200, res.status()); let res = get_apub_community_followers(query.clone().into(), context.reset_request_count()).await?; @@ -219,7 +211,8 @@ pub(crate) mod tests { let res = get_apub_community_moderators(query.clone().into(), context.reset_request_count()).await?; assert_eq!(200, res.status()); - let res = get_apub_community_outbox(query.into(), context.reset_request_count()).await?; + let res = + get_apub_community_outbox(query.into(), context.reset_request_count(), request).await?; assert_eq!(200, res.status()); Instance::delete(&mut context.pool(), instance.id).await?; @@ -231,6 +224,7 @@ pub(crate) mod tests { async fn test_get_deleted_community() -> LemmyResult<()> { let context = LemmyContext::init_test_context().await; let (instance, community) = init(true, CommunityVisibility::LocalOnly, &context).await?; + let request = TestRequest::default().to_http_request(); // should return tombstone let query = CommunityQuery { @@ -241,8 +235,12 @@ pub(crate) mod tests { let res_tombstone = decode_response::(res).await; assert!(res_tombstone.is_ok()); - let res = - get_apub_community_featured(query.clone().into(), context.reset_request_count()).await; + let res = get_apub_community_featured( + query.clone().into(), + context.reset_request_count(), + request.clone(), + ) + .await; assert!(res.is_err()); let res = get_apub_community_followers(query.clone().into(), context.reset_request_count()).await; @@ -250,7 +248,7 @@ pub(crate) mod tests { let res = get_apub_community_moderators(query.clone().into(), context.reset_request_count()).await; assert!(res.is_err()); - let res = get_apub_community_outbox(query.into(), context.reset_request_count()).await; + let res = get_apub_community_outbox(query.into(), context.reset_request_count(), request).await; assert!(res.is_err()); //Community::delete(&mut context.pool(), community.id).await?; @@ -263,14 +261,19 @@ pub(crate) mod tests { async fn test_get_local_only_community() -> LemmyResult<()> { let context = LemmyContext::init_test_context().await; let (instance, community) = init(false, CommunityVisibility::LocalOnly, &context).await?; + let request = TestRequest::default().to_http_request(); let query = CommunityQuery { community_name: community.name.clone(), }; let res = get_apub_community_http(query.clone().into(), context.reset_request_count()).await; assert!(res.is_err()); - let res = - get_apub_community_featured(query.clone().into(), context.reset_request_count()).await; + let res = get_apub_community_featured( + query.clone().into(), + context.reset_request_count(), + request.clone(), + ) + .await; assert!(res.is_err()); let res = get_apub_community_followers(query.clone().into(), context.reset_request_count()).await; @@ -278,7 +281,7 @@ pub(crate) mod tests { let res = get_apub_community_moderators(query.clone().into(), context.reset_request_count()).await; assert!(res.is_err()); - let res = get_apub_community_outbox(query.into(), context.reset_request_count()).await; + let res = get_apub_community_outbox(query.into(), context.reset_request_count(), request).await; assert!(res.is_err()); Instance::delete(&mut context.pool(), instance.id).await?; diff --git a/crates/apub/src/http/mod.rs b/crates/apub/src/http/mod.rs index dad39881f..d79cd3d55 100644 --- a/crates/apub/src/http/mod.rs +++ b/crates/apub/src/http/mod.rs @@ -1,11 +1,11 @@ use crate::{ activity_lists::SharedInboxActivities, - fetcher::user_or_community::UserOrCommunity, + fetcher::{site_or_community_or_user::SiteOrCommunityOrUser, user_or_community::UserOrCommunity}, protocol::objects::tombstone::Tombstone, FEDERATION_CONTEXT, }; use activitypub_federation::{ - actix_web::inbox::receive_activity, + actix_web::{inbox::receive_activity, signing_actor}, config::Data, protocol::context::WithContext, FEDERATION_CONTENT_TYPE, @@ -17,7 +17,8 @@ use lemmy_db_schema::{ source::{activity::SentActivity, community::Community}, CommunityVisibility, }; -use lemmy_utils::error::{LemmyErrorType, LemmyResult}; +use lemmy_db_views_actor::structs::CommunityFollowerView; +use lemmy_utils::error::{FederationError, LemmyErrorType, LemmyResult}; use serde::{Deserialize, Serialize}; use std::{ops::Deref, time::Duration}; use tokio::time::timeout; @@ -45,7 +46,7 @@ pub async fn shared_inbox( // consider the activity broken and move on. timeout(INCOMING_ACTIVITY_TIMEOUT, receive_fut) .await - .map_err(|_| LemmyErrorType::InboxTimeout)? + .map_err(|_| FederationError::InboxTimeout)? } /// Convert the data to json and turn it into an HTTP Response with the correct ActivityPub @@ -106,7 +107,9 @@ pub(crate) async fn get_activity( info.id ))? .into(); - let activity = SentActivity::read_from_apub_id(&mut context.pool(), &activity_id).await?; + let activity = SentActivity::read_from_apub_id(&mut context.pool(), &activity_id) + .await + .map_err(|_| FederationError::CouldntFindActivity)?; let sensitive = activity.sensitive; if sensitive { @@ -117,12 +120,46 @@ pub(crate) async fn get_activity( } /// Ensure that the community is public and not removed/deleted. -fn check_community_public(community: &Community) -> LemmyResult<()> { - if community.deleted || community.removed { - Err(LemmyErrorType::Deleted)? - } - if community.visibility != CommunityVisibility::Public { +fn check_community_fetchable(community: &Community) -> LemmyResult<()> { + check_community_removed_or_deleted(community)?; + if community.visibility == CommunityVisibility::LocalOnly { return Err(LemmyErrorType::NotFound.into()); } Ok(()) } + +/// Check if posts or comments in the community are allowed to be fetched +async fn check_community_content_fetchable( + community: &Community, + request: &HttpRequest, + context: &Data, +) -> LemmyResult<()> { + use CommunityVisibility::*; + check_community_removed_or_deleted(community)?; + match community.visibility { + // content in public community can always be fetched + Public => Ok(()), + // no federation for local only community + LocalOnly => Err(LemmyErrorType::NotFound.into()), + // for private community check http signature of request, if there is any approved follower + // from the fetching instance then fetching is allowed + Private => { + let signing_actor = signing_actor::(request, None, context).await?; + Ok( + CommunityFollowerView::check_has_followers_from_instance( + community.id, + signing_actor.instance_id(), + &mut context.pool(), + ) + .await?, + ) + } + } +} + +fn check_community_removed_or_deleted(community: &Community) -> LemmyResult<()> { + if community.deleted || community.removed { + Err(LemmyErrorType::Deleted)? + } + Ok(()) +} diff --git a/crates/apub/src/http/person.rs b/crates/apub/src/http/person.rs index e8e072a97..f8afceb94 100644 --- a/crates/apub/src/http/person.rs +++ b/crates/apub/src/http/person.rs @@ -1,20 +1,13 @@ use crate::{ - activity_lists::PersonInboxActivities, - fetcher::user_or_community::UserOrCommunity, http::{create_apub_response, create_apub_tombstone_response}, objects::person::ApubPerson, protocol::collections::empty_outbox::EmptyOutbox, }; -use activitypub_federation::{ - actix_web::inbox::receive_activity, - config::Data, - protocol::context::WithContext, - traits::Object, -}; -use actix_web::{web, web::Bytes, HttpRequest, HttpResponse}; +use activitypub_federation::{config::Data, traits::Object}; +use actix_web::{web, HttpResponse}; use lemmy_api_common::{context::LemmyContext, utils::generate_outbox_url}; use lemmy_db_schema::{source::person::Person, traits::ApubActor}; -use lemmy_utils::{error::LemmyResult, LemmyErrorType}; +use lemmy_utils::error::{LemmyErrorType, LemmyResult}; use serde::Deserialize; #[derive(Deserialize)] @@ -44,18 +37,6 @@ pub(crate) async fn get_apub_person_http( } } -#[tracing::instrument(skip_all)] -pub async fn person_inbox( - request: HttpRequest, - body: Bytes, - data: Data, -) -> LemmyResult { - receive_activity::, UserOrCommunity, LemmyContext>( - request, body, &data, - ) - .await -} - #[tracing::instrument(skip_all)] pub(crate) async fn get_apub_person_outbox( info: web::Path, diff --git a/crates/apub/src/http/post.rs b/crates/apub/src/http/post.rs index ce6612826..6afb9fc3e 100644 --- a/crates/apub/src/http/post.rs +++ b/crates/apub/src/http/post.rs @@ -1,14 +1,10 @@ +use super::check_community_content_fetchable; use crate::{ - http::{ - check_community_public, - create_apub_response, - create_apub_tombstone_response, - redirect_remote_object, - }, + http::{create_apub_response, create_apub_tombstone_response, redirect_remote_object}, objects::post::ApubPost, }; use activitypub_federation::{config::Data, traits::Object}; -use actix_web::{web, HttpResponse}; +use actix_web::{web, HttpRequest, HttpResponse}; use lemmy_api_common::context::LemmyContext; use lemmy_db_schema::{ newtypes::PostId, @@ -28,12 +24,14 @@ pub(crate) struct PostQuery { pub(crate) async fn get_apub_post( info: web::Path, context: Data, + request: HttpRequest, ) -> LemmyResult { let id = PostId(info.post_id.parse::()?); // Can't use PostView here because it excludes deleted/removed/local-only items let post: ApubPost = Post::read(&mut context.pool(), id).await?.into(); let community = Community::read(&mut context.pool(), post.community_id).await?; - check_community_public(&community)?; + + check_community_content_fetchable(&community, &request, &context).await?; if !post.local { Ok(redirect_remote_object(&post.ap_id)) diff --git a/crates/apub/src/http/routes.rs b/crates/apub/src/http/routes.rs index ab046afe1..9479e6312 100644 --- a/crates/apub/src/http/routes.rs +++ b/crates/apub/src/http/routes.rs @@ -1,7 +1,6 @@ use crate::http::{ comment::get_apub_comment, community::{ - community_inbox, get_apub_community_featured, get_apub_community_followers, get_apub_community_http, @@ -9,7 +8,7 @@ use crate::http::{ get_apub_community_outbox, }, get_activity, - person::{get_apub_person_http, get_apub_person_outbox, person_inbox}, + person::{get_apub_person_http, get_apub_person_outbox}, post::get_apub_post, shared_inbox, site::{get_apub_site_http, get_apub_site_outbox}, @@ -56,8 +55,6 @@ pub fn config(cfg: &mut web::ServiceConfig) { cfg.service( web::scope("") .guard(InboxRequestGuard) - .route("/c/{community_name}/inbox", web::post().to(community_inbox)) - .route("/u/{user_name}/inbox", web::post().to(person_inbox)) .route("/inbox", web::post().to(shared_inbox)), ); } diff --git a/crates/apub/src/lib.rs b/crates/apub/src/lib.rs index c8506da52..e11475d6c 100644 --- a/crates/apub/src/lib.rs +++ b/crates/apub/src/lib.rs @@ -10,7 +10,7 @@ use lemmy_db_schema::{ utils::{ActualDbPool, DbPool}, }; use lemmy_utils::{ - error::{LemmyError, LemmyErrorType, LemmyResult}, + error::{FederationError, LemmyError, LemmyErrorType, LemmyResult}, CACHE_DURATION_FEDERATION, }; use moka::future::Cache; @@ -51,17 +51,27 @@ impl UrlVerifier for VerifyUrlData { let local_site_data = local_site_data_cached(&mut (&self.0).into()) .await .expect("read local site data"); + use FederationError::*; check_apub_id_valid(url, &local_site_data).map_err(|err| match err { LemmyError { - error_type: LemmyErrorType::FederationDisabled, + error_type: + LemmyErrorType::FederationError { + error: Some(FederationDisabled), + }, .. } => ActivityPubError::Other("Federation disabled".into()), LemmyError { - error_type: LemmyErrorType::DomainBlocked(domain), + error_type: + LemmyErrorType::FederationError { + error: Some(DomainBlocked(domain)), + }, .. } => ActivityPubError::Other(format!("Domain {domain:?} is blocked")), LemmyError { - error_type: LemmyErrorType::DomainNotInAllowList(domain), + error_type: + LemmyErrorType::FederationError { + error: Some(DomainNotInAllowList(domain)), + }, .. } => ActivityPubError::Other(format!("Domain {domain:?} is not in allowlist")), _ => ActivityPubError::Other("Failed validating apub id".into()), @@ -81,7 +91,7 @@ impl UrlVerifier for VerifyUrlData { fn check_apub_id_valid(apub_id: &Url, local_site_data: &LocalSiteData) -> LemmyResult<()> { let domain = apub_id .domain() - .ok_or(LemmyErrorType::UrlWithoutDomain)? + .ok_or(FederationError::UrlWithoutDomain)? .to_string(); if !local_site_data @@ -90,7 +100,7 @@ fn check_apub_id_valid(apub_id: &Url, local_site_data: &LocalSiteData) -> LemmyR .map(|l| l.federation_enabled) .unwrap_or(true) { - Err(LemmyErrorType::FederationDisabled)? + Err(FederationError::FederationDisabled)? } if local_site_data @@ -98,7 +108,7 @@ fn check_apub_id_valid(apub_id: &Url, local_site_data: &LocalSiteData) -> LemmyR .iter() .any(|i| domain.to_lowercase().eq(&i.domain.to_lowercase())) { - Err(LemmyErrorType::DomainBlocked(domain.clone()))? + Err(FederationError::DomainBlocked(domain.clone()))? } // Only check this if there are instances in the allowlist @@ -108,7 +118,7 @@ fn check_apub_id_valid(apub_id: &Url, local_site_data: &LocalSiteData) -> LemmyR .iter() .any(|i| domain.to_lowercase().eq(&i.domain.to_lowercase())) { - Err(LemmyErrorType::DomainNotInAllowList(domain))? + Err(FederationError::DomainNotInAllowList(domain))? } Ok(()) @@ -164,7 +174,7 @@ pub(crate) async fn check_apub_id_valid_with_strictness( ) -> LemmyResult<()> { let domain = apub_id .domain() - .ok_or(LemmyErrorType::UrlWithoutDomain)? + .ok_or(FederationError::UrlWithoutDomain)? .to_string(); let local_instance = context .settings() @@ -194,10 +204,10 @@ pub(crate) async fn check_apub_id_valid_with_strictness( let domain = apub_id .domain() - .ok_or(LemmyErrorType::UrlWithoutDomain)? + .ok_or(FederationError::UrlWithoutDomain)? .to_string(); if !allowed_and_local.contains(&domain) { - Err(LemmyErrorType::FederationDisabledByStrictAllowList)? + Err(FederationError::FederationDisabledByStrictAllowList)? } } Ok(()) diff --git a/crates/apub/src/mentions.rs b/crates/apub/src/mentions.rs index 6aa1d97fb..cb46be52a 100644 --- a/crates/apub/src/mentions.rs +++ b/crates/apub/src/mentions.rs @@ -11,7 +11,10 @@ use lemmy_db_schema::{ traits::Crud, utils::DbPool, }; -use lemmy_utils::{error::LemmyResult, utils::mention::scrape_text_for_mentions, LemmyErrorType}; +use lemmy_utils::{ + error::{FederationError, LemmyResult}, + utils::mention::scrape_text_for_mentions, +}; use serde::{Deserialize, Serialize}; use serde_json::Value; use url::Url; @@ -57,7 +60,7 @@ pub async fn collect_non_local_mentions( &parent_creator .id() .domain() - .ok_or(LemmyErrorType::UrlWithoutDomain)? + .ok_or(FederationError::UrlWithoutDomain)? )), kind: MentionType::Mention, }; diff --git a/crates/apub/src/objects/comment.rs b/crates/apub/src/objects/comment.rs index b6b411ec6..b7c6a5f51 100644 --- a/crates/apub/src/objects/comment.rs +++ b/crates/apub/src/objects/comment.rs @@ -1,9 +1,9 @@ use crate::{ - activities::{verify_is_public, verify_person_in_community}, + activities::{generate_to, verify_person_in_community, verify_visibility}, check_apub_id_valid_with_strictness, fetcher::markdown_links::markdown_rewrite_remote_links, mentions::collect_non_local_mentions, - objects::{read_from_string_or_source, verify_is_remote_object}, + objects::{append_attachments_to_comment, read_from_string_or_source, verify_is_remote_object}, protocol::{ objects::{note::Note, LanguageTag}, InCommunity, @@ -12,7 +12,7 @@ use crate::{ }; use activitypub_federation::{ config::Data, - kinds::{object::NoteType, public}, + kinds::object::NoteType, protocol::{values::MediaTypeMarkdownOrHtml, verification::verify_domains_match}, traits::Object, }; @@ -33,7 +33,7 @@ use lemmy_db_schema::{ utils::naive_now, }; use lemmy_utils::{ - error::{LemmyError, LemmyErrorType, LemmyResult}, + error::{FederationError, LemmyError, LemmyResult}, utils::markdown::markdown_to_html, }; use std::ops::Deref; @@ -112,7 +112,7 @@ impl Object for ApubComment { r#type: NoteType::Note, id: self.ap_id.clone().into(), attributed_to: creator.actor_id.into(), - to: vec![public()], + to: vec![generate_to(&community)?], cc: maa.ccs, content: markdown_to_html(&self.content), media_type: Some(MediaTypeMarkdownOrHtml::Html), @@ -124,6 +124,7 @@ impl Object for ApubComment { distinguished: Some(self.distinguished), language, audience: Some(community.actor_id.into()), + attachment: vec![], }; Ok(note) @@ -139,8 +140,8 @@ impl Object for ApubComment { ) -> LemmyResult<()> { verify_domains_match(note.id.inner(), expected_domain)?; verify_domains_match(note.attributed_to.inner(), note.id.inner())?; - verify_is_public(¬e.to, ¬e.cc)?; let community = Box::pin(note.community(context)).await?; + verify_visibility(¬e.to, ¬e.cc, &community)?; Box::pin(check_apub_id_valid_with_strictness( note.id.inner(), @@ -162,7 +163,7 @@ impl Object for ApubComment { .await .is_ok(); if post.locked && !is_mod_or_admin { - Err(LemmyErrorType::PostIsLocked)? + Err(FederationError::PostIsLocked)? } else { Ok(()) } @@ -181,6 +182,7 @@ impl Object for ApubComment { let local_site = LocalSite::read(&mut context.pool()).await.ok(); let slur_regex = &local_site_opt_to_slur_regex(&local_site); let url_blocklist = get_url_blocklist(context).await?; + let content = append_attachments_to_comment(content, ¬e.attachment, context).await?; let content = process_markdown(&content, slur_regex, &url_blocklist, context).await?; let content = markdown_rewrite_remote_links(content, context).await; let language_id = Some( @@ -247,13 +249,13 @@ pub(crate) mod tests { } async fn cleanup( - data: (ApubPerson, ApubCommunity, ApubPost, ApubSite), + (person, community, post, site): (ApubPerson, ApubCommunity, ApubPost, ApubSite), context: &LemmyContext, ) -> LemmyResult<()> { - Post::delete(&mut context.pool(), data.2.id).await?; - Community::delete(&mut context.pool(), data.1.id).await?; - Person::delete(&mut context.pool(), data.0.id).await?; - Site::delete(&mut context.pool(), data.3.id).await?; + Post::delete(&mut context.pool(), post.id).await?; + Community::delete(&mut context.pool(), community.id).await?; + Person::delete(&mut context.pool(), person.id).await?; + Site::delete(&mut context.pool(), site.id).await?; LocalSite::delete(&mut context.pool()).await?; Ok(()) } diff --git a/crates/apub/src/objects/community.rs b/crates/apub/src/objects/community.rs index 2c60b87b3..efa2c5247 100644 --- a/crates/apub/src/objects/community.rs +++ b/crates/apub/src/objects/community.rs @@ -5,7 +5,7 @@ use crate::{ local_site_data_cached, objects::{instance::fetch_instance_actor_for_object, read_from_string_or_source_opt}, protocol::{ - objects::{group::Group, Endpoints, LanguageTag}, + objects::{group::Group, LanguageTag}, ImageObject, Source, }, @@ -13,6 +13,7 @@ use crate::{ use activitypub_federation::{ config::Data, kinds::actor::GroupType, + protocol::values::MediaTypeHtml, traits::{Actor, Object}, }; use chrono::{DateTime, Utc}; @@ -38,6 +39,7 @@ use lemmy_db_schema::{ }, traits::{ApubActor, Crud}, utils::naive_now, + CommunityVisibility, }; use lemmy_db_views_actor::structs::CommunityFollowerView; use lemmy_utils::{ @@ -107,8 +109,10 @@ impl Object for ApubCommunity { id: self.id().into(), preferred_username: self.name.clone(), name: Some(self.title.clone()), - summary: self.description.as_ref().map(|b| markdown_to_html(b)), - source: self.description.clone().map(Source::new), + content: self.sidebar.as_ref().map(|d| markdown_to_html(d)), + source: self.sidebar.clone().map(Source::new), + summary: self.description.clone(), + media_type: self.sidebar.as_ref().map(|_| MediaTypeHtml::Html), icon: self.icon.clone().map(ImageObject::new), image: self.banner.clone().map(ImageObject::new), sensitive: Some(self.nsfw), @@ -116,15 +120,14 @@ impl Object for ApubCommunity { inbox: self.inbox_url.clone().into(), outbox: generate_outbox_url(&self.actor_id)?.into(), followers: self.followers_url.clone().map(Into::into), - endpoints: self.shared_inbox_url.clone().map(|s| Endpoints { - shared_inbox: s.into(), - }), + endpoints: None, public_key: self.public_key(), language, published: Some(self.published), updated: self.updated, posting_restricted_to_mods: Some(self.posting_restricted_to_mods), attributed_to: Some(generate_moderators_url(&self.actor_id)?.into()), + manually_approves_followers: Some(self.visibility == CommunityVisibility::Private), }; Ok(group) } @@ -146,13 +149,16 @@ impl Object for ApubCommunity { let local_site = LocalSite::read(&mut context.pool()).await.ok(); let slur_regex = &local_site_opt_to_slur_regex(&local_site); let url_blocklist = get_url_blocklist(context).await?; - let description = read_from_string_or_source_opt(&group.summary, &None, &group.source); - let description = - process_markdown_opt(&description, slur_regex, &url_blocklist, context).await?; - let description = markdown_rewrite_remote_links_opt(description, context).await; + let sidebar = read_from_string_or_source_opt(&group.content, &None, &group.source); + let sidebar = process_markdown_opt(&sidebar, slur_regex, &url_blocklist, context).await?; + let sidebar = markdown_rewrite_remote_links_opt(sidebar, context).await; let icon = proxy_image_link_opt_apub(group.icon.map(|i| i.url), context).await?; let banner = proxy_image_link_opt_apub(group.image.map(|i| i.url), context).await?; - + let visibility = Some(if group.manually_approves_followers.unwrap_or_default() { + CommunityVisibility::Private + } else { + CommunityVisibility::Public + }); let form = CommunityInsertForm { published: group.published, updated: group.updated, @@ -163,13 +169,20 @@ impl Object for ApubCommunity { last_refreshed_at: Some(naive_now()), icon, banner, - description, + sidebar, + description: group.summary, followers_url: group.followers.clone().map(Into::into), - inbox_url: Some(group.inbox.into()), - shared_inbox_url: group.endpoints.map(|e| e.shared_inbox.into()), + inbox_url: Some( + group + .endpoints + .map(|e| e.shared_inbox) + .unwrap_or(group.inbox) + .into(), + ), moderators_url: group.attributed_to.clone().map(Into::into), posting_restricted_to_mods: group.posting_restricted_to_mods, featured_url: group.featured.clone().map(Into::into), + visibility, ..CommunityInsertForm::new( instance_id, group.preferred_username.clone(), @@ -225,7 +238,7 @@ impl Actor for ApubCommunity { } fn shared_inbox(&self) -> Option { - self.shared_inbox_url.clone().map(Into::into) + None } } @@ -296,10 +309,16 @@ pub(crate) mod tests { assert_eq!(community.title, "Ten Forward"); assert!(!community.local); + + // Test the sidebar and description assert_eq!( - community.description.as_ref().map(std::string::String::len), + community.sidebar.as_ref().map(std::string::String::len), Some(63) ); + assert_eq!( + community.description, + Some("A description of ten forward.".into()) + ); Community::delete(&mut context.pool(), community.id).await?; Site::delete(&mut context.pool(), site.id).await?; diff --git a/crates/apub/src/objects/instance.rs b/crates/apub/src/objects/instance.rs index 6ee0a41dc..a123c85ba 100644 --- a/crates/apub/src/objects/instance.rs +++ b/crates/apub/src/objects/instance.rs @@ -42,12 +42,11 @@ use lemmy_db_schema::{ utils::naive_now, }; use lemmy_utils::{ - error::{LemmyError, LemmyResult}, + error::{FederationError, LemmyError, LemmyResult}, utils::{ markdown::markdown_to_html, slurs::{check_slurs, check_slurs_opt}, }, - LemmyErrorType, }; use std::ops::Deref; use tracing::debug; @@ -89,7 +88,7 @@ impl Object for ApubSite { } async fn delete(self, _data: &Data) -> LemmyResult<()> { - Err(LemmyErrorType::CantDeleteSite.into()) + Err(FederationError::CantDeleteSite.into()) } #[tracing::instrument(skip_all)] @@ -144,7 +143,7 @@ impl Object for ApubSite { .id .inner() .domain() - .ok_or(LemmyErrorType::UrlWithoutDomain)?; + .ok_or(FederationError::UrlWithoutDomain)?; let instance = DbInstance::read_or_create(&mut context.pool(), domain.to_string()).await?; let local_site = LocalSite::read(&mut context.pool()).await.ok(); @@ -220,7 +219,7 @@ pub(in crate::objects) async fn fetch_instance_actor_for_object + C debug!("Failed to dereference site for {}: {}", &instance_id, e); let domain = instance_id .domain() - .ok_or(LemmyErrorType::UrlWithoutDomain)?; + .ok_or(FederationError::UrlWithoutDomain)?; Ok( DbInstance::read_or_create(&mut context.pool(), domain.to_string()) .await? diff --git a/crates/apub/src/objects/mod.rs b/crates/apub/src/objects/mod.rs index e199ebfad..f837f7ad3 100644 --- a/crates/apub/src/objects/mod.rs +++ b/crates/apub/src/objects/mod.rs @@ -1,4 +1,4 @@ -use crate::protocol::Source; +use crate::protocol::{objects::page::Attachment, Source}; use activitypub_federation::{ config::Data, fetch::object_id::ObjectId, @@ -46,6 +46,23 @@ pub(crate) fn read_from_string_or_source_opt( .map(|content| read_from_string_or_source(content, media_type, source)) } +pub(crate) async fn append_attachments_to_comment( + content: String, + attachments: &[Attachment], + context: &Data, +) -> LemmyResult { + let mut content = content; + // Don't modify comments with no attachments + if !attachments.is_empty() { + content += "\n"; + for attachment in attachments { + content = content + "\n" + &attachment.as_markdown(context).await?; + } + } + + Ok(content) +} + /// When for example a Post is made in a remote community, the community will send it back, /// wrapped in Announce. If we simply receive this like any other federated object, overwrite the /// existing, local Post. In particular, it will set the field local = false, so that the object diff --git a/crates/apub/src/objects/person.rs b/crates/apub/src/objects/person.rs index 406a77d94..737579662 100644 --- a/crates/apub/src/objects/person.rs +++ b/crates/apub/src/objects/person.rs @@ -6,10 +6,7 @@ use crate::{ local_site_data_cached, objects::{instance::fetch_instance_actor_for_object, read_from_string_or_source_opt}, protocol::{ - objects::{ - person::{Person, UserTypes}, - Endpoints, - }, + objects::person::{Person, UserTypes}, ImageObject, Source, }, @@ -118,9 +115,7 @@ impl Object for ApubPerson { matrix_user_id: self.matrix_user_id.clone(), published: Some(self.published), outbox: generate_outbox_url(&self.actor_id)?.into(), - endpoints: self.shared_inbox_url.clone().map(|s| Endpoints { - shared_inbox: s.into(), - }), + endpoints: None, public_key: self.public_key(), updated: self.updated, inbox: self.inbox_url.clone().into(), @@ -182,8 +177,13 @@ impl Object for ApubPerson { private_key: None, public_key: person.public_key.public_key_pem, last_refreshed_at: Some(naive_now()), - inbox_url: Some(person.inbox.into()), - shared_inbox_url: person.endpoints.map(|e| e.shared_inbox.into()), + inbox_url: Some( + person + .endpoints + .map(|e| e.shared_inbox) + .unwrap_or(person.inbox) + .into(), + ), matrix_user_id: person.matrix_user_id, instance_id, }; @@ -211,7 +211,7 @@ impl Actor for ApubPerson { } fn shared_inbox(&self) -> Option { - self.shared_inbox_url.clone().map(Into::into) + None } } @@ -285,9 +285,12 @@ pub(crate) mod tests { Ok(()) } - async fn cleanup(data: (ApubPerson, ApubSite), context: &LemmyContext) -> LemmyResult<()> { - DbPerson::delete(&mut context.pool(), data.0.id).await?; - Site::delete(&mut context.pool(), data.1.id).await?; + async fn cleanup( + (person, site): (ApubPerson, ApubSite), + context: &LemmyContext, + ) -> LemmyResult<()> { + DbPerson::delete(&mut context.pool(), person.id).await?; + Site::delete(&mut context.pool(), site.id).await?; Ok(()) } } diff --git a/crates/apub/src/objects/post.rs b/crates/apub/src/objects/post.rs index ee88cf3ec..b72fa1728 100644 --- a/crates/apub/src/objects/post.rs +++ b/crates/apub/src/objects/post.rs @@ -1,5 +1,5 @@ use crate::{ - activities::{verify_is_public, verify_person_in_community}, + activities::{generate_to, verify_person_in_community, verify_visibility}, check_apub_id_valid_with_strictness, fetcher::markdown_links::{markdown_rewrite_remote_links_opt, to_local_url}, local_site_data_cached, @@ -16,7 +16,6 @@ use crate::{ }; use activitypub_federation::{ config::Data, - kinds::public, protocol::{values::MediaTypeMarkdownOrHtml, verification::verify_domains_match}, traits::Object, }; @@ -135,7 +134,7 @@ impl Object for ApubPost { kind: PageType::Page, id: self.ap_id.clone().into(), attributed_to: AttributedTo::Lemmy(creator.actor_id.into()), - to: vec![community.actor_id.clone().into(), public()], + to: vec![generate_to(&community)?], cc: vec![], name: Some(self.name.clone()), content: self.body.as_ref().map(|b| markdown_to_html(b)), @@ -172,7 +171,7 @@ impl Object for ApubPost { check_slurs_opt(&page.name, slur_regex)?; verify_domains_match(page.creator()?.inner(), page.id.inner())?; - verify_is_public(&page.to, &page.cc)?; + verify_visibility(&page.to, &page.cc, &community)?; Ok(()) } diff --git a/crates/apub/src/objects/private_message.rs b/crates/apub/src/objects/private_message.rs index 3ed5b3572..f3a9f140c 100644 --- a/crates/apub/src/objects/private_message.rs +++ b/crates/apub/src/objects/private_message.rs @@ -16,7 +16,12 @@ use activitypub_federation::{ use chrono::{DateTime, Utc}; use lemmy_api_common::{ context::LemmyContext, - utils::{get_url_blocklist, local_site_opt_to_slur_regex, process_markdown}, + utils::{ + check_private_messages_enabled, + get_url_blocklist, + local_site_opt_to_slur_regex, + process_markdown, + }, }; use lemmy_db_schema::{ source::{ @@ -28,8 +33,9 @@ use lemmy_db_schema::{ traits::Crud, utils::naive_now, }; +use lemmy_db_views::structs::LocalUserView; use lemmy_utils::{ - error::{LemmyError, LemmyErrorType, LemmyResult}, + error::{FederationError, LemmyError, LemmyErrorType, LemmyResult}, utils::markdown::markdown_to_html, }; use std::ops::Deref; @@ -113,7 +119,7 @@ impl Object for ApubPrivateMessage { check_apub_id_valid_with_strictness(note.id.inner(), false, context).await?; let person = note.attributed_to.dereference(context).await?; if person.banned { - Err(LemmyErrorType::PersonIsBannedFromSite( + Err(FederationError::PersonIsBannedFromSite( person.actor_id.to_string(), ))? } else { @@ -130,9 +136,16 @@ impl Object for ApubPrivateMessage { let recipient = note.to[0].dereference(context).await?; PersonBlock::read(&mut context.pool(), recipient.id, creator.id).await?; + // Check that they can receive private messages + if let Ok(recipient_local_user) = + LocalUserView::read_person(&mut context.pool(), recipient.id).await + { + check_private_messages_enabled(&recipient_local_user)?; + } let local_site = LocalSite::read(&mut context.pool()).await.ok(); let slur_regex = &local_site_opt_to_slur_regex(&local_site); let url_blocklist = get_url_blocklist(context).await?; + let content = read_from_string_or_source(¬e.content, &None, ¬e.source); let content = process_markdown(&content, slur_regex, &url_blocklist, context).await?; let content = markdown_rewrite_remote_links(content, context).await; @@ -186,12 +199,12 @@ mod tests { } async fn cleanup( - data: (ApubPerson, ApubPerson, ApubSite), + (person1, person2, site): (ApubPerson, ApubPerson, ApubSite), context: &Data, ) -> LemmyResult<()> { - Person::delete(&mut context.pool(), data.0.id).await?; - Person::delete(&mut context.pool(), data.1.id).await?; - Site::delete(&mut context.pool(), data.2.id).await?; + Person::delete(&mut context.pool(), person1.id).await?; + Person::delete(&mut context.pool(), person2.id).await?; + Site::delete(&mut context.pool(), site.id).await?; Ok(()) } diff --git a/crates/apub/src/protocol/activities/following/mod.rs b/crates/apub/src/protocol/activities/following/mod.rs index ec263adae..1bb805608 100644 --- a/crates/apub/src/protocol/activities/following/mod.rs +++ b/crates/apub/src/protocol/activities/following/mod.rs @@ -1,5 +1,6 @@ pub(crate) mod accept; pub mod follow; +pub(crate) mod reject; pub mod undo_follow; #[cfg(test)] diff --git a/crates/apub/src/protocol/activities/following/reject.rs b/crates/apub/src/protocol/activities/following/reject.rs new file mode 100644 index 000000000..1584dfb11 --- /dev/null +++ b/crates/apub/src/protocol/activities/following/reject.rs @@ -0,0 +1,24 @@ +use crate::{ + objects::{community::ApubCommunity, person::ApubPerson}, + protocol::activities::following::follow::Follow, +}; +use activitypub_federation::{ + fetch::object_id::ObjectId, + kinds::activity::RejectType, + protocol::helpers::deserialize_skip_error, +}; +use serde::{Deserialize, Serialize}; +use url::Url; + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct RejectFollow { + pub(crate) actor: ObjectId, + /// Optional, for compatibility with platforms that always expect recipient field + #[serde(deserialize_with = "deserialize_skip_error", default)] + pub(crate) to: Option<[ObjectId; 1]>, + pub(crate) object: Follow, + #[serde(rename = "type")] + pub(crate) kind: RejectType, + pub(crate) id: Url, +} diff --git a/crates/apub/src/protocol/activities/voting/vote.rs b/crates/apub/src/protocol/activities/voting/vote.rs index 9fae264a5..883fc85fb 100644 --- a/crates/apub/src/protocol/activities/voting/vote.rs +++ b/crates/apub/src/protocol/activities/voting/vote.rs @@ -6,7 +6,7 @@ use crate::{ }; use activitypub_federation::{config::Data, fetch::object_id::ObjectId}; use lemmy_api_common::context::LemmyContext; -use lemmy_utils::error::{LemmyError, LemmyErrorType, LemmyResult}; +use lemmy_utils::error::{FederationError, LemmyError, LemmyResult}; use serde::{Deserialize, Serialize}; use strum::Display; use url::Url; @@ -35,7 +35,7 @@ impl TryFrom for VoteType { match value { 1 => Ok(VoteType::Like), -1 => Ok(VoteType::Dislike), - _ => Err(LemmyErrorType::InvalidVoteValue.into()), + _ => Err(FederationError::InvalidVoteValue.into()), } } } diff --git a/crates/apub/src/protocol/objects/group.rs b/crates/apub/src/protocol/objects/group.rs index 8f138e001..dbf4af892 100644 --- a/crates/apub/src/protocol/objects/group.rs +++ b/crates/apub/src/protocol/objects/group.rs @@ -7,7 +7,7 @@ use crate::{ community_outbox::ApubCommunityOutbox, }, local_site_data_cached, - objects::{community::ApubCommunity, read_from_string_or_source_opt}, + objects::community::ApubCommunity, protocol::{ objects::{Endpoints, LanguageTag}, ImageObject, @@ -21,6 +21,7 @@ use activitypub_federation::{ protocol::{ helpers::deserialize_skip_error, public_key::PublicKey, + values::MediaTypeHtml, verification::verify_domains_match, }, }; @@ -50,9 +51,13 @@ pub struct Group { /// title pub(crate) name: Option, - pub(crate) summary: Option, + // sidebar + pub(crate) content: Option, #[serde(deserialize_with = "deserialize_skip_error", default)] pub(crate) source: Option, + pub(crate) media_type: Option, + // short instance description + pub(crate) summary: Option, #[serde(deserialize_with = "deserialize_skip_error", default)] pub(crate) icon: Option, /// banner @@ -68,6 +73,8 @@ pub struct Group { pub(crate) featured: Option>, #[serde(default)] pub(crate) language: Vec, + /// True if this is a private community + pub(crate) manually_approves_followers: Option, pub(crate) published: Option>, pub(crate) updated: Option>, } @@ -86,8 +93,7 @@ impl Group { check_slurs(&self.preferred_username, slur_regex)?; check_slurs_opt(&self.name, slur_regex)?; - let description = read_from_string_or_source_opt(&self.summary, &None, &self.source); - check_slurs_opt(&description, slur_regex)?; + check_slurs_opt(&self.summary, slur_regex)?; Ok(()) } } diff --git a/crates/apub/src/protocol/objects/instance.rs b/crates/apub/src/protocol/objects/instance.rs index 1f21e76da..0eef948e7 100644 --- a/crates/apub/src/protocol/objects/instance.rs +++ b/crates/apub/src/protocol/objects/instance.rs @@ -32,9 +32,9 @@ pub struct Instance { pub(crate) content: Option, #[serde(deserialize_with = "deserialize_skip_error", default)] pub(crate) source: Option, + pub(crate) media_type: Option, // short instance description pub(crate) summary: Option, - pub(crate) media_type: Option, /// instance icon pub(crate) icon: Option, /// instance banner diff --git a/crates/apub/src/protocol/objects/mod.rs b/crates/apub/src/protocol/objects/mod.rs index dbba1bb8a..00fe26d2b 100644 --- a/crates/apub/src/protocol/objects/mod.rs +++ b/crates/apub/src/protocol/objects/mod.rs @@ -145,7 +145,8 @@ mod tests { #[test] fn test_parse_objects_mastodon() -> LemmyResult<()> { test_json::("assets/mastodon/objects/person.json")?; - test_json::("assets/mastodon/objects/note.json")?; + test_json::("assets/mastodon/objects/note_1.json")?; + test_json::("assets/mastodon/objects/note_2.json")?; test_json::("assets/mastodon/objects/page.json")?; Ok(()) } diff --git a/crates/apub/src/protocol/objects/note.rs b/crates/apub/src/protocol/objects/note.rs index e3e204254..fc38b9b5e 100644 --- a/crates/apub/src/protocol/objects/note.rs +++ b/crates/apub/src/protocol/objects/note.rs @@ -3,7 +3,11 @@ use crate::{ fetcher::post_or_comment::PostOrComment, mentions::MentionOrValue, objects::{comment::ApubComment, community::ApubCommunity, person::ApubPerson, post::ApubPost}, - protocol::{objects::LanguageTag, InCommunity, Source}, + protocol::{ + objects::{page::Attachment, LanguageTag}, + InCommunity, + Source, + }, }; use activitypub_federation::{ config::Data, @@ -20,7 +24,10 @@ use lemmy_db_schema::{ source::{community::Community, post::Post}, traits::Crud, }; -use lemmy_utils::{error::LemmyResult, LemmyErrorType, MAX_COMMENT_DEPTH_LIMIT}; +use lemmy_utils::{ + error::{LemmyErrorType, LemmyResult}, + MAX_COMMENT_DEPTH_LIMIT, +}; use serde::{Deserialize, Serialize}; use serde_with::skip_serializing_none; use url::Url; @@ -50,6 +57,8 @@ pub struct Note { pub(crate) distinguished: Option, pub(crate) language: Option, pub(crate) audience: Option>, + #[serde(default)] + pub(crate) attachment: Vec, } impl Note { diff --git a/crates/apub/src/protocol/objects/page.rs b/crates/apub/src/protocol/objects/page.rs index 9a9866cfb..3ce720bc0 100644 --- a/crates/apub/src/protocol/objects/page.rs +++ b/crates/apub/src/protocol/objects/page.rs @@ -19,8 +19,8 @@ use activitypub_federation::{ }; use chrono::{DateTime, Utc}; use itertools::Itertools; -use lemmy_api_common::context::LemmyContext; -use lemmy_utils::error::{LemmyError, LemmyErrorType, LemmyResult}; +use lemmy_api_common::{context::LemmyContext, utils::proxy_image_link}; +use lemmy_utils::error::{FederationError, LemmyError, LemmyErrorType, LemmyResult}; use serde::{de::Error, Deserialize, Deserializer, Serialize}; use serde_with::skip_serializing_none; use url::Url; @@ -93,6 +93,7 @@ pub(crate) struct Document { #[serde(rename = "type")] kind: DocumentType, url: Url, + media_type: Option, /// Used for alt_text name: Option, } @@ -124,6 +125,24 @@ impl Attachment { _ => None, } } + + pub(crate) async fn as_markdown(&self, context: &Data) -> LemmyResult { + let (url, name, media_type) = match self { + Attachment::Image(i) => (i.url.clone(), i.name.clone(), Some(String::from("image"))), + Attachment::Document(d) => (d.url.clone(), d.name.clone(), d.media_type.clone()), + Attachment::Link(l) => (l.href.clone(), None, l.media_type.clone()), + }; + + let is_image = + media_type.is_some_and(|media| media.starts_with("video") || media.starts_with("image")); + + if is_image { + let url = proxy_image_link(url, context).await?; + Ok(format!("![{}]({url})", name.unwrap_or_default())) + } else { + Ok(format!("[{url}]({url})")) + } + } } #[derive(Clone, Debug, Deserialize, Serialize)] @@ -162,7 +181,7 @@ impl Page { .iter() .find(|a| a.kind == PersonOrGroupType::Person) .map(|a| ObjectId::::from(a.id.clone().into_inner())) - .ok_or_else(|| LemmyErrorType::PageDoesNotSpecifyCreator.into()), + .ok_or_else(|| FederationError::PageDoesNotSpecifyCreator.into()), } } } diff --git a/crates/db_perf/src/main.rs b/crates/db_perf/src/main.rs index 0fa5c0549..02796a906 100644 --- a/crates/db_perf/src/main.rs +++ b/crates/db_perf/src/main.rs @@ -54,7 +54,7 @@ async fn main() -> anyhow::Result<()> { async fn try_main() -> LemmyResult<()> { let args = CmdArgs::parse(); - let pool = &build_db_pool().await?; + let pool = &build_db_pool()?; let pool = &mut pool.into(); let conn = &mut get_conn(pool).await?; diff --git a/crates/db_schema/Cargo.toml b/crates/db_schema/Cargo.toml index aeaa55c93..c52629ce3 100644 --- a/crates/db_schema/Cargo.toml +++ b/crates/db_schema/Cargo.toml @@ -37,6 +37,8 @@ full = [ "tokio-postgres-rustls", "rustls", "i-love-jesus", + "tuplex", + "diesel-bind-if-some", ] [dependencies] @@ -76,12 +78,11 @@ rustls = { workspace = true, optional = true } uuid = { workspace = true, features = ["v4"] } i-love-jesus = { workspace = true, optional = true } anyhow = { workspace = true } +diesel-bind-if-some = { workspace = true, optional = true } moka.workspace = true derive-new.workspace = true +tuplex = { workspace = true, optional = true } [dev-dependencies] serial_test = { workspace = true } pretty_assertions = { workspace = true } - -[package.metadata.cargo-machete] -ignored = ["strum"] diff --git a/crates/db_schema/replaceable_schema/triggers.sql b/crates/db_schema/replaceable_schema/triggers.sql index 973d3325f..6c55ce3d6 100644 --- a/crates/db_schema/replaceable_schema/triggers.sql +++ b/crates/db_schema/replaceable_schema/triggers.sql @@ -38,7 +38,7 @@ AS $a$ BEGIN EXECUTE replace($b$ -- When a thing gets a vote, update its aggregates and its creator's aggregates - CALL r.create_triggers ('thing_like', $$ + CALL r.create_triggers ('thing_actions', $$ BEGIN WITH thing_diff AS ( UPDATE thing_aggregates AS a @@ -46,7 +46,8 @@ BEGIN score = a.score + diff.upvotes - diff.downvotes, upvotes = a.upvotes + diff.upvotes, downvotes = a.downvotes + diff.downvotes, controversy_rank = r.controversy_rank ((a.upvotes + diff.upvotes)::numeric, (a.downvotes + diff.downvotes)::numeric) FROM ( SELECT - (thing_like).thing_id, coalesce(sum(count_diff) FILTER (WHERE (thing_like).score = 1), 0) AS upvotes, coalesce(sum(count_diff) FILTER (WHERE (thing_like).score != 1), 0) AS downvotes FROM select_old_and_new_rows AS old_and_new_rows GROUP BY (thing_like).thing_id) AS diff + (thing_actions).thing_id, coalesce(sum(count_diff) FILTER (WHERE (thing_actions).like_score = 1), 0) AS upvotes, coalesce(sum(count_diff) FILTER (WHERE (thing_actions).like_score != 1), 0) AS downvotes FROM select_old_and_new_rows AS old_and_new_rows + WHERE (thing_actions).like_score IS NOT NULL GROUP BY (thing_actions).thing_id) AS diff WHERE a.thing_id = diff.thing_id AND (diff.upvotes, diff.downvotes) != (0, 0) @@ -360,7 +361,7 @@ CREATE TRIGGER comment_count -- Count subscribers for communities. -- subscribers should be updated only when a local community is followed by a local or remote person. -- subscribers_local should be updated only when a local person follows a local or remote community. -CALL r.create_triggers ('community_follower', $$ +CALL r.create_triggers ('community_actions', $$ BEGIN UPDATE community_aggregates AS a @@ -368,10 +369,11 @@ BEGIN subscribers = a.subscribers + diff.subscribers, subscribers_local = a.subscribers_local + diff.subscribers_local FROM ( SELECT - (community_follower).community_id, coalesce(sum(count_diff) FILTER (WHERE community.local), 0) AS subscribers, coalesce(sum(count_diff) FILTER (WHERE person.local), 0) AS subscribers_local + (community_actions).community_id, coalesce(sum(count_diff) FILTER (WHERE community.local), 0) AS subscribers, coalesce(sum(count_diff) FILTER (WHERE person.local), 0) AS subscribers_local FROM select_old_and_new_rows AS old_and_new_rows - LEFT JOIN community ON community.id = (community_follower).community_id - LEFT JOIN person ON person.id = (community_follower).person_id GROUP BY (community_follower).community_id) AS diff + LEFT JOIN community ON community.id = (community_actions).community_id + LEFT JOIN person ON person.id = (community_actions).person_id + WHERE (community_actions).followed IS NOT NULL GROUP BY (community_actions).community_id) AS diff WHERE a.community_id = diff.community_id AND (diff.subscribers, diff.subscribers_local) != (0, 0); @@ -541,7 +543,7 @@ CREATE FUNCTION r.delete_follow_before_person () LANGUAGE plpgsql AS $$ BEGIN - DELETE FROM community_follower AS c + DELETE FROM community_actions AS c WHERE c.person_id = OLD.id; RETURN OLD; END; diff --git a/crates/db_schema/src/aggregates/comment_aggregates.rs b/crates/db_schema/src/aggregates/comment_aggregates.rs index fc825ec99..b26d27736 100644 --- a/crates/db_schema/src/aggregates/comment_aggregates.rs +++ b/crates/db_schema/src/aggregates/comment_aggregates.rs @@ -51,7 +51,7 @@ mod tests { #[tokio::test] #[serial] async fn test_crud() -> Result<(), Error> { - let pool = &build_db_pool_for_tests().await; + let pool = &build_db_pool_for_tests(); let pool = &mut pool.into(); let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?; @@ -96,7 +96,6 @@ mod tests { let comment_like = CommentLikeForm { comment_id: inserted_comment.id, - post_id: inserted_post.id, person_id: inserted_person.id, score: 1, }; @@ -112,7 +111,6 @@ mod tests { // Add a post dislike from the other person let comment_dislike = CommentLikeForm { comment_id: inserted_comment.id, - post_id: inserted_post.id, person_id: another_inserted_person.id, score: -1, }; diff --git a/crates/db_schema/src/aggregates/community_aggregates.rs b/crates/db_schema/src/aggregates/community_aggregates.rs index 0359d8632..3ec56d73d 100644 --- a/crates/db_schema/src/aggregates/community_aggregates.rs +++ b/crates/db_schema/src/aggregates/community_aggregates.rs @@ -37,7 +37,13 @@ mod tests { aggregates::community_aggregates::CommunityAggregates, source::{ comment::{Comment, CommentInsertForm}, - community::{Community, CommunityFollower, CommunityFollowerForm, CommunityInsertForm}, + community::{ + Community, + CommunityFollower, + CommunityFollowerForm, + CommunityFollowerState, + CommunityInsertForm, + }, instance::Instance, person::{Person, PersonInsertForm}, post::{Post, PostInsertForm}, @@ -52,7 +58,7 @@ mod tests { #[tokio::test] #[serial] async fn test_crud() -> Result<(), Error> { - let pool = &build_db_pool_for_tests().await; + let pool = &build_db_pool_for_tests(); let pool = &mut pool.into(); let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?; @@ -84,7 +90,8 @@ mod tests { let first_person_follow = CommunityFollowerForm { community_id: inserted_community.id, person_id: inserted_person.id, - pending: false, + state: Some(CommunityFollowerState::Accepted), + approver_id: None, }; CommunityFollower::follow(pool, &first_person_follow).await?; @@ -92,7 +99,8 @@ mod tests { let second_person_follow = CommunityFollowerForm { community_id: inserted_community.id, person_id: another_inserted_person.id, - pending: false, + state: Some(CommunityFollowerState::Accepted), + approver_id: None, }; CommunityFollower::follow(pool, &second_person_follow).await?; @@ -100,7 +108,8 @@ mod tests { let another_community_follow = CommunityFollowerForm { community_id: another_inserted_community.id, person_id: inserted_person.id, - pending: false, + state: Some(CommunityFollowerState::Accepted), + approver_id: None, }; CommunityFollower::follow(pool, &another_community_follow).await?; diff --git a/crates/db_schema/src/aggregates/person_aggregates.rs b/crates/db_schema/src/aggregates/person_aggregates.rs index c68c90b0d..62aa9b609 100644 --- a/crates/db_schema/src/aggregates/person_aggregates.rs +++ b/crates/db_schema/src/aggregates/person_aggregates.rs @@ -36,7 +36,7 @@ mod tests { #[tokio::test] #[serial] async fn test_crud() -> Result<(), Error> { - let pool = &build_db_pool_for_tests().await; + let pool = &build_db_pool_for_tests(); let pool = &mut pool.into(); let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?; @@ -82,7 +82,6 @@ mod tests { let mut comment_like = CommentLikeForm { comment_id: inserted_comment.id, person_id: inserted_person.id, - post_id: inserted_post.id, score: 1, }; @@ -99,7 +98,6 @@ mod tests { let child_comment_like = CommentLikeForm { comment_id: inserted_child_comment.id, person_id: another_inserted_person.id, - post_id: inserted_post.id, score: 1, }; diff --git a/crates/db_schema/src/aggregates/person_post_aggregates.rs b/crates/db_schema/src/aggregates/person_post_aggregates.rs index f6e108ee9..63a50af9c 100644 --- a/crates/db_schema/src/aggregates/person_post_aggregates.rs +++ b/crates/db_schema/src/aggregates/person_post_aggregates.rs @@ -2,10 +2,17 @@ use crate::{ aggregates::structs::{PersonPostAggregates, PersonPostAggregatesForm}, diesel::OptionalExtension, newtypes::{PersonId, PostId}, - schema::person_post_aggregates::dsl::{person_id, person_post_aggregates, post_id}, - utils::{get_conn, DbPool}, + schema::post_actions, + utils::{find_action, get_conn, now, DbPool}, +}; +use diesel::{ + expression::SelectableHelper, + insert_into, + result::Error, + ExpressionMethods, + NullableExpressionMethods, + QueryDsl, }; -use diesel::{insert_into, result::Error, QueryDsl}; use diesel_async::RunQueryDsl; impl PersonPostAggregates { @@ -14,11 +21,13 @@ impl PersonPostAggregates { form: &PersonPostAggregatesForm, ) -> Result { let conn = &mut get_conn(pool).await?; - insert_into(person_post_aggregates) + let form = (form, post_actions::read_comments.eq(now().nullable())); + insert_into(post_actions::table) .values(form) - .on_conflict((person_id, post_id)) + .on_conflict((post_actions::person_id, post_actions::post_id)) .do_update() .set(form) + .returning(Self::as_select()) .get_result::(conn) .await } @@ -28,8 +37,8 @@ impl PersonPostAggregates { post_id_: PostId, ) -> Result, Error> { let conn = &mut get_conn(pool).await?; - person_post_aggregates - .find((person_id_, post_id_)) + find_action(post_actions::read_comments, (person_id_, post_id_)) + .select(Self::as_select()) .first(conn) .await .optional() diff --git a/crates/db_schema/src/aggregates/post_aggregates.rs b/crates/db_schema/src/aggregates/post_aggregates.rs index b63017317..46747b076 100644 --- a/crates/db_schema/src/aggregates/post_aggregates.rs +++ b/crates/db_schema/src/aggregates/post_aggregates.rs @@ -70,7 +70,7 @@ mod tests { #[tokio::test] #[serial] async fn test_crud() -> Result<(), Error> { - let pool = &build_db_pool_for_tests().await; + let pool = &build_db_pool_for_tests(); let pool = &mut pool.into(); let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?; @@ -182,7 +182,7 @@ mod tests { #[tokio::test] #[serial] async fn test_soft_delete() -> Result<(), Error> { - let pool = &build_db_pool_for_tests().await; + let pool = &build_db_pool_for_tests(); let pool = &mut pool.into(); let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?; diff --git a/crates/db_schema/src/aggregates/site_aggregates.rs b/crates/db_schema/src/aggregates/site_aggregates.rs index 379ddd2d9..2df566290 100644 --- a/crates/db_schema/src/aggregates/site_aggregates.rs +++ b/crates/db_schema/src/aggregates/site_aggregates.rs @@ -65,7 +65,7 @@ mod tests { #[tokio::test] #[serial] async fn test_crud() -> Result<(), Error> { - let pool = &build_db_pool_for_tests().await; + let pool = &build_db_pool_for_tests(); let pool = &mut pool.into(); let (inserted_instance, inserted_person, inserted_site, inserted_community) = @@ -136,7 +136,7 @@ mod tests { #[tokio::test] #[serial] async fn test_soft_delete() -> Result<(), Error> { - let pool = &build_db_pool_for_tests().await; + let pool = &build_db_pool_for_tests(); let pool = &mut pool.into(); let (inserted_instance, inserted_person, inserted_site, inserted_community) = diff --git a/crates/db_schema/src/aggregates/structs.rs b/crates/db_schema/src/aggregates/structs.rs index fd7f70409..c2e54ae5c 100644 --- a/crates/db_schema/src/aggregates/structs.rs +++ b/crates/db_schema/src/aggregates/structs.rs @@ -4,12 +4,14 @@ use crate::schema::{ comment_aggregates, community_aggregates, person_aggregates, - person_post_aggregates, + post_actions, post_aggregates, site_aggregates, }; use chrono::{DateTime, Utc}; #[cfg(feature = "full")] +use diesel::{dsl, expression_methods::NullableExpressionMethods}; +#[cfg(feature = "full")] use i_love_jesus::CursorKeysModule; use serde::{Deserialize, Serialize}; #[cfg(feature = "full")] @@ -151,7 +153,7 @@ pub struct PostAggregates { feature = "full", derive(Queryable, Selectable, Associations, Identifiable) )] -#[cfg_attr(feature = "full", diesel(table_name = person_post_aggregates))] +#[cfg_attr(feature = "full", diesel(table_name = post_actions))] #[cfg_attr(feature = "full", diesel(primary_key(person_id, post_id)))] #[cfg_attr(feature = "full", diesel(belongs_to(crate::source::person::Person)))] #[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] @@ -162,18 +164,22 @@ pub struct PersonPostAggregates { /// The number of comments they've read on that post. /// /// This is updated to the current post comment count every time they view a post. + #[cfg_attr(feature = "full", diesel(select_expression = post_actions::read_comments_amount.assume_not_null()))] + #[cfg_attr(feature = "full", diesel(select_expression_type = dsl::AssumeNotNull))] pub read_comments: i64, + #[cfg_attr(feature = "full", diesel(select_expression = post_actions::read_comments.assume_not_null()))] + #[cfg_attr(feature = "full", diesel(select_expression_type = dsl::AssumeNotNull))] pub published: DateTime, } #[derive(Clone, Default)] #[cfg_attr(feature = "full", derive(Insertable, AsChangeset))] -#[cfg_attr(feature = "full", diesel(table_name = person_post_aggregates))] +#[cfg_attr(feature = "full", diesel(table_name = post_actions))] pub struct PersonPostAggregatesForm { pub person_id: PersonId, pub post_id: PostId, + #[cfg_attr(feature = "full", diesel(column_name = read_comments_amount))] pub read_comments: i64, - pub published: Option>, } #[derive(PartialEq, Eq, Debug, Serialize, Deserialize, Clone, Copy, Hash)] diff --git a/crates/db_schema/src/impls/activity.rs b/crates/db_schema/src/impls/activity.rs index fff0c2f0c..d2cc6dcef 100644 --- a/crates/db_schema/src/impls/activity.rs +++ b/crates/db_schema/src/impls/activity.rs @@ -71,7 +71,7 @@ mod tests { #[tokio::test] #[serial] async fn receive_activity_duplicate() -> LemmyResult<()> { - let pool = &build_db_pool_for_tests().await; + let pool = &build_db_pool_for_tests(); let pool = &mut pool.into(); let ap_id: DbUrl = Url::parse("http://example.com/activity/531")?.into(); @@ -86,7 +86,7 @@ mod tests { #[tokio::test] #[serial] async fn sent_activity_write_read() -> LemmyResult<()> { - let pool = &build_db_pool_for_tests().await; + let pool = &build_db_pool_for_tests(); let pool = &mut pool.into(); let ap_id: DbUrl = Url::parse("http://example.com/activity/412")?.into(); let data = json!({ diff --git a/crates/db_schema/src/impls/actor_language.rs b/crates/db_schema/src/impls/actor_language.rs index b4575d432..8d4c471be 100644 --- a/crates/db_schema/src/impls/actor_language.rs +++ b/crates/db_schema/src/impls/actor_language.rs @@ -447,7 +447,7 @@ mod tests { #[tokio::test] #[serial] async fn test_convert_update_languages() -> Result<(), Error> { - let pool = &build_db_pool_for_tests().await; + let pool = &build_db_pool_for_tests(); let pool = &mut pool.into(); // call with empty vec, returns all languages @@ -466,7 +466,7 @@ mod tests { #[serial] async fn test_convert_read_languages() -> Result<(), Error> { use crate::schema::language::dsl::{id, language}; - let pool = &build_db_pool_for_tests().await; + let pool = &build_db_pool_for_tests(); let pool = &mut pool.into(); // call with all languages, returns empty vec @@ -486,7 +486,7 @@ mod tests { #[tokio::test] #[serial] async fn test_site_languages() -> Result<(), Error> { - let pool = &build_db_pool_for_tests().await; + let pool = &build_db_pool_for_tests(); let pool = &mut pool.into(); let (site, instance) = create_test_site(pool).await?; @@ -511,7 +511,7 @@ mod tests { #[tokio::test] #[serial] async fn test_user_languages() -> Result<(), Error> { - let pool = &build_db_pool_for_tests().await; + let pool = &build_db_pool_for_tests(); let pool = &mut pool.into(); let (site, instance) = create_test_site(pool).await?; @@ -544,7 +544,7 @@ mod tests { #[tokio::test] #[serial] async fn test_community_languages() -> Result<(), Error> { - let pool = &build_db_pool_for_tests().await; + let pool = &build_db_pool_for_tests(); let pool = &mut pool.into(); let (site, instance) = create_test_site(pool).await?; let test_langs = test_langs1(pool).await?; @@ -599,8 +599,8 @@ mod tests { #[tokio::test] #[serial] - async fn test_default_post_language() -> LemmyResult<()> { - let pool = &build_db_pool_for_tests().await; + async fn test_validate_post_language() -> LemmyResult<()> { + let pool = &build_db_pool_for_tests(); let pool = &mut pool.into(); let (site, instance) = create_test_site(pool).await?; let test_langs = test_langs1(pool).await?; diff --git a/crates/db_schema/src/impls/captcha_answer.rs b/crates/db_schema/src/impls/captcha_answer.rs index d7183e4fb..8be8fc5de 100644 --- a/crates/db_schema/src/impls/captcha_answer.rs +++ b/crates/db_schema/src/impls/captcha_answer.rs @@ -13,7 +13,7 @@ use diesel::{ QueryDsl, }; use diesel_async::RunQueryDsl; -use lemmy_utils::{error::LemmyResult, LemmyErrorType}; +use lemmy_utils::error::{LemmyErrorType, LemmyResult}; impl CaptchaAnswer { pub async fn insert(pool: &mut DbPool<'_>, captcha: &CaptchaAnswerForm) -> Result { @@ -62,7 +62,7 @@ mod tests { #[tokio::test] #[serial] async fn test_captcha_happy_path() { - let pool = &build_db_pool_for_tests().await; + let pool = &build_db_pool_for_tests(); let pool = &mut pool.into(); let inserted = CaptchaAnswer::insert( @@ -89,7 +89,7 @@ mod tests { #[tokio::test] #[serial] async fn test_captcha_repeat_answer_fails() { - let pool = &build_db_pool_for_tests().await; + let pool = &build_db_pool_for_tests(); let pool = &mut pool.into(); let inserted = CaptchaAnswer::insert( diff --git a/crates/db_schema/src/impls/comment.rs b/crates/db_schema/src/impls/comment.rs index ec246c121..96ec70fa2 100644 --- a/crates/db_schema/src/impls/comment.rs +++ b/crates/db_schema/src/impls/comment.rs @@ -1,7 +1,7 @@ use crate::{ diesel::{DecoratableTarget, OptionalExtension}, newtypes::{CommentId, DbUrl, PersonId}, - schema::comment, + schema::{comment, comment_actions}, source::comment::{ Comment, CommentInsertForm, @@ -12,10 +12,25 @@ use crate::{ CommentUpdateForm, }, traits::{Crud, Likeable, Saveable}, - utils::{functions::coalesce, get_conn, naive_now, DbPool, DELETED_REPLACEMENT_TEXT}, + utils::{ + functions::coalesce, + get_conn, + naive_now, + now, + uplete, + DbPool, + DELETED_REPLACEMENT_TEXT, + }, }; use chrono::{DateTime, Utc}; -use diesel::{dsl::insert_into, result::Error, ExpressionMethods, QueryDsl}; +use diesel::{ + dsl::insert_into, + expression::SelectableHelper, + result::Error, + ExpressionMethods, + NullableExpressionMethods, + QueryDsl, +}; use diesel_async::RunQueryDsl; use diesel_ltree::Ltree; use url::Url; @@ -141,13 +156,17 @@ impl Likeable for CommentLike { type Form = CommentLikeForm; type IdType = CommentId; async fn like(pool: &mut DbPool<'_>, comment_like_form: &CommentLikeForm) -> Result { - use crate::schema::comment_like::dsl::{comment_id, comment_like, person_id}; let conn = &mut get_conn(pool).await?; - insert_into(comment_like) + let comment_like_form = ( + comment_like_form, + comment_actions::liked.eq(now().nullable()), + ); + insert_into(comment_actions::table) .values(comment_like_form) - .on_conflict((comment_id, person_id)) + .on_conflict((comment_actions::comment_id, comment_actions::person_id)) .do_update() .set(comment_like_form) + .returning(Self::as_select()) .get_result::(conn) .await } @@ -155,11 +174,12 @@ impl Likeable for CommentLike { pool: &mut DbPool<'_>, person_id: PersonId, comment_id: CommentId, - ) -> Result { - use crate::schema::comment_like::dsl::comment_like; + ) -> Result { let conn = &mut get_conn(pool).await?; - diesel::delete(comment_like.find((person_id, comment_id))) - .execute(conn) + uplete::new(comment_actions::table.find((person_id, comment_id))) + .set_null(comment_actions::like_score) + .set_null(comment_actions::liked) + .get_result(conn) .await } } @@ -171,26 +191,30 @@ impl Saveable for CommentSaved { pool: &mut DbPool<'_>, comment_saved_form: &CommentSavedForm, ) -> Result { - use crate::schema::comment_saved::dsl::{comment_id, comment_saved, person_id}; let conn = &mut get_conn(pool).await?; - insert_into(comment_saved) + let comment_saved_form = ( + comment_saved_form, + comment_actions::saved.eq(now().nullable()), + ); + insert_into(comment_actions::table) .values(comment_saved_form) - .on_conflict((comment_id, person_id)) + .on_conflict((comment_actions::comment_id, comment_actions::person_id)) .do_update() .set(comment_saved_form) + .returning(Self::as_select()) .get_result::(conn) .await } async fn unsave( pool: &mut DbPool<'_>, comment_saved_form: &CommentSavedForm, - ) -> Result { - use crate::schema::comment_saved::dsl::comment_saved; + ) -> Result { let conn = &mut get_conn(pool).await?; - diesel::delete( - comment_saved.find((comment_saved_form.person_id, comment_saved_form.comment_id)), + uplete::new( + comment_actions::table.find((comment_saved_form.person_id, comment_saved_form.comment_id)), ) - .execute(conn) + .set_null(comment_actions::saved) + .get_result(conn) .await } } @@ -216,7 +240,7 @@ mod tests { post::{Post, PostInsertForm}, }, traits::{Crud, Likeable, Saveable}, - utils::build_db_pool_for_tests, + utils::{build_db_pool_for_tests, uplete}, }; use diesel_ltree::Ltree; use lemmy_utils::error::LemmyResult; @@ -227,7 +251,7 @@ mod tests { #[tokio::test] #[serial] async fn test_crud() -> LemmyResult<()> { - let pool = &build_db_pool_for_tests().await; + let pool = &build_db_pool_for_tests(); let pool = &mut pool.into(); let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?; @@ -289,7 +313,6 @@ mod tests { // Comment Like let comment_like_form = CommentLikeForm { comment_id: inserted_comment.id, - post_id: inserted_post.id, person_id: inserted_person.id, score: 1, }; @@ -298,7 +321,6 @@ mod tests { let expected_comment_like = CommentLike { comment_id: inserted_comment.id, - post_id: inserted_post.id, person_id: inserted_person.id, published: inserted_comment_like.published, score: 1, @@ -344,8 +366,8 @@ mod tests { format!("0.{}.{}", expected_comment.id, inserted_child_comment.id), inserted_child_comment.path.0, ); - assert_eq!(1, like_removed); - assert_eq!(1, saved_removed); + assert_eq!(uplete::Count::only_updated(1), like_removed); + assert_eq!(uplete::Count::only_deleted(1), saved_removed); assert_eq!(1, num_deleted); Ok(()) diff --git a/crates/db_schema/src/impls/community.rs b/crates/db_schema/src/impls/community.rs index a054059a4..03cc12558 100644 --- a/crates/db_schema/src/impls/community.rs +++ b/crates/db_schema/src/impls/community.rs @@ -1,20 +1,14 @@ use crate::{ diesel::{DecoratableTarget, OptionalExtension}, newtypes::{CommunityId, DbUrl, PersonId}, - schema::{ - community, - community_follower, - community_moderator, - community_person_ban, - instance, - post, - }, + schema::{community, community_actions, instance, post}, source::{ actor_language::CommunityLanguage, community::{ Community, CommunityFollower, CommunityFollowerForm, + CommunityFollowerState, CommunityInsertForm, CommunityModerator, CommunityModeratorForm, @@ -26,8 +20,12 @@ use crate::{ }, traits::{ApubActor, Bannable, Crud, Followable, Joinable}, utils::{ + action_query, + find_action, functions::{coalesce, lower}, get_conn, + now, + uplete, DbPool, }, ListingType, @@ -37,6 +35,7 @@ use chrono::{DateTime, Utc}; use diesel::{ deserialize, dsl::{self, exists, insert_into, not}, + expression::SelectableHelper, pg::Pg, result::Error, select, @@ -92,8 +91,19 @@ impl Joinable for CommunityModerator { community_moderator_form: &CommunityModeratorForm, ) -> Result { let conn = &mut get_conn(pool).await?; - insert_into(community_moderator::table) + let community_moderator_form = ( + community_moderator_form, + community_actions::became_moderator.eq(now().nullable()), + ); + insert_into(community_actions::table) .values(community_moderator_form) + .on_conflict(( + community_actions::person_id, + community_actions::community_id, + )) + .do_update() + .set(community_moderator_form) + .returning(Self::as_select()) .get_result::(conn) .await } @@ -101,13 +111,14 @@ impl Joinable for CommunityModerator { async fn leave( pool: &mut DbPool<'_>, community_moderator_form: &CommunityModeratorForm, - ) -> Result { + ) -> Result { let conn = &mut get_conn(pool).await?; - diesel::delete(community_moderator::table.find(( + uplete::new(community_actions::table.find(( community_moderator_form.person_id, community_moderator_form.community_id, ))) - .execute(conn) + .set_null(community_actions::became_moderator) + .get_result(conn) .await } } @@ -224,26 +235,26 @@ impl CommunityModerator { pub async fn delete_for_community( pool: &mut DbPool<'_>, for_community_id: CommunityId, - ) -> Result { + ) -> Result { let conn = &mut get_conn(pool).await?; - diesel::delete( - community_moderator::table.filter(community_moderator::community_id.eq(for_community_id)), + uplete::new( + community_actions::table.filter(community_actions::community_id.eq(for_community_id)), ) - .execute(conn) + .set_null(community_actions::became_moderator) + .get_result(conn) .await } pub async fn leave_all_communities( pool: &mut DbPool<'_>, for_person_id: PersonId, - ) -> Result { + ) -> Result { let conn = &mut get_conn(pool).await?; - diesel::delete( - community_moderator::table.filter(community_moderator::person_id.eq(for_person_id)), - ) - .execute(conn) - .await + uplete::new(community_actions::table.filter(community_actions::person_id.eq(for_person_id))) + .set_null(community_actions::became_moderator) + .get_result(conn) + .await } pub async fn get_person_moderated_communities( @@ -251,9 +262,9 @@ impl CommunityModerator { for_person_id: PersonId, ) -> Result, Error> { let conn = &mut get_conn(pool).await?; - community_moderator::table - .filter(community_moderator::person_id.eq(for_person_id)) - .select(community_moderator::community_id) + action_query(community_actions::became_moderator) + .filter(community_actions::person_id.eq(for_person_id)) + .select(community_actions::community_id) .load::(conn) .await } @@ -272,16 +283,17 @@ impl CommunityModerator { persons.push(mod_person_id); persons.dedup(); - let res = community_moderator::table - .filter(community_moderator::community_id.eq(for_community_id)) - .filter(community_moderator::person_id.eq_any(persons)) - .order_by(community_moderator::published) + let res = action_query(community_actions::became_moderator) + .filter(community_actions::community_id.eq(for_community_id)) + .filter(community_actions::person_id.eq_any(persons)) + .order_by(community_actions::became_moderator) + .select(community_actions::person_id) // This does a limit 1 select first - .first::(conn) + .first::(conn) .await?; // If the first result sorted by published is the acting mod - if res.person_id == mod_person_id { + if res == mod_person_id { Ok(()) } else { Err(LemmyErrorType::NotHigherMod)? @@ -297,14 +309,19 @@ impl Bannable for CommunityPersonBan { community_person_ban_form: &CommunityPersonBanForm, ) -> Result { let conn = &mut get_conn(pool).await?; - insert_into(community_person_ban::table) + let community_person_ban_form = ( + community_person_ban_form, + community_actions::received_ban.eq(now().nullable()), + ); + insert_into(community_actions::table) .values(community_person_ban_form) .on_conflict(( - community_person_ban::community_id, - community_person_ban::person_id, + community_actions::community_id, + community_actions::person_id, )) .do_update() .set(community_person_ban_form) + .returning(Self::as_select()) .get_result::(conn) .await } @@ -312,34 +329,22 @@ impl Bannable for CommunityPersonBan { async fn unban( pool: &mut DbPool<'_>, community_person_ban_form: &CommunityPersonBanForm, - ) -> Result { + ) -> Result { let conn = &mut get_conn(pool).await?; - diesel::delete(community_person_ban::table.find(( + uplete::new(community_actions::table.find(( community_person_ban_form.person_id, community_person_ban_form.community_id, ))) - .execute(conn) + .set_null(community_actions::received_ban) + .set_null(community_actions::ban_expires) + .get_result(conn) .await } } impl CommunityFollower { - pub fn to_subscribed_type(follower: &Option) -> SubscribedType { - match follower { - Some(f) => { - if f.pending { - SubscribedType::Pending - } else { - SubscribedType::Subscribed - } - } - // If the row doesn't exist, the person isn't a follower. - None => SubscribedType::NotSubscribed, - } - } - - pub fn select_subscribed_type() -> dsl::Nullable { - community_follower::pending.nullable() + pub fn select_subscribed_type() -> dsl::Nullable { + community_actions::follow_state.nullable() } /// Check if a remote instance has any followers on local instance. For this it is enough to check @@ -349,22 +354,46 @@ impl CommunityFollower { remote_community_id: CommunityId, ) -> LemmyResult<()> { let conn = &mut get_conn(pool).await?; - select(exists(community_follower::table.filter( - community_follower::community_id.eq(remote_community_id), - ))) + select(exists( + action_query(community_actions::followed) + .filter(community_actions::community_id.eq(remote_community_id)), + )) .get_result::(conn) .await? .then_some(()) .ok_or(LemmyErrorType::CommunityHasNoFollowers.into()) } + + pub async fn approve( + pool: &mut DbPool<'_>, + community_id: CommunityId, + follower_id: PersonId, + approver_id: PersonId, + ) -> LemmyResult<()> { + let conn = &mut get_conn(pool).await?; + diesel::update(find_action( + community_actions::followed, + (follower_id, community_id), + )) + .set(( + community_actions::follow_state.eq(CommunityFollowerState::Accepted), + community_actions::follow_approver_id.eq(approver_id), + )) + .execute(conn) + .await?; + Ok(()) + } } -impl Queryable, Pg> for SubscribedType { - type Row = Option; +impl Queryable, Pg> + for SubscribedType +{ + type Row = Option; fn build(row: Self::Row) -> deserialize::Result { Ok(match row { - Some(true) => SubscribedType::Pending, - Some(false) => SubscribedType::Subscribed, + Some(CommunityFollowerState::Pending) => SubscribedType::Pending, + Some(CommunityFollowerState::Accepted) => SubscribedType::Subscribed, + Some(CommunityFollowerState::ApprovalRequired) => SubscribedType::ApprovalRequired, None => SubscribedType::NotSubscribed, }) } @@ -375,14 +404,16 @@ impl Followable for CommunityFollower { type Form = CommunityFollowerForm; async fn follow(pool: &mut DbPool<'_>, form: &CommunityFollowerForm) -> Result { let conn = &mut get_conn(pool).await?; - insert_into(community_follower::table) + let form = (form, community_actions::followed.eq(now().nullable())); + insert_into(community_actions::table) .values(form) .on_conflict(( - community_follower::community_id, - community_follower::person_id, + community_actions::community_id, + community_actions::person_id, )) .do_update() .set(form) + .returning(Self::as_select()) .get_result::(conn) .await } @@ -392,16 +423,25 @@ impl Followable for CommunityFollower { person_id: PersonId, ) -> Result { let conn = &mut get_conn(pool).await?; - diesel::update(community_follower::table.find((person_id, community_id))) - .set(community_follower::pending.eq(false)) - .get_result::(conn) - .await + diesel::update(find_action( + community_actions::follow_state, + (person_id, community_id), + )) + .set(community_actions::follow_state.eq(Some(CommunityFollowerState::Accepted))) + .returning(Self::as_select()) + .get_result::(conn) + .await } - - async fn unfollow(pool: &mut DbPool<'_>, form: &CommunityFollowerForm) -> Result { + async fn unfollow( + pool: &mut DbPool<'_>, + form: &CommunityFollowerForm, + ) -> Result { let conn = &mut get_conn(pool).await?; - diesel::delete(community_follower::table.find((form.person_id, form.community_id))) - .execute(conn) + uplete::new(community_actions::table.find((form.person_id, form.community_id))) + .set_null(community_actions::followed) + .set_null(community_actions::follow_state) + .set_null(community_actions::follow_approver_id) + .get_result(conn) .await } } @@ -463,6 +503,7 @@ mod tests { Community, CommunityFollower, CommunityFollowerForm, + CommunityFollowerState, CommunityInsertForm, CommunityModerator, CommunityModeratorForm, @@ -475,7 +516,7 @@ mod tests { person::{Person, PersonInsertForm}, }, traits::{Bannable, Crud, Followable, Joinable}, - utils::build_db_pool_for_tests, + utils::{build_db_pool_for_tests, uplete}, CommunityVisibility, }; use lemmy_utils::error::LemmyResult; @@ -485,7 +526,7 @@ mod tests { #[tokio::test] #[serial] async fn test_crud() -> LemmyResult<()> { - let pool = &build_db_pool_for_tests().await; + let pool = &build_db_pool_for_tests(); let pool = &mut pool.into(); let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?; @@ -508,6 +549,7 @@ mod tests { id: inserted_community.id, name: "TIL".into(), title: "nada".to_owned(), + sidebar: None, description: None, nsfw: false, removed: false, @@ -523,7 +565,6 @@ mod tests { banner: None, followers_url: inserted_community.followers_url.clone(), inbox_url: inserted_community.inbox_url.clone(), - shared_inbox_url: None, moderators_url: None, featured_url: None, hidden: false, @@ -535,7 +576,8 @@ mod tests { let community_follower_form = CommunityFollowerForm { community_id: inserted_community.id, person_id: inserted_bobby.id, - pending: false, + state: Some(CommunityFollowerState::Accepted), + approver_id: None, }; let inserted_community_follower = @@ -544,8 +586,9 @@ mod tests { let expected_community_follower = CommunityFollower { community_id: inserted_community.id, person_id: inserted_bobby.id, - pending: false, + state: CommunityFollowerState::Accepted, published: inserted_community_follower.published, + approver_id: None, }; let bobby_moderator_form = CommunityModeratorForm { @@ -640,9 +683,9 @@ mod tests { assert_eq!(expected_community_follower, inserted_community_follower); assert_eq!(expected_community_moderator, inserted_bobby_moderator); assert_eq!(expected_community_person_ban, inserted_community_person_ban); - assert_eq!(1, ignored_community); - assert_eq!(1, left_community); - assert_eq!(1, unban); + assert_eq!(uplete::Count::only_updated(1), ignored_community); + assert_eq!(uplete::Count::only_updated(1), left_community); + assert_eq!(uplete::Count::only_deleted(1), unban); // assert_eq!(2, loaded_count); assert_eq!(1, num_deleted); diff --git a/crates/db_schema/src/impls/community_block.rs b/crates/db_schema/src/impls/community_block.rs index cd541cd8b..c520e43e8 100644 --- a/crates/db_schema/src/impls/community_block.rs +++ b/crates/db_schema/src/impls/community_block.rs @@ -1,22 +1,24 @@ use crate::{ newtypes::{CommunityId, PersonId}, - schema::{community, community_block}, + schema::{community, community_actions}, source::{ community::Community, community_block::{CommunityBlock, CommunityBlockForm}, }, traits::Blockable, - utils::{get_conn, DbPool}, + utils::{action_query, find_action, get_conn, now, uplete, DbPool}, }; use diesel::{ dsl::{exists, insert_into, not}, + expression::SelectableHelper, result::Error, select, ExpressionMethods, + NullableExpressionMethods, QueryDsl, }; use diesel_async::RunQueryDsl; -use lemmy_utils::{error::LemmyResult, LemmyErrorType}; +use lemmy_utils::error::{LemmyErrorType, LemmyResult}; impl CommunityBlock { pub async fn read( @@ -25,9 +27,10 @@ impl CommunityBlock { for_community_id: CommunityId, ) -> LemmyResult<()> { let conn = &mut get_conn(pool).await?; - select(not(exists( - community_block::table.find((for_person_id, for_community_id)), - ))) + select(not(exists(find_action( + community_actions::blocked, + (for_person_id, for_community_id), + )))) .get_result::(conn) .await? .then_some(()) @@ -39,13 +42,13 @@ impl CommunityBlock { person_id: PersonId, ) -> Result, Error> { let conn = &mut get_conn(pool).await?; - community_block::table + action_query(community_actions::blocked) .inner_join(community::table) .select(community::all_columns) - .filter(community_block::person_id.eq(person_id)) + .filter(community_actions::person_id.eq(person_id)) .filter(community::deleted.eq(false)) .filter(community::removed.eq(false)) - .order_by(community_block::published) + .order_by(community_actions::blocked) .load::(conn) .await } @@ -56,24 +59,33 @@ impl Blockable for CommunityBlock { type Form = CommunityBlockForm; async fn block(pool: &mut DbPool<'_>, community_block_form: &Self::Form) -> Result { let conn = &mut get_conn(pool).await?; - insert_into(community_block::table) + let community_block_form = ( + community_block_form, + community_actions::blocked.eq(now().nullable()), + ); + insert_into(community_actions::table) .values(community_block_form) - .on_conflict((community_block::person_id, community_block::community_id)) + .on_conflict(( + community_actions::person_id, + community_actions::community_id, + )) .do_update() .set(community_block_form) + .returning(Self::as_select()) .get_result::(conn) .await } async fn unblock( pool: &mut DbPool<'_>, community_block_form: &Self::Form, - ) -> Result { + ) -> Result { let conn = &mut get_conn(pool).await?; - diesel::delete(community_block::table.find(( + uplete::new(community_actions::table.find(( community_block_form.person_id, community_block_form.community_id, ))) - .execute(conn) + .set_null(community_actions::blocked) + .get_result(conn) .await } } diff --git a/crates/db_schema/src/impls/federation_allowlist.rs b/crates/db_schema/src/impls/federation_allowlist.rs index cbfd14b03..099e0b231 100644 --- a/crates/db_schema/src/impls/federation_allowlist.rs +++ b/crates/db_schema/src/impls/federation_allowlist.rs @@ -61,7 +61,7 @@ mod tests { #[tokio::test] #[serial] async fn test_allowlist_insert_and_clear() -> Result<(), Error> { - let pool = &build_db_pool_for_tests().await; + let pool = &build_db_pool_for_tests(); let pool = &mut pool.into(); let domains = vec![ "tld1.xyz".to_string(), diff --git a/crates/db_schema/src/impls/instance_block.rs b/crates/db_schema/src/impls/instance_block.rs index 1eb6e8f04..1722e8318 100644 --- a/crates/db_schema/src/impls/instance_block.rs +++ b/crates/db_schema/src/impls/instance_block.rs @@ -1,22 +1,24 @@ use crate::{ newtypes::{InstanceId, PersonId}, - schema::{instance, instance_block}, + schema::{instance, instance_actions}, source::{ instance::Instance, instance_block::{InstanceBlock, InstanceBlockForm}, }, traits::Blockable, - utils::{get_conn, DbPool}, + utils::{action_query, find_action, get_conn, now, uplete, DbPool}, }; use diesel::{ dsl::{exists, insert_into, not}, + expression::SelectableHelper, result::Error, select, ExpressionMethods, + NullableExpressionMethods, QueryDsl, }; use diesel_async::RunQueryDsl; -use lemmy_utils::{error::LemmyResult, LemmyErrorType}; +use lemmy_utils::error::{LemmyErrorType, LemmyResult}; impl InstanceBlock { pub async fn read( @@ -25,9 +27,10 @@ impl InstanceBlock { for_instance_id: InstanceId, ) -> LemmyResult<()> { let conn = &mut get_conn(pool).await?; - select(not(exists( - instance_block::table.find((for_person_id, for_instance_id)), - ))) + select(not(exists(find_action( + instance_actions::blocked, + (for_person_id, for_instance_id), + )))) .get_result::(conn) .await? .then_some(()) @@ -39,11 +42,11 @@ impl InstanceBlock { person_id: PersonId, ) -> Result, Error> { let conn = &mut get_conn(pool).await?; - instance_block::table + action_query(instance_actions::blocked) .inner_join(instance::table) .select(instance::all_columns) - .filter(instance_block::person_id.eq(person_id)) - .order_by(instance_block::published) + .filter(instance_actions::person_id.eq(person_id)) + .order_by(instance_actions::blocked) .load::(conn) .await } @@ -54,24 +57,30 @@ impl Blockable for InstanceBlock { type Form = InstanceBlockForm; async fn block(pool: &mut DbPool<'_>, instance_block_form: &Self::Form) -> Result { let conn = &mut get_conn(pool).await?; - insert_into(instance_block::table) + let instance_block_form = ( + instance_block_form, + instance_actions::blocked.eq(now().nullable()), + ); + insert_into(instance_actions::table) .values(instance_block_form) - .on_conflict((instance_block::person_id, instance_block::instance_id)) + .on_conflict((instance_actions::person_id, instance_actions::instance_id)) .do_update() .set(instance_block_form) + .returning(Self::as_select()) .get_result::(conn) .await } async fn unblock( pool: &mut DbPool<'_>, instance_block_form: &Self::Form, - ) -> Result { + ) -> Result { let conn = &mut get_conn(pool).await?; - diesel::delete(instance_block::table.find(( + uplete::new(instance_actions::table.find(( instance_block_form.person_id, instance_block_form.instance_id, ))) - .execute(conn) + .set_null(instance_actions::blocked) + .get_result(conn) .await } } diff --git a/crates/db_schema/src/impls/language.rs b/crates/db_schema/src/impls/language.rs index 57420fcd4..3b8bc1d20 100644 --- a/crates/db_schema/src/impls/language.rs +++ b/crates/db_schema/src/impls/language.rs @@ -46,7 +46,7 @@ mod tests { #[tokio::test] #[serial] async fn test_languages() -> Result<(), Error> { - let pool = &build_db_pool_for_tests().await; + let pool = &build_db_pool_for_tests(); let pool = &mut pool.into(); let all = Language::read_all(pool).await?; diff --git a/crates/db_schema/src/impls/local_user.rs b/crates/db_schema/src/impls/local_user.rs index 235f053c1..3b695a97e 100644 --- a/crates/db_schema/src/impls/local_user.rs +++ b/crates/db_schema/src/impls/local_user.rs @@ -1,6 +1,6 @@ use crate::{ newtypes::{CommunityId, DbUrl, LanguageId, LocalUserId, PersonId}, - schema::{community, community_moderator, local_user, person, registration_application}, + schema::{community, community_actions, local_user, person, registration_application}, source::{ actor_language::LocalUserLanguage, local_user::{LocalUser, LocalUserInsertForm, LocalUserUpdateForm}, @@ -8,6 +8,7 @@ use crate::{ site::Site, }, utils::{ + action_query, functions::{coalesce, lower}, get_conn, now, @@ -155,55 +156,54 @@ impl LocalUser { ) -> Result { use crate::schema::{ comment, - comment_saved, + comment_actions, community, - community_block, - community_follower, + community_actions, instance, - instance_block, - person_block, + instance_actions, + person_actions, post, - post_saved, + post_actions, }; let conn = &mut get_conn(pool).await?; - let followed_communities = community_follower::dsl::community_follower - .filter(community_follower::person_id.eq(person_id_)) - .inner_join(community::table.on(community_follower::community_id.eq(community::id))) - .select(community::actor_id) - .get_results(conn) - .await?; - - let saved_posts = post_saved::dsl::post_saved - .filter(post_saved::person_id.eq(person_id_)) - .inner_join(post::table.on(post_saved::post_id.eq(post::id))) - .select(post::ap_id) - .get_results(conn) - .await?; - - let saved_comments = comment_saved::dsl::comment_saved - .filter(comment_saved::person_id.eq(person_id_)) - .inner_join(comment::table.on(comment_saved::comment_id.eq(comment::id))) - .select(comment::ap_id) - .get_results(conn) - .await?; - - let blocked_communities = community_block::dsl::community_block - .filter(community_block::person_id.eq(person_id_)) + let followed_communities = action_query(community_actions::followed) + .filter(community_actions::person_id.eq(person_id_)) .inner_join(community::table) .select(community::actor_id) .get_results(conn) .await?; - let blocked_users = person_block::dsl::person_block - .filter(person_block::person_id.eq(person_id_)) - .inner_join(person::table.on(person_block::target_id.eq(person::id))) + let saved_posts = action_query(post_actions::saved) + .filter(post_actions::person_id.eq(person_id_)) + .inner_join(post::table) + .select(post::ap_id) + .get_results(conn) + .await?; + + let saved_comments = action_query(comment_actions::saved) + .filter(comment_actions::person_id.eq(person_id_)) + .inner_join(comment::table) + .select(comment::ap_id) + .get_results(conn) + .await?; + + let blocked_communities = action_query(community_actions::blocked) + .filter(community_actions::person_id.eq(person_id_)) + .inner_join(community::table) + .select(community::actor_id) + .get_results(conn) + .await?; + + let blocked_users = action_query(person_actions::blocked) + .filter(person_actions::person_id.eq(person_id_)) + .inner_join(person::table.on(person_actions::target_id.eq(person::id))) .select(person::actor_id) .get_results(conn) .await?; - let blocked_instances = instance_block::dsl::instance_block - .filter(instance_block::person_id.eq(person_id_)) + let blocked_instances = action_query(instance_actions::blocked) + .filter(instance_actions::person_id.eq(person_id_)) .inner_join(instance::table) .select(instance::domain) .get_results(conn) @@ -270,11 +270,11 @@ impl LocalUser { .order_by(local_user::id) .select(local_user::person_id); - let mods = community_moderator::table - .filter(community_moderator::community_id.eq(for_community_id)) - .filter(community_moderator::person_id.eq_any(&persons)) - .order_by(community_moderator::published) - .select(community_moderator::person_id); + let mods = action_query(community_actions::became_moderator) + .filter(community_actions::community_id.eq(for_community_id)) + .filter(community_actions::person_id.eq_any(&persons)) + .order_by(community_actions::became_moderator) + .select(community_actions::person_id); let res = admins.union_all(mods).get_results::(conn).await?; let first_person = res.as_slice().first().ok_or(LemmyErrorType::NotHigherMod)?; @@ -331,6 +331,7 @@ impl LocalUserOptionHelper for Option<&LocalUser> { .unwrap_or(site.content_warning.is_some()) } + // TODO: use this function for private community checks, but the generics get extremely confusing fn visible_communities_only(&self, query: Q) -> Q where Q: diesel::query_dsl::methods::FilterDsl< @@ -385,7 +386,7 @@ mod tests { #[tokio::test] #[serial] async fn test_admin_higher_check() -> LemmyResult<()> { - let pool = &build_db_pool_for_tests().await; + let pool = &build_db_pool_for_tests(); let pool = &mut pool.into(); let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?; @@ -424,7 +425,7 @@ mod tests { #[tokio::test] #[serial] async fn test_email_taken() -> LemmyResult<()> { - let pool = &build_db_pool_for_tests().await; + let pool = &build_db_pool_for_tests(); let pool = &mut pool.into(); let darwin_email = "charles.darwin@gmail.com"; diff --git a/crates/db_schema/src/impls/login_token.rs b/crates/db_schema/src/impls/login_token.rs index c8c44c506..f4f7a7aae 100644 --- a/crates/db_schema/src/impls/login_token.rs +++ b/crates/db_schema/src/impls/login_token.rs @@ -7,7 +7,7 @@ use crate::{ }; use diesel::{delete, dsl::exists, insert_into, result::Error, select}; use diesel_async::RunQueryDsl; -use lemmy_utils::{error::LemmyResult, LemmyErrorType}; +use lemmy_utils::error::{LemmyErrorType, LemmyResult}; impl LoginToken { pub async fn create(pool: &mut DbPool<'_>, form: LoginTokenCreateForm) -> Result { diff --git a/crates/db_schema/src/impls/moderator.rs b/crates/db_schema/src/impls/moderator.rs index b2ef26e69..8deb56258 100644 --- a/crates/db_schema/src/impls/moderator.rs +++ b/crates/db_schema/src/impls/moderator.rs @@ -533,7 +533,7 @@ mod tests { #[tokio::test] #[serial] async fn test_crud() -> Result<(), Error> { - let pool = &build_db_pool_for_tests().await; + let pool = &build_db_pool_for_tests(); let pool = &mut pool.into(); let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?; diff --git a/crates/db_schema/src/impls/password_reset_request.rs b/crates/db_schema/src/impls/password_reset_request.rs index 015db5581..a9ac3a9c2 100644 --- a/crates/db_schema/src/impls/password_reset_request.rs +++ b/crates/db_schema/src/impls/password_reset_request.rs @@ -61,7 +61,7 @@ mod tests { #[tokio::test] #[serial] async fn test_password_reset() -> LemmyResult<()> { - let pool = &build_db_pool_for_tests().await; + let pool = &build_db_pool_for_tests(); let pool = &mut pool.into(); // Setup diff --git a/crates/db_schema/src/impls/person.rs b/crates/db_schema/src/impls/person.rs index 0e6dc8556..3ae355b87 100644 --- a/crates/db_schema/src/impls/person.rs +++ b/crates/db_schema/src/impls/person.rs @@ -1,7 +1,7 @@ use crate::{ diesel::OptionalExtension, newtypes::{CommunityId, DbUrl, InstanceId, PersonId}, - schema::{comment, community, instance, local_user, person, person_follower, post}, + schema::{comment, community, instance, local_user, person, person_actions, post}, source::person::{ Person, PersonFollower, @@ -10,18 +10,20 @@ use crate::{ PersonUpdateForm, }, traits::{ApubActor, Crud, Followable}, - utils::{functions::lower, get_conn, naive_now, DbPool}, + utils::{action_query, functions::lower, get_conn, naive_now, now, uplete, DbPool}, }; use diesel::{ dsl::{insert_into, not}, + expression::SelectableHelper, result::Error, CombineDsl, ExpressionMethods, JoinOnDsl, + NullableExpressionMethods, QueryDsl, }; use diesel_async::RunQueryDsl; -use lemmy_utils::{error::LemmyResult, LemmyErrorType}; +use lemmy_utils::error::{LemmyErrorType, LemmyResult}; #[async_trait] impl Crud for Person { @@ -197,11 +199,13 @@ impl Followable for PersonFollower { type Form = PersonFollowerForm; async fn follow(pool: &mut DbPool<'_>, form: &PersonFollowerForm) -> Result { let conn = &mut get_conn(pool).await?; - insert_into(person_follower::table) + let form = (form, person_actions::followed.eq(now().nullable())); + insert_into(person_actions::table) .values(form) - .on_conflict((person_follower::follower_id, person_follower::person_id)) + .on_conflict((person_actions::person_id, person_actions::target_id)) .do_update() .set(form) + .returning(Self::as_select()) .get_result::(conn) .await } @@ -211,10 +215,15 @@ impl Followable for PersonFollower { Err(Error::NotFound) } - async fn unfollow(pool: &mut DbPool<'_>, form: &PersonFollowerForm) -> Result { + async fn unfollow( + pool: &mut DbPool<'_>, + form: &PersonFollowerForm, + ) -> Result { let conn = &mut get_conn(pool).await?; - diesel::delete(person_follower::table.find((form.follower_id, form.person_id))) - .execute(conn) + uplete::new(person_actions::table.find((form.follower_id, form.person_id))) + .set_null(person_actions::followed) + .set_null(person_actions::follow_pending) + .get_result(conn) .await } } @@ -225,9 +234,9 @@ impl PersonFollower { for_person_id: PersonId, ) -> Result, Error> { let conn = &mut get_conn(pool).await?; - person_follower::table - .inner_join(person::table.on(person_follower::follower_id.eq(person::id))) - .filter(person_follower::person_id.eq(for_person_id)) + action_query(person_actions::followed) + .inner_join(person::table.on(person_actions::person_id.eq(person::id))) + .filter(person_actions::target_id.eq(for_person_id)) .select(person::all_columns) .load(conn) .await @@ -243,7 +252,7 @@ mod tests { person::{Person, PersonFollower, PersonFollowerForm, PersonInsertForm, PersonUpdateForm}, }, traits::{Crud, Followable}, - utils::build_db_pool_for_tests, + utils::{build_db_pool_for_tests, uplete}, }; use lemmy_utils::error::LemmyResult; use pretty_assertions::assert_eq; @@ -252,7 +261,7 @@ mod tests { #[tokio::test] #[serial] async fn test_crud() -> LemmyResult<()> { - let pool = &build_db_pool_for_tests().await; + let pool = &build_db_pool_for_tests(); let pool = &mut pool.into(); let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?; @@ -279,7 +288,6 @@ mod tests { public_key: "pubkey".to_owned(), last_refreshed_at: inserted_person.published, inbox_url: inserted_person.inbox_url.clone(), - shared_inbox_url: None, matrix_user_id: None, ban_expires: None, instance_id: inserted_instance.id, @@ -307,7 +315,7 @@ mod tests { #[tokio::test] #[serial] async fn follow() -> LemmyResult<()> { - let pool = &build_db_pool_for_tests().await; + let pool = &build_db_pool_for_tests(); let pool = &mut pool.into(); let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?; @@ -330,7 +338,7 @@ mod tests { assert_eq!(vec![person_2], followers); let unfollow = PersonFollower::unfollow(pool, &follow_form).await?; - assert_eq!(1, unfollow); + assert_eq!(uplete::Count::only_deleted(1), unfollow); Ok(()) } diff --git a/crates/db_schema/src/impls/person_block.rs b/crates/db_schema/src/impls/person_block.rs index 7f2286616..363a2d3d1 100644 --- a/crates/db_schema/src/impls/person_block.rs +++ b/crates/db_schema/src/impls/person_block.rs @@ -1,23 +1,25 @@ use crate::{ newtypes::PersonId, - schema::{person, person_block}, + schema::{person, person_actions}, source::{ person::Person, person_block::{PersonBlock, PersonBlockForm}, }, traits::Blockable, - utils::{get_conn, DbPool}, + utils::{action_query, find_action, get_conn, now, uplete, DbPool}, }; use diesel::{ dsl::{exists, insert_into, not}, + expression::SelectableHelper, result::Error, select, ExpressionMethods, JoinOnDsl, + NullableExpressionMethods, QueryDsl, }; use diesel_async::RunQueryDsl; -use lemmy_utils::{error::LemmyResult, LemmyErrorType}; +use lemmy_utils::error::{LemmyErrorType, LemmyResult}; impl PersonBlock { pub async fn read( @@ -26,9 +28,10 @@ impl PersonBlock { for_recipient_id: PersonId, ) -> LemmyResult<()> { let conn = &mut get_conn(pool).await?; - select(not(exists( - person_block::table.find((for_person_id, for_recipient_id)), - ))) + select(not(exists(find_action( + person_actions::blocked, + (for_person_id, for_recipient_id), + )))) .get_result::(conn) .await? .then_some(()) @@ -42,15 +45,15 @@ impl PersonBlock { let conn = &mut get_conn(pool).await?; let target_person_alias = diesel::alias!(person as person1); - person_block::table - .inner_join(person::table.on(person_block::person_id.eq(person::id))) + action_query(person_actions::blocked) + .inner_join(person::table.on(person_actions::person_id.eq(person::id))) .inner_join( - target_person_alias.on(person_block::target_id.eq(target_person_alias.field(person::id))), + target_person_alias.on(person_actions::target_id.eq(target_person_alias.field(person::id))), ) .select(target_person_alias.fields(person::all_columns)) - .filter(person_block::person_id.eq(person_id)) + .filter(person_actions::person_id.eq(person_id)) .filter(target_person_alias.field(person::deleted).eq(false)) - .order_by(person_block::published) + .order_by(person_actions::blocked) .load::(conn) .await } @@ -64,20 +67,29 @@ impl Blockable for PersonBlock { person_block_form: &PersonBlockForm, ) -> Result { let conn = &mut get_conn(pool).await?; - insert_into(person_block::table) + let person_block_form = ( + person_block_form, + person_actions::blocked.eq(now().nullable()), + ); + insert_into(person_actions::table) .values(person_block_form) - .on_conflict((person_block::person_id, person_block::target_id)) + .on_conflict((person_actions::person_id, person_actions::target_id)) .do_update() .set(person_block_form) + .returning(Self::as_select()) .get_result::(conn) .await } - async fn unblock(pool: &mut DbPool<'_>, person_block_form: &Self::Form) -> Result { + async fn unblock( + pool: &mut DbPool<'_>, + person_block_form: &Self::Form, + ) -> Result { let conn = &mut get_conn(pool).await?; - diesel::delete( - person_block::table.find((person_block_form.person_id, person_block_form.target_id)), + uplete::new( + person_actions::table.find((person_block_form.person_id, person_block_form.target_id)), ) - .execute(conn) + .set_null(person_actions::blocked) + .get_result(conn) .await } } diff --git a/crates/db_schema/src/impls/post.rs b/crates/db_schema/src/impls/post.rs index fb6245585..5be9d7aae 100644 --- a/crates/db_schema/src/impls/post.rs +++ b/crates/db_schema/src/impls/post.rs @@ -1,7 +1,7 @@ use crate::{ diesel::{BoolExpressionMethods, OptionalExtension}, newtypes::{CommunityId, DbUrl, PersonId, PostId}, - schema::{community, person, post, post_hide, post_like, post_read, post_saved}, + schema::{community, person, post, post_actions}, source::post::{ Post, PostHide, @@ -21,6 +21,7 @@ use crate::{ get_conn, naive_now, now, + uplete, DbPool, DELETED_REPLACEMENT_TEXT, FETCH_LIMIT_MAX, @@ -32,9 +33,11 @@ use ::url::Url; use chrono::{DateTime, Utc}; use diesel::{ dsl::{count, insert_into, not}, + expression::SelectableHelper, result::Error, DecoratableTarget, ExpressionMethods, + NullableExpressionMethods, QueryDsl, TextExpressionMethods, }; @@ -278,11 +281,13 @@ impl Likeable for PostLike { type IdType = PostId; async fn like(pool: &mut DbPool<'_>, post_like_form: &PostLikeForm) -> Result { let conn = &mut get_conn(pool).await?; - insert_into(post_like::table) + let post_like_form = (post_like_form, post_actions::liked.eq(now().nullable())); + insert_into(post_actions::table) .values(post_like_form) - .on_conflict((post_like::post_id, post_like::person_id)) + .on_conflict((post_actions::post_id, post_actions::person_id)) .do_update() .set(post_like_form) + .returning(Self::as_select()) .get_result::(conn) .await } @@ -290,10 +295,12 @@ impl Likeable for PostLike { pool: &mut DbPool<'_>, person_id: PersonId, post_id: PostId, - ) -> Result { + ) -> Result { let conn = &mut get_conn(pool).await?; - diesel::delete(post_like::table.find((person_id, post_id))) - .execute(conn) + uplete::new(post_actions::table.find((person_id, post_id))) + .set_null(post_actions::like_score) + .set_null(post_actions::liked) + .get_result(conn) .await } } @@ -303,18 +310,24 @@ impl Saveable for PostSaved { type Form = PostSavedForm; async fn save(pool: &mut DbPool<'_>, post_saved_form: &PostSavedForm) -> Result { let conn = &mut get_conn(pool).await?; - insert_into(post_saved::table) + let post_saved_form = (post_saved_form, post_actions::saved.eq(now().nullable())); + insert_into(post_actions::table) .values(post_saved_form) - .on_conflict((post_saved::post_id, post_saved::person_id)) + .on_conflict((post_actions::post_id, post_actions::person_id)) .do_update() .set(post_saved_form) + .returning(Self::as_select()) .get_result::(conn) .await } - async fn unsave(pool: &mut DbPool<'_>, post_saved_form: &PostSavedForm) -> Result { + async fn unsave( + pool: &mut DbPool<'_>, + post_saved_form: &PostSavedForm, + ) -> Result { let conn = &mut get_conn(pool).await?; - diesel::delete(post_saved::table.find((post_saved_form.person_id, post_saved_form.post_id))) - .execute(conn) + uplete::new(post_actions::table.find((post_saved_form.person_id, post_saved_form.post_id))) + .set_null(post_actions::saved) + .get_result(conn) .await } } @@ -329,11 +342,18 @@ impl PostRead { let forms = post_ids .into_iter() - .map(|post_id| PostReadForm { post_id, person_id }) - .collect::>(); - insert_into(post_read::table) + .map(|post_id| { + ( + PostReadForm { post_id, person_id }, + post_actions::read.eq(now().nullable()), + ) + }) + .collect::>(); + insert_into(post_actions::table) .values(forms) - .on_conflict_do_nothing() + .on_conflict((post_actions::person_id, post_actions::post_id)) + .do_update() + .set(post_actions::read.eq(now().nullable())) .execute(conn) .await } @@ -342,15 +362,16 @@ impl PostRead { pool: &mut DbPool<'_>, post_id_: HashSet, person_id_: PersonId, - ) -> Result { + ) -> Result { let conn = &mut get_conn(pool).await?; - diesel::delete( - post_read::table - .filter(post_read::post_id.eq_any(post_id_)) - .filter(post_read::person_id.eq(person_id_)), + uplete::new( + post_actions::table + .filter(post_actions::post_id.eq_any(post_id_)) + .filter(post_actions::person_id.eq(person_id_)), ) - .execute(conn) + .set_null(post_actions::read) + .get_result(conn) .await } } @@ -365,11 +386,18 @@ impl PostHide { let forms = post_ids .into_iter() - .map(|post_id| PostHideForm { post_id, person_id }) - .collect::>(); - insert_into(post_hide::table) + .map(|post_id| { + ( + PostHideForm { post_id, person_id }, + post_actions::hidden.eq(now().nullable()), + ) + }) + .collect::>(); + insert_into(post_actions::table) .values(forms) - .on_conflict_do_nothing() + .on_conflict((post_actions::person_id, post_actions::post_id)) + .do_update() + .set(post_actions::hidden.eq(now().nullable())) .execute(conn) .await } @@ -378,15 +406,16 @@ impl PostHide { pool: &mut DbPool<'_>, post_id_: HashSet, person_id_: PersonId, - ) -> Result { + ) -> Result { let conn = &mut get_conn(pool).await?; - diesel::delete( - post_hide::table - .filter(post_hide::post_id.eq_any(post_id_)) - .filter(post_hide::person_id.eq(person_id_)), + uplete::new( + post_actions::table + .filter(post_actions::post_id.eq_any(post_id_)) + .filter(post_actions::person_id.eq(person_id_)), ) - .execute(conn) + .set_null(post_actions::hidden) + .get_result(conn) .await } } @@ -411,7 +440,7 @@ mod tests { }, }, traits::{Crud, Likeable, Saveable}, - utils::build_db_pool_for_tests, + utils::{build_db_pool_for_tests, uplete}, }; use chrono::DateTime; use lemmy_utils::error::LemmyResult; @@ -423,7 +452,7 @@ mod tests { #[tokio::test] #[serial] async fn test_crud() -> LemmyResult<()> { - let pool = &build_db_pool_for_tests().await; + let pool = &build_db_pool_for_tests(); let pool = &mut pool.into(); let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?; @@ -540,16 +569,16 @@ mod tests { assert_eq!(1, scheduled_post_count); let like_removed = PostLike::remove(pool, inserted_person.id, inserted_post.id).await?; - assert_eq!(1, like_removed); + assert_eq!(uplete::Count::only_updated(1), like_removed); let saved_removed = PostSaved::unsave(pool, &post_saved_form).await?; - assert_eq!(1, saved_removed); + assert_eq!(uplete::Count::only_updated(1), saved_removed); let read_removed = PostRead::mark_as_unread( pool, HashSet::from([inserted_post.id, inserted_post2.id]), inserted_person.id, ) .await?; - assert_eq!(2, read_removed); + assert_eq!(uplete::Count::only_deleted(2), read_removed); let num_deleted = Post::delete(pool, inserted_post.id).await? + Post::delete(pool, inserted_post2.id).await? diff --git a/crates/db_schema/src/impls/post_report.rs b/crates/db_schema/src/impls/post_report.rs index 5507423e1..e7d27aee9 100644 --- a/crates/db_schema/src/impls/post_report.rs +++ b/crates/db_schema/src/impls/post_report.rs @@ -126,7 +126,7 @@ mod tests { #[tokio::test] #[serial] async fn test_resolve_post_report() -> Result<(), Error> { - let pool = &build_db_pool_for_tests().await; + let pool = &build_db_pool_for_tests(); let pool = &mut pool.into(); let (person, report) = init(pool).await?; @@ -146,7 +146,7 @@ mod tests { #[tokio::test] #[serial] async fn test_resolve_all_post_reports() -> Result<(), Error> { - let pool = &build_db_pool_for_tests().await; + let pool = &build_db_pool_for_tests(); let pool = &mut pool.into(); let (person, report) = init(pool).await?; diff --git a/crates/db_schema/src/impls/private_message.rs b/crates/db_schema/src/impls/private_message.rs index 264175fe2..e08b4cf7f 100644 --- a/crates/db_schema/src/impls/private_message.rs +++ b/crates/db_schema/src/impls/private_message.rs @@ -104,7 +104,7 @@ mod tests { #[tokio::test] #[serial] async fn test_crud() -> LemmyResult<()> { - let pool = &build_db_pool_for_tests().await; + let pool = &build_db_pool_for_tests(); let pool = &mut pool.into(); let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?; diff --git a/crates/db_schema/src/impls/site.rs b/crates/db_schema/src/impls/site.rs index 8f57647a3..e993639fa 100644 --- a/crates/db_schema/src/impls/site.rs +++ b/crates/db_schema/src/impls/site.rs @@ -10,7 +10,7 @@ use crate::{ }; use diesel::{dsl::insert_into, result::Error, ExpressionMethods, OptionalExtension, QueryDsl}; use diesel_async::RunQueryDsl; -use lemmy_utils::{error::LemmyResult, LemmyErrorType}; +use lemmy_utils::error::{LemmyErrorType, LemmyResult}; use url::Url; #[async_trait] diff --git a/crates/db_schema/src/lib.rs b/crates/db_schema/src/lib.rs index dbadaaf95..7ee60cc1e 100644 --- a/crates/db_schema/src/lib.rs +++ b/crates/db_schema/src/lib.rs @@ -30,11 +30,11 @@ pub mod sensitive; pub mod schema; #[cfg(feature = "full")] pub mod aliases { - use crate::schema::{community_moderator, person}; + use crate::schema::{community_actions, person}; diesel::alias!( + community_actions as creator_community_actions: CreatorCommunityActions, person as person1: Person1, person as person2: Person2, - community_moderator as community_moderator1: CommunityModerator1 ); } pub mod source; @@ -191,6 +191,7 @@ pub enum SubscribedType { Subscribed, NotSubscribed, Pending, + ApprovalRequired, } #[derive(EnumString, Display, Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Hash)] @@ -241,14 +242,14 @@ pub enum PostFeatureType { #[cfg_attr(feature = "full", DbValueStyle = "verbatim")] #[cfg_attr(feature = "full", ts(export))] /// Defines who can browse and interact with content in a community. -/// -/// TODO: Also use this to define private communities pub enum CommunityVisibility { /// Public community, any local or federated user can interact. #[default] Public, /// Unfederated community, only local users can interact. LocalOnly, + /// Users need to be approved by mods before they are able to browse or post. + Private, } #[derive( diff --git a/crates/db_schema/src/newtypes.rs b/crates/db_schema/src/newtypes.rs index fe1febef5..c28be8222 100644 --- a/crates/db_schema/src/newtypes.rs +++ b/crates/db_schema/src/newtypes.rs @@ -174,8 +174,9 @@ pub struct LtreeDef(pub String); #[repr(transparent)] #[derive(Clone, PartialEq, Eq, Serialize, Deserialize, Debug, Hash)] -#[cfg_attr(feature = "full", derive(AsExpression, FromSqlRow))] +#[cfg_attr(feature = "full", derive(AsExpression, FromSqlRow, TS))] #[cfg_attr(feature = "full", diesel(sql_type = diesel::sql_types::Text))] +#[cfg_attr(feature = "full", ts(export))] pub struct DbUrl(pub(crate) Box); impl DbUrl { @@ -248,19 +249,6 @@ impl Deref for DbUrl { } } -#[cfg(feature = "full")] -impl TS for DbUrl { - fn name() -> String { - "string".to_string() - } - fn dependencies() -> Vec { - Vec::new() - } - fn transparent() -> bool { - true - } -} - #[cfg(feature = "full")] impl ToSql for DbUrl { fn to_sql(&self, out: &mut Output) -> diesel::serialize::Result { diff --git a/crates/db_schema/src/schema.rs b/crates/db_schema/src/schema.rs index 5534c4f60..1d6f3f1d0 100644 --- a/crates/db_schema/src/schema.rs +++ b/crates/db_schema/src/schema.rs @@ -1,39 +1,43 @@ // @generated automatically by Diesel CLI. pub mod sql_types { - #[derive(diesel::sql_types::SqlType)] + #[derive(diesel::query_builder::QueryId, diesel::sql_types::SqlType)] #[diesel(postgres_type(name = "actor_type_enum"))] pub struct ActorTypeEnum; - #[derive(diesel::sql_types::SqlType)] + #[derive(diesel::query_builder::QueryId, diesel::sql_types::SqlType)] #[diesel(postgres_type(name = "comment_sort_type_enum"))] pub struct CommentSortTypeEnum; - #[derive(diesel::sql_types::SqlType)] + #[derive(diesel::query_builder::QueryId, diesel::sql_types::SqlType)] + #[diesel(postgres_type(name = "community_follower_state"))] + pub struct CommunityFollowerState; + + #[derive(diesel::query_builder::QueryId, diesel::sql_types::SqlType)] #[diesel(postgres_type(name = "community_visibility"))] pub struct CommunityVisibility; - #[derive(diesel::sql_types::SqlType)] + #[derive(diesel::query_builder::QueryId, diesel::sql_types::SqlType)] #[diesel(postgres_type(name = "federation_mode_enum"))] pub struct FederationModeEnum; - #[derive(diesel::sql_types::SqlType)] + #[derive(diesel::query_builder::QueryId, diesel::sql_types::SqlType)] #[diesel(postgres_type(name = "listing_type_enum"))] pub struct ListingTypeEnum; - #[derive(diesel::sql_types::SqlType)] + #[derive(diesel::query_builder::QueryId, diesel::sql_types::SqlType)] #[diesel(postgres_type(name = "ltree"))] pub struct Ltree; - #[derive(diesel::sql_types::SqlType)] + #[derive(diesel::query_builder::QueryId, diesel::sql_types::SqlType)] #[diesel(postgres_type(name = "post_listing_mode_enum"))] pub struct PostListingModeEnum; - #[derive(diesel::sql_types::SqlType)] + #[derive(diesel::query_builder::QueryId, diesel::sql_types::SqlType)] #[diesel(postgres_type(name = "post_sort_type_enum"))] pub struct PostSortTypeEnum; - #[derive(diesel::sql_types::SqlType)] + #[derive(diesel::query_builder::QueryId, diesel::sql_types::SqlType)] #[diesel(postgres_type(name = "registration_mode_enum"))] pub struct RegistrationModeEnum; } @@ -106,6 +110,16 @@ diesel::table! { } } +diesel::table! { + comment_actions (person_id, comment_id) { + person_id -> Int4, + comment_id -> Int4, + like_score -> Nullable, + liked -> Nullable, + saved -> Nullable, + } +} + diesel::table! { comment_aggregates (comment_id) { comment_id -> Int4, @@ -119,16 +133,6 @@ diesel::table! { } } -diesel::table! { - comment_like (person_id, comment_id) { - person_id -> Int4, - comment_id -> Int4, - post_id -> Int4, - score -> Int2, - published -> Timestamptz, - } -} - diesel::table! { comment_reply (id) { id -> Int4, @@ -153,14 +157,6 @@ diesel::table! { } } -diesel::table! { - comment_saved (person_id, comment_id) { - comment_id -> Int4, - person_id -> Int4, - published -> Timestamptz, - } -} - diesel::table! { use diesel::sql_types::*; use super::sql_types::CommunityVisibility; @@ -171,7 +167,7 @@ diesel::table! { name -> Varchar, #[max_length = 255] title -> Varchar, - description -> Nullable, + sidebar -> Nullable, removed -> Bool, published -> Timestamptz, updated -> Nullable, @@ -189,8 +185,6 @@ diesel::table! { followers_url -> Nullable, #[max_length = 255] inbox_url -> Varchar, - #[max_length = 255] - shared_inbox_url -> Nullable, hidden -> Bool, posting_restricted_to_mods -> Bool, instance_id -> Int4, @@ -199,6 +193,25 @@ diesel::table! { #[max_length = 255] featured_url -> Nullable, visibility -> CommunityVisibility, + #[max_length = 150] + description -> Nullable, + } +} + +diesel::table! { + use diesel::sql_types::*; + use super::sql_types::CommunityFollowerState; + + community_actions (person_id, community_id) { + community_id -> Int4, + person_id -> Int4, + followed -> Nullable, + follow_state -> Nullable, + follow_approver_id -> Nullable, + blocked -> Nullable, + became_moderator -> Nullable, + received_ban -> Nullable, + ban_expires -> Nullable, } } @@ -218,23 +231,6 @@ diesel::table! { } } -diesel::table! { - community_block (person_id, community_id) { - person_id -> Int4, - community_id -> Int4, - published -> Timestamptz, - } -} - -diesel::table! { - community_follower (person_id, community_id) { - community_id -> Int4, - person_id -> Int4, - published -> Timestamptz, - pending -> Bool, - } -} - diesel::table! { community_language (community_id, language_id) { community_id -> Int4, @@ -242,23 +238,6 @@ diesel::table! { } } -diesel::table! { - community_moderator (person_id, community_id) { - community_id -> Int4, - person_id -> Int4, - published -> Timestamptz, - } -} - -diesel::table! { - community_person_ban (person_id, community_id) { - community_id -> Int4, - person_id -> Int4, - published -> Timestamptz, - expires -> Nullable, - } -} - diesel::table! { custom_emoji (id) { id -> Int4, @@ -340,10 +319,10 @@ diesel::table! { } diesel::table! { - instance_block (person_id, instance_id) { + instance_actions (person_id, instance_id) { person_id -> Int4, instance_id -> Int4, - published -> Timestamptz, + blocked -> Nullable, } } @@ -473,6 +452,7 @@ diesel::table! { totp_2fa_enabled -> Bool, enable_keyboard_navigation -> Bool, enable_animated_images -> Bool, + enable_private_messages -> Bool, collapse_bot_comments -> Bool, default_comment_sort_type -> CommentSortTypeEnum, } @@ -687,8 +667,6 @@ diesel::table! { deleted -> Bool, #[max_length = 255] inbox_url -> Varchar, - #[max_length = 255] - shared_inbox_url -> Nullable, matrix_user_id -> Nullable, bot_account -> Bool, ban_expires -> Nullable, @@ -696,6 +674,16 @@ diesel::table! { } } +diesel::table! { + person_actions (person_id, target_id) { + target_id -> Int4, + person_id -> Int4, + followed -> Nullable, + follow_pending -> Nullable, + blocked -> Nullable, + } +} + diesel::table! { person_aggregates (person_id) { person_id -> Int4, @@ -713,23 +701,6 @@ diesel::table! { } } -diesel::table! { - person_block (person_id, target_id) { - person_id -> Int4, - target_id -> Int4, - published -> Timestamptz, - } -} - -diesel::table! { - person_follower (follower_id, person_id) { - person_id -> Int4, - follower_id -> Int4, - published -> Timestamptz, - pending -> Bool, - } -} - diesel::table! { person_mention (id) { id -> Int4, @@ -740,15 +711,6 @@ diesel::table! { } } -diesel::table! { - person_post_aggregates (person_id, post_id) { - person_id -> Int4, - post_id -> Int4, - read_comments -> Int8, - published -> Timestamptz, - } -} - diesel::table! { post (id) { id -> Int4, @@ -781,6 +743,20 @@ diesel::table! { } } +diesel::table! { + post_actions (person_id, post_id) { + post_id -> Int4, + person_id -> Int4, + read -> Nullable, + read_comments -> Nullable, + read_comments_amount -> Nullable, + saved -> Nullable, + liked -> Nullable, + like_score -> Nullable, + hidden -> Nullable, + } +} + diesel::table! { post_aggregates (post_id) { post_id -> Int4, @@ -803,31 +779,6 @@ diesel::table! { } } -diesel::table! { - post_hide (person_id, post_id) { - post_id -> Int4, - person_id -> Int4, - published -> Timestamptz, - } -} - -diesel::table! { - post_like (person_id, post_id) { - post_id -> Int4, - person_id -> Int4, - score -> Int2, - published -> Timestamptz, - } -} - -diesel::table! { - post_read (person_id, post_id) { - post_id -> Int4, - person_id -> Int4, - published -> Timestamptz, - } -} - diesel::table! { post_report (id) { id -> Int4, @@ -845,14 +796,6 @@ diesel::table! { } } -diesel::table! { - post_saved (person_id, post_id) { - post_id -> Int4, - person_id -> Int4, - published -> Timestamptz, - } -} - diesel::table! { private_message (id) { id -> Int4, @@ -996,34 +939,24 @@ diesel::joinable!(admin_purge_post -> person (admin_person_id)); diesel::joinable!(comment -> language (language_id)); diesel::joinable!(comment -> person (creator_id)); diesel::joinable!(comment -> post (post_id)); +diesel::joinable!(comment_actions -> comment (comment_id)); +diesel::joinable!(comment_actions -> person (person_id)); diesel::joinable!(comment_aggregates -> comment (comment_id)); -diesel::joinable!(comment_like -> comment (comment_id)); -diesel::joinable!(comment_like -> person (person_id)); -diesel::joinable!(comment_like -> post (post_id)); diesel::joinable!(comment_reply -> comment (comment_id)); diesel::joinable!(comment_reply -> person (recipient_id)); diesel::joinable!(comment_report -> comment (comment_id)); -diesel::joinable!(comment_saved -> comment (comment_id)); -diesel::joinable!(comment_saved -> person (person_id)); diesel::joinable!(community -> instance (instance_id)); +diesel::joinable!(community_actions -> community (community_id)); diesel::joinable!(community_aggregates -> community (community_id)); -diesel::joinable!(community_block -> community (community_id)); -diesel::joinable!(community_block -> person (person_id)); -diesel::joinable!(community_follower -> community (community_id)); -diesel::joinable!(community_follower -> person (person_id)); diesel::joinable!(community_language -> community (community_id)); diesel::joinable!(community_language -> language (language_id)); -diesel::joinable!(community_moderator -> community (community_id)); -diesel::joinable!(community_moderator -> person (person_id)); -diesel::joinable!(community_person_ban -> community (community_id)); -diesel::joinable!(community_person_ban -> person (person_id)); diesel::joinable!(custom_emoji_keyword -> custom_emoji (custom_emoji_id)); diesel::joinable!(email_verification -> local_user (local_user_id)); diesel::joinable!(federation_allowlist -> instance (instance_id)); diesel::joinable!(federation_blocklist -> instance (instance_id)); diesel::joinable!(federation_queue_state -> instance (instance_id)); -diesel::joinable!(instance_block -> instance (instance_id)); -diesel::joinable!(instance_block -> person (person_id)); +diesel::joinable!(instance_actions -> instance (instance_id)); +diesel::joinable!(instance_actions -> person (person_id)); diesel::joinable!(local_image -> local_user (local_user_id)); diesel::joinable!(local_site -> site (site_id)); diesel::joinable!(local_site_rate_limit -> local_site (local_site_id)); @@ -1055,24 +988,16 @@ diesel::joinable!(person_aggregates -> person (person_id)); diesel::joinable!(person_ban -> person (person_id)); diesel::joinable!(person_mention -> comment (comment_id)); diesel::joinable!(person_mention -> person (recipient_id)); -diesel::joinable!(person_post_aggregates -> person (person_id)); -diesel::joinable!(person_post_aggregates -> post (post_id)); diesel::joinable!(post -> community (community_id)); diesel::joinable!(post -> language (language_id)); diesel::joinable!(post -> person (creator_id)); +diesel::joinable!(post_actions -> person (person_id)); +diesel::joinable!(post_actions -> post (post_id)); diesel::joinable!(post_aggregates -> community (community_id)); diesel::joinable!(post_aggregates -> instance (instance_id)); diesel::joinable!(post_aggregates -> person (creator_id)); diesel::joinable!(post_aggregates -> post (post_id)); -diesel::joinable!(post_hide -> person (person_id)); -diesel::joinable!(post_hide -> post (post_id)); -diesel::joinable!(post_like -> person (person_id)); -diesel::joinable!(post_like -> post (post_id)); -diesel::joinable!(post_read -> person (person_id)); -diesel::joinable!(post_read -> post (post_id)); diesel::joinable!(post_report -> post (post_id)); -diesel::joinable!(post_saved -> person (person_id)); -diesel::joinable!(post_saved -> post (post_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)); @@ -1088,18 +1013,14 @@ diesel::allow_tables_to_appear_in_same_query!( admin_purge_post, captcha_answer, comment, + comment_actions, comment_aggregates, - comment_like, comment_reply, comment_report, - comment_saved, community, + community_actions, community_aggregates, - community_block, - community_follower, community_language, - community_moderator, - community_person_ban, custom_emoji, custom_emoji_keyword, email_verification, @@ -1108,7 +1029,7 @@ diesel::allow_tables_to_appear_in_same_query!( federation_queue_state, image_details, instance, - instance_block, + instance_actions, language, local_image, local_site, @@ -1133,19 +1054,14 @@ diesel::allow_tables_to_appear_in_same_query!( oauth_provider, password_reset_request, person, + person_actions, person_aggregates, person_ban, - person_block, - person_follower, person_mention, - person_post_aggregates, post, + post_actions, post_aggregates, - post_hide, - post_like, - post_read, post_report, - post_saved, private_message, private_message_report, received_activity, diff --git a/crates/db_schema/src/sensitive.rs b/crates/db_schema/src/sensitive.rs index 340679e2f..5d1d449fb 100644 --- a/crates/db_schema/src/sensitive.rs +++ b/crates/db_schema/src/sensitive.rs @@ -4,8 +4,9 @@ use std::{fmt::Debug, ops::Deref}; use ts_rs::TS; #[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Serialize, Default)] -#[cfg_attr(feature = "full", derive(DieselNewType))] +#[cfg_attr(feature = "full", derive(DieselNewType, TS))] #[serde(transparent)] +#[cfg_attr(feature = "full", ts(export))] pub struct SensitiveString(String); impl SensitiveString { @@ -39,19 +40,3 @@ impl From for SensitiveString { SensitiveString(t) } } - -#[cfg(feature = "full")] -impl TS for SensitiveString { - fn name() -> String { - "string".to_string() - } - fn name_with_type_args(_args: Vec) -> String { - "string".to_string() - } - fn dependencies() -> Vec { - Vec::new() - } - fn transparent() -> bool { - true - } -} diff --git a/crates/db_schema/src/source/comment.rs b/crates/db_schema/src/source/comment.rs index e7d031c68..be9aa7873 100644 --- a/crates/db_schema/src/source/comment.rs +++ b/crates/db_schema/src/source/comment.rs @@ -2,9 +2,11 @@ use crate::newtypes::LtreeDef; use crate::newtypes::{CommentId, DbUrl, LanguageId, PersonId, PostId}; #[cfg(feature = "full")] -use crate::schema::{comment, comment_like, comment_saved}; +use crate::schema::{comment, comment_actions}; use chrono::{DateTime, Utc}; #[cfg(feature = "full")] +use diesel::{dsl, expression_methods::NullableExpressionMethods}; +#[cfg(feature = "full")] use diesel_ltree::Ltree; use serde::{Deserialize, Serialize}; use serde_with::skip_serializing_none; @@ -30,6 +32,7 @@ pub struct Comment { /// Whether the comment has been removed. pub removed: bool, pub published: DateTime, + #[cfg_attr(feature = "full", ts(optional))] pub updated: Option>, /// Whether the comment has been deleted by its creator. pub deleted: bool, @@ -96,24 +99,27 @@ pub struct CommentUpdateForm { derive(Identifiable, Queryable, Selectable, Associations) )] #[cfg_attr(feature = "full", diesel(belongs_to(crate::source::comment::Comment)))] -#[cfg_attr(feature = "full", diesel(table_name = comment_like))] +#[cfg_attr(feature = "full", diesel(table_name = comment_actions))] #[cfg_attr(feature = "full", diesel(primary_key(person_id, comment_id)))] #[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] pub struct CommentLike { pub person_id: PersonId, pub comment_id: CommentId, - pub post_id: PostId, // TODO this is redundant + #[cfg_attr(feature = "full", diesel(select_expression = comment_actions::like_score.assume_not_null()))] + #[cfg_attr(feature = "full", diesel(select_expression_type = dsl::AssumeNotNull))] pub score: i16, + #[cfg_attr(feature = "full", diesel(select_expression = comment_actions::liked.assume_not_null()))] + #[cfg_attr(feature = "full", diesel(select_expression_type = dsl::AssumeNotNull))] pub published: DateTime, } #[derive(Clone)] #[cfg_attr(feature = "full", derive(Insertable, AsChangeset))] -#[cfg_attr(feature = "full", diesel(table_name = comment_like))] +#[cfg_attr(feature = "full", diesel(table_name = comment_actions))] pub struct CommentLikeForm { pub person_id: PersonId, pub comment_id: CommentId, - pub post_id: PostId, // TODO this is redundant + #[cfg_attr(feature = "full", diesel(column_name = like_score))] pub score: i16, } @@ -123,17 +129,19 @@ pub struct CommentLikeForm { derive(Identifiable, Queryable, Selectable, Associations) )] #[cfg_attr(feature = "full", diesel(belongs_to(crate::source::comment::Comment)))] -#[cfg_attr(feature = "full", diesel(table_name = comment_saved))] +#[cfg_attr(feature = "full", diesel(table_name = comment_actions))] #[cfg_attr(feature = "full", diesel(primary_key(person_id, comment_id)))] #[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] pub struct CommentSaved { pub comment_id: CommentId, pub person_id: PersonId, + #[cfg_attr(feature = "full", diesel(select_expression = comment_actions::saved.assume_not_null()))] + #[cfg_attr(feature = "full", diesel(select_expression_type = dsl::AssumeNotNull))] pub published: DateTime, } #[cfg_attr(feature = "full", derive(Insertable, AsChangeset))] -#[cfg_attr(feature = "full", diesel(table_name = comment_saved))] +#[cfg_attr(feature = "full", diesel(table_name = comment_actions))] pub struct CommentSavedForm { pub comment_id: CommentId, pub person_id: PersonId, diff --git a/crates/db_schema/src/source/comment_report.rs b/crates/db_schema/src/source/comment_report.rs index 73dadc945..a19b6925a 100644 --- a/crates/db_schema/src/source/comment_report.rs +++ b/crates/db_schema/src/source/comment_report.rs @@ -25,8 +25,10 @@ pub struct CommentReport { pub original_comment_text: String, pub reason: String, pub resolved: bool, + #[cfg_attr(feature = "full", ts(optional))] pub resolver_id: Option, pub published: DateTime, + #[cfg_attr(feature = "full", ts(optional))] pub updated: Option>, } diff --git a/crates/db_schema/src/source/community.rs b/crates/db_schema/src/source/community.rs index ced85bf4c..f65ef06f9 100644 --- a/crates/db_schema/src/source/community.rs +++ b/crates/db_schema/src/source/community.rs @@ -1,5 +1,5 @@ #[cfg(feature = "full")] -use crate::schema::{community, community_follower, community_moderator, community_person_ban}; +use crate::schema::{community, community_actions}; use crate::{ newtypes::{CommunityId, DbUrl, InstanceId, PersonId}, sensitive::SensitiveString, @@ -7,8 +7,11 @@ use crate::{ CommunityVisibility, }; use chrono::{DateTime, Utc}; +#[cfg(feature = "full")] +use diesel::{dsl, expression_methods::NullableExpressionMethods}; use serde::{Deserialize, Serialize}; use serde_with::skip_serializing_none; +use strum::{Display, EnumString}; #[cfg(feature = "full")] use ts_rs::TS; @@ -24,11 +27,13 @@ pub struct Community { pub name: String, /// A longer title, that can contain other characters, and doesn't have to be unique. pub title: String, - /// A sidebar / markdown description. - pub description: Option, + /// A sidebar for the community in markdown. + #[cfg_attr(feature = "full", ts(optional))] + pub sidebar: Option, /// Whether the community is removed by a mod. pub removed: bool, pub published: DateTime, + #[cfg_attr(feature = "full", ts(optional))] pub updated: Option>, /// Whether the community has been deleted by its creator. pub deleted: bool, @@ -45,8 +50,10 @@ pub struct Community { #[serde(skip)] pub last_refreshed_at: DateTime, /// A URL for an icon. + #[cfg_attr(feature = "full", ts(optional))] pub icon: Option, /// A URL for a banner. + #[cfg_attr(feature = "full", ts(optional))] pub banner: Option, #[cfg_attr(feature = "full", ts(skip))] #[serde(skip)] @@ -54,8 +61,6 @@ pub struct Community { #[cfg_attr(feature = "full", ts(skip))] #[serde(skip, default = "placeholder_apub_url")] pub inbox_url: DbUrl, - #[serde(skip)] - pub shared_inbox_url: Option, /// Whether the community is hidden. pub hidden: bool, /// Whether posting is restricted to mods only. @@ -68,6 +73,9 @@ pub struct Community { #[serde(skip)] pub featured_url: Option, pub visibility: CommunityVisibility, + /// A shorter, one-line description of the site. + #[cfg_attr(feature = "full", ts(optional))] + pub description: Option, } #[derive(Debug, Clone, derive_new::new)] @@ -79,7 +87,7 @@ pub struct CommunityInsertForm { pub title: String, pub public_key: String, #[new(default)] - pub description: Option, + pub sidebar: Option, #[new(default)] pub removed: Option, #[new(default)] @@ -107,8 +115,6 @@ pub struct CommunityInsertForm { #[new(default)] pub inbox_url: Option, #[new(default)] - pub shared_inbox_url: Option, - #[new(default)] pub moderators_url: Option, #[new(default)] pub featured_url: Option, @@ -118,6 +124,8 @@ pub struct CommunityInsertForm { pub posting_restricted_to_mods: Option, #[new(default)] pub visibility: Option, + #[new(default)] + pub description: Option, } #[derive(Debug, Clone, Default)] @@ -125,7 +133,7 @@ pub struct CommunityInsertForm { #[cfg_attr(feature = "full", diesel(table_name = community))] pub struct CommunityUpdateForm { pub title: Option, - pub description: Option>, + pub sidebar: Option>, pub removed: Option, pub published: Option>, pub updated: Option>>, @@ -140,12 +148,12 @@ pub struct CommunityUpdateForm { pub banner: Option>, pub followers_url: Option, pub inbox_url: Option, - pub shared_inbox_url: Option>, pub moderators_url: Option, pub featured_url: Option, pub hidden: Option, pub posting_restricted_to_mods: Option, pub visibility: Option, + pub description: Option>, } #[derive(PartialEq, Eq, Debug)] @@ -157,18 +165,20 @@ pub struct CommunityUpdateForm { feature = "full", diesel(belongs_to(crate::source::community::Community)) )] -#[cfg_attr(feature = "full", diesel(table_name = community_moderator))] +#[cfg_attr(feature = "full", diesel(table_name = community_actions))] #[cfg_attr(feature = "full", diesel(primary_key(person_id, community_id)))] #[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] pub struct CommunityModerator { pub community_id: CommunityId, pub person_id: PersonId, + #[cfg_attr(feature = "full", diesel(select_expression = community_actions::became_moderator.assume_not_null()))] + #[cfg_attr(feature = "full", diesel(select_expression_type = dsl::AssumeNotNull))] pub published: DateTime, } #[derive(Clone)] #[cfg_attr(feature = "full", derive(Insertable, AsChangeset))] -#[cfg_attr(feature = "full", diesel(table_name = community_moderator))] +#[cfg_attr(feature = "full", diesel(table_name = community_actions))] pub struct CommunityModeratorForm { pub community_id: CommunityId, pub person_id: PersonId, @@ -183,25 +193,43 @@ pub struct CommunityModeratorForm { feature = "full", diesel(belongs_to(crate::source::community::Community)) )] -#[cfg_attr(feature = "full", diesel(table_name = community_person_ban))] +#[cfg_attr(feature = "full", diesel(table_name = community_actions))] #[cfg_attr(feature = "full", diesel(primary_key(person_id, community_id)))] #[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] pub struct CommunityPersonBan { pub community_id: CommunityId, pub person_id: PersonId, + #[cfg_attr(feature = "full", diesel(select_expression = community_actions::received_ban.assume_not_null()))] + #[cfg_attr(feature = "full", diesel(select_expression_type = dsl::AssumeNotNull))] pub published: DateTime, + #[cfg_attr(feature = "full", diesel(column_name = ban_expires))] pub expires: Option>, } #[derive(Clone)] #[cfg_attr(feature = "full", derive(Insertable, AsChangeset))] -#[cfg_attr(feature = "full", diesel(table_name = community_person_ban))] +#[cfg_attr(feature = "full", diesel(table_name = community_actions))] pub struct CommunityPersonBanForm { pub community_id: CommunityId, pub person_id: PersonId, + #[cfg_attr(feature = "full", diesel(column_name = ban_expires))] pub expires: Option>>, } +#[derive(EnumString, Display, Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "full", derive(DbEnum, TS))] +#[cfg_attr( + feature = "full", + ExistingTypePath = "crate::schema::sql_types::CommunityFollowerState" +)] +#[cfg_attr(feature = "full", DbValueStyle = "verbatim")] +#[cfg_attr(feature = "full", ts(export))] +pub enum CommunityFollowerState { + Accepted, + Pending, + ApprovalRequired, +} + #[derive(PartialEq, Eq, Debug)] #[cfg_attr( feature = "full", @@ -211,21 +239,32 @@ pub struct CommunityPersonBanForm { feature = "full", diesel(belongs_to(crate::source::community::Community)) )] -#[cfg_attr(feature = "full", diesel(table_name = community_follower))] +#[cfg_attr(feature = "full", diesel(table_name = community_actions))] #[cfg_attr(feature = "full", diesel(primary_key(person_id, community_id)))] #[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] pub struct CommunityFollower { pub community_id: CommunityId, pub person_id: PersonId, + #[cfg_attr(feature = "full", diesel(select_expression = community_actions::followed.assume_not_null()))] + #[cfg_attr(feature = "full", diesel(select_expression_type = dsl::AssumeNotNull))] pub published: DateTime, - pub pending: bool, + #[cfg_attr(feature = "full", diesel(select_expression = community_actions::follow_state.assume_not_null()))] + #[cfg_attr(feature = "full", diesel(select_expression_type = dsl::AssumeNotNull))] + pub state: CommunityFollowerState, + #[cfg_attr(feature = "full", diesel(column_name = follow_approver_id))] + pub approver_id: Option, } -#[derive(Clone)] +#[derive(Clone, derive_new::new)] #[cfg_attr(feature = "full", derive(Insertable, AsChangeset))] -#[cfg_attr(feature = "full", diesel(table_name = community_follower))] +#[cfg_attr(feature = "full", diesel(table_name = community_actions))] pub struct CommunityFollowerForm { pub community_id: CommunityId, pub person_id: PersonId, - pub pending: bool, + #[new(default)] + #[cfg_attr(feature = "full", diesel(column_name = follow_state))] + pub state: Option, + #[new(default)] + #[cfg_attr(feature = "full", diesel(column_name = follow_approver_id))] + pub approver_id: Option, } diff --git a/crates/db_schema/src/source/community_block.rs b/crates/db_schema/src/source/community_block.rs index 7d43af173..a7c23419c 100644 --- a/crates/db_schema/src/source/community_block.rs +++ b/crates/db_schema/src/source/community_block.rs @@ -1,7 +1,9 @@ use crate::newtypes::{CommunityId, PersonId}; #[cfg(feature = "full")] -use crate::schema::community_block; +use crate::schema::community_actions; use chrono::{DateTime, Utc}; +#[cfg(feature = "full")] +use diesel::{dsl, expression_methods::NullableExpressionMethods}; use serde::{Deserialize, Serialize}; #[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)] @@ -13,17 +15,19 @@ use serde::{Deserialize, Serialize}; feature = "full", diesel(belongs_to(crate::source::community::Community)) )] -#[cfg_attr(feature = "full", diesel(table_name = community_block))] +#[cfg_attr(feature = "full", diesel(table_name = community_actions))] #[cfg_attr(feature = "full", diesel(primary_key(person_id, community_id)))] #[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] pub struct CommunityBlock { pub person_id: PersonId, pub community_id: CommunityId, + #[cfg_attr(feature = "full", diesel(select_expression = community_actions::blocked.assume_not_null()))] + #[cfg_attr(feature = "full", diesel(select_expression_type = dsl::AssumeNotNull))] pub published: DateTime, } #[cfg_attr(feature = "full", derive(Insertable, AsChangeset))] -#[cfg_attr(feature = "full", diesel(table_name = community_block))] +#[cfg_attr(feature = "full", diesel(table_name = community_actions))] pub struct CommunityBlockForm { pub person_id: PersonId, pub community_id: CommunityId, diff --git a/crates/db_schema/src/source/custom_emoji.rs b/crates/db_schema/src/source/custom_emoji.rs index f5a92ea46..bb95cb7c8 100644 --- a/crates/db_schema/src/source/custom_emoji.rs +++ b/crates/db_schema/src/source/custom_emoji.rs @@ -21,6 +21,7 @@ pub struct CustomEmoji { pub alt_text: String, pub category: String, pub published: DateTime, + #[cfg_attr(feature = "full", ts(optional))] pub updated: Option>, } diff --git a/crates/db_schema/src/source/federation_queue_state.rs b/crates/db_schema/src/source/federation_queue_state.rs index 134dfe452..27e464d1f 100644 --- a/crates/db_schema/src/source/federation_queue_state.rs +++ b/crates/db_schema/src/source/federation_queue_state.rs @@ -19,10 +19,13 @@ use ts_rs::TS; pub struct FederationQueueState { pub instance_id: InstanceId, /// the last successfully sent activity id + #[cfg_attr(feature = "full", ts(optional))] pub last_successful_id: Option, + #[cfg_attr(feature = "full", ts(optional))] pub last_successful_published_time: Option>, /// how many failed attempts have been made to send the next activity pub fail_count: i32, /// timestamp of the last retry attempt (when the last failing activity was resent) + #[cfg_attr(feature = "full", ts(optional))] pub last_retry: Option>, } diff --git a/crates/db_schema/src/source/images.rs b/crates/db_schema/src/source/images.rs index 22f5e6eb4..acd339d8e 100644 --- a/crates/db_schema/src/source/images.rs +++ b/crates/db_schema/src/source/images.rs @@ -23,6 +23,7 @@ use ts_rs::TS; #[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] #[cfg_attr(feature = "full", diesel(primary_key(pictrs_alias)))] pub struct LocalImage { + #[cfg_attr(feature = "full", ts(optional))] pub local_user_id: Option, pub pictrs_alias: String, pub pictrs_delete_token: String, diff --git a/crates/db_schema/src/source/instance.rs b/crates/db_schema/src/source/instance.rs index 8c27a2cb6..f622751cc 100644 --- a/crates/db_schema/src/source/instance.rs +++ b/crates/db_schema/src/source/instance.rs @@ -19,8 +19,11 @@ pub struct Instance { pub id: InstanceId, pub domain: String, pub published: DateTime, + #[cfg_attr(feature = "full", ts(optional))] pub updated: Option>, + #[cfg_attr(feature = "full", ts(optional))] pub software: Option, + #[cfg_attr(feature = "full", ts(optional))] pub version: Option, } diff --git a/crates/db_schema/src/source/instance_block.rs b/crates/db_schema/src/source/instance_block.rs index 4eebbf1a8..e1963c894 100644 --- a/crates/db_schema/src/source/instance_block.rs +++ b/crates/db_schema/src/source/instance_block.rs @@ -1,7 +1,9 @@ use crate::newtypes::{InstanceId, PersonId}; #[cfg(feature = "full")] -use crate::schema::instance_block; +use crate::schema::instance_actions; use chrono::{DateTime, Utc}; +#[cfg(feature = "full")] +use diesel::{dsl, expression_methods::NullableExpressionMethods}; use serde::{Deserialize, Serialize}; #[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)] @@ -13,17 +15,19 @@ use serde::{Deserialize, Serialize}; feature = "full", diesel(belongs_to(crate::source::instance::Instance)) )] -#[cfg_attr(feature = "full", diesel(table_name = instance_block))] +#[cfg_attr(feature = "full", diesel(table_name = instance_actions))] #[cfg_attr(feature = "full", diesel(primary_key(person_id, instance_id)))] #[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] pub struct InstanceBlock { pub person_id: PersonId, pub instance_id: InstanceId, + #[cfg_attr(feature = "full", diesel(select_expression = instance_actions::blocked.assume_not_null()))] + #[cfg_attr(feature = "full", diesel(select_expression_type = dsl::AssumeNotNull))] pub published: DateTime, } #[cfg_attr(feature = "full", derive(Insertable, AsChangeset))] -#[cfg_attr(feature = "full", diesel(table_name = instance_block))] +#[cfg_attr(feature = "full", diesel(table_name = instance_actions))] pub struct InstanceBlockForm { pub person_id: PersonId, pub instance_id: InstanceId, diff --git a/crates/db_schema/src/source/local_site.rs b/crates/db_schema/src/source/local_site.rs index 5fa57fe3b..b5bcebc58 100644 --- a/crates/db_schema/src/source/local_site.rs +++ b/crates/db_schema/src/source/local_site.rs @@ -33,6 +33,7 @@ pub struct LocalSite { /// Whether emails are required. pub require_email_verification: bool, /// An optional registration application questionnaire in markdown. + #[cfg_attr(feature = "full", ts(optional))] pub application_question: Option, /// Whether the instance is private or public. pub private_instance: bool, @@ -40,12 +41,14 @@ pub struct LocalSite { pub default_theme: String, pub default_post_listing_type: ListingType, /// An optional legal disclaimer page. + #[cfg_attr(feature = "full", ts(optional))] pub legal_information: Option, /// Whether to hide mod names on the modlog. pub hide_modlog_mod_names: bool, /// Whether new applications email admins. pub application_email_admins: bool, /// An optional regex to filter words. + #[cfg_attr(feature = "full", ts(optional))] pub slur_filter_regex: Option, /// The max actor name length. pub actor_name_max_length: i32, @@ -56,6 +59,7 @@ pub struct LocalSite { /// The captcha difficulty. pub captcha_difficulty: String, pub published: DateTime, + #[cfg_attr(feature = "full", ts(optional))] pub updated: Option>, pub registration_mode: RegistrationMode, /// Whether to email admins on new reports. diff --git a/crates/db_schema/src/source/local_site_rate_limit.rs b/crates/db_schema/src/source/local_site_rate_limit.rs index f7f25f5c1..af424a248 100644 --- a/crates/db_schema/src/source/local_site_rate_limit.rs +++ b/crates/db_schema/src/source/local_site_rate_limit.rs @@ -34,6 +34,7 @@ pub struct LocalSiteRateLimit { pub search: i32, pub search_per_second: i32, pub published: DateTime, + #[cfg_attr(feature = "full", ts(optional))] pub updated: Option>, pub import_user_settings: i32, pub import_user_settings_per_second: i32, diff --git a/crates/db_schema/src/source/local_site_url_blocklist.rs b/crates/db_schema/src/source/local_site_url_blocklist.rs index 4ac0893ec..d6127a78a 100644 --- a/crates/db_schema/src/source/local_site_url_blocklist.rs +++ b/crates/db_schema/src/source/local_site_url_blocklist.rs @@ -16,6 +16,7 @@ pub struct LocalSiteUrlBlocklist { pub id: i32, pub url: String, pub published: DateTime, + #[cfg_attr(feature = "full", ts(optional))] pub updated: Option>, } diff --git a/crates/db_schema/src/source/local_user.rs b/crates/db_schema/src/source/local_user.rs index 37da70908..fd15253cc 100644 --- a/crates/db_schema/src/source/local_user.rs +++ b/crates/db_schema/src/source/local_user.rs @@ -27,6 +27,7 @@ pub struct LocalUser { pub person_id: PersonId, #[serde(skip)] pub password_encrypted: Option, + #[cfg_attr(feature = "full", ts(optional))] pub email: Option, /// Whether to show NSFW content. pub show_nsfw: bool, @@ -62,6 +63,8 @@ pub struct LocalUser { /// Whether user avatars and inline images in the UI that are gifs should be allowed to play or /// should be paused pub enable_animated_images: bool, + /// Whether a user can send / receive private messages + pub enable_private_messages: bool, /// Whether to auto-collapse bot comments. pub collapse_bot_comments: bool, pub default_comment_sort_type: CommentSortType, @@ -116,6 +119,8 @@ pub struct LocalUserInsertForm { #[new(default)] pub enable_animated_images: Option, #[new(default)] + pub enable_private_messages: Option, + #[new(default)] pub collapse_bot_comments: Option, #[new(default)] pub default_comment_sort_type: Option, @@ -147,6 +152,7 @@ pub struct LocalUserUpdateForm { pub totp_2fa_enabled: Option, pub enable_keyboard_navigation: Option, pub enable_animated_images: Option, + pub enable_private_messages: Option, pub collapse_bot_comments: Option, pub default_comment_sort_type: Option, } diff --git a/crates/db_schema/src/source/login_token.rs b/crates/db_schema/src/source/login_token.rs index 38aac33ef..20d81afb0 100644 --- a/crates/db_schema/src/source/login_token.rs +++ b/crates/db_schema/src/source/login_token.rs @@ -24,7 +24,9 @@ pub struct LoginToken { pub published: DateTime, /// IP address where login was made from, allows invalidating logins by IP address. /// Could be stored in truncated format, or store derived information for better privacy. + #[cfg_attr(feature = "full", ts(optional))] pub ip: Option, + #[cfg_attr(feature = "full", ts(optional))] pub user_agent: Option, } diff --git a/crates/db_schema/src/source/moderator.rs b/crates/db_schema/src/source/moderator.rs index c1f58ebc8..b4fdcc676 100644 --- a/crates/db_schema/src/source/moderator.rs +++ b/crates/db_schema/src/source/moderator.rs @@ -34,6 +34,7 @@ pub struct ModRemovePost { pub id: i32, pub mod_person_id: PersonId, pub post_id: PostId, + #[cfg_attr(feature = "full", ts(optional))] pub reason: Option, pub removed: bool, pub when_: DateTime, @@ -105,6 +106,7 @@ pub struct ModRemoveComment { pub id: i32, pub mod_person_id: PersonId, pub comment_id: CommentId, + #[cfg_attr(feature = "full", ts(optional))] pub reason: Option, pub removed: bool, pub when_: DateTime, @@ -130,6 +132,7 @@ pub struct ModRemoveCommunity { pub id: i32, pub mod_person_id: PersonId, pub community_id: CommunityId, + #[cfg_attr(feature = "full", ts(optional))] pub reason: Option, pub removed: bool, pub when_: DateTime, @@ -156,8 +159,10 @@ pub struct ModBanFromCommunity { pub mod_person_id: PersonId, pub other_person_id: PersonId, pub community_id: CommunityId, + #[cfg_attr(feature = "full", ts(optional))] pub reason: Option, pub banned: bool, + #[cfg_attr(feature = "full", ts(optional))] pub expires: Option>, pub when_: DateTime, } @@ -184,8 +189,10 @@ pub struct ModBan { pub id: i32, pub mod_person_id: PersonId, pub other_person_id: PersonId, + #[cfg_attr(feature = "full", ts(optional))] pub reason: Option, pub banned: bool, + #[cfg_attr(feature = "full", ts(optional))] pub expires: Option>, pub when_: DateTime, } @@ -211,6 +218,7 @@ pub struct ModHideCommunity { pub community_id: CommunityId, pub mod_person_id: PersonId, pub when_: DateTime, + #[cfg_attr(feature = "full", ts(optional))] pub reason: Option, pub hidden: bool, } @@ -303,6 +311,7 @@ pub struct ModAddForm { pub struct AdminPurgePerson { pub id: i32, pub admin_person_id: PersonId, + #[cfg_attr(feature = "full", ts(optional))] pub reason: Option, pub when_: DateTime, } @@ -324,6 +333,7 @@ pub struct AdminPurgePersonForm { pub struct AdminPurgeCommunity { pub id: i32, pub admin_person_id: PersonId, + #[cfg_attr(feature = "full", ts(optional))] pub reason: Option, pub when_: DateTime, } @@ -346,6 +356,7 @@ 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, } @@ -369,6 +380,7 @@ 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, } diff --git a/crates/db_schema/src/source/oauth_account.rs b/crates/db_schema/src/source/oauth_account.rs index 83b578e22..b7d190c35 100644 --- a/crates/db_schema/src/source/oauth_account.rs +++ b/crates/db_schema/src/source/oauth_account.rs @@ -19,6 +19,7 @@ pub struct OAuthAccount { pub oauth_provider_id: OAuthProviderId, pub oauth_user_id: String, pub published: DateTime, + #[cfg_attr(feature = "full", ts(optional))] pub updated: Option>, } diff --git a/crates/db_schema/src/source/oauth_provider.rs b/crates/db_schema/src/source/oauth_provider.rs index 75b989805..a70405a5e 100644 --- a/crates/db_schema/src/source/oauth_provider.rs +++ b/crates/db_schema/src/source/oauth_provider.rs @@ -60,6 +60,7 @@ pub struct OAuthProvider { /// switch to enable or disable an oauth provider pub enabled: bool, pub published: DateTime, + #[cfg_attr(feature = "full", ts(optional))] pub updated: Option>, } diff --git a/crates/db_schema/src/source/person.rs b/crates/db_schema/src/source/person.rs index 332b46eb5..9c2a2d426 100644 --- a/crates/db_schema/src/source/person.rs +++ b/crates/db_schema/src/source/person.rs @@ -1,11 +1,13 @@ #[cfg(feature = "full")] -use crate::schema::{person, person_follower}; +use crate::schema::{person, person_actions}; use crate::{ newtypes::{DbUrl, InstanceId, PersonId}, sensitive::SensitiveString, source::placeholder_apub_url, }; use chrono::{DateTime, Utc}; +#[cfg(feature = "full")] +use diesel::{dsl, expression_methods::NullableExpressionMethods}; use serde::{Deserialize, Serialize}; use serde_with::skip_serializing_none; #[cfg(feature = "full")] @@ -22,16 +24,20 @@ pub struct Person { pub id: PersonId, pub name: String, /// A shorter display name. + #[cfg_attr(feature = "full", ts(optional))] pub display_name: Option, /// A URL for an avatar. + #[cfg_attr(feature = "full", ts(optional))] pub avatar: Option, /// Whether the person is banned. pub banned: bool, pub published: DateTime, + #[cfg_attr(feature = "full", ts(optional))] pub updated: Option>, /// The federated actor_id. pub actor_id: DbUrl, /// An optional bio, in markdown. + #[cfg_attr(feature = "full", ts(optional))] pub bio: Option, /// Whether the person is local to our site. pub local: bool, @@ -42,19 +48,20 @@ pub struct Person { #[serde(skip)] pub last_refreshed_at: DateTime, /// A URL for a banner. + #[cfg_attr(feature = "full", ts(optional))] pub banner: Option, /// Whether the person is deleted. pub deleted: bool, #[cfg_attr(feature = "full", ts(skip))] #[serde(skip, default = "placeholder_apub_url")] pub inbox_url: DbUrl, - #[serde(skip)] - pub shared_inbox_url: Option, /// A matrix id, usually given an @person:matrix.org + #[cfg_attr(feature = "full", ts(optional))] pub matrix_user_id: Option, /// Whether the person is a bot account. pub bot_account: bool, /// When their ban, if it exists, expires, if at all. + #[cfg_attr(feature = "full", ts(optional))] pub ban_expires: Option>, pub instance_id: InstanceId, } @@ -93,8 +100,6 @@ pub struct PersonInsertForm { #[new(default)] pub inbox_url: Option, #[new(default)] - pub shared_inbox_url: Option, - #[new(default)] pub matrix_user_id: Option, #[new(default)] pub bot_account: Option, @@ -119,7 +124,6 @@ pub struct PersonUpdateForm { pub banner: Option>, pub deleted: Option, pub inbox_url: Option, - pub shared_inbox_url: Option>, pub matrix_user_id: Option>, pub bot_account: Option, pub ban_expires: Option>>, @@ -131,21 +135,30 @@ pub struct PersonUpdateForm { derive(Identifiable, Queryable, Selectable, Associations) )] #[cfg_attr(feature = "full", diesel(belongs_to(crate::source::person::Person)))] -#[cfg_attr(feature = "full", diesel(table_name = person_follower))] -#[cfg_attr(feature = "full", diesel(primary_key(follower_id, person_id)))] +#[cfg_attr(feature = "full", diesel(table_name = person_actions))] +#[cfg_attr(feature = "full", diesel(primary_key(person_id, target_id)))] #[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] pub struct PersonFollower { + #[cfg_attr(feature = "full", diesel(column_name = target_id))] pub person_id: PersonId, + #[cfg_attr(feature = "full", diesel(column_name = person_id))] pub follower_id: PersonId, + #[cfg_attr(feature = "full", diesel(select_expression = person_actions::followed.assume_not_null()))] + #[cfg_attr(feature = "full", diesel(select_expression_type = dsl::AssumeNotNull))] pub published: DateTime, + #[cfg_attr(feature = "full", diesel(select_expression = person_actions::follow_pending.assume_not_null()))] + #[cfg_attr(feature = "full", diesel(select_expression_type = dsl::AssumeNotNull))] pub pending: bool, } #[derive(Clone)] #[cfg_attr(feature = "full", derive(Insertable, AsChangeset))] -#[cfg_attr(feature = "full", diesel(table_name = person_follower))] +#[cfg_attr(feature = "full", diesel(table_name = person_actions))] pub struct PersonFollowerForm { + #[cfg_attr(feature = "full", diesel(column_name = target_id))] pub person_id: PersonId, + #[cfg_attr(feature = "full", diesel(column_name = person_id))] pub follower_id: PersonId, + #[cfg_attr(feature = "full", diesel(column_name = follow_pending))] pub pending: bool, } diff --git a/crates/db_schema/src/source/person_block.rs b/crates/db_schema/src/source/person_block.rs index 43048fb39..ec988a60f 100644 --- a/crates/db_schema/src/source/person_block.rs +++ b/crates/db_schema/src/source/person_block.rs @@ -1,7 +1,9 @@ use crate::newtypes::PersonId; #[cfg(feature = "full")] -use crate::schema::person_block; +use crate::schema::person_actions; use chrono::{DateTime, Utc}; +#[cfg(feature = "full")] +use diesel::{dsl, expression_methods::NullableExpressionMethods}; use serde::{Deserialize, Serialize}; #[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)] @@ -10,17 +12,19 @@ use serde::{Deserialize, Serialize}; derive(Queryable, Selectable, Associations, Identifiable) )] #[cfg_attr(feature = "full", diesel(belongs_to(crate::source::person::Person)))] -#[cfg_attr(feature = "full", diesel(table_name = person_block))] +#[cfg_attr(feature = "full", diesel(table_name = person_actions))] #[cfg_attr(feature = "full", diesel(primary_key(person_id, target_id)))] #[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] pub struct PersonBlock { pub person_id: PersonId, pub target_id: PersonId, + #[cfg_attr(feature = "full", diesel(select_expression = person_actions::blocked.assume_not_null()))] + #[cfg_attr(feature = "full", diesel(select_expression_type = dsl::AssumeNotNull))] pub published: DateTime, } #[cfg_attr(feature = "full", derive(Insertable, AsChangeset))] -#[cfg_attr(feature = "full", diesel(table_name = person_block))] +#[cfg_attr(feature = "full", diesel(table_name = person_actions))] pub struct PersonBlockForm { pub person_id: PersonId, pub target_id: PersonId, diff --git a/crates/db_schema/src/source/post.rs b/crates/db_schema/src/source/post.rs index 3819bd773..bed659a10 100644 --- a/crates/db_schema/src/source/post.rs +++ b/crates/db_schema/src/source/post.rs @@ -1,7 +1,9 @@ use crate::newtypes::{CommunityId, DbUrl, LanguageId, PersonId, PostId}; #[cfg(feature = "full")] -use crate::schema::{post, post_hide, post_like, post_read, post_saved}; +use crate::schema::{post, post_actions}; use chrono::{DateTime, Utc}; +#[cfg(feature = "full")] +use diesel::{dsl, expression_methods::NullableExpressionMethods}; use serde::{Deserialize, Serialize}; use serde_with::skip_serializing_none; #[cfg(feature = "full")] @@ -17,10 +19,11 @@ use ts_rs::TS; pub struct Post { pub id: PostId, pub name: String, - #[cfg_attr(feature = "full", ts(type = "string"))] /// An optional link / url for the post. + #[cfg_attr(feature = "full", ts(optional))] pub url: Option, /// An optional post body, in markdown. + #[cfg_attr(feature = "full", ts(optional))] pub body: Option, pub creator_id: PersonId, pub community_id: CommunityId, @@ -29,35 +32,40 @@ pub struct Post { /// Whether the post is locked. pub locked: bool, pub published: DateTime, + #[cfg_attr(feature = "full", ts(optional))] pub updated: Option>, /// Whether the post is deleted. pub deleted: bool, /// Whether the post is NSFW. pub nsfw: bool, /// A title for the link. + #[cfg_attr(feature = "full", ts(optional))] pub embed_title: Option, /// A description for the link. + #[cfg_attr(feature = "full", ts(optional))] pub embed_description: Option, - #[cfg_attr(feature = "full", ts(type = "string"))] /// A thumbnail picture url. + #[cfg_attr(feature = "full", ts(optional))] pub thumbnail_url: Option, - #[cfg_attr(feature = "full", ts(type = "string"))] /// The federated activity id / ap_id. pub ap_id: DbUrl, /// Whether the post is local. pub local: bool, - #[cfg_attr(feature = "full", ts(type = "string"))] /// A video url for the link. + #[cfg_attr(feature = "full", ts(optional))] pub embed_video_url: Option, pub language_id: LanguageId, /// Whether the post is featured to its community. pub featured_community: bool, /// Whether the post is featured to its site. pub featured_local: bool, + #[cfg_attr(feature = "full", ts(optional))] pub url_content_type: Option, /// An optional alt_text, usable for image posts. + #[cfg_attr(feature = "full", ts(optional))] pub alt_text: Option, /// Time at which the post will be published. None means publish immediately. + #[cfg_attr(feature = "full", ts(optional))] pub scheduled_publish_time: Option>, } @@ -143,22 +151,27 @@ pub struct PostUpdateForm { derive(Identifiable, Queryable, Selectable, Associations) )] #[cfg_attr(feature = "full", diesel(belongs_to(crate::source::post::Post)))] -#[cfg_attr(feature = "full", diesel(table_name = post_like))] +#[cfg_attr(feature = "full", diesel(table_name = post_actions))] #[cfg_attr(feature = "full", diesel(primary_key(person_id, post_id)))] #[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] pub struct PostLike { pub post_id: PostId, pub person_id: PersonId, + #[cfg_attr(feature = "full", diesel(select_expression = post_actions::like_score.assume_not_null()))] + #[cfg_attr(feature = "full", diesel(select_expression_type = dsl::AssumeNotNull))] pub score: i16, + #[cfg_attr(feature = "full", diesel(select_expression = post_actions::liked.assume_not_null()))] + #[cfg_attr(feature = "full", diesel(select_expression_type = dsl::AssumeNotNull))] pub published: DateTime, } #[derive(Clone)] #[cfg_attr(feature = "full", derive(Insertable, AsChangeset))] -#[cfg_attr(feature = "full", diesel(table_name = post_like))] +#[cfg_attr(feature = "full", diesel(table_name = post_actions))] pub struct PostLikeForm { pub post_id: PostId, pub person_id: PersonId, + #[cfg_attr(feature = "full", diesel(column_name = like_score))] pub score: i16, } @@ -168,17 +181,19 @@ pub struct PostLikeForm { derive(Identifiable, Queryable, Selectable, Associations) )] #[cfg_attr(feature = "full", diesel(belongs_to(crate::source::post::Post)))] -#[cfg_attr(feature = "full", diesel(table_name = post_saved))] -#[cfg_attr(feature = "full", diesel(primary_key(post_id, person_id)))] +#[cfg_attr(feature = "full", diesel(table_name = post_actions))] +#[cfg_attr(feature = "full", diesel(primary_key(person_id, post_id)))] #[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] pub struct PostSaved { pub post_id: PostId, pub person_id: PersonId, + #[cfg_attr(feature = "full", diesel(select_expression = post_actions::saved.assume_not_null()))] + #[cfg_attr(feature = "full", diesel(select_expression_type = dsl::AssumeNotNull))] pub published: DateTime, } #[cfg_attr(feature = "full", derive(Insertable, AsChangeset))] -#[cfg_attr(feature = "full", diesel(table_name = post_saved))] +#[cfg_attr(feature = "full", diesel(table_name = post_actions))] pub struct PostSavedForm { pub post_id: PostId, pub person_id: PersonId, @@ -190,17 +205,19 @@ pub struct PostSavedForm { derive(Identifiable, Queryable, Selectable, Associations) )] #[cfg_attr(feature = "full", diesel(belongs_to(crate::source::post::Post)))] -#[cfg_attr(feature = "full", diesel(table_name = post_read))] -#[cfg_attr(feature = "full", diesel(primary_key(post_id, person_id)))] +#[cfg_attr(feature = "full", diesel(table_name = post_actions))] +#[cfg_attr(feature = "full", diesel(primary_key(person_id, post_id)))] #[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] pub struct PostRead { pub post_id: PostId, pub person_id: PersonId, + #[cfg_attr(feature = "full", diesel(select_expression = post_actions::read.assume_not_null()))] + #[cfg_attr(feature = "full", diesel(select_expression_type = dsl::AssumeNotNull))] pub published: DateTime, } #[cfg_attr(feature = "full", derive(Insertable, AsChangeset))] -#[cfg_attr(feature = "full", diesel(table_name = post_read))] +#[cfg_attr(feature = "full", diesel(table_name = post_actions))] pub(crate) struct PostReadForm { pub post_id: PostId, pub person_id: PersonId, @@ -212,17 +229,19 @@ pub(crate) struct PostReadForm { derive(Identifiable, Queryable, Selectable, Associations) )] #[cfg_attr(feature = "full", diesel(belongs_to(crate::source::post::Post)))] -#[cfg_attr(feature = "full", diesel(table_name = post_hide))] -#[cfg_attr(feature = "full", diesel(primary_key(post_id, person_id)))] +#[cfg_attr(feature = "full", diesel(table_name = post_actions))] +#[cfg_attr(feature = "full", diesel(primary_key(person_id, post_id)))] #[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] pub struct PostHide { pub post_id: PostId, pub person_id: PersonId, + #[cfg_attr(feature = "full", diesel(select_expression = post_actions::hidden.assume_not_null()))] + #[cfg_attr(feature = "full", diesel(select_expression_type = dsl::AssumeNotNull))] pub published: DateTime, } #[cfg_attr(feature = "full", derive(Insertable, AsChangeset))] -#[cfg_attr(feature = "full", diesel(table_name = post_hide))] +#[cfg_attr(feature = "full", diesel(table_name = post_actions))] pub(crate) struct PostHideForm { pub post_id: PostId, pub person_id: PersonId, diff --git a/crates/db_schema/src/source/post_report.rs b/crates/db_schema/src/source/post_report.rs index 9aee9ed97..610e495ae 100644 --- a/crates/db_schema/src/source/post_report.rs +++ b/crates/db_schema/src/source/post_report.rs @@ -25,13 +25,17 @@ pub struct PostReport { /// The original post title. pub original_post_name: String, /// The original post url. + #[cfg_attr(feature = "full", ts(optional))] pub original_post_url: Option, /// The original post body. + #[cfg_attr(feature = "full", ts(optional))] pub original_post_body: Option, pub reason: String, pub resolved: bool, + #[cfg_attr(feature = "full", ts(optional))] pub resolver_id: Option, pub published: DateTime, + #[cfg_attr(feature = "full", ts(optional))] pub updated: Option>, } diff --git a/crates/db_schema/src/source/private_message.rs b/crates/db_schema/src/source/private_message.rs index 8afaa14f1..f15373907 100644 --- a/crates/db_schema/src/source/private_message.rs +++ b/crates/db_schema/src/source/private_message.rs @@ -29,6 +29,7 @@ pub struct PrivateMessage { pub deleted: bool, pub read: bool, pub published: DateTime, + #[cfg_attr(feature = "full", ts(optional))] pub updated: Option>, pub ap_id: DbUrl, pub local: bool, diff --git a/crates/db_schema/src/source/private_message_report.rs b/crates/db_schema/src/source/private_message_report.rs index 7b4c8c637..570f55584 100644 --- a/crates/db_schema/src/source/private_message_report.rs +++ b/crates/db_schema/src/source/private_message_report.rs @@ -29,8 +29,10 @@ pub struct PrivateMessageReport { pub original_pm_text: String, pub reason: String, pub resolved: bool, + #[cfg_attr(feature = "full", ts(optional))] pub resolver_id: Option, pub published: DateTime, + #[cfg_attr(feature = "full", ts(optional))] pub updated: Option>, } diff --git a/crates/db_schema/src/source/registration_application.rs b/crates/db_schema/src/source/registration_application.rs index 2ac973f34..f01c042d9 100644 --- a/crates/db_schema/src/source/registration_application.rs +++ b/crates/db_schema/src/source/registration_application.rs @@ -18,7 +18,9 @@ pub struct RegistrationApplication { pub id: RegistrationApplicationId, pub local_user_id: LocalUserId, pub answer: String, + #[cfg_attr(feature = "full", ts(optional))] pub admin_id: Option, + #[cfg_attr(feature = "full", ts(optional))] pub deny_reason: Option, pub published: DateTime, } diff --git a/crates/db_schema/src/source/site.rs b/crates/db_schema/src/source/site.rs index 0ec4043e4..0fe33de01 100644 --- a/crates/db_schema/src/source/site.rs +++ b/crates/db_schema/src/source/site.rs @@ -21,14 +21,19 @@ pub struct Site { pub id: SiteId, pub name: String, /// A sidebar for the site in markdown. + #[cfg_attr(feature = "full", ts(optional))] pub sidebar: Option, pub published: DateTime, + #[cfg_attr(feature = "full", ts(optional))] pub updated: Option>, /// An icon URL. + #[cfg_attr(feature = "full", ts(optional))] pub icon: Option, /// A banner url. + #[cfg_attr(feature = "full", ts(optional))] pub banner: Option, /// A shorter, one-line description of the site. + #[cfg_attr(feature = "full", ts(optional))] pub description: Option, /// The federated actor_id. pub actor_id: DbUrl, @@ -43,6 +48,7 @@ pub struct Site { pub instance_id: InstanceId, /// If present, nsfw content is visible by default. Should be displayed by frontends/clients /// when the site is first opened by a user. + #[cfg_attr(feature = "full", ts(optional))] pub content_warning: Option, } diff --git a/crates/db_schema/src/source/tagline.rs b/crates/db_schema/src/source/tagline.rs index 05f7e0520..80c045a0a 100644 --- a/crates/db_schema/src/source/tagline.rs +++ b/crates/db_schema/src/source/tagline.rs @@ -17,6 +17,7 @@ pub struct Tagline { pub id: i32, pub content: String, pub published: DateTime, + #[cfg_attr(feature = "full", ts(optional))] pub updated: Option>, } diff --git a/crates/db_schema/src/traits.rs b/crates/db_schema/src/traits.rs index 74f5ea009..bc30c6fb9 100644 --- a/crates/db_schema/src/traits.rs +++ b/crates/db_schema/src/traits.rs @@ -1,6 +1,6 @@ use crate::{ newtypes::{CommunityId, DbUrl, PersonId}, - utils::{get_conn, DbPool}, + utils::{get_conn, uplete, DbPool}, }; use diesel::{ associations::HasTable, @@ -76,7 +76,7 @@ pub trait Followable { ) -> Result where Self: Sized; - async fn unfollow(pool: &mut DbPool<'_>, form: &Self::Form) -> Result + async fn unfollow(pool: &mut DbPool<'_>, form: &Self::Form) -> Result where Self: Sized; } @@ -87,7 +87,7 @@ pub trait Joinable { async fn join(pool: &mut DbPool<'_>, form: &Self::Form) -> Result where Self: Sized; - async fn leave(pool: &mut DbPool<'_>, form: &Self::Form) -> Result + async fn leave(pool: &mut DbPool<'_>, form: &Self::Form) -> Result where Self: Sized; } @@ -103,7 +103,7 @@ pub trait Likeable { pool: &mut DbPool<'_>, person_id: PersonId, item_id: Self::IdType, - ) -> Result + ) -> Result where Self: Sized; } @@ -114,7 +114,7 @@ pub trait Bannable { async fn ban(pool: &mut DbPool<'_>, form: &Self::Form) -> Result where Self: Sized; - async fn unban(pool: &mut DbPool<'_>, form: &Self::Form) -> Result + async fn unban(pool: &mut DbPool<'_>, form: &Self::Form) -> Result where Self: Sized; } @@ -125,7 +125,7 @@ pub trait Saveable { async fn save(pool: &mut DbPool<'_>, form: &Self::Form) -> Result where Self: Sized; - async fn unsave(pool: &mut DbPool<'_>, form: &Self::Form) -> Result + async fn unsave(pool: &mut DbPool<'_>, form: &Self::Form) -> Result where Self: Sized; } @@ -136,7 +136,7 @@ pub trait Blockable { async fn block(pool: &mut DbPool<'_>, form: &Self::Form) -> Result where Self: Sized; - async fn unblock(pool: &mut DbPool<'_>, form: &Self::Form) -> Result + async fn unblock(pool: &mut DbPool<'_>, form: &Self::Form) -> Result where Self: Sized; } diff --git a/crates/db_schema/src/utils.rs b/crates/db_schema/src/utils.rs index 1e56563bc..bb7edb13f 100644 --- a/crates/db_schema/src/utils.rs +++ b/crates/db_schema/src/utils.rs @@ -1,18 +1,30 @@ +pub mod uplete; + use crate::{newtypes::DbUrl, CommentSortType, PostSortType}; use chrono::{DateTime, TimeDelta, Utc}; use deadpool::Runtime; use diesel::{ + dsl, + expression::AsExpression, helper_types::AsExprOf, pg::Pg, query_builder::{Query, QueryFragment}, - query_dsl::methods::LimitDsl, + query_dsl::methods::{FilterDsl, FindDsl, LimitDsl}, + query_source::{Alias, AliasSource, AliasedField}, result::{ ConnectionError, ConnectionResult, Error::{self as DieselError, QueryBuilderError}, }, - sql_types::{self, Timestamptz}, + sql_types::{self, SingleValue, Timestamptz}, + Column, + Expression, + ExpressionMethods, IntoSql, + JoinOnDsl, + NullableExpressionMethods, + QuerySource, + Table, }; use diesel_async::{ pg::AsyncPgConnection, @@ -22,8 +34,8 @@ use diesel_async::{ ManagerConfig, }, AsyncConnection, - RunQueryDsl, }; +use diesel_bind_if_some::BindIfSome; use futures_util::{future::BoxFuture, Future, FutureExt}; use i_love_jesus::CursorKey; use lemmy_utils::{ @@ -47,7 +59,7 @@ use rustls::{ }; use std::{ ops::{Deref, DerefMut}, - sync::{Arc, LazyLock}, + sync::{Arc, LazyLock, OnceLock}, time::Duration, }; use tracing::error; @@ -59,6 +71,8 @@ pub const SITEMAP_LIMIT: i64 = 50000; pub const SITEMAP_DAYS: Option = TimeDelta::try_days(31); pub const RANK_DEFAULT: f64 = 0.0001; +/// Some connection options to speed up queries +const CONNECTION_OPTIONS: [&str; 1] = ["geqo_threshold=12"]; pub type ActualDbPool = Pool; /// References a pool or connection. Functions must take `&mut DbPool<'_>` to allow implicit @@ -345,10 +359,37 @@ pub fn diesel_url_create(opt: Option<&str>) -> LemmyResult> { } } +/// Sets a few additional config options necessary for starting lemmy +fn build_config_options_uri_segment(config: &str) -> String { + let mut url = Url::parse(config).expect("Couldn't parse postgres connection URI"); + + // Set `lemmy.protocol_and_hostname` so triggers can use it + let lemmy_protocol_and_hostname_option = + "lemmy.protocol_and_hostname=".to_owned() + &SETTINGS.get_protocol_and_hostname(); + let mut options = CONNECTION_OPTIONS.to_vec(); + options.push(&lemmy_protocol_and_hostname_option); + + // Create the connection uri portion + let options_segments = options + .iter() + .map(|o| "-c ".to_owned() + o) + .collect::>() + .join(" "); + + url.set_query(Some(&format!("options={options_segments}"))); + url.into() +} + fn establish_connection(config: &str) -> BoxFuture> { let fut = async { + /// Use a once_lock to create the postgres connection config, since this config never changes + static POSTGRES_CONFIG_WITH_OPTIONS: OnceLock = OnceLock::new(); + + let config = + POSTGRES_CONFIG_WITH_OPTIONS.get_or_init(|| build_config_options_uri_segment(config)); + // We only support TLS with sslmode=require currently - let mut conn = if config.contains("sslmode=require") { + let conn = if config.contains("sslmode=require") { let rustls_config = DangerousClientConfigBuilder { cfg: ClientConfig::builder(), } @@ -369,24 +410,6 @@ fn establish_connection(config: &str) -> BoxFuture LemmyResult { +pub fn build_db_pool() -> LemmyResult { let db_url = SETTINGS.get_database_url(); // diesel-async does not support any TLS connections out of the box, so we need to manually // provide a setup function which handles creating the connection @@ -472,8 +495,8 @@ pub async fn build_db_pool() -> LemmyResult { Ok(pool) } -pub async fn build_db_pool_for_tests() -> ActualDbPool { - build_db_pool().await.expect("db pool missing") +pub fn build_db_pool_for_tests() -> ActualDbPool { + build_db_pool().expect("db pool missing") } pub fn naive_now() -> DateTime { @@ -498,7 +521,7 @@ static EMAIL_REGEX: LazyLock = LazyLock::new(|| { }); pub mod functions { - use diesel::sql_types::{BigInt, Bool, Text, Timestamptz}; + use diesel::sql_types::{BigInt, Text, Timestamptz}; sql_function! { #[sql_name = "r.hot_rank"] @@ -521,8 +544,6 @@ pub mod functions { // really this function is variadic, this just adds the two-argument version sql_function!(fn coalesce(x: diesel::sql_types::Nullable, y: T) -> T); - - sql_function!(fn set_config(setting_name: Text, new_value: Text, is_local: Bool) -> Text); } pub const DELETED_REPLACEMENT_TEXT: &str = "*Permanently Deleted*"; @@ -532,6 +553,117 @@ pub fn now() -> AsExprOf { diesel::dsl::now.into_sql::() } +/// Trait alias for a type that can be converted to an SQL tuple using `IntoSql::into_sql` +pub trait AsRecord: Expression + AsExpression> +where + Self::SqlType: 'static, +{ +} + +impl>> AsRecord for T where + T::SqlType: 'static +{ +} + +/// Output of `IntoSql::into_sql` for a type that implements `AsRecord` +pub type AsRecordOutput = dsl::AsExprOf::SqlType>>; + +/// Output of `t.on((l0, l1).into_sql().eq((r0, r1)))` +type OnTupleEq = dsl::On, (R0, R1)>>; + +/// Creates an `ON` clause for a table where a person ID and another column are used as the +/// primary key. Use with the `QueryDsl::left_join` method. +/// +/// This example modifies a query to make columns in `community_actions` available: +/// +/// ``` +/// community::table +/// .left_join(actions( +/// community_actions::table, +/// my_person_id, +/// community::id, +/// )) +/// ``` +pub fn actions( + actions_table: T, + person_id: Option

, + target_id: C, +) -> OnTupleEq, K1, BindIfSome>, C> +where + T: Table + Copy, + K0: Expression, + P: AsExpression, + (dsl::Nullable, K1): AsRecord, + (BindIfSome>, C): + AsExpression<, K1)> as Expression>::SqlType>, +{ + let (k0, k1) = actions_table.primary_key(); + actions_table.on((k0.nullable(), k1).into_sql().eq(( + BindIfSome(person_id.map(diesel::IntoSql::into_sql)), + target_id, + ))) +} + +/// Like `actions` but `actions_table` is an alias and person id is not nullable +#[allow(clippy::type_complexity)] +pub fn actions_alias( + actions_table: Alias, + person_id: P, + target_id: C, +) -> OnTupleEq, AliasedField, AliasedField, P, C> +where + Alias: QuerySource + Copy, + T: AliasSource> + Default, + K0: Column, + K1: Column
, + (AliasedField, AliasedField): AsRecord, + (P, C): AsExpression< + , AliasedField)> as Expression>::SqlType, + >, +{ + let (k0, k1) = T::default().target().primary_key(); + actions_table.on( + (actions_table.field(k0), actions_table.field(k1)) + .into_sql() + .eq((person_id, target_id)), + ) +} + +/// `action_query(table_name::action_name)` is the same as +/// `table_name::table.filter(table_name::action_name.is_not_null())`. +pub fn action_query(column: C) -> dsl::Filter> +where + C: Column>, SqlType: SingleValue>, +{ + action_query_with_fn(column, |t| t) +} + +/// `find_action(table_name::action_name, key)` is the same as +/// `table_name::table.find(key).filter(table_name::action_name.is_not_null())`. +pub fn find_action( + column: C, + key: K, +) -> dsl::Filter, dsl::IsNotNull> +where + C: + Column>>, SqlType: SingleValue>, +{ + action_query_with_fn(column, |t| t.find(key)) +} + +/// `action_query_with_fn(table_name::action_name, f)` is the same as +/// `f(table_name::table).filter(table_name::action_name.is_not_null())`. +fn action_query_with_fn( + column: C, + f: impl FnOnce(C::Table) -> Q, +) -> dsl::Filter> +where + C: Column, + Q: FilterDsl>, +{ + f(C::Table::default()).filter(column.is_not_null()) +} + pub type ResultFuture<'a, T> = BoxFuture<'a, Result>; pub trait ReadFn<'a, T, Args>: Fn(DbConn<'a>, Args) -> ResultFuture<'a, T> {} diff --git a/crates/db_schema/src/utils/uplete.rs b/crates/db_schema/src/utils/uplete.rs new file mode 100644 index 000000000..8c5262b90 --- /dev/null +++ b/crates/db_schema/src/utils/uplete.rs @@ -0,0 +1,423 @@ +use diesel::{ + associations::HasTable, + dsl, + expression::{is_aggregate, ValidGrouping}, + pg::Pg, + query_builder::{AsQuery, AstPass, Query, QueryFragment, QueryId}, + query_dsl::methods::{FilterDsl, SelectDsl}, + result::Error, + sql_types, + Column, + Expression, + Table, +}; +use std::any::TypeId; +use tuplex::IntoArray; + +/// Set columns (each specified with `UpleteBuilder::set_null`) to null in the rows found by +/// `query`, and delete rows that have no remaining non-null values outside of the primary key +pub fn new(query: Q) -> UpleteBuilder::PrimaryKey>> +where + Q: AsQuery + HasTable, + Q::Table: Default, + Q::Query: SelectDsl<::PrimaryKey>, + + // For better error messages + UpleteBuilder: AsQuery, +{ + UpleteBuilder { + query: query.as_query().select(Q::Table::default().primary_key()), + set_null_columns: Vec::new(), + } +} + +pub struct UpleteBuilder { + query: Q, + set_null_columns: Vec, +} + +impl UpleteBuilder { + pub fn set_null + Into>(mut self, column: C) -> Self { + self.set_null_columns.push(column.into()); + self + } +} + +impl AsQuery for UpleteBuilder +where + Q: HasTable, + Q::Table: Default + QueryFragment + Send + 'static, + ::PrimaryKey: IntoArray + QueryFragment + Send + 'static, + ::AllColumns: IntoArray, + <::PrimaryKey as IntoArray>::Output: IntoIterator, + <::AllColumns as IntoArray>::Output: IntoIterator, + Q: Clone + FilterDsl + FilterDsl>, + dsl::Filter: QueryFragment + Send + 'static, + dsl::Filter>: QueryFragment + Send + 'static, +{ + type Query = UpleteQuery; + + type SqlType = (sql_types::BigInt, sql_types::BigInt); + + fn as_query(self) -> Self::Query { + let table = Q::Table::default; + let deletion_condition = AllNull( + Q::Table::all_columns() + .into_array() + .into_iter() + .filter(|c: &DynColumn| { + table() + .primary_key() + .into_array() + .into_iter() + .chain(self.set_null_columns.iter().cloned()) + .all(|excluded_column| excluded_column.type_id != c.type_id) + }) + .collect::>(), + ); + UpleteQuery { + // Updated rows and deleted rows must not overlap, so updating all rows and using the returned + // new rows to determine which ones to delete is not an option. + // + // https://www.postgresql.org/docs/16/queries-with.html#QUERIES-WITH-MODIFYING + // + // "Trying to update the same row twice in a single statement is not supported. Only one of + // the modifications takes place, but it is not easy (and sometimes not possible) to reliably + // predict which one. This also applies to deleting a row that was already updated in the same + // statement: only the update is performed." + update_subquery: Box::new( + self + .query + .clone() + .filter(dsl::not(deletion_condition.clone())), + ), + delete_subquery: Box::new(self.query.filter(deletion_condition)), + table: Box::new(table()), + primary_key: Box::new(table().primary_key()), + set_null_columns: self.set_null_columns, + } + } +} + +pub struct UpleteQuery { + update_subquery: Box + Send + 'static>, + delete_subquery: Box + Send + 'static>, + table: Box + Send + 'static>, + primary_key: Box + Send + 'static>, + set_null_columns: Vec, +} + +impl QueryId for UpleteQuery { + type QueryId = (); + + const HAS_STATIC_QUERY_ID: bool = false; +} + +impl Query for UpleteQuery { + type SqlType = (sql_types::BigInt, sql_types::BigInt); +} + +impl QueryFragment for UpleteQuery { + fn walk_ast<'b>(&'b self, mut out: AstPass<'_, 'b, Pg>) -> Result<(), Error> { + assert_ne!(self.set_null_columns.len(), 0, "`set_null` was not called"); + + // Declare `update_keys` and `delete_keys` CTEs, which select primary keys + for (prefix, subquery) in [ + ("WITH update_keys", &self.update_subquery), + (", delete_keys", &self.delete_subquery), + ] { + out.push_sql(prefix); + out.push_sql(" AS ("); + subquery.walk_ast(out.reborrow())?; + out.push_sql(" FOR UPDATE)"); + } + + // Update rows that are referenced in `update_keys` + out.push_sql(", update_result AS (UPDATE "); + self.table.walk_ast(out.reborrow())?; + let mut item_prefix = " SET "; + for column in &self.set_null_columns { + out.push_sql(item_prefix); + out.push_identifier(column.name)?; + out.push_sql(" = NULL"); + item_prefix = ","; + } + out.push_sql(" WHERE ("); + self.primary_key.walk_ast(out.reborrow())?; + out.push_sql(") = ANY (SELECT * FROM update_keys) RETURNING 1)"); + + // Delete rows that are referenced in `delete_keys` + out.push_sql(", delete_result AS (DELETE FROM "); + self.table.walk_ast(out.reborrow())?; + out.push_sql(" WHERE ("); + self.primary_key.walk_ast(out.reborrow())?; + out.push_sql(") = ANY (SELECT * FROM delete_keys) RETURNING 1)"); + + // Count updated rows and deleted rows (`RETURNING 1` makes this possible) + out.push_sql(" SELECT (SELECT count(*) FROM update_result)"); + out.push_sql(", (SELECT count(*) FROM delete_result)"); + + Ok(()) + } +} + +// Types other than `DynColumn` are only used in tests +#[derive(Clone)] +pub struct AllNull(Vec); + +impl Expression for AllNull { + type SqlType = sql_types::Bool; +} + +impl ValidGrouping<()> for AllNull { + type IsAggregate = is_aggregate::No; +} + +impl> QueryFragment for AllNull { + fn walk_ast<'b>(&'b self, mut out: AstPass<'_, 'b, Pg>) -> Result<(), Error> { + // Must produce a valid expression even if `self.0` is empty + out.push_sql("(TRUE"); + for item in &self.0 { + out.push_sql(" AND ("); + item.walk_ast(out.reborrow())?; + out.push_sql(" IS NULL)"); + } + out.push_sql(")"); + + Ok(()) + } +} + +#[derive(Clone)] +pub struct DynColumn { + type_id: TypeId, + name: &'static str, +} + +impl From for DynColumn { + fn from(_value: T) -> Self { + DynColumn { + type_id: TypeId::of::(), + name: T::NAME, + } + } +} + +impl QueryFragment for DynColumn { + fn walk_ast<'b>(&'b self, mut out: AstPass<'_, 'b, Pg>) -> Result<(), Error> { + out.push_identifier(self.name) + } +} + +#[derive(Queryable, PartialEq, Eq, Debug)] +pub struct Count { + pub updated: i64, + pub deleted: i64, +} + +impl Count { + pub fn only_updated(n: i64) -> Self { + Count { + updated: n, + deleted: 0, + } + } + + pub fn only_deleted(n: i64) -> Self { + Count { + updated: 0, + deleted: n, + } + } +} + +#[cfg(test)] +mod tests { + use super::AllNull; + use crate::utils::{build_db_pool_for_tests, get_conn, DbConn}; + use diesel::{ + debug_query, + insert_into, + pg::Pg, + query_builder::{AsQuery, QueryId}, + select, + sql_types, + AppearsOnTable, + ExpressionMethods, + IntoSql, + QueryDsl, + SelectableExpression, + }; + use diesel_async::{RunQueryDsl, SimpleAsyncConnection}; + use lemmy_utils::error::LemmyResult; + use pretty_assertions::assert_eq; + use serial_test::serial; + + impl AppearsOnTable for AllNull {} + + impl SelectableExpression for AllNull {} + + impl QueryId for AllNull { + type QueryId = (); + const HAS_STATIC_QUERY_ID: bool = false; + } + + diesel::table! { + t (id1, id2) { + // uplete doesn't work for non-tuple primary key + id1 -> Int4, + id2 -> Int4, + a -> Nullable, + b -> Nullable, + } + } + + async fn expect_rows( + conn: &mut DbConn<'_>, + expected: &[(Option, Option)], + ) -> LemmyResult<()> { + let rows: Vec<(Option, Option)> = t::table + .select((t::a, t::b)) + .order_by(t::id1) + .load(conn) + .await?; + assert_eq!(expected, &rows); + + Ok(()) + } + + // Main purpose of this test is to check accuracy of the returned `Count`, which other modules' + // tests rely on + #[tokio::test] + #[serial] + async fn test_count() -> LemmyResult<()> { + let pool = &build_db_pool_for_tests(); + let pool = &mut pool.into(); + let mut conn = get_conn(pool).await?; + + conn + .batch_execute("CREATE TABLE t (id1 serial, id2 int NOT NULL DEFAULT 1, a int, b int, PRIMARY KEY (id1, id2));") + .await?; + expect_rows(&mut conn, &[]).await?; + + insert_into(t::table) + .values(&[ + (t::a.eq(Some(1)), t::b.eq(Some(2))), + (t::a.eq(Some(3)), t::b.eq(None)), + (t::a.eq(Some(4)), t::b.eq(Some(5))), + ]) + .execute(&mut conn) + .await?; + expect_rows( + &mut conn, + &[(Some(1), Some(2)), (Some(3), None), (Some(4), Some(5))], + ) + .await?; + + let count1 = super::new(t::table) + .set_null(t::a) + .get_result(&mut conn) + .await?; + assert_eq!( + super::Count { + updated: 2, + deleted: 1 + }, + count1 + ); + expect_rows(&mut conn, &[(None, Some(2)), (None, Some(5))]).await?; + + let count2 = super::new(t::table) + .set_null(t::b) + .get_result(&mut conn) + .await?; + assert_eq!(super::Count::only_deleted(2), count2); + expect_rows(&mut conn, &[]).await?; + + conn.batch_execute("DROP TABLE t;").await?; + + Ok(()) + } + + fn expected_sql(check_null: &str, set_null: &str) -> String { + let with_queries = { + let key = r#""t"."id1", "t"."id2""#; + let t = r#""t""#; + + let update_keys = format!("SELECT {key} FROM {t} WHERE NOT (({check_null})) FOR UPDATE"); + let delete_keys = format!("SELECT {key} FROM {t} WHERE ({check_null}) FOR UPDATE"); + let update_result = format!( + "UPDATE {t} SET {set_null} WHERE ({key}) = ANY (SELECT * FROM update_keys) RETURNING 1" + ); + let delete_result = + format!("DELETE FROM {t} WHERE ({key}) = ANY (SELECT * FROM delete_keys) RETURNING 1"); + + format!("update_keys AS ({update_keys}), delete_keys AS ({delete_keys}), update_result AS ({update_result}), delete_result AS ({delete_result})") + }; + let update_count = "SELECT count(*) FROM update_result"; + let delete_count = "SELECT count(*) FROM delete_result"; + + format!(r#"WITH {with_queries} SELECT ({update_count}), ({delete_count}) -- binds: []"#) + } + + #[test] + fn test_generated_sql() { + // Unlike the `get_result` method, `debug_query` does not automatically call `as_query` + assert_eq!( + debug_query::(&super::new(t::table).set_null(t::b).as_query()).to_string(), + expected_sql(r#"TRUE AND ("a" IS NULL)"#, r#""b" = NULL"#) + ); + assert_eq!( + debug_query::( + &super::new(t::table) + .set_null(t::a) + .set_null(t::b) + .as_query() + ) + .to_string(), + expected_sql(r#"TRUE"#, r#""a" = NULL,"b" = NULL"#) + ); + } + + #[test] + fn test_count_methods() { + assert_eq!( + super::Count::only_updated(1), + super::Count { + updated: 1, + deleted: 0 + } + ); + assert_eq!( + super::Count::only_deleted(1), + super::Count { + updated: 0, + deleted: 1 + } + ); + } + + #[tokio::test] + #[serial] + async fn test_all_null() -> LemmyResult<()> { + let pool = &build_db_pool_for_tests(); + let pool = &mut pool.into(); + let mut conn = get_conn(pool).await?; + + let some = Some(1).into_sql::>(); + let none = None::.into_sql::>(); + + // Allows type inference for `vec![]` + let mut all_null = |items| select(AllNull(items)).get_result::(&mut conn); + + assert!(all_null(vec![]).await?); + assert!(all_null(vec![none]).await?); + assert!(all_null(vec![none, none]).await?); + assert!(all_null(vec![none, none, none]).await?); + assert!(!all_null(vec![some]).await?); + assert!(!all_null(vec![some, none]).await?); + assert!(!all_null(vec![none, some, none]).await?); + + Ok(()) + } +} diff --git a/crates/db_views/src/comment_report_view.rs b/crates/db_views/src/comment_report_view.rs index 753304f70..278dc5c22 100644 --- a/crates/db_views/src/comment_report_view.rs +++ b/crates/db_views/src/comment_report_view.rs @@ -11,24 +11,33 @@ use diesel::{ }; use diesel_async::RunQueryDsl; use lemmy_db_schema::{ - aliases, + aliases::{self, creator_community_actions}, newtypes::{CommentId, CommentReportId, CommunityId, PersonId}, schema::{ comment, + comment_actions, comment_aggregates, - comment_like, comment_report, - comment_saved, community, - community_follower, - community_moderator, - community_person_ban, + community_actions, local_user, person, - person_block, + person_actions, post, }, - utils::{get_conn, limit_and_offset, DbConn, DbPool, ListFn, Queries, ReadFn}, + source::community::CommunityFollower, + utils::{ + actions, + actions_alias, + functions::coalesce, + get_conn, + limit_and_offset, + DbConn, + DbPool, + ListFn, + Queries, + ReadFn, + }, }; fn queries<'a>() -> Queries< @@ -45,40 +54,20 @@ fn queries<'a>() -> Queries< .inner_join( comment_aggregates::table.on(comment_report::comment_id.eq(comment_aggregates::comment_id)), ) - .left_join( - comment_like::table.on( - comment::id - .eq(comment_like::comment_id) - .and(comment_like::person_id.eq(my_person_id)), - ), - ) + .left_join(actions( + comment_actions::table, + Some(my_person_id), + comment_report::comment_id, + )) .left_join( aliases::person2 .on(comment_report::resolver_id.eq(aliases::person2.field(person::id).nullable())), ) - .left_join( - community_person_ban::table.on( - community::id - .eq(community_person_ban::community_id) - .and(community_person_ban::person_id.eq(comment::creator_id)) - .and( - community_person_ban::expires - .is_null() - .or(community_person_ban::expires.gt(now)), - ), - ), - ) - .left_join( - aliases::community_moderator1.on( - community::id - .eq(aliases::community_moderator1.field(community_moderator::community_id)) - .and( - aliases::community_moderator1 - .field(community_moderator::person_id) - .eq(comment::creator_id), - ), - ), - ) + .left_join(actions_alias( + creator_community_actions, + comment::creator_id, + post::community_id, + )) .left_join( local_user::table.on( comment::creator_id @@ -86,27 +75,16 @@ fn queries<'a>() -> Queries< .and(local_user::admin.eq(true)), ), ) - .left_join( - person_block::table.on( - comment::creator_id - .eq(person_block::target_id) - .and(person_block::person_id.eq(my_person_id)), - ), - ) - .left_join( - community_follower::table.on( - post::community_id - .eq(community_follower::community_id) - .and(community_follower::person_id.eq(my_person_id)), - ), - ) - .left_join( - comment_saved::table.on( - comment::id - .eq(comment_saved::comment_id) - .and(comment_saved::person_id.eq(my_person_id)), - ), - ) + .left_join(actions( + person_actions::table, + Some(my_person_id), + comment::creator_id, + )) + .left_join(actions( + community_actions::table, + Some(my_person_id), + post::community_id, + )) .select(( comment_report::all_columns, comment::all_columns, @@ -115,16 +93,28 @@ fn queries<'a>() -> Queries< person::all_columns, aliases::person1.fields(person::all_columns), comment_aggregates::all_columns, - community_person_ban::community_id.nullable().is_not_null(), - aliases::community_moderator1 - .field(community_moderator::community_id) + coalesce( + creator_community_actions + .field(community_actions::received_ban) + .nullable() + .is_not_null() + .or( + creator_community_actions + .field(community_actions::ban_expires) + .nullable() + .gt(now), + ), + false, + ), + creator_community_actions + .field(community_actions::became_moderator) .nullable() .is_not_null(), local_user::admin.nullable().is_not_null(), - person_block::target_id.nullable().is_not_null(), - community_follower::pending.nullable(), - comment_saved::published.nullable().is_not_null(), - comment_like::score.nullable(), + person_actions::blocked.nullable().is_not_null(), + CommunityFollower::select_subscribed_type(), + comment_actions::saved.nullable().is_not_null(), + comment_actions::like_score.nullable(), aliases::person2.fields(person::all_columns).nullable(), )) }; @@ -166,19 +156,10 @@ fn queries<'a>() -> Queries< // If its not an admin, get only the ones you mod if !user.local_user.admin { - query - .inner_join( - community_moderator::table.on( - community_moderator::community_id - .eq(post::community_id) - .and(community_moderator::person_id.eq(user.person.id)), - ), - ) - .load::(&mut conn) - .await - } else { - query.load::(&mut conn).await + query = query.filter(community_actions::became_moderator.is_not_null()); } + + query.load::(&mut conn).await }; Queries::new(read, list) @@ -221,10 +202,11 @@ impl CommentReportView { if !admin { query .inner_join( - community_moderator::table.on( - community_moderator::community_id + community_actions::table.on( + community_actions::community_id .eq(post::community_id) - .and(community_moderator::person_id.eq(my_person_id)), + .and(community_actions::person_id.eq(my_person_id)) + .and(community_actions::became_moderator.is_not_null()), ), ) .select(count(comment_report::id)) @@ -290,7 +272,7 @@ mod tests { #[tokio::test] #[serial] async fn test_crud() -> LemmyResult<()> { - let pool = &build_db_pool_for_tests().await; + let pool = &build_db_pool_for_tests(); let pool = &mut pool.into(); let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?; @@ -391,6 +373,7 @@ mod tests { actor_id: inserted_community.actor_id.clone(), local: true, title: inserted_community.title, + sidebar: None, description: None, updated: None, banner: None, @@ -402,7 +385,6 @@ mod tests { last_refreshed_at: inserted_community.last_refreshed_at, followers_url: inserted_community.followers_url, inbox_url: inserted_community.inbox_url, - shared_inbox_url: inserted_community.shared_inbox_url, moderators_url: inserted_community.moderators_url, featured_url: inserted_community.featured_url, instance_id: inserted_instance.id, @@ -423,7 +405,6 @@ mod tests { banner: None, updated: None, inbox_url: inserted_jessica.inbox_url.clone(), - shared_inbox_url: None, matrix_user_id: None, ban_expires: None, instance_id: inserted_instance.id, @@ -446,7 +427,6 @@ mod tests { banner: None, updated: None, inbox_url: inserted_timmy.inbox_url.clone(), - shared_inbox_url: None, matrix_user_id: None, ban_expires: None, instance_id: inserted_instance.id, @@ -488,7 +468,6 @@ mod tests { banner: None, updated: None, inbox_url: inserted_sara.inbox_url.clone(), - shared_inbox_url: None, matrix_user_id: None, ban_expires: None, instance_id: inserted_instance.id, @@ -550,7 +529,6 @@ mod tests { private_key: inserted_timmy.private_key.clone(), public_key: inserted_timmy.public_key.clone(), last_refreshed_at: inserted_timmy.last_refreshed_at, - shared_inbox_url: None, matrix_user_id: None, ban_expires: None, instance_id: inserted_instance.id, diff --git a/crates/db_views/src/comment_view.rs b/crates/db_views/src/comment_view.rs index dc9aceec0..22b7b3de4 100644 --- a/crates/db_views/src/comment_view.rs +++ b/crates/db_views/src/comment_view.rs @@ -3,11 +3,8 @@ use diesel::{ dsl::{exists, not}, pg::Pg, result::Error, - sql_types, BoolExpressionMethods, - BoxableExpression, ExpressionMethods, - IntoSql, JoinOnDsl, NullableExpressionMethods, PgTextExpressionMethods, @@ -16,28 +13,40 @@ use diesel::{ use diesel_async::RunQueryDsl; use diesel_ltree::{nlevel, subpath, Ltree, LtreeExtensions}; use lemmy_db_schema::{ + aliases::creator_community_actions, impls::local_user::LocalUserOptionHelper, newtypes::{CommentId, CommunityId, LocalUserId, PersonId, PostId}, schema::{ comment, + comment_actions, comment_aggregates, - comment_like, - comment_saved, community, - community_block, - community_follower, - community_moderator, - community_person_ban, - instance_block, + community_actions, + instance_actions, local_user, local_user_language, person, - person_block, + person_actions, post, }, - source::{local_user::LocalUser, site::Site}, - utils::{fuzzy_search, limit_and_offset, DbConn, DbPool, ListFn, Queries, ReadFn}, + source::{ + community::{CommunityFollower, CommunityFollowerState}, + local_user::LocalUser, + site::Site, + }, + utils::{ + actions, + actions_alias, + fuzzy_search, + limit_and_offset, + DbConn, + DbPool, + ListFn, + Queries, + ReadFn, + }, CommentSortType, + CommunityVisibility, ListingType, }; @@ -45,64 +54,6 @@ fn queries<'a>() -> Queries< impl ReadFn<'a, CommentView, (CommentId, Option<&'a LocalUser>)>, impl ListFn<'a, CommentView, (CommentQuery<'a>, &'a Site)>, > { - let is_creator_banned_from_community = exists( - community_person_ban::table.filter( - community::id - .eq(community_person_ban::community_id) - .and(community_person_ban::person_id.eq(comment::creator_id)), - ), - ); - - let is_local_user_banned_from_community = |person_id| { - exists( - community_person_ban::table.filter( - community::id - .eq(community_person_ban::community_id) - .and(community_person_ban::person_id.eq(person_id)), - ), - ) - }; - - let is_community_followed = |person_id| { - community_follower::table - .filter( - post::community_id - .eq(community_follower::community_id) - .and(community_follower::person_id.eq(person_id)), - ) - .select(community_follower::pending.nullable()) - .single_value() - }; - - let is_creator_blocked = |person_id| { - exists( - person_block::table.filter( - comment::creator_id - .eq(person_block::target_id) - .and(person_block::person_id.eq(person_id)), - ), - ) - }; - - let score = |person_id| { - comment_like::table - .filter( - comment::id - .eq(comment_like::comment_id) - .and(comment_like::person_id.eq(person_id)), - ) - .select(comment_like::score.nullable()) - .single_value() - }; - - let creator_is_moderator = exists( - community_moderator::table.filter( - community::id - .eq(community_moderator::community_id) - .and(community_moderator::person_id.eq(comment::creator_id)), - ), - ); - let creator_is_admin = exists( local_user::table.filter( comment::creator_id @@ -112,63 +63,56 @@ fn queries<'a>() -> Queries< ); let all_joins = move |query: comment::BoxedQuery<'a, Pg>, my_person_id: Option| { - let is_local_user_banned_from_community_selection: Box< - dyn BoxableExpression<_, Pg, SqlType = sql_types::Bool>, - > = if let Some(person_id) = my_person_id { - Box::new(is_local_user_banned_from_community(person_id)) - } else { - Box::new(false.into_sql::()) - }; - - let score_selection: Box< - dyn BoxableExpression<_, Pg, SqlType = sql_types::Nullable>, - > = if let Some(person_id) = my_person_id { - Box::new(score(person_id)) - } else { - Box::new(None::.into_sql::>()) - }; - - let subscribed_type_selection: Box< - dyn BoxableExpression<_, Pg, SqlType = sql_types::Nullable>, - > = if let Some(person_id) = my_person_id { - Box::new(is_community_followed(person_id)) - } else { - Box::new(None::.into_sql::>()) - }; - - let is_creator_blocked_selection: Box> = - if let Some(person_id) = my_person_id { - Box::new(is_creator_blocked(person_id)) - } else { - Box::new(false.into_sql::()) - }; - query .inner_join(person::table) .inner_join(post::table) .inner_join(community::table.on(post::community_id.eq(community::id))) .inner_join(comment_aggregates::table) - .left_join( - comment_saved::table.on( - comment::id - .eq(comment_saved::comment_id) - .and(comment_saved::person_id.eq(my_person_id.unwrap_or(PersonId(-1)))), - ), - ) + .left_join(actions( + community_actions::table, + my_person_id, + post::community_id, + )) + .left_join(actions( + comment_actions::table, + my_person_id, + comment_aggregates::comment_id, + )) + .left_join(actions( + person_actions::table, + my_person_id, + comment::creator_id, + )) + .left_join(actions( + instance_actions::table, + my_person_id, + community::instance_id, + )) + .left_join(actions_alias( + creator_community_actions, + comment::creator_id, + post::community_id, + )) .select(( comment::all_columns, person::all_columns, post::all_columns, community::all_columns, comment_aggregates::all_columns, - is_creator_banned_from_community, - is_local_user_banned_from_community_selection, - creator_is_moderator, + creator_community_actions + .field(community_actions::received_ban) + .nullable() + .is_not_null(), + community_actions::received_ban.nullable().is_not_null(), + creator_community_actions + .field(community_actions::became_moderator) + .nullable() + .is_not_null(), creator_is_admin, - subscribed_type_selection, - comment_saved::person_id.nullable().is_not_null(), - is_creator_blocked_selection, - score_selection, + CommunityFollower::select_subscribed_type(), + comment_actions::saved.nullable().is_not_null(), + person_actions::blocked.nullable().is_not_null(), + comment_actions::like_score.nullable(), )) }; @@ -179,12 +123,23 @@ fn queries<'a>() -> Queries< my_local_user.person_id(), ); query = my_local_user.visible_communities_only(query); + + // Check permissions to view private community content. + // Specifically, if the community is private then only accepted followers may view its + // content, otherwise it is filtered out. Admins can view private community content + // without restriction. + if !my_local_user.is_admin() { + query = query.filter( + community::visibility + .ne(CommunityVisibility::Private) + .or(community_actions::follow_state.eq(CommunityFollowerState::Accepted)), + ); + } query.first(&mut conn).await }; let list = move |mut conn: DbConn<'a>, (options, site): (CommentQuery<'a>, &'a Site)| async move { // The left join below will return None in this case - let person_id_join = options.local_user.person_id().unwrap_or(PersonId(-1)); let local_user_id_join = options .local_user .local_user_id() @@ -216,13 +171,7 @@ fn queries<'a>() -> Queries< query = query.filter(post::community_id.eq(community_id)); } - let is_subscribed = exists( - community_follower::table.filter( - post::community_id - .eq(community_follower::community_id) - .and(community_follower::person_id.eq(person_id_join)), - ), - ); + let is_subscribed = community_actions::followed.is_not_null(); match options.listing_type.unwrap_or_default() { ListingType::Subscribed => query = query.filter(is_subscribed), /* TODO could be this: and(community_follower::person_id.eq(person_id_join)), */ @@ -233,29 +182,27 @@ fn queries<'a>() -> Queries< } ListingType::All => query = query.filter(community::hidden.eq(false).or(is_subscribed)), ListingType::ModeratorView => { - query = query.filter(exists( - community_moderator::table.filter( - post::community_id - .eq(community_moderator::community_id) - .and(community_moderator::person_id.eq(person_id_join)), - ), - )); + query = query.filter(community_actions::became_moderator.is_not_null()); } } // If its saved only, then filter, and order by the saved time, not the comment creation time. if options.saved_only.unwrap_or_default() { query = query - .filter(comment_saved::person_id.is_not_null()) - .then_order_by(comment_saved::published.desc()); + .filter(comment_actions::saved.is_not_null()) + .then_order_by(comment_actions::saved.desc()); } if let Some(my_id) = options.local_user.person_id() { let not_creator_filter = comment::creator_id.ne(my_id); if options.liked_only.unwrap_or_default() { - query = query.filter(not_creator_filter).filter(score(my_id).eq(1)); + query = query + .filter(not_creator_filter) + .filter(comment_actions::like_score.eq(1)); } else if options.disliked_only.unwrap_or_default() { - query = query.filter(not_creator_filter).filter(score(my_id).eq(-1)); + query = query + .filter(not_creator_filter) + .filter(comment_actions::like_score.eq(-1)); } } @@ -276,21 +223,10 @@ fn queries<'a>() -> Queries< )); // Don't show blocked communities or persons - query = query.filter(not(exists( - instance_block::table.filter( - community::instance_id - .eq(instance_block::instance_id) - .and(instance_block::person_id.eq(person_id_join)), - ), - ))); - query = query.filter(not(exists( - community_block::table.filter( - community::id - .eq(community_block::community_id) - .and(community_block::person_id.eq(person_id_join)), - ), - ))); - query = query.filter(not(is_creator_blocked(person_id_join))); + query = query + .filter(instance_actions::blocked.is_null()) + .filter(community_actions::blocked.is_null()) + .filter(person_actions::blocked.is_null()); }; if !options.local_user.show_nsfw(site) { @@ -301,6 +237,14 @@ fn queries<'a>() -> Queries< query = options.local_user.visible_communities_only(query); + if !options.local_user.is_admin() { + query = query.filter( + community::visibility + .ne(CommunityVisibility::Private) + .or(community_actions::follow_state.eq(CommunityFollowerState::Accepted)), + ); + } + // A Max depth given means its a tree fetch let (limit, offset) = if let Some(max_depth) = options.max_depth { let depth_limit = if let Some(parent_path) = options.parent_path.as_ref() { @@ -445,6 +389,9 @@ mod tests { }, community::{ Community, + CommunityFollower, + CommunityFollowerForm, + CommunityFollowerState, CommunityInsertForm, CommunityModerator, CommunityModeratorForm, @@ -461,7 +408,7 @@ mod tests { post::{Post, PostInsertForm, PostUpdateForm}, site::{Site, SiteInsertForm}, }, - traits::{Bannable, Blockable, Crud, Joinable, Likeable, Saveable}, + traits::{Bannable, Blockable, Crud, Followable, Joinable, Likeable, Saveable}, utils::{build_db_pool_for_tests, RANK_DEFAULT}, CommunityVisibility, SubscribedType, @@ -601,7 +548,6 @@ mod tests { let comment_like_form = CommentLikeForm { comment_id: inserted_comment_0.id, - post_id: inserted_post.id, person_id: inserted_timmy_person.id, score: 1, }; @@ -632,7 +578,7 @@ mod tests { #[tokio::test] #[serial] async fn test_crud() -> LemmyResult<()> { - let pool = &build_db_pool_for_tests().await; + let pool = &build_db_pool_for_tests(); let pool = &mut pool.into(); let data = init_data(pool).await?; @@ -687,7 +633,7 @@ mod tests { #[tokio::test] #[serial] async fn test_liked_only() -> LemmyResult<()> { - let pool = &build_db_pool_for_tests().await; + let pool = &build_db_pool_for_tests(); let pool = &mut pool.into(); let data = init_data(pool).await?; @@ -701,7 +647,6 @@ mod tests { // Like a new comment let comment_like_form = CommentLikeForm { comment_id: data.inserted_comment_1.id, - post_id: data.inserted_post.id, person_id: data.timmy_local_user_view.person.id, score: 1, }; @@ -739,7 +684,7 @@ mod tests { #[tokio::test] #[serial] async fn test_comment_tree() -> LemmyResult<()> { - let pool = &build_db_pool_for_tests().await; + let pool = &build_db_pool_for_tests(); let pool = &mut pool.into(); let data = init_data(pool).await?; @@ -812,7 +757,7 @@ mod tests { #[tokio::test] #[serial] async fn test_languages() -> LemmyResult<()> { - let pool = &build_db_pool_for_tests().await; + let pool = &build_db_pool_for_tests(); let pool = &mut pool.into(); let data = init_data(pool).await?; @@ -871,7 +816,7 @@ mod tests { #[tokio::test] #[serial] async fn test_distinguished_first() -> LemmyResult<()> { - let pool = &build_db_pool_for_tests().await; + let pool = &build_db_pool_for_tests(); let pool = &mut pool.into(); let data = init_data(pool).await?; @@ -896,7 +841,7 @@ mod tests { #[tokio::test] #[serial] async fn test_creator_is_moderator() -> LemmyResult<()> { - let pool = &build_db_pool_for_tests().await; + let pool = &build_db_pool_for_tests(); let pool = &mut pool.into(); let data = init_data(pool).await?; @@ -927,7 +872,7 @@ mod tests { #[tokio::test] #[serial] async fn test_creator_is_admin() -> LemmyResult<()> { - let pool = &build_db_pool_for_tests().await; + let pool = &build_db_pool_for_tests(); let pool = &mut pool.into(); let data = init_data(pool).await?; @@ -952,7 +897,7 @@ mod tests { #[tokio::test] #[serial] async fn test_saved_order() -> LemmyResult<()> { - let pool = &build_db_pool_for_tests().await; + let pool = &build_db_pool_for_tests(); let pool = &mut pool.into(); let data = init_data(pool).await?; @@ -1051,7 +996,6 @@ mod tests { banner: None, updated: None, inbox_url: data.timmy_local_user_view.person.inbox_url.clone(), - shared_inbox_url: None, matrix_user_id: None, ban_expires: None, instance_id: data.inserted_instance.id, @@ -1095,6 +1039,7 @@ mod tests { actor_id: data.inserted_community.actor_id.clone(), local: true, title: "nada".to_owned(), + sidebar: None, description: None, updated: None, banner: None, @@ -1107,7 +1052,6 @@ mod tests { last_refreshed_at: data.inserted_community.last_refreshed_at, followers_url: data.inserted_community.followers_url.clone(), inbox_url: data.inserted_community.inbox_url.clone(), - shared_inbox_url: data.inserted_community.shared_inbox_url.clone(), moderators_url: data.inserted_community.moderators_url.clone(), featured_url: data.inserted_community.featured_url.clone(), visibility: CommunityVisibility::Public, @@ -1128,7 +1072,7 @@ mod tests { #[tokio::test] #[serial] async fn local_only_instance() -> LemmyResult<()> { - let pool = &build_db_pool_for_tests().await; + let pool = &build_db_pool_for_tests(); let pool = &mut pool.into(); let data = init_data(pool).await?; @@ -1174,7 +1118,7 @@ mod tests { #[tokio::test] #[serial] async fn comment_listing_local_user_banned_from_community() -> LemmyResult<()> { - let pool = &build_db_pool_for_tests().await; + let pool = &build_db_pool_for_tests(); let pool = &mut pool.into(); let data = init_data(pool).await?; @@ -1216,7 +1160,7 @@ mod tests { #[tokio::test] #[serial] async fn comment_listing_local_user_not_banned_from_community() -> LemmyResult<()> { - let pool = &build_db_pool_for_tests().await; + let pool = &build_db_pool_for_tests(); let pool = &mut pool.into(); let data = init_data(pool).await?; @@ -1235,7 +1179,7 @@ mod tests { #[tokio::test] #[serial] async fn comment_listings_hide_nsfw() -> LemmyResult<()> { - let pool = &build_db_pool_for_tests().await; + let pool = &build_db_pool_for_tests(); let pool = &mut pool.into(); let data = init_data(pool).await?; @@ -1260,4 +1204,96 @@ mod tests { cleanup(data, pool).await } + + #[tokio::test] + #[serial] + async fn comment_listing_private_community() -> LemmyResult<()> { + let pool = &build_db_pool_for_tests(); + let pool = &mut pool.into(); + let mut data = init_data(pool).await?; + + // Mark community as private + Community::update( + pool, + data.inserted_community.id, + &CommunityUpdateForm { + visibility: Some(CommunityVisibility::Private), + ..Default::default() + }, + ) + .await?; + + // No comments returned without auth + let read_comment_listing = CommentQuery::default().list(&data.site, pool).await?; + assert_eq!(0, read_comment_listing.len()); + let comment_view = CommentView::read(pool, data.inserted_comment_0.id, None).await; + assert!(comment_view.is_err()); + + // No comments returned for non-follower who is not admin + data.timmy_local_user_view.local_user.admin = false; + let read_comment_listing = CommentQuery { + community_id: Some(data.inserted_community.id), + local_user: Some(&data.timmy_local_user_view.local_user), + ..Default::default() + } + .list(&data.site, pool) + .await?; + assert_eq!(0, read_comment_listing.len()); + let comment_view = CommentView::read( + pool, + data.inserted_comment_0.id, + Some(&data.timmy_local_user_view.local_user), + ) + .await; + assert!(comment_view.is_err()); + + // Admin can view content without following + data.timmy_local_user_view.local_user.admin = true; + let read_comment_listing = CommentQuery { + community_id: Some(data.inserted_community.id), + local_user: Some(&data.timmy_local_user_view.local_user), + ..Default::default() + } + .list(&data.site, pool) + .await?; + assert_eq!(5, read_comment_listing.len()); + let comment_view = CommentView::read( + pool, + data.inserted_comment_0.id, + Some(&data.timmy_local_user_view.local_user), + ) + .await; + assert!(comment_view.is_ok()); + data.timmy_local_user_view.local_user.admin = false; + + // User can view after following + CommunityFollower::follow( + pool, + &CommunityFollowerForm { + state: Some(CommunityFollowerState::Accepted), + ..CommunityFollowerForm::new( + data.inserted_community.id, + data.timmy_local_user_view.person.id, + ) + }, + ) + .await?; + let read_comment_listing = CommentQuery { + community_id: Some(data.inserted_community.id), + local_user: Some(&data.timmy_local_user_view.local_user), + ..Default::default() + } + .list(&data.site, pool) + .await?; + assert_eq!(5, read_comment_listing.len()); + let comment_view = CommentView::read( + pool, + data.inserted_comment_0.id, + Some(&data.timmy_local_user_view.local_user), + ) + .await; + assert!(comment_view.is_ok()); + + cleanup(data, pool).await + } } diff --git a/crates/db_views/src/custom_emoji_view.rs b/crates/db_views/src/custom_emoji_view.rs index a346c086d..606e807e9 100644 --- a/crates/db_views/src/custom_emoji_view.rs +++ b/crates/db_views/src/custom_emoji_view.rs @@ -76,16 +76,16 @@ impl CustomEmojiView { fn from_tuple_to_vec(items: Vec) -> Vec { let mut result = Vec::new(); let mut hash: HashMap> = HashMap::new(); - for item in &items { - let emoji_id: CustomEmojiId = item.0.id; + for (emoji, keyword) in &items { + let emoji_id: CustomEmojiId = emoji.id; if let std::collections::hash_map::Entry::Vacant(e) = hash.entry(emoji_id) { e.insert(Vec::new()); result.push(CustomEmojiView { - custom_emoji: item.0.clone(), + custom_emoji: emoji.clone(), keywords: Vec::new(), }) } - if let Some(item_keyword) = &item.1 { + if let Some(item_keyword) = &keyword { if let Some(keywords) = hash.get_mut(&emoji_id) { keywords.push(item_keyword.clone()) } diff --git a/crates/db_views/src/local_user_view.rs b/crates/db_views/src/local_user_view.rs index b8ae14d5e..8d55b96fe 100644 --- a/crates/db_views/src/local_user_view.rs +++ b/crates/db_views/src/local_user_view.rs @@ -5,6 +5,12 @@ use diesel_async::RunQueryDsl; use lemmy_db_schema::{ newtypes::{LocalUserId, OAuthProviderId, PersonId}, schema::{local_user, local_user_vote_display_mode, oauth_account, person, person_aggregates}, + source::{ + instance::Instance, + local_user::{LocalUser, LocalUserInsertForm}, + person::{Person, PersonInsertForm}, + }, + traits::Crud, utils::{ functions::{coalesce, lower}, DbConn, @@ -134,6 +140,31 @@ impl LocalUserView { pub async fn list_admins_with_emails(pool: &mut DbPool<'_>) -> Result, Error> { queries().list(pool, ListMode::AdminsWithEmails).await } + + pub async fn create_test_user( + pool: &mut DbPool<'_>, + name: &str, + bio: &str, + admin: bool, + ) -> Result { + let instance_id = Instance::read_or_create(pool, "example.com".to_string()) + .await? + .id; + let person_form = PersonInsertForm { + display_name: Some(name.to_owned()), + bio: Some(bio.to_owned()), + ..PersonInsertForm::test_form(instance_id, name) + }; + let person = Person::create(pool, &person_form).await?; + + let user_form = match admin { + true => LocalUserInsertForm::test_form_admin(person.id), + false => LocalUserInsertForm::test_form(person.id), + }; + let local_user = LocalUser::create(pool, &user_form, vec![]).await?; + + LocalUserView::read(pool, local_user.id).await + } } impl FromRequest for LocalUserView { diff --git a/crates/db_views/src/post_report_view.rs b/crates/db_views/src/post_report_view.rs index 82e4c5d5b..c6c19bf6f 100644 --- a/crates/db_views/src/post_report_view.rs +++ b/crates/db_views/src/post_report_view.rs @@ -10,26 +10,23 @@ use diesel::{ }; use diesel_async::RunQueryDsl; use lemmy_db_schema::{ - aliases, + aliases::{self, creator_community_actions}, newtypes::{CommunityId, PersonId, PostId, PostReportId}, schema::{ community, - community_follower, - community_moderator, - community_person_ban, + community_actions, local_user, person, - person_block, - person_post_aggregates, + person_actions, post, + post_actions, post_aggregates, - post_hide, - post_like, - post_read, post_report, - post_saved, }, + source::community::CommunityFollower, utils::{ + actions, + actions_alias, functions::coalesce, get_conn, limit_and_offset, @@ -51,25 +48,16 @@ fn queries<'a>() -> Queries< .inner_join(community::table.on(post::community_id.eq(community::id))) .inner_join(person::table.on(post_report::creator_id.eq(person::id))) .inner_join(aliases::person1.on(post::creator_id.eq(aliases::person1.field(person::id)))) - .left_join( - community_person_ban::table.on( - post::community_id - .eq(community_person_ban::community_id) - .and(community_person_ban::person_id.eq(post::creator_id)), - ), - ) - .left_join( - aliases::community_moderator1.on( - aliases::community_moderator1 - .field(community_moderator::community_id) - .eq(post::community_id) - .and( - aliases::community_moderator1 - .field(community_moderator::person_id) - .eq(my_person_id), - ), - ), - ) + .left_join(actions_alias( + creator_community_actions, + post::creator_id, + post::community_id, + )) + .left_join(actions( + community_actions::table, + Some(my_person_id), + post::community_id, + )) .left_join( local_user::table.on( post::creator_id @@ -77,55 +65,12 @@ fn queries<'a>() -> Queries< .and(local_user::admin.eq(true)), ), ) - .left_join( - post_saved::table.on( - post::id - .eq(post_saved::post_id) - .and(post_saved::person_id.eq(my_person_id)), - ), - ) - .left_join( - post_read::table.on( - post::id - .eq(post_read::post_id) - .and(post_read::person_id.eq(my_person_id)), - ), - ) - .left_join( - post_hide::table.on( - post::id - .eq(post_hide::post_id) - .and(post_hide::person_id.eq(my_person_id)), - ), - ) - .left_join( - person_block::table.on( - post::creator_id - .eq(person_block::target_id) - .and(person_block::person_id.eq(my_person_id)), - ), - ) - .left_join( - person_post_aggregates::table.on( - post::id - .eq(person_post_aggregates::post_id) - .and(person_post_aggregates::person_id.eq(my_person_id)), - ), - ) - .left_join( - community_follower::table.on( - post::community_id - .eq(community_follower::community_id) - .and(community_follower::person_id.eq(my_person_id)), - ), - ) - .left_join( - post_like::table.on( - post::id - .eq(post_like::post_id) - .and(post_like::person_id.eq(my_person_id)), - ), - ) + .left_join(actions(post_actions::table, Some(my_person_id), post::id)) + .left_join(actions( + person_actions::table, + Some(my_person_id), + post::creator_id, + )) .inner_join(post_aggregates::table.on(post_report::post_id.eq(post_aggregates::post_id))) .left_join( aliases::person2 @@ -137,20 +82,23 @@ fn queries<'a>() -> Queries< community::all_columns, person::all_columns, aliases::person1.fields(person::all_columns), - community_person_ban::community_id.nullable().is_not_null(), - aliases::community_moderator1 - .field(community_moderator::community_id) + creator_community_actions + .field(community_actions::received_ban) + .nullable() + .is_not_null(), + creator_community_actions + .field(community_actions::became_moderator) .nullable() .is_not_null(), local_user::admin.nullable().is_not_null(), - community_follower::pending.nullable(), - post_saved::post_id.nullable().is_not_null(), - post_read::post_id.nullable().is_not_null(), - post_hide::post_id.nullable().is_not_null(), - person_block::target_id.nullable().is_not_null(), - post_like::score.nullable(), + CommunityFollower::select_subscribed_type(), + post_actions::saved.nullable().is_not_null(), + post_actions::read.nullable().is_not_null(), + post_actions::hidden.nullable().is_not_null(), + person_actions::blocked.nullable().is_not_null(), + post_actions::like_score.nullable(), coalesce( - post_aggregates::comments.nullable() - person_post_aggregates::read_comments.nullable(), + post_aggregates::comments.nullable() - post_actions::read_comments_amount.nullable(), post_aggregates::comments, ), post_aggregates::all_columns, @@ -194,19 +142,10 @@ fn queries<'a>() -> Queries< // If its not an admin, get only the ones you mod if !user.local_user.admin { - query - .inner_join( - community_moderator::table.on( - community_moderator::community_id - .eq(post::community_id) - .and(community_moderator::person_id.eq(user.person.id)), - ), - ) - .load::(&mut conn) - .await - } else { - query.load::(&mut conn).await + query = query.filter(community_actions::became_moderator.is_not_null()); } + + query.load::(&mut conn).await }; Queries::new(read, list) @@ -246,10 +185,11 @@ impl PostReportView { if !admin { query .inner_join( - community_moderator::table.on( - community_moderator::community_id + community_actions::table.on( + community_actions::community_id .eq(post::community_id) - .and(community_moderator::person_id.eq(my_person_id)), + .and(community_actions::person_id.eq(my_person_id)) + .and(community_actions::became_moderator.is_not_null()), ), ) .select(count(post_report::id)) @@ -312,7 +252,7 @@ mod tests { #[tokio::test] #[serial] async fn test_crud() -> LemmyResult<()> { - let pool = &build_db_pool_for_tests().await; + let pool = &build_db_pool_for_tests(); let pool = &mut pool.into(); let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?; diff --git a/crates/db_views/src/post_view.rs b/crates/db_views/src/post_view.rs index e07133471..2469422c2 100644 --- a/crates/db_views/src/post_view.rs +++ b/crates/db_views/src/post_view.rs @@ -5,11 +5,8 @@ use diesel::{ pg::Pg, query_builder::AsQuery, result::Error, - sql_types, BoolExpressionMethods, - BoxableExpression, ExpressionMethods, - IntoSql, JoinOnDsl, NullableExpressionMethods, OptionalExtension, @@ -20,30 +17,31 @@ use diesel_async::RunQueryDsl; use i_love_jesus::PaginatedQueryBuilder; use lemmy_db_schema::{ aggregates::structs::{post_aggregates_keys as key, PostAggregates}, + aliases::creator_community_actions, impls::local_user::LocalUserOptionHelper, newtypes::{CommunityId, LocalUserId, PersonId, PostId}, schema::{ community, - community_block, - community_follower, - community_moderator, - community_person_ban, + community_actions, image_details, - instance_block, + instance_actions, local_user, local_user_language, person, - person_block, - person_post_aggregates, + person_actions, post, + post_actions, post_aggregates, - post_hide, - post_like, - post_read, - post_saved, }, - source::{local_user::LocalUser, site::Site}, + source::{ + community::{CommunityFollower, CommunityFollowerState}, + local_user::LocalUser, + site::Site, + }, utils::{ + action_query, + actions, + actions_alias, functions::coalesce, fuzzy_search, get_conn, @@ -57,6 +55,7 @@ use lemmy_db_schema::{ ReadFn, ReverseTimestampKey, }, + CommunityVisibility, ListingType, PostSortType, }; @@ -67,32 +66,6 @@ fn queries<'a>() -> Queries< impl ReadFn<'a, PostView, (PostId, Option<&'a LocalUser>, bool)>, impl ListFn<'a, PostView, (PostQuery<'a>, &'a Site)>, > { - let is_creator_banned_from_community = exists( - community_person_ban::table.filter( - post_aggregates::community_id - .eq(community_person_ban::community_id) - .and(community_person_ban::person_id.eq(post_aggregates::creator_id)), - ), - ); - - let is_local_user_banned_from_community = |person_id| { - exists( - community_person_ban::table.filter( - post_aggregates::community_id - .eq(community_person_ban::community_id) - .and(community_person_ban::person_id.eq(person_id)), - ), - ) - }; - - let creator_is_moderator = exists( - community_moderator::table.filter( - post_aggregates::community_id - .eq(community_moderator::community_id) - .and(community_moderator::person_id.eq(post_aggregates::creator_id)), - ), - ); - let creator_is_admin = exists( local_user::table.filter( post_aggregates::creator_id @@ -101,151 +74,63 @@ fn queries<'a>() -> Queries< ), ); - let is_read = |person_id| { - exists( - post_read::table.filter( - post_aggregates::post_id - .eq(post_read::post_id) - .and(post_read::person_id.eq(person_id)), - ), - ) - }; - - let is_hidden = |person_id| { - exists( - post_hide::table.filter( - post_aggregates::post_id - .eq(post_hide::post_id) - .and(post_hide::person_id.eq(person_id)), - ), - ) - }; - - let is_creator_blocked = |person_id| { - exists( - person_block::table.filter( - post_aggregates::creator_id - .eq(person_block::target_id) - .and(person_block::person_id.eq(person_id)), - ), - ) - }; - - let score = |person_id| { - post_like::table - .filter( - post_aggregates::post_id - .eq(post_like::post_id) - .and(post_like::person_id.eq(person_id)), - ) - .select(post_like::score.nullable()) - .single_value() - }; - // TODO maybe this should go to localuser also let all_joins = move |query: post_aggregates::BoxedQuery<'a, Pg>, my_person_id: Option| { - let is_local_user_banned_from_community_selection: Box< - dyn BoxableExpression<_, Pg, SqlType = sql_types::Bool>, - > = if let Some(person_id) = my_person_id { - Box::new(is_local_user_banned_from_community(person_id)) - } else { - Box::new(false.into_sql::()) - }; - - let is_read_selection: Box> = - if let Some(person_id) = my_person_id { - Box::new(is_read(person_id)) - } else { - Box::new(false.into_sql::()) - }; - - let is_hidden_selection: Box> = - if let Some(person_id) = my_person_id { - Box::new(is_hidden(person_id)) - } else { - Box::new(false.into_sql::()) - }; - - let is_creator_blocked_selection: Box> = - if let Some(person_id) = my_person_id { - Box::new(is_creator_blocked(person_id)) - } else { - Box::new(false.into_sql::()) - }; - - let subscribed_type_selection: Box< - dyn BoxableExpression<_, Pg, SqlType = sql_types::Nullable>, - > = if let Some(person_id) = my_person_id { - Box::new( - community_follower::table - .filter( - post_aggregates::community_id - .eq(community_follower::community_id) - .and(community_follower::person_id.eq(person_id)), - ) - .select(community_follower::pending.nullable()) - .single_value(), - ) - } else { - Box::new(None::.into_sql::>()) - }; - - let score_selection: Box< - dyn BoxableExpression<_, Pg, SqlType = sql_types::Nullable>, - > = if let Some(person_id) = my_person_id { - Box::new(score(person_id)) - } else { - Box::new(None::.into_sql::>()) - }; - - let read_comments: Box< - dyn BoxableExpression<_, Pg, SqlType = sql_types::Nullable>, - > = if let Some(person_id) = my_person_id { - Box::new( - person_post_aggregates::table - .filter( - post_aggregates::post_id - .eq(person_post_aggregates::post_id) - .and(person_post_aggregates::person_id.eq(person_id)), - ) - .select(person_post_aggregates::read_comments.nullable()) - .single_value(), - ) - } else { - Box::new(None::.into_sql::>()) - }; - query .inner_join(person::table) .inner_join(community::table) .inner_join(post::table) .left_join(image_details::table.on(post::thumbnail_url.eq(image_details::link.nullable()))) - .left_join( - post_saved::table.on( - post_aggregates::post_id - .eq(post_saved::post_id) - .and(post_saved::person_id.eq(my_person_id.unwrap_or(PersonId(-1)))), - ), - ) + .left_join(actions( + community_actions::table, + my_person_id, + post_aggregates::community_id, + )) + .left_join(actions( + person_actions::table, + my_person_id, + post_aggregates::creator_id, + )) + .left_join(actions( + post_actions::table, + my_person_id, + post_aggregates::post_id, + )) + .left_join(actions( + instance_actions::table, + my_person_id, + post_aggregates::instance_id, + )) + .left_join(actions_alias( + creator_community_actions, + post_aggregates::creator_id, + post_aggregates::community_id, + )) .select(( post::all_columns, person::all_columns, community::all_columns, image_details::all_columns.nullable(), - is_creator_banned_from_community, - is_local_user_banned_from_community_selection, - creator_is_moderator, + creator_community_actions + .field(community_actions::received_ban) + .nullable() + .is_not_null(), + community_actions::received_ban.nullable().is_not_null(), + creator_community_actions + .field(community_actions::became_moderator) + .nullable() + .is_not_null(), creator_is_admin, post_aggregates::all_columns, - subscribed_type_selection, - post_saved::person_id.nullable().is_not_null(), - is_read_selection, - is_hidden_selection, - is_creator_blocked_selection, - score_selection, + CommunityFollower::select_subscribed_type(), + post_actions::saved.nullable().is_not_null(), + post_actions::read.nullable().is_not_null(), + post_actions::hidden.nullable().is_not_null(), + person_actions::blocked.nullable().is_not_null(), + post_actions::like_score.nullable(), coalesce( - post_aggregates::comments.nullable() - read_comments, + post_aggregates::comments.nullable() - post_actions::read_comments_amount.nullable(), post_aggregates::comments, ), )) @@ -291,6 +176,12 @@ fn queries<'a>() -> Queries< post::deleted .eq(false) .or(post::creator_id.eq(person_id_join)), + ) + // private communities can only by browsed by accepted followers + .filter( + community::visibility + .ne(CommunityVisibility::Private) + .or(community_actions::follow_state.eq(CommunityFollowerState::Accepted)), ); } @@ -304,7 +195,6 @@ fn queries<'a>() -> Queries< let list = move |mut conn: DbConn<'a>, (options, site): (PostQuery<'a>, &'a Site)| async move { // The left join below will return None in this case - let person_id_join = options.local_user.person_id().unwrap_or(PersonId(-1)); let local_user_id_join = options .local_user .local_user_id() @@ -346,13 +236,7 @@ fn queries<'a>() -> Queries< query = query.filter(post_aggregates::creator_id.eq(creator_id)); } - let is_subscribed = exists( - community_follower::table.filter( - post_aggregates::community_id - .eq(community_follower::community_id) - .and(community_follower::person_id.eq(person_id_join)), - ), - ); + let is_subscribed = community_actions::followed.is_not_null(); match options.listing_type.unwrap_or_default() { ListingType::Subscribed => query = query.filter(is_subscribed), ListingType::Local => { @@ -362,13 +246,7 @@ fn queries<'a>() -> Queries< } ListingType::All => query = query.filter(community::hidden.eq(false).or(is_subscribed)), ListingType::ModeratorView => { - query = query.filter(exists( - community_moderator::table.filter( - post::community_id - .eq(community_moderator::community_id) - .and(community_moderator::person_id.eq(person_id_join)), - ), - )); + query = query.filter(community_actions::became_moderator.is_not_null()); } } @@ -401,11 +279,16 @@ fn queries<'a>() -> Queries< query = query.filter(person::bot_account.eq(false)); }; + // Filter to show only posts with no comments + if options.no_comments_only.unwrap_or_default() { + query = query.filter(post_aggregates::comments.eq(0)); + }; + // If its saved only, then filter, and order by the saved time, not the comment creation time. if options.saved_only.unwrap_or_default() { query = query - .filter(post_saved::person_id.is_not_null()) - .then_order_by(post_saved::published.desc()); + .filter(post_actions::saved.is_not_null()) + .then_order_by(post_actions::saved.desc()); } // Only hide the read posts, if the saved_only is false. Otherwise ppl with the hide_read // setting wont be able to see saved posts. @@ -415,59 +298,56 @@ fn queries<'a>() -> Queries< { // Do not hide read posts when it is a user profile view // Or, only hide read posts on non-profile views - if let (None, Some(person_id)) = (options.creator_id, options.local_user.person_id()) { - query = query.filter(not(is_read(person_id))); + if options.creator_id.is_none() { + query = query.filter(post_actions::read.is_null()); } } - if !options.show_hidden.unwrap_or_default() { - // If a creator id isn't given (IE its on home or community pages), hide the hidden posts - if let (None, Some(person_id)) = (options.creator_id, options.local_user.person_id()) { - query = query.filter(not(is_hidden(person_id))); - } + // If a creator id isn't given (IE its on home or community pages), hide the hidden posts + if !options.show_hidden.unwrap_or_default() && options.creator_id.is_none() { + query = query.filter(post_actions::hidden.is_null()); } if let Some(my_id) = options.local_user.person_id() { let not_creator_filter = post_aggregates::creator_id.ne(my_id); if options.liked_only.unwrap_or_default() { - query = query.filter(not_creator_filter).filter(score(my_id).eq(1)); + query = query + .filter(not_creator_filter) + .filter(post_actions::like_score.eq(1)); } else if options.disliked_only.unwrap_or_default() { - query = query.filter(not_creator_filter).filter(score(my_id).eq(-1)); + query = query + .filter(not_creator_filter) + .filter(post_actions::like_score.eq(-1)); } }; query = options.local_user.visible_communities_only(query); + if !options.local_user.is_admin() { + query = query.filter( + community::visibility + .ne(CommunityVisibility::Private) + .or(community_actions::follow_state.eq(CommunityFollowerState::Accepted)), + ); + } + // Dont filter blocks or missing languages for moderator view type - if let (Some(person_id), false) = ( - options.local_user.person_id(), - options.listing_type.unwrap_or_default() == ListingType::ModeratorView, - ) { - // Filter out the rows with missing languages - query = query.filter(exists( - local_user_language::table.filter( - post::language_id - .eq(local_user_language::language_id) - .and(local_user_language::local_user_id.eq(local_user_id_join)), - ), - )); + if options.listing_type.unwrap_or_default() != ListingType::ModeratorView { + // Filter out the rows with missing languages if user is logged in + if options.local_user.is_some() { + query = query.filter(exists( + local_user_language::table.filter( + post::language_id + .eq(local_user_language::language_id) + .and(local_user_language::local_user_id.eq(local_user_id_join)), + ), + )); + } // Don't show blocked instances, communities or persons - query = query.filter(not(exists( - community_block::table.filter( - post_aggregates::community_id - .eq(community_block::community_id) - .and(community_block::person_id.eq(person_id_join)), - ), - ))); - query = query.filter(not(exists( - instance_block::table.filter( - post_aggregates::instance_id - .eq(instance_block::instance_id) - .and(instance_block::person_id.eq(person_id_join)), - ), - ))); - query = query.filter(not(is_creator_blocked(person_id))); + query = query.filter(community_actions::blocked.is_null()); + query = query.filter(instance_actions::blocked.is_null()); + query = query.filter(person_actions::blocked.is_null()); } let (limit, offset) = limit_and_offset(options.page, options.limit)?; @@ -617,6 +497,7 @@ pub struct PostQuery<'a> { pub show_hidden: Option, pub show_read: Option, pub show_nsfw: Option, + pub no_comments_only: Option, } impl<'a> PostQuery<'a> { @@ -636,13 +517,10 @@ impl<'a> PostQuery<'a> { // covers the "worst case" of the whole page consisting of posts from one community // but using the largest community decreases the pagination-frame so make the real query more // efficient. - use lemmy_db_schema::schema::{ - community_aggregates::dsl::{community_aggregates, community_id, users_active_month}, - community_follower::dsl::{ - community_follower, - community_id as follower_community_id, - person_id, - }, + use lemmy_db_schema::schema::community_aggregates::dsl::{ + community_aggregates, + community_id, + users_active_month, }; let (limit, offset) = limit_and_offset(self.page, self.limit)?; if offset != 0 && self.page_after.is_some() { @@ -653,9 +531,9 @@ impl<'a> PostQuery<'a> { let self_person_id = self.local_user.expect("part of the above if").person_id; let largest_subscribed = { let conn = &mut get_conn(pool).await?; - community_follower - .filter(person_id.eq(self_person_id)) - .inner_join(community_aggregates.on(community_id.eq(follower_community_id))) + action_query(community_actions::followed) + .filter(community_actions::person_id.eq(self_person_id)) + .inner_join(community_aggregates.on(community_id.eq(community_actions::community_id))) .order_by(users_active_month.desc()) .select(community_id) .limit(1) @@ -728,6 +606,7 @@ mod tests { structs::LocalUserView, }; use chrono::Utc; + use diesel_async::SimpleAsyncConnection; use lemmy_db_schema::{ aggregates::structs::PostAggregates, impls::actor_language::UNDETERMINED_ID, @@ -739,6 +618,7 @@ mod tests { Community, CommunityFollower, CommunityFollowerForm, + CommunityFollowerState, CommunityInsertForm, CommunityModerator, CommunityModeratorForm, @@ -768,7 +648,7 @@ mod tests { site::Site, }, traits::{Bannable, Blockable, Crud, Followable, Joinable, Likeable, Saveable}, - utils::{build_db_pool, build_db_pool_for_tests, DbPool, RANK_DEFAULT}, + utils::{build_db_pool, build_db_pool_for_tests, get_conn, uplete, DbPool, RANK_DEFAULT}, CommunityVisibility, PostSortType, SubscribedType, @@ -776,7 +656,10 @@ mod tests { use lemmy_utils::error::LemmyResult; use pretty_assertions::assert_eq; use serial_test::serial; - use std::{collections::HashSet, time::Duration}; + use std::{ + collections::HashSet, + time::{Duration, Instant}, + }; use url::Url; const POST_WITH_ANOTHER_TITLE: &str = "Another title"; @@ -927,7 +810,7 @@ mod tests { #[tokio::test] #[serial] async fn post_listing_with_person() -> LemmyResult<()> { - let pool = &build_db_pool().await?; + let pool = &build_db_pool()?; let pool = &mut pool.into(); let mut data = init_data(pool).await?; @@ -987,7 +870,7 @@ mod tests { #[tokio::test] #[serial] async fn post_listing_no_person() -> LemmyResult<()> { - let pool = &build_db_pool().await?; + let pool = &build_db_pool()?; let pool = &mut pool.into(); let data = init_data(pool).await?; @@ -1025,7 +908,7 @@ mod tests { #[tokio::test] #[serial] async fn post_listing_title_only() -> LemmyResult<()> { - let pool = &build_db_pool().await?; + let pool = &build_db_pool()?; let pool = &mut pool.into(); let data = init_data(pool).await?; @@ -1084,7 +967,7 @@ mod tests { #[tokio::test] #[serial] async fn post_listing_block_community() -> LemmyResult<()> { - let pool = &build_db_pool().await?; + let pool = &build_db_pool()?; let pool = &mut pool.into(); let data = init_data(pool).await?; @@ -1110,7 +993,7 @@ mod tests { #[tokio::test] #[serial] async fn post_listing_like() -> LemmyResult<()> { - let pool = &build_db_pool().await?; + let pool = &build_db_pool()?; let pool = &mut pool.into(); let mut data = init_data(pool).await?; @@ -1161,14 +1044,14 @@ mod tests { let like_removed = PostLike::remove(pool, data.local_user_view.person.id, data.inserted_post.id).await?; - assert_eq!(1, like_removed); + assert_eq!(uplete::Count::only_deleted(1), like_removed); cleanup(data, pool).await } #[tokio::test] #[serial] async fn post_listing_liked_only() -> LemmyResult<()> { - let pool = &build_db_pool().await?; + let pool = &build_db_pool()?; let pool = &mut pool.into(); let data = init_data(pool).await?; @@ -1217,7 +1100,7 @@ mod tests { #[tokio::test] #[serial] async fn post_listing_saved_only() -> LemmyResult<()> { - let pool = &build_db_pool().await?; + let pool = &build_db_pool()?; let pool = &mut pool.into(); let data = init_data(pool).await?; @@ -1247,7 +1130,7 @@ mod tests { #[tokio::test] #[serial] async fn creator_info() -> LemmyResult<()> { - let pool = &build_db_pool().await?; + let pool = &build_db_pool()?; let pool = &mut pool.into(); let data = init_data(pool).await?; @@ -1285,7 +1168,7 @@ mod tests { async fn post_listing_person_language() -> LemmyResult<()> { const EL_POSTO: &str = "el posto"; - let pool = &build_db_pool().await?; + let pool = &build_db_pool()?; let pool = &mut pool.into(); let data = init_data(pool).await?; @@ -1346,7 +1229,7 @@ mod tests { #[tokio::test] #[serial] async fn post_listings_removed() -> LemmyResult<()> { - let pool = &build_db_pool().await?; + let pool = &build_db_pool()?; let pool = &mut pool.into(); let mut data = init_data(pool).await?; @@ -1381,7 +1264,7 @@ mod tests { #[tokio::test] #[serial] async fn post_listings_deleted() -> LemmyResult<()> { - let pool = &build_db_pool().await?; + let pool = &build_db_pool()?; let pool = &mut pool.into(); let data = init_data(pool).await?; @@ -1420,7 +1303,7 @@ mod tests { #[tokio::test] #[serial] async fn post_listings_hidden_community() -> LemmyResult<()> { - let pool = &build_db_pool().await?; + let pool = &build_db_pool()?; let pool = &mut pool.into(); let data = init_data(pool).await?; @@ -1442,9 +1325,8 @@ mod tests { // Follow the community let form = CommunityFollowerForm { - community_id: data.inserted_community.id, - person_id: data.local_user_view.person.id, - pending: false, + state: Some(CommunityFollowerState::Accepted), + ..CommunityFollowerForm::new(data.inserted_community.id, data.local_user_view.person.id) }; CommunityFollower::follow(pool, &form).await?; @@ -1459,7 +1341,7 @@ mod tests { async fn post_listing_instance_block() -> LemmyResult<()> { const POST_FROM_BLOCKED_INSTANCE: &str = "post on blocked instance"; - let pool = &build_db_pool().await?; + let pool = &build_db_pool()?; let pool = &mut pool.into(); let data = init_data(pool).await?; @@ -1519,7 +1401,7 @@ mod tests { #[tokio::test] #[serial] async fn pagination_includes_each_post_once() -> LemmyResult<()> { - let pool = &build_db_pool().await?; + let pool = &build_db_pool()?; let pool = &mut pool.into(); let data = init_data(pool).await?; @@ -1629,7 +1511,7 @@ mod tests { #[tokio::test] #[serial] async fn post_listings_hide_read() -> LemmyResult<()> { - let pool = &build_db_pool().await?; + let pool = &build_db_pool()?; let pool = &mut pool.into(); let mut data = init_data(pool).await?; @@ -1679,7 +1561,7 @@ mod tests { #[tokio::test] #[serial] async fn post_listings_hide_hidden() -> LemmyResult<()> { - let pool = &build_db_pool().await?; + let pool = &build_db_pool()?; let pool = &mut pool.into(); let data = init_data(pool).await?; @@ -1715,7 +1597,7 @@ mod tests { #[tokio::test] #[serial] async fn post_listings_hide_nsfw() -> LemmyResult<()> { - let pool = &build_db_pool().await?; + let pool = &build_db_pool()?; let pool = &mut pool.into(); let data = init_data(pool).await?; @@ -1812,7 +1694,6 @@ mod tests { banner: None, updated: None, inbox_url: inserted_person.inbox_url.clone(), - shared_inbox_url: None, matrix_user_id: None, ban_expires: None, instance_id: data.inserted_instance.id, @@ -1835,6 +1716,7 @@ mod tests { actor_id: inserted_community.actor_id.clone(), local: true, title: "nada".to_owned(), + sidebar: None, description: None, updated: None, banner: None, @@ -1847,7 +1729,6 @@ mod tests { last_refreshed_at: inserted_community.last_refreshed_at, followers_url: inserted_community.followers_url.clone(), inbox_url: inserted_community.inbox_url.clone(), - shared_inbox_url: inserted_community.shared_inbox_url.clone(), moderators_url: inserted_community.moderators_url.clone(), featured_url: inserted_community.featured_url.clone(), visibility: CommunityVisibility::Public, @@ -1882,7 +1763,7 @@ mod tests { #[tokio::test] #[serial] async fn local_only_instance() -> LemmyResult<()> { - let pool = &build_db_pool_for_tests().await; + let pool = &build_db_pool_for_tests(); let pool = &mut pool.into(); let data = init_data(pool).await?; @@ -1930,7 +1811,7 @@ mod tests { #[tokio::test] #[serial] async fn post_listing_local_user_banned_from_community() -> LemmyResult<()> { - let pool = &build_db_pool().await?; + let pool = &build_db_pool()?; let pool = &mut pool.into(); let data = init_data(pool).await?; @@ -1973,7 +1854,7 @@ mod tests { #[tokio::test] #[serial] async fn post_listing_local_user_not_banned_from_community() -> LemmyResult<()> { - let pool = &build_db_pool().await?; + let pool = &build_db_pool()?; let pool = &mut pool.into(); let data = init_data(pool).await?; @@ -1989,4 +1870,187 @@ mod tests { cleanup(data, pool).await } + + #[tokio::test] + #[serial] + async fn speed_check() -> LemmyResult<()> { + let pool = &build_db_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); + + // Create some dummy posts + let num_posts = 1000; + for x in 1..num_posts { + let name = format!("post_{x}"); + let url = Some(Url::parse(&format!("https://google.com/{name}"))?.into()); + + let post_form = PostInsertForm { + url, + ..PostInsertForm::new( + name, + data.local_user_view.person.id, + data.inserted_community.id, + ) + }; + Post::create(pool, &post_form).await?; + } + + // Manually trigger and wait for a statistics update to ensure consistent and high amount of + // accuracy in the statistics used for query planning + println!("🧮 updating database statistics"); + let conn = &mut get_conn(pool).await?; + conn.batch_execute("ANALYZE;").await?; + + // Time how fast the query took + let now = Instant::now(); + PostQuery { + sort: Some(PostSortType::Active), + local_user: Some(&data.local_user_view.local_user), + ..Default::default() + } + .list(&data.site, pool) + .await?; + + let elapsed = now.elapsed(); + println!("Elapsed: {:.0?}", elapsed); + + assert!( + elapsed.lt(&duration_max), + "Query took {:.0?}, longer than the max of {:.0?}", + elapsed, + duration_max + ); + + cleanup(data, pool).await + } + + #[tokio::test] + #[serial] + async fn post_listings_no_comments_only() -> LemmyResult<()> { + let pool = &build_db_pool()?; + let pool = &mut pool.into(); + let data = init_data(pool).await?; + + // Create a comment for a post + let comment_form = CommentInsertForm::new( + data.local_user_view.person.id, + data.inserted_post.id, + "a comment".to_owned(), + ); + Comment::create(pool, &comment_form, None).await?; + + // Make sure it doesnt come back with the no_comments option + let post_listings_no_comments = PostQuery { + sort: Some(PostSortType::New), + no_comments_only: Some(true), + local_user: Some(&data.local_user_view.local_user), + ..Default::default() + } + .list(&data.site, pool) + .await?; + + assert_eq!(vec![POST_BY_BOT], names(&post_listings_no_comments)); + + cleanup(data, pool).await + } + + #[tokio::test] + #[serial] + async fn post_listing_private_community() -> LemmyResult<()> { + let pool = &build_db_pool()?; + let pool = &mut pool.into(); + let mut data = init_data(pool).await?; + + // Mark community as private + Community::update( + pool, + data.inserted_community.id, + &CommunityUpdateForm { + visibility: Some(CommunityVisibility::Private), + ..Default::default() + }, + ) + .await?; + + // No posts returned without auth + let read_post_listing = PostQuery { + community_id: Some(data.inserted_community.id), + ..Default::default() + } + .list(&data.site, pool) + .await?; + assert_eq!(0, read_post_listing.len()); + let post_view = PostView::read(pool, data.inserted_post.id, None, false).await; + assert!(post_view.is_err()); + + // No posts returned for non-follower who is not admin + data.local_user_view.local_user.admin = false; + let read_post_listing = PostQuery { + community_id: Some(data.inserted_community.id), + local_user: Some(&data.local_user_view.local_user), + ..Default::default() + } + .list(&data.site, pool) + .await?; + assert_eq!(0, read_post_listing.len()); + let post_view = PostView::read( + pool, + data.inserted_post.id, + Some(&data.local_user_view.local_user), + false, + ) + .await; + assert!(post_view.is_err()); + + // Admin can view content without following + data.local_user_view.local_user.admin = true; + let read_post_listing = PostQuery { + community_id: Some(data.inserted_community.id), + local_user: Some(&data.local_user_view.local_user), + ..Default::default() + } + .list(&data.site, pool) + .await?; + assert_eq!(2, read_post_listing.len()); + let post_view = PostView::read( + pool, + data.inserted_post.id, + Some(&data.local_user_view.local_user), + true, + ) + .await; + assert!(post_view.is_ok()); + data.local_user_view.local_user.admin = false; + + // User can view after following + CommunityFollower::follow( + pool, + &CommunityFollowerForm { + state: Some(CommunityFollowerState::Accepted), + ..CommunityFollowerForm::new(data.inserted_community.id, data.local_user_view.person.id) + }, + ) + .await?; + let read_post_listing = PostQuery { + community_id: Some(data.inserted_community.id), + local_user: Some(&data.local_user_view.local_user), + ..Default::default() + } + .list(&data.site, pool) + .await?; + assert_eq!(2, read_post_listing.len()); + let post_view = PostView::read( + pool, + data.inserted_post.id, + Some(&data.local_user_view.local_user), + true, + ) + .await; + assert!(post_view.is_ok()); + + cleanup(data, pool).await + } } diff --git a/crates/db_views/src/private_message_report_view.rs b/crates/db_views/src/private_message_report_view.rs index 56d0d6e7b..e59d99608 100644 --- a/crates/db_views/src/private_message_report_view.rs +++ b/crates/db_views/src/private_message_report_view.rs @@ -133,7 +133,7 @@ mod tests { #[tokio::test] #[serial] async fn test_crud() -> LemmyResult<()> { - let pool = &build_db_pool_for_tests().await; + let pool = &build_db_pool_for_tests(); let pool = &mut pool.into(); let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?; diff --git a/crates/db_views/src/private_message_view.rs b/crates/db_views/src/private_message_view.rs index 0fbc0ee16..2286b7dc6 100644 --- a/crates/db_views/src/private_message_view.rs +++ b/crates/db_views/src/private_message_view.rs @@ -12,8 +12,8 @@ use diesel_async::RunQueryDsl; use lemmy_db_schema::{ aliases, newtypes::{PersonId, PrivateMessageId}, - schema::{instance_block, person, person_block, private_message}, - utils::{get_conn, limit_and_offset, DbConn, DbPool, ListFn, Queries, ReadFn}, + schema::{instance_actions, person, person_actions, private_message}, + utils::{actions, get_conn, limit_and_offset, DbConn, DbPool, ListFn, Queries, ReadFn}, }; use tracing::debug; @@ -27,20 +27,16 @@ fn queries<'a>() -> Queries< .inner_join( aliases::person1.on(private_message::recipient_id.eq(aliases::person1.field(person::id))), ) - .left_join( - person_block::table.on( - private_message::creator_id - .eq(person_block::target_id) - .and(person_block::person_id.eq(aliases::person1.field(person::id))), - ), - ) - .left_join( - instance_block::table.on( - person::instance_id - .eq(instance_block::instance_id) - .and(instance_block::person_id.eq(aliases::person1.field(person::id))), - ), - ) + .left_join(actions( + person_actions::table, + Some(aliases::person1.field(person::id)), + private_message::creator_id, + )) + .left_join(actions( + instance_actions::table, + Some(aliases::person1.field(person::id)), + person::instance_id, + )) }; let selection = ( @@ -62,9 +58,9 @@ fn queries<'a>() -> Queries< let mut query = all_joins(private_message::table.into_boxed()) .select(selection) // Dont show replies from blocked users - .filter(person_block::person_id.is_null()) + .filter(person_actions::blocked.is_null()) // Dont show replies from blocked instances - .filter(instance_block::person_id.is_null()); + .filter(instance_actions::blocked.is_null()); // If its unread, I only want the ones to me if options.unread_only { @@ -127,24 +123,20 @@ impl PrivateMessageView { private_message::table // Necessary to get the senders instance_id .inner_join(person::table.on(private_message::creator_id.eq(person::id))) - .left_join( - person_block::table.on( - private_message::creator_id - .eq(person_block::target_id) - .and(person_block::person_id.eq(my_person_id)), - ), - ) - .left_join( - instance_block::table.on( - person::instance_id - .eq(instance_block::instance_id) - .and(instance_block::person_id.eq(my_person_id)), - ), - ) + .left_join(actions( + person_actions::table, + Some(my_person_id), + private_message::creator_id, + )) + .left_join(actions( + instance_actions::table, + Some(my_person_id), + person::instance_id, + )) // Dont count replies from blocked users - .filter(person_block::person_id.is_null()) + .filter(person_actions::blocked.is_null()) // Dont count replies from blocked instances - .filter(instance_block::person_id.is_null()) + .filter(instance_actions::blocked.is_null()) .filter(private_message::read.eq(false)) .filter(private_message::recipient_id.eq(my_person_id)) .filter(private_message::deleted.eq(false)) @@ -251,7 +243,7 @@ mod tests { #[tokio::test] #[serial] async fn read_private_messages() -> LemmyResult<()> { - let pool = &build_db_pool_for_tests().await; + let pool = &build_db_pool_for_tests(); let pool = &mut pool.into(); let Data { timmy, @@ -322,7 +314,7 @@ mod tests { #[tokio::test] #[serial] async fn ensure_person_block() -> LemmyResult<()> { - let pool = &build_db_pool_for_tests().await; + let pool = &build_db_pool_for_tests(); let pool = &mut pool.into(); let Data { timmy, @@ -365,7 +357,7 @@ mod tests { #[tokio::test] #[serial] async fn ensure_instance_block() -> LemmyResult<()> { - let pool = &build_db_pool_for_tests().await; + let pool = &build_db_pool_for_tests(); let pool = &mut pool.into(); let Data { timmy, diff --git a/crates/db_views/src/registration_application_view.rs b/crates/db_views/src/registration_application_view.rs index 6460becb7..b5821ef26 100644 --- a/crates/db_views/src/registration_application_view.rs +++ b/crates/db_views/src/registration_application_view.rs @@ -162,7 +162,7 @@ mod tests { #[tokio::test] #[serial] async fn test_crud() -> LemmyResult<()> { - let pool = &build_db_pool_for_tests().await; + let pool = &build_db_pool_for_tests(); let pool = &mut pool.into(); let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?; @@ -240,6 +240,7 @@ mod tests { totp_2fa_enabled: inserted_sara_local_user.totp_2fa_enabled, enable_keyboard_navigation: inserted_sara_local_user.enable_keyboard_navigation, enable_animated_images: inserted_sara_local_user.enable_animated_images, + enable_private_messages: inserted_sara_local_user.enable_private_messages, collapse_bot_comments: inserted_sara_local_user.collapse_bot_comments, }, creator: Person { @@ -258,7 +259,6 @@ mod tests { banner: None, updated: None, inbox_url: inserted_sara_person.inbox_url.clone(), - shared_inbox_url: None, matrix_user_id: None, instance_id: inserted_instance.id, private_key: inserted_sara_person.private_key, @@ -328,7 +328,6 @@ mod tests { banner: None, updated: None, inbox_url: inserted_timmy_person.inbox_url.clone(), - shared_inbox_url: None, matrix_user_id: None, instance_id: inserted_instance.id, private_key: inserted_timmy_person.private_key, diff --git a/crates/db_views/src/site_view.rs b/crates/db_views/src/site_view.rs index 6014ad964..ed9aeb498 100644 --- a/crates/db_views/src/site_view.rs +++ b/crates/db_views/src/site_view.rs @@ -5,7 +5,7 @@ use lemmy_db_schema::{ schema::{local_site, local_site_rate_limit, site, site_aggregates}, utils::{get_conn, DbPool}, }; -use lemmy_utils::{error::LemmyResult, LemmyErrorType}; +use lemmy_utils::error::{LemmyErrorType, LemmyResult}; impl SiteView { pub async fn read_local(pool: &mut DbPool<'_>) -> LemmyResult { diff --git a/crates/db_views/src/structs.rs b/crates/db_views/src/structs.rs index 3c219d63f..4586fbcac 100644 --- a/crates/db_views/src/structs.rs +++ b/crates/db_views/src/structs.rs @@ -48,7 +48,9 @@ pub struct CommentReportView { pub creator_blocked: bool, pub subscribed: SubscribedType, pub saved: bool, + #[cfg_attr(feature = "full", ts(optional))] pub my_vote: Option, + #[cfg_attr(feature = "full", ts(optional))] pub resolver: Option, } @@ -71,6 +73,7 @@ pub struct CommentView { pub subscribed: SubscribedType, pub saved: bool, pub creator_blocked: bool, + #[cfg_attr(feature = "full", ts(optional))] pub my_vote: Option, } @@ -106,9 +109,11 @@ pub struct PostReportView { pub read: bool, pub hidden: bool, pub creator_blocked: bool, + #[cfg_attr(feature = "full", ts(optional))] pub my_vote: Option, pub unread_comments: i64, pub counts: PostAggregates, + #[cfg_attr(feature = "full", ts(optional))] pub resolver: Option, } @@ -131,6 +136,7 @@ pub struct PostView { pub post: Post, pub creator: Person, pub community: Community, + #[cfg_attr(feature = "full", ts(optional))] pub image_details: Option, pub creator_banned_from_community: bool, pub banned_from_community: bool, @@ -142,6 +148,7 @@ pub struct PostView { pub read: bool, pub hidden: bool, pub creator_blocked: bool, + #[cfg_attr(feature = "full", ts(optional))] pub my_vote: Option, pub unread_comments: i64, } @@ -168,6 +175,7 @@ pub struct PrivateMessageReportView { pub private_message: PrivateMessage, pub private_message_creator: Person, pub creator: Person, + #[cfg_attr(feature = "full", ts(optional))] pub resolver: Option, } @@ -181,6 +189,7 @@ pub struct RegistrationApplicationView { pub registration_application: RegistrationApplication, pub creator_local_user: LocalUser, pub creator: Person, + #[cfg_attr(feature = "full", ts(optional))] pub admin: Option, } diff --git a/crates/db_views/src/vote_view.rs b/crates/db_views/src/vote_view.rs index 9c5fef855..9af0bd756 100644 --- a/crates/db_views/src/vote_view.rs +++ b/crates/db_views/src/vote_view.rs @@ -1,17 +1,11 @@ use crate::structs::VoteView; -use diesel::{ - result::Error, - BoolExpressionMethods, - ExpressionMethods, - JoinOnDsl, - NullableExpressionMethods, - QueryDsl, -}; +use diesel::{result::Error, ExpressionMethods, NullableExpressionMethods, QueryDsl}; use diesel_async::RunQueryDsl; use lemmy_db_schema::{ + aliases::creator_community_actions, newtypes::{CommentId, PostId}, - schema::{comment_like, community_person_ban, person, post, post_like}, - utils::{get_conn, limit_and_offset, DbPool}, + schema::{comment, comment_actions, community_actions, person, post, post_actions}, + utils::{action_query, actions_alias, get_conn, limit_and_offset, DbPool}, }; impl VoteView { @@ -24,24 +18,24 @@ impl VoteView { let conn = &mut get_conn(pool).await?; let (limit, offset) = limit_and_offset(page, limit)?; - post_like::table + action_query(post_actions::like_score) .inner_join(person::table) .inner_join(post::table) - // Join to community_person_ban to get creator_banned_from_community - .left_join( - community_person_ban::table.on( - post::community_id - .eq(community_person_ban::community_id) - .and(community_person_ban::person_id.eq(post_like::person_id)), - ), - ) - .filter(post_like::post_id.eq(post_id)) + .left_join(actions_alias( + creator_community_actions, + post_actions::person_id, + post::community_id, + )) + .filter(post_actions::post_id.eq(post_id)) .select(( person::all_columns, - community_person_ban::community_id.nullable().is_not_null(), - post_like::score, + creator_community_actions + .field(community_actions::received_ban) + .nullable() + .is_not_null(), + post_actions::like_score.assume_not_null(), )) - .order_by(post_like::score) + .order_by(post_actions::like_score) .limit(limit) .offset(offset) .load::(conn) @@ -57,24 +51,24 @@ impl VoteView { let conn = &mut get_conn(pool).await?; let (limit, offset) = limit_and_offset(page, limit)?; - comment_like::table + action_query(comment_actions::like_score) .inner_join(person::table) - .inner_join(post::table) - // Join to community_person_ban to get creator_banned_from_community - .left_join( - community_person_ban::table.on( - post::community_id - .eq(community_person_ban::community_id) - .and(community_person_ban::person_id.eq(comment_like::person_id)), - ), - ) - .filter(comment_like::comment_id.eq(comment_id)) + .inner_join(comment::table.inner_join(post::table)) + .left_join(actions_alias( + creator_community_actions, + comment_actions::person_id, + post::community_id, + )) + .filter(comment_actions::comment_id.eq(comment_id)) .select(( person::all_columns, - community_person_ban::community_id.nullable().is_not_null(), - comment_like::score, + creator_community_actions + .field(community_actions::received_ban) + .nullable() + .is_not_null(), + comment_actions::like_score.assume_not_null(), )) - .order_by(comment_like::score) + .order_by(comment_actions::like_score) .limit(limit) .offset(offset) .load::(conn) @@ -104,7 +98,7 @@ mod tests { #[tokio::test] #[serial] async fn post_and_comment_vote_views() -> LemmyResult<()> { - let pool = &build_db_pool_for_tests().await; + let pool = &build_db_pool_for_tests(); let pool = &mut pool.into(); let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?; @@ -173,7 +167,6 @@ mod tests { // Timothy votes down his own comment let timmy_comment_vote_form = CommentLikeForm { - post_id: inserted_post.id, comment_id: inserted_comment.id, person_id: inserted_timmy.id, score: -1, @@ -182,7 +175,6 @@ mod tests { // Sara upvotes timmy's comment let sara_comment_vote_form = CommentLikeForm { - post_id: inserted_post.id, comment_id: inserted_comment.id, person_id: inserted_sara.id, score: 1, diff --git a/crates/db_views_actor/Cargo.toml b/crates/db_views_actor/Cargo.toml index c29b7d66d..18a79826b 100644 --- a/crates/db_views_actor/Cargo.toml +++ b/crates/db_views_actor/Cargo.toml @@ -46,8 +46,4 @@ serial_test = { workspace = true } tokio = { workspace = true } pretty_assertions = { workspace = true } url.workspace = true -lemmy_db_views.workspace = true -lemmy_utils.workspace = true - -[package.metadata.cargo-machete] -ignored = ["strum"] +lemmy_db_views = { workspace = true, features = ["full"] } diff --git a/crates/db_views_actor/src/comment_reply_view.rs b/crates/db_views_actor/src/comment_reply_view.rs index 1b657866a..6c5442e6a 100644 --- a/crates/db_views_actor/src/comment_reply_view.rs +++ b/crates/db_views_actor/src/comment_reply_view.rs @@ -3,36 +3,40 @@ use diesel::{ dsl::{exists, not}, pg::Pg, result::Error, - sql_types, BoolExpressionMethods, - BoxableExpression, ExpressionMethods, - IntoSql, JoinOnDsl, NullableExpressionMethods, QueryDsl, }; use diesel_async::RunQueryDsl; use lemmy_db_schema::{ - aliases, + aliases::{self, creator_community_actions}, newtypes::{CommentReplyId, PersonId}, schema::{ comment, + comment_actions, comment_aggregates, - comment_like, comment_reply, - comment_saved, community, - community_follower, - community_moderator, - community_person_ban, + community_actions, local_user, person, - person_block, + person_actions, post, }, - source::local_user::LocalUser, - utils::{get_conn, limit_and_offset, DbConn, DbPool, ListFn, Queries, ReadFn}, + source::{community::CommunityFollower, local_user::LocalUser}, + utils::{ + actions, + actions_alias, + get_conn, + limit_and_offset, + DbConn, + DbPool, + ListFn, + Queries, + ReadFn, + }, CommentSortType, }; @@ -40,74 +44,6 @@ fn queries<'a>() -> Queries< impl ReadFn<'a, CommentReplyView, (CommentReplyId, Option)>, impl ListFn<'a, CommentReplyView, CommentReplyQuery>, > { - let is_creator_banned_from_community = exists( - community_person_ban::table.filter( - community::id - .eq(community_person_ban::community_id) - .and(community_person_ban::person_id.eq(comment::creator_id)), - ), - ); - - let is_local_user_banned_from_community = |person_id| { - exists( - community_person_ban::table.filter( - community::id - .eq(community_person_ban::community_id) - .and(community_person_ban::person_id.eq(person_id)), - ), - ) - }; - - let is_saved = |person_id| { - exists( - comment_saved::table.filter( - comment::id - .eq(comment_saved::comment_id) - .and(comment_saved::person_id.eq(person_id)), - ), - ) - }; - - let is_community_followed = |person_id| { - community_follower::table - .filter( - post::community_id - .eq(community_follower::community_id) - .and(community_follower::person_id.eq(person_id)), - ) - .select(community_follower::pending.nullable()) - .single_value() - }; - - let is_creator_blocked = |person_id| { - exists( - person_block::table.filter( - comment::creator_id - .eq(person_block::target_id) - .and(person_block::person_id.eq(person_id)), - ), - ) - }; - - let score = |person_id| { - comment_like::table - .filter( - comment::id - .eq(comment_like::comment_id) - .and(comment_like::person_id.eq(person_id)), - ) - .select(comment_like::score.nullable()) - .single_value() - }; - - let creator_is_moderator = exists( - community_moderator::table.filter( - community::id - .eq(community_moderator::community_id) - .and(community_moderator::person_id.eq(comment::creator_id)), - ), - ); - let creator_is_admin = exists( local_user::table.filter( comment::creator_id @@ -118,44 +54,6 @@ fn queries<'a>() -> Queries< let all_joins = move |query: comment_reply::BoxedQuery<'a, Pg>, my_person_id: Option| { - let is_local_user_banned_from_community_selection: Box< - dyn BoxableExpression<_, Pg, SqlType = sql_types::Bool>, - > = if let Some(person_id) = my_person_id { - Box::new(is_local_user_banned_from_community(person_id)) - } else { - Box::new(false.into_sql::()) - }; - - let score_selection: Box< - dyn BoxableExpression<_, Pg, SqlType = sql_types::Nullable>, - > = if let Some(person_id) = my_person_id { - Box::new(score(person_id)) - } else { - Box::new(None::.into_sql::>()) - }; - - let subscribed_type_selection: Box< - dyn BoxableExpression<_, Pg, SqlType = sql_types::Nullable>, - > = if let Some(person_id) = my_person_id { - Box::new(is_community_followed(person_id)) - } else { - Box::new(None::.into_sql::>()) - }; - - let is_saved_selection: Box> = - if let Some(person_id) = my_person_id { - Box::new(is_saved(person_id)) - } else { - Box::new(false.into_sql::()) - }; - - let is_creator_blocked_selection: Box> = - if let Some(person_id) = my_person_id { - Box::new(is_creator_blocked(person_id)) - } else { - Box::new(false.into_sql::()) - }; - query .inner_join(comment::table) .inner_join(person::table.on(comment::creator_id.eq(person::id))) @@ -163,6 +61,22 @@ fn queries<'a>() -> Queries< .inner_join(community::table.on(post::community_id.eq(community::id))) .inner_join(aliases::person1) .inner_join(comment_aggregates::table.on(comment::id.eq(comment_aggregates::comment_id))) + .left_join(actions(comment_actions::table, my_person_id, comment::id)) + .left_join(actions( + community_actions::table, + my_person_id, + post::community_id, + )) + .left_join(actions( + person_actions::table, + my_person_id, + comment::creator_id, + )) + .left_join(actions_alias( + creator_community_actions, + comment::creator_id, + post::community_id, + )) .select(( comment_reply::all_columns, comment::all_columns, @@ -171,14 +85,20 @@ fn queries<'a>() -> Queries< community::all_columns, aliases::person1.fields(person::all_columns), comment_aggregates::all_columns, - is_creator_banned_from_community, - is_local_user_banned_from_community_selection, - creator_is_moderator, + creator_community_actions + .field(community_actions::received_ban) + .nullable() + .is_not_null(), + community_actions::received_ban.nullable().is_not_null(), + creator_community_actions + .field(community_actions::became_moderator) + .nullable() + .is_not_null(), creator_is_admin, - subscribed_type_selection, - is_saved_selection, - is_creator_blocked_selection, - score_selection, + CommunityFollower::select_subscribed_type(), + comment_actions::saved.nullable().is_not_null(), + person_actions::blocked.nullable().is_not_null(), + comment_actions::like_score.nullable(), )) }; @@ -221,9 +141,7 @@ fn queries<'a>() -> Queries< }; // Don't show replies from blocked persons - if let Some(my_person_id) = options.my_person_id { - query = query.filter(not(is_creator_blocked(my_person_id))); - } + query = query.filter(person_actions::blocked.is_null()); let (limit, offset) = limit_and_offset(options.page, options.limit)?; @@ -257,13 +175,11 @@ impl CommentReplyView { let mut query = comment_reply::table .inner_join(comment::table) - .left_join( - person_block::table.on( - comment::creator_id - .eq(person_block::target_id) - .and(person_block::person_id.eq(local_user.person_id)), - ), - ) + .left_join(actions( + person_actions::table, + Some(local_user.person_id), + comment::creator_id, + )) .inner_join(person::table.on(comment::creator_id.eq(person::id))) .into_boxed(); @@ -274,7 +190,7 @@ impl CommentReplyView { query // Don't count replies from blocked users - .filter(person_block::person_id.is_null()) + .filter(person_actions::blocked.is_null()) .filter(comment_reply::recipient_id.eq(local_user.person_id)) .filter(comment_reply::read.eq(false)) .filter(comment::deleted.eq(false)) @@ -328,7 +244,7 @@ mod tests { #[tokio::test] #[serial] async fn test_crud() -> LemmyResult<()> { - let pool = &build_db_pool_for_tests().await; + let pool = &build_db_pool_for_tests(); let pool = &mut pool.into(); let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?; diff --git a/crates/db_views_actor/src/community_follower_view.rs b/crates/db_views_actor/src/community_follower_view.rs index 7b942e043..d3015c182 100644 --- a/crates/db_views_actor/src/community_follower_view.rs +++ b/crates/db_views_actor/src/community_follower_view.rs @@ -1,17 +1,27 @@ -use crate::structs::CommunityFollowerView; +use crate::structs::{CommunityFollowerView, PendingFollow}; use chrono::Utc; use diesel::{ - dsl::{count_star, not}, + dsl::{count, count_star, exists, not}, result::Error, + select, + BoolExpressionMethods, ExpressionMethods, + JoinOnDsl, QueryDsl, }; use diesel_async::RunQueryDsl; use lemmy_db_schema::{ newtypes::{CommunityId, DbUrl, InstanceId, PersonId}, - schema::{community, community_follower, person}, - utils::{functions::coalesce, get_conn, DbPool}, + schema::{community, community_actions, person}, + source::{ + community::{Community, CommunityFollower, CommunityFollowerState}, + person::Person, + }, + utils::{action_query, get_conn, limit_and_offset, DbPool}, + CommunityVisibility, + SubscribedType, }; +use lemmy_utils::error::{LemmyErrorType, LemmyResult}; impl CommunityFollowerView { /// return a list of local community ids and remote inboxes that at least one user of the given @@ -29,18 +39,15 @@ impl CommunityFollowerView { // that would work for all instances that support fully shared inboxes. // It would be a bit more complicated though to keep it in sync. - community_follower::table + community_actions::table .inner_join(community::table) - .inner_join(person::table) + .inner_join(person::table.on(community_actions::person_id.eq(person::id))) .filter(person::instance_id.eq(instance_id)) .filter(community::local) // this should be a no-op since community_followers table only has // local-person+remote-community or remote-person+local-community .filter(not(person::local)) - .filter(community_follower::published.gt(published_since.naive_utc())) - .select(( - community::id, - coalesce(person::shared_inbox_url, person::inbox_url), - )) + .filter(community_actions::followed.gt(published_since.naive_utc())) + .select((community::id, person::inbox_url)) .distinct() // only need each community_id, inbox combination once .load::<(CommunityId, DbUrl)>(conn) .await @@ -50,11 +57,11 @@ impl CommunityFollowerView { community_id: CommunityId, ) -> Result, Error> { let conn = &mut get_conn(pool).await?; - let res = community_follower::table - .filter(community_follower::community_id.eq(community_id)) + let res = action_query(community_actions::followed) + .filter(community_actions::community_id.eq(community_id)) .filter(not(person::local)) - .inner_join(person::table) - .select(coalesce(person::shared_inbox_url, person::inbox_url)) + .inner_join(person::table.on(community_actions::person_id.eq(person::id))) + .select(person::inbox_url) .distinct() .load::(conn) .await?; @@ -66,8 +73,8 @@ impl CommunityFollowerView { community_id: CommunityId, ) -> Result { let conn = &mut get_conn(pool).await?; - let res = community_follower::table - .filter(community_follower::community_id.eq(community_id)) + let res = action_query(community_actions::followed) + .filter(community_actions::community_id.eq(community_id)) .select(count_star()) .first::(conn) .await?; @@ -77,15 +84,231 @@ impl CommunityFollowerView { pub async fn for_person(pool: &mut DbPool<'_>, person_id: PersonId) -> Result, Error> { let conn = &mut get_conn(pool).await?; - community_follower::table + action_query(community_actions::followed) .inner_join(community::table) - .inner_join(person::table) + .inner_join(person::table.on(community_actions::person_id.eq(person::id))) .select((community::all_columns, person::all_columns)) - .filter(community_follower::person_id.eq(person_id)) + .filter(community_actions::person_id.eq(person_id)) .filter(community::deleted.eq(false)) .filter(community::removed.eq(false)) .order_by(community::title) .load::(conn) .await } + + pub async fn list_approval_required( + pool: &mut DbPool<'_>, + person_id: PersonId, + // TODO: if this is true dont check for community mod, but only check for local community + // also need to check is_admin() + all_communities: bool, + pending_only: bool, + page: Option, + limit: Option, + ) -> Result, Error> { + let conn = &mut get_conn(pool).await?; + let (limit, offset) = limit_and_offset(page, limit)?; + let (person_alias, community_follower_alias) = diesel::alias!( + person as person_alias, + community_actions as community_follower_alias + ); + + // check if the community already has an accepted follower from the same instance + let is_new_instance = not(exists( + person_alias + .inner_join( + community_follower_alias.on( + person_alias + .field(person::id) + .eq(community_follower_alias.field(community_actions::person_id)), + ), + ) + .filter( + person::instance_id + .eq(person_alias.field(person::instance_id)) + .and( + community_follower_alias + .field(community_actions::community_id) + .eq(community_actions::community_id), + ) + .and( + community_follower_alias + .field(community_actions::follow_state) + .eq(CommunityFollowerState::Accepted), + ), + ), + )); + + let mut query = action_query(community_actions::followed) + .inner_join(person::table.on(community_actions::person_id.eq(person::id))) + .inner_join(community::table) + .into_boxed(); + if all_communities { + // if param is false, only return items for communities where user is a mod + query = query + .filter(community_actions::became_moderator.is_not_null()) + .filter(community_actions::person_id.eq(person_id)); + } + if pending_only { + query = + query.filter(community_actions::follow_state.eq(CommunityFollowerState::ApprovalRequired)); + } + let res = query + .order_by(community_actions::followed.asc()) + .limit(limit) + .offset(offset) + .select(( + person::all_columns, + community::all_columns, + is_new_instance, + CommunityFollower::select_subscribed_type(), + )) + .load::<(Person, Community, bool, SubscribedType)>(conn) + .await?; + Ok( + res + .into_iter() + .map( + |(person, community, is_new_instance, subscribed)| PendingFollow { + person, + community, + is_new_instance, + subscribed, + }, + ) + .collect(), + ) + } + + pub async fn count_approval_required( + pool: &mut DbPool<'_>, + community_id: CommunityId, + ) -> Result { + let conn = &mut get_conn(pool).await?; + action_query(community_actions::followed) + .inner_join(person::table.on(community_actions::person_id.eq(person::id))) + .filter(community_actions::community_id.eq(community_id)) + .filter(community_actions::follow_state.eq(CommunityFollowerState::ApprovalRequired)) + .select(count(community_actions::community_id)) + .first::(conn) + .await + } + pub async fn check_private_community_action( + pool: &mut DbPool<'_>, + from_person_id: PersonId, + community: &Community, + ) -> LemmyResult<()> { + if community.visibility != CommunityVisibility::Private { + return Ok(()); + } + let conn = &mut get_conn(pool).await?; + select(exists( + action_query(community_actions::followed) + .filter(community_actions::community_id.eq(community.id)) + .filter(community_actions::person_id.eq(from_person_id)) + .filter(community_actions::follow_state.eq(CommunityFollowerState::Accepted)), + )) + .get_result::(conn) + .await? + .then_some(()) + .ok_or(LemmyErrorType::NotFound.into()) + } + pub async fn check_has_followers_from_instance( + community_id: CommunityId, + instance_id: InstanceId, + pool: &mut DbPool<'_>, + ) -> Result<(), Error> { + let conn = &mut get_conn(pool).await?; + select(exists( + action_query(community_actions::followed) + .inner_join(person::table.on(community_actions::person_id.eq(person::id))) + .filter(community_actions::community_id.eq(community_id)) + .filter(person::instance_id.eq(instance_id)) + .filter(community_actions::follow_state.eq(CommunityFollowerState::Accepted)), + )) + .get_result::(conn) + .await? + .then_some(()) + .ok_or(diesel::NotFound) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use lemmy_db_schema::{ + source::{ + community::{CommunityFollower, CommunityFollowerForm, CommunityInsertForm}, + instance::Instance, + person::PersonInsertForm, + }, + traits::{Crud, Followable}, + utils::build_db_pool_for_tests, + }; + use serial_test::serial; + + #[tokio::test] + #[serial] + async fn test_has_followers_from_instance() -> LemmyResult<()> { + let pool = &build_db_pool_for_tests(); + let pool = &mut pool.into(); + + // insert local community + let local_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?; + let community_form = CommunityInsertForm::new( + local_instance.id, + "test_community_3".to_string(), + "nada".to_owned(), + "pubkey".to_string(), + ); + let community = Community::create(pool, &community_form).await?; + + // insert remote user + let remote_instance = Instance::read_or_create(pool, "other_domain.tld".to_string()).await?; + let person_form = + PersonInsertForm::new("name".to_string(), "pubkey".to_string(), remote_instance.id); + let person = Person::create(pool, &person_form).await?; + + // community has no follower from remote instance, returns error + let has_followers = CommunityFollowerView::check_has_followers_from_instance( + community.id, + remote_instance.id, + pool, + ) + .await; + assert!(has_followers.is_err()); + + // insert unapproved follower + let mut follower_form = CommunityFollowerForm { + state: Some(CommunityFollowerState::ApprovalRequired), + ..CommunityFollowerForm::new(community.id, person.id) + }; + CommunityFollower::follow(pool, &follower_form).await?; + + // still returns error + let has_followers = CommunityFollowerView::check_has_followers_from_instance( + community.id, + remote_instance.id, + pool, + ) + .await; + assert!(has_followers.is_err()); + + // mark follower as accepted + follower_form.state = Some(CommunityFollowerState::Accepted); + CommunityFollower::follow(pool, &follower_form).await?; + + // now returns ok + let has_followers = CommunityFollowerView::check_has_followers_from_instance( + community.id, + remote_instance.id, + pool, + ) + .await; + assert!(has_followers.is_ok()); + + Instance::delete(pool, local_instance.id).await?; + Instance::delete(pool, remote_instance.id).await?; + Ok(()) + } } diff --git a/crates/db_views_actor/src/community_moderator_view.rs b/crates/db_views_actor/src/community_moderator_view.rs index ebcdcbd25..a9ada92e1 100644 --- a/crates/db_views_actor/src/community_moderator_view.rs +++ b/crates/db_views_actor/src/community_moderator_view.rs @@ -1,14 +1,14 @@ use crate::structs::CommunityModeratorView; -use diesel::{dsl::exists, result::Error, select, ExpressionMethods, QueryDsl}; +use diesel::{dsl::exists, result::Error, select, ExpressionMethods, JoinOnDsl, QueryDsl}; use diesel_async::RunQueryDsl; use lemmy_db_schema::{ impls::local_user::LocalUserOptionHelper, newtypes::{CommunityId, PersonId}, - schema::{community, community_moderator, person}, + schema::{community, community_actions, person}, source::local_user::LocalUser, - utils::{get_conn, DbPool}, + utils::{action_query, find_action, get_conn, DbPool}, }; -use lemmy_utils::{error::LemmyResult, LemmyErrorType}; +use lemmy_utils::error::{LemmyErrorType, LemmyResult}; impl CommunityModeratorView { pub async fn check_is_community_moderator( @@ -16,17 +16,11 @@ impl CommunityModeratorView { find_community_id: CommunityId, find_person_id: PersonId, ) -> LemmyResult<()> { - use lemmy_db_schema::schema::community_moderator::dsl::{ - community_id, - community_moderator, - person_id, - }; let conn = &mut get_conn(pool).await?; - select(exists( - community_moderator - .filter(community_id.eq(find_community_id)) - .filter(person_id.eq(find_person_id)), - )) + select(exists(find_action( + community_actions::became_moderator, + (find_person_id, find_community_id), + ))) .get_result::(conn) .await? .then_some(()) @@ -37,10 +31,10 @@ impl CommunityModeratorView { pool: &mut DbPool<'_>, find_person_id: PersonId, ) -> LemmyResult<()> { - use lemmy_db_schema::schema::community_moderator::dsl::{community_moderator, person_id}; let conn = &mut get_conn(pool).await?; select(exists( - community_moderator.filter(person_id.eq(find_person_id)), + action_query(community_actions::became_moderator) + .filter(community_actions::person_id.eq(find_person_id)), )) .get_result::(conn) .await? @@ -53,12 +47,12 @@ impl CommunityModeratorView { community_id: CommunityId, ) -> Result, Error> { let conn = &mut get_conn(pool).await?; - community_moderator::table + action_query(community_actions::became_moderator) .inner_join(community::table) - .inner_join(person::table) - .filter(community_moderator::community_id.eq(community_id)) + .inner_join(person::table.on(person::id.eq(community_actions::person_id))) + .filter(community_actions::community_id.eq(community_id)) .select((community::all_columns, person::all_columns)) - .order_by(community_moderator::published) + .order_by(community_actions::became_moderator) .load::(conn) .await } @@ -69,10 +63,10 @@ impl CommunityModeratorView { local_user: Option<&LocalUser>, ) -> Result, Error> { let conn = &mut get_conn(pool).await?; - let mut query = community_moderator::table + let mut query = action_query(community_actions::became_moderator) .inner_join(community::table) - .inner_join(person::table) - .filter(community_moderator::person_id.eq(person_id)) + .inner_join(person::table.on(person::id.eq(community_actions::person_id))) + .filter(community_actions::person_id.eq(person_id)) .select((community::all_columns, person::all_columns)) .into_boxed(); @@ -95,16 +89,16 @@ impl CommunityModeratorView { /// Ideally this should be a group by, but diesel doesn't support it yet pub async fn get_community_first_mods(pool: &mut DbPool<'_>) -> Result, Error> { let conn = &mut get_conn(pool).await?; - community_moderator::table + action_query(community_actions::became_moderator) .inner_join(community::table) - .inner_join(person::table) + .inner_join(person::table.on(person::id.eq(community_actions::person_id))) .select((community::all_columns, person::all_columns)) // A hacky workaround instead of group_bys // https://stackoverflow.com/questions/24042359/how-to-join-only-one-row-in-joined-table-with-postgres - .distinct_on(community_moderator::community_id) + .distinct_on(community_actions::community_id) .order_by(( - community_moderator::community_id, - community_moderator::published, + community_actions::community_id, + community_actions::became_moderator, )) .load::(conn) .await diff --git a/crates/db_views_actor/src/community_person_ban_view.rs b/crates/db_views_actor/src/community_person_ban_view.rs index 5543222f3..224ea8d53 100644 --- a/crates/db_views_actor/src/community_person_ban_view.rs +++ b/crates/db_views_actor/src/community_person_ban_view.rs @@ -2,16 +2,14 @@ use crate::structs::CommunityPersonBanView; use diesel::{ dsl::{exists, not}, select, - ExpressionMethods, - QueryDsl, }; use diesel_async::RunQueryDsl; use lemmy_db_schema::{ newtypes::{CommunityId, PersonId}, - schema::community_person_ban, - utils::{get_conn, DbPool}, + schema::community_actions, + utils::{find_action, get_conn, DbPool}, }; -use lemmy_utils::{error::LemmyResult, LemmyErrorType}; +use lemmy_utils::error::{LemmyErrorType, LemmyResult}; impl CommunityPersonBanView { pub async fn check( @@ -20,11 +18,10 @@ impl CommunityPersonBanView { from_community_id: CommunityId, ) -> LemmyResult<()> { let conn = &mut get_conn(pool).await?; - select(not(exists( - community_person_ban::table - .filter(community_person_ban::community_id.eq(from_community_id)) - .filter(community_person_ban::person_id.eq(from_person_id)), - ))) + select(not(exists(find_action( + community_actions::received_ban, + (from_person_id, from_community_id), + )))) .get_result::(conn) .await? .then_some(()) diff --git a/crates/db_views_actor/src/community_view.rs b/crates/db_views_actor/src/community_view.rs index 9ff6fadce..f42340bdb 100644 --- a/crates/db_views_actor/src/community_view.rs +++ b/crates/db_views_actor/src/community_view.rs @@ -4,7 +4,6 @@ use diesel::{ result::Error, BoolExpressionMethods, ExpressionMethods, - JoinOnDsl, NullableExpressionMethods, PgTextExpressionMethods, QueryDsl, @@ -13,16 +12,14 @@ use diesel_async::RunQueryDsl; use lemmy_db_schema::{ impls::local_user::LocalUserOptionHelper, newtypes::{CommunityId, PersonId}, - schema::{ - community, - community_aggregates, - community_block, - community_follower, - community_person_ban, - instance_block, + schema::{community, community_actions, community_aggregates, instance_actions}, + source::{ + community::{CommunityFollower, CommunityFollowerState}, + local_user::LocalUser, + site::Site, }, - source::{community::CommunityFollower, local_user::LocalUser, site::Site}, utils::{ + actions, functions::lower, fuzzy_search, limit_and_offset, @@ -35,54 +32,33 @@ use lemmy_db_schema::{ ListingType, PostSortType, }; -use lemmy_utils::{error::LemmyResult, LemmyErrorType}; +use lemmy_utils::error::{LemmyErrorType, LemmyResult}; fn queries<'a>() -> Queries< impl ReadFn<'a, CommunityView, (CommunityId, Option<&'a LocalUser>, bool)>, impl ListFn<'a, CommunityView, (CommunityQuery<'a>, &'a Site)>, > { let all_joins = |query: community::BoxedQuery<'a, Pg>, my_local_user: Option<&'a LocalUser>| { - // The left join below will return None in this case - let person_id_join = my_local_user.person_id().unwrap_or(PersonId(-1)); - query .inner_join(community_aggregates::table) - .left_join( - community_follower::table.on( - community::id - .eq(community_follower::community_id) - .and(community_follower::person_id.eq(person_id_join)), - ), - ) - .left_join( - instance_block::table.on( - community::instance_id - .eq(instance_block::instance_id) - .and(instance_block::person_id.eq(person_id_join)), - ), - ) - .left_join( - community_block::table.on( - community::id - .eq(community_block::community_id) - .and(community_block::person_id.eq(person_id_join)), - ), - ) - .left_join( - community_person_ban::table.on( - community::id - .eq(community_person_ban::community_id) - .and(community_person_ban::person_id.eq(person_id_join)), - ), - ) + .left_join(actions( + community_actions::table, + my_local_user.person_id(), + community::id, + )) + .left_join(actions( + instance_actions::table, + my_local_user.person_id(), + community::instance_id, + )) }; let selection = ( community::all_columns, CommunityFollower::select_subscribed_type(), - community_block::community_id.nullable().is_not_null(), + community_actions::blocked.nullable().is_not_null(), community_aggregates::all_columns, - community_person_ban::person_id.nullable().is_not_null(), + community_actions::received_ban.nullable().is_not_null(), ); let not_removed_or_deleted = community::removed @@ -114,9 +90,6 @@ fn queries<'a>() -> Queries< let list = move |mut conn: DbConn<'a>, (options, site): (CommunityQuery<'a>, &'a Site)| async move { use CommunitySortType::*; - // The left join below will return None in this case - let person_id_join = options.local_user.person_id().unwrap_or(PersonId(-1)); - let mut query = all_joins(community::table.into_boxed(), options.local_user).select(selection); if let Some(search_term) = options.search_term { @@ -136,7 +109,7 @@ fn queries<'a>() -> Queries< query = query.filter(not_removed_or_deleted).filter( community::hidden .eq(false) - .or(community_follower::person_id.eq(person_id_join)), + .or(community_actions::follow_state.is_not_null()), ); } @@ -163,7 +136,9 @@ fn queries<'a>() -> Queries< if let Some(listing_type) = options.listing_type { query = match listing_type { - ListingType::Subscribed => query.filter(community_follower::pending.is_not_null()), /* TODO could be this: and(community_follower::person_id.eq(person_id_join)), */ + ListingType::Subscribed => { + query.filter(community_actions::follow_state.eq(Some(CommunityFollowerState::Accepted))) + } ListingType::Local => query.filter(community::local.eq(true)), _ => query, }; @@ -171,8 +146,8 @@ fn queries<'a>() -> Queries< // Don't show blocked communities and communities on blocked instances. nsfw communities are // also hidden (based on profile setting) - query = query.filter(instance_block::person_id.is_null()); - query = query.filter(community_block::person_id.is_null()); + query = query.filter(instance_actions::blocked.is_null()); + query = query.filter(community_actions::blocked.is_null()); if !(options.local_user.show_nsfw(site) || options.show_nsfw) { query = query.filter(community::nsfw.eq(false)); } @@ -293,44 +268,52 @@ mod tests { }; use lemmy_db_schema::{ source::{ - community::{Community, CommunityInsertForm, CommunityUpdateForm}, + community::{ + Community, + CommunityFollower, + CommunityFollowerForm, + CommunityFollowerState, + CommunityInsertForm, + CommunityUpdateForm, + }, instance::Instance, local_user::{LocalUser, LocalUserInsertForm}, person::{Person, PersonInsertForm}, site::Site, }, - traits::Crud, + traits::{Crud, Followable}, utils::{build_db_pool_for_tests, DbPool}, CommunityVisibility, + SubscribedType, }; use lemmy_utils::error::LemmyResult; use serial_test::serial; use url::Url; struct Data { - inserted_instance: Instance, + instance: Instance, local_user: LocalUser, - inserted_communities: [Community; 3], + communities: [Community; 3], site: Site, } async fn init_data(pool: &mut DbPool<'_>) -> LemmyResult { - let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?; + let instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?; let person_name = "tegan".to_string(); - let new_person = PersonInsertForm::test_form(inserted_instance.id, &person_name); + let new_person = PersonInsertForm::test_form(instance.id, &person_name); let inserted_person = Person::create(pool, &new_person).await?; let local_user_form = LocalUserInsertForm::test_form(inserted_person.id); let local_user = LocalUser::create(pool, &local_user_form, vec![]).await?; - let inserted_communities = [ + let communities = [ Community::create( pool, &CommunityInsertForm::new( - inserted_instance.id, + instance.id, "test_community_1".to_string(), "nada1".to_owned(), "pubkey".to_string(), @@ -340,7 +323,7 @@ mod tests { Community::create( pool, &CommunityInsertForm::new( - inserted_instance.id, + instance.id, "test_community_2".to_string(), "nada2".to_owned(), "pubkey".to_string(), @@ -350,7 +333,7 @@ mod tests { Community::create( pool, &CommunityInsertForm::new( - inserted_instance.id, + instance.id, "test_community_3".to_string(), "nada3".to_owned(), "pubkey".to_string(), @@ -379,33 +362,93 @@ mod tests { }; Ok(Data { - inserted_instance, + instance, local_user, - inserted_communities, + communities, site, }) } async fn cleanup(data: Data, pool: &mut DbPool<'_>) -> LemmyResult<()> { - for Community { id, .. } in data.inserted_communities { + for Community { id, .. } in data.communities { Community::delete(pool, id).await?; } Person::delete(pool, data.local_user.person_id).await?; - Instance::delete(pool, data.inserted_instance.id).await?; + Instance::delete(pool, data.instance.id).await?; Ok(()) } + #[tokio::test] + #[serial] + async fn subscribe_state() -> LemmyResult<()> { + let pool = &build_db_pool_for_tests(); + let pool = &mut pool.into(); + let data = init_data(pool).await?; + let community = &data.communities[0]; + + let unauthenticated = CommunityView::read(pool, community.id, None, false).await?; + assert_eq!(SubscribedType::NotSubscribed, unauthenticated.subscribed); + + let authenticated = + CommunityView::read(pool, community.id, Some(&data.local_user), false).await?; + assert_eq!(SubscribedType::NotSubscribed, authenticated.subscribed); + + let form = CommunityFollowerForm { + state: Some(CommunityFollowerState::Pending), + ..CommunityFollowerForm::new(community.id, data.local_user.person_id) + }; + CommunityFollower::follow(pool, &form).await?; + + let with_pending_follow = + CommunityView::read(pool, community.id, Some(&data.local_user), false).await?; + assert_eq!(SubscribedType::Pending, with_pending_follow.subscribed); + + // mark community private and set follow as approval required + Community::update( + pool, + community.id, + &CommunityUpdateForm { + visibility: Some(CommunityVisibility::Private), + ..Default::default() + }, + ) + .await?; + let form = CommunityFollowerForm { + state: Some(CommunityFollowerState::ApprovalRequired), + ..CommunityFollowerForm::new(community.id, data.local_user.person_id) + }; + CommunityFollower::follow(pool, &form).await?; + + let with_approval_required_follow = + CommunityView::read(pool, community.id, Some(&data.local_user), false).await?; + assert_eq!( + SubscribedType::ApprovalRequired, + with_approval_required_follow.subscribed + ); + + let form = CommunityFollowerForm { + state: Some(CommunityFollowerState::Accepted), + ..CommunityFollowerForm::new(community.id, data.local_user.person_id) + }; + CommunityFollower::follow(pool, &form).await?; + let with_accepted_follow = + CommunityView::read(pool, community.id, Some(&data.local_user), false).await?; + assert_eq!(SubscribedType::Subscribed, with_accepted_follow.subscribed); + + cleanup(data, pool).await + } + #[tokio::test] #[serial] async fn local_only_community() -> LemmyResult<()> { - let pool = &build_db_pool_for_tests().await; + let pool = &build_db_pool_for_tests(); let pool = &mut pool.into(); let data = init_data(pool).await?; Community::update( pool, - data.inserted_communities[0].id, + data.communities[0].id, &CommunityUpdateForm { visibility: Some(CommunityVisibility::LocalOnly), ..Default::default() @@ -418,10 +461,7 @@ mod tests { } .list(&data.site, pool) .await?; - assert_eq!( - data.inserted_communities.len() - 1, - unauthenticated_query.len() - ); + assert_eq!(data.communities.len() - 1, unauthenticated_query.len()); let authenticated_query = CommunityQuery { local_user: Some(&data.local_user), @@ -429,19 +469,14 @@ mod tests { } .list(&data.site, pool) .await?; - assert_eq!(data.inserted_communities.len(), authenticated_query.len()); + assert_eq!(data.communities.len(), authenticated_query.len()); let unauthenticated_community = - CommunityView::read(pool, data.inserted_communities[0].id, None, false).await; + CommunityView::read(pool, data.communities[0].id, None, false).await; assert!(unauthenticated_community.is_err()); - let authenticated_community = CommunityView::read( - pool, - data.inserted_communities[0].id, - Some(&data.local_user), - false, - ) - .await; + let authenticated_community = + CommunityView::read(pool, data.communities[0].id, Some(&data.local_user), false).await; assert!(authenticated_community.is_ok()); cleanup(data, pool).await @@ -450,7 +485,7 @@ mod tests { #[tokio::test] #[serial] async fn community_sort_name() -> LemmyResult<()> { - let pool = &build_db_pool_for_tests().await; + let pool = &build_db_pool_for_tests(); let pool = &mut pool.into(); let data = init_data(pool).await?; diff --git a/crates/db_views_actor/src/person_mention_view.rs b/crates/db_views_actor/src/person_mention_view.rs index 2478c0183..08be67a82 100644 --- a/crates/db_views_actor/src/person_mention_view.rs +++ b/crates/db_views_actor/src/person_mention_view.rs @@ -3,36 +3,40 @@ use diesel::{ dsl::{exists, not}, pg::Pg, result::Error, - sql_types, BoolExpressionMethods, - BoxableExpression, ExpressionMethods, - IntoSql, JoinOnDsl, NullableExpressionMethods, QueryDsl, }; use diesel_async::RunQueryDsl; use lemmy_db_schema::{ - aliases, + aliases::{self, creator_community_actions}, newtypes::{PersonId, PersonMentionId}, schema::{ comment, + comment_actions, comment_aggregates, - comment_like, - comment_saved, community, - community_follower, - community_moderator, - community_person_ban, + community_actions, local_user, person, - person_block, + person_actions, person_mention, post, }, - source::local_user::LocalUser, - utils::{get_conn, limit_and_offset, DbConn, DbPool, ListFn, Queries, ReadFn}, + source::{community::CommunityFollower, local_user::LocalUser}, + utils::{ + actions, + actions_alias, + get_conn, + limit_and_offset, + DbConn, + DbPool, + ListFn, + Queries, + ReadFn, + }, CommentSortType, }; @@ -40,74 +44,6 @@ fn queries<'a>() -> Queries< impl ReadFn<'a, PersonMentionView, (PersonMentionId, Option)>, impl ListFn<'a, PersonMentionView, PersonMentionQuery>, > { - let is_creator_banned_from_community = exists( - community_person_ban::table.filter( - community::id - .eq(community_person_ban::community_id) - .and(community_person_ban::person_id.eq(comment::creator_id)), - ), - ); - - let is_local_user_banned_from_community = |person_id| { - exists( - community_person_ban::table.filter( - community::id - .eq(community_person_ban::community_id) - .and(community_person_ban::person_id.eq(person_id)), - ), - ) - }; - - let is_saved = |person_id| { - exists( - comment_saved::table.filter( - comment::id - .eq(comment_saved::comment_id) - .and(comment_saved::person_id.eq(person_id)), - ), - ) - }; - - let is_community_followed = |person_id| { - community_follower::table - .filter( - post::community_id - .eq(community_follower::community_id) - .and(community_follower::person_id.eq(person_id)), - ) - .select(community_follower::pending.nullable()) - .single_value() - }; - - let is_creator_blocked = |person_id| { - exists( - person_block::table.filter( - comment::creator_id - .eq(person_block::target_id) - .and(person_block::person_id.eq(person_id)), - ), - ) - }; - - let score = |person_id| { - comment_like::table - .filter( - comment::id - .eq(comment_like::comment_id) - .and(comment_like::person_id.eq(person_id)), - ) - .select(comment_like::score.nullable()) - .single_value() - }; - - let creator_is_moderator = exists( - community_moderator::table.filter( - community::id - .eq(community_moderator::community_id) - .and(community_moderator::person_id.eq(comment::creator_id)), - ), - ); - let creator_is_admin = exists( local_user::table.filter( comment::creator_id @@ -118,43 +54,6 @@ fn queries<'a>() -> Queries< let all_joins = move |query: person_mention::BoxedQuery<'a, Pg>, my_person_id: Option| { - let is_local_user_banned_from_community_selection: Box< - dyn BoxableExpression<_, Pg, SqlType = sql_types::Bool>, - > = if let Some(person_id) = my_person_id { - Box::new(is_local_user_banned_from_community(person_id)) - } else { - Box::new(false.into_sql::()) - }; - let score_selection: Box< - dyn BoxableExpression<_, Pg, SqlType = sql_types::Nullable>, - > = if let Some(person_id) = my_person_id { - Box::new(score(person_id)) - } else { - Box::new(None::.into_sql::>()) - }; - - let subscribed_type_selection: Box< - dyn BoxableExpression<_, Pg, SqlType = sql_types::Nullable>, - > = if let Some(person_id) = my_person_id { - Box::new(is_community_followed(person_id)) - } else { - Box::new(None::.into_sql::>()) - }; - - let is_saved_selection: Box> = - if let Some(person_id) = my_person_id { - Box::new(is_saved(person_id)) - } else { - Box::new(false.into_sql::()) - }; - - let is_creator_blocked_selection: Box> = - if let Some(person_id) = my_person_id { - Box::new(is_creator_blocked(person_id)) - } else { - Box::new(false.into_sql::()) - }; - query .inner_join(comment::table) .inner_join(person::table.on(comment::creator_id.eq(person::id))) @@ -162,6 +61,22 @@ fn queries<'a>() -> Queries< .inner_join(community::table.on(post::community_id.eq(community::id))) .inner_join(aliases::person1) .inner_join(comment_aggregates::table.on(comment::id.eq(comment_aggregates::comment_id))) + .left_join(actions( + community_actions::table, + my_person_id, + post::community_id, + )) + .left_join(actions(comment_actions::table, my_person_id, comment::id)) + .left_join(actions( + person_actions::table, + my_person_id, + comment::creator_id, + )) + .left_join(actions_alias( + creator_community_actions, + comment::creator_id, + post::community_id, + )) .select(( person_mention::all_columns, comment::all_columns, @@ -170,14 +85,20 @@ fn queries<'a>() -> Queries< community::all_columns, aliases::person1.fields(person::all_columns), comment_aggregates::all_columns, - is_creator_banned_from_community, - is_local_user_banned_from_community_selection, - creator_is_moderator, + creator_community_actions + .field(community_actions::received_ban) + .nullable() + .is_not_null(), + community_actions::received_ban.nullable().is_not_null(), + creator_community_actions + .field(community_actions::became_moderator) + .nullable() + .is_not_null(), creator_is_admin, - subscribed_type_selection, - is_saved_selection, - is_creator_blocked_selection, - score_selection, + CommunityFollower::select_subscribed_type(), + comment_actions::saved.nullable().is_not_null(), + person_actions::blocked.nullable().is_not_null(), + comment_actions::like_score.nullable(), )) }; @@ -220,9 +141,7 @@ fn queries<'a>() -> Queries< }; // Don't show mentions from blocked persons - if let Some(my_person_id) = options.my_person_id { - query = query.filter(not(is_creator_blocked(my_person_id))); - } + query = query.filter(person_actions::blocked.is_null()); let (limit, offset) = limit_and_offset(options.page, options.limit)?; @@ -257,13 +176,11 @@ impl PersonMentionView { let mut query = person_mention::table .inner_join(comment::table) - .left_join( - person_block::table.on( - comment::creator_id - .eq(person_block::target_id) - .and(person_block::person_id.eq(local_user.person_id)), - ), - ) + .left_join(actions( + person_actions::table, + Some(local_user.person_id), + comment::creator_id, + )) .inner_join(person::table.on(comment::creator_id.eq(person::id))) .into_boxed(); @@ -274,7 +191,7 @@ impl PersonMentionView { query // Don't count replies from blocked users - .filter(person_block::person_id.is_null()) + .filter(person_actions::blocked.is_null()) .filter(person_mention::recipient_id.eq(local_user.person_id)) .filter(person_mention::read.eq(false)) .filter(comment::deleted.eq(false)) @@ -328,7 +245,7 @@ mod tests { #[tokio::test] #[serial] async fn test_crud() -> LemmyResult<()> { - let pool = &build_db_pool_for_tests().await; + let pool = &build_db_pool_for_tests(); let pool = &mut pool.into(); let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?; diff --git a/crates/db_views_actor/src/person_view.rs b/crates/db_views_actor/src/person_view.rs index 724a700ad..39d1ac27c 100644 --- a/crates/db_views_actor/src/person_view.rs +++ b/crates/db_views_actor/src/person_view.rs @@ -229,7 +229,7 @@ mod tests { #[tokio::test] #[serial] async fn exclude_deleted() -> LemmyResult<()> { - let pool = &build_db_pool_for_tests().await; + let pool = &build_db_pool_for_tests(); let pool = &mut pool.into(); let data = init_data(pool).await?; @@ -261,7 +261,7 @@ mod tests { #[tokio::test] #[serial] async fn list_banned() -> LemmyResult<()> { - let pool = &build_db_pool_for_tests().await; + let pool = &build_db_pool_for_tests(); let pool = &mut pool.into(); let data = init_data(pool).await?; @@ -285,7 +285,7 @@ mod tests { #[tokio::test] #[serial] async fn list_admins() -> LemmyResult<()> { - let pool = &build_db_pool_for_tests().await; + let pool = &build_db_pool_for_tests(); let pool = &mut pool.into(); let data = init_data(pool).await?; @@ -315,7 +315,7 @@ mod tests { #[tokio::test] #[serial] async fn listing_type() -> LemmyResult<()> { - let pool = &build_db_pool_for_tests().await; + let pool = &build_db_pool_for_tests(); let pool = &mut pool.into(); let data = init_data(pool).await?; diff --git a/crates/db_views_actor/src/structs.rs b/crates/db_views_actor/src/structs.rs index ecf9ba11d..6b609a753 100644 --- a/crates/db_views_actor/src/structs.rs +++ b/crates/db_views_actor/src/structs.rs @@ -109,6 +109,7 @@ pub struct PersonMentionView { pub subscribed: SubscribedType, pub saved: bool, pub creator_blocked: bool, + #[cfg_attr(feature = "full", ts(optional))] pub my_vote: Option, } @@ -133,6 +134,7 @@ pub struct CommentReplyView { pub subscribed: SubscribedType, pub saved: bool, pub creator_blocked: bool, + #[cfg_attr(feature = "full", ts(optional))] pub my_vote: Option, } @@ -146,3 +148,14 @@ pub struct PersonView { pub counts: PersonAggregates, pub is_admin: bool, } + +#[derive(Debug, Serialize, Deserialize, Clone)] +#[cfg_attr(feature = "full", derive(TS, Queryable))] +#[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] +#[cfg_attr(feature = "full", ts(export))] +pub struct PendingFollow { + pub person: Person, + pub community: Community, + pub is_new_instance: bool, + pub subscribed: SubscribedType, +} diff --git a/crates/db_views_moderator/src/structs.rs b/crates/db_views_moderator/src/structs.rs index 10ad78942..27ee82522 100644 --- a/crates/db_views_moderator/src/structs.rs +++ b/crates/db_views_moderator/src/structs.rs @@ -39,6 +39,7 @@ use ts_rs::TS; /// When someone is added as a community moderator. pub struct ModAddCommunityView { pub mod_add_community: ModAddCommunity, + #[cfg_attr(feature = "full", ts(optional))] pub moderator: Option, pub community: Community, pub modded_person: Person, @@ -52,6 +53,7 @@ pub struct ModAddCommunityView { /// When someone is added as a site moderator. pub struct ModAddView { pub mod_add: ModAdd, + #[cfg_attr(feature = "full", ts(optional))] pub moderator: Option, pub modded_person: Person, } @@ -64,6 +66,7 @@ pub struct ModAddView { /// When someone is banned from a community. pub struct ModBanFromCommunityView { pub mod_ban_from_community: ModBanFromCommunity, + #[cfg_attr(feature = "full", ts(optional))] pub moderator: Option, pub community: Community, pub banned_person: Person, @@ -77,6 +80,7 @@ pub struct ModBanFromCommunityView { /// When someone is banned from the site. pub struct ModBanView { pub mod_ban: ModBan, + #[cfg_attr(feature = "full", ts(optional))] pub moderator: Option, pub banned_person: Person, } @@ -89,6 +93,7 @@ pub struct ModBanView { /// When a community is hidden from public view. pub struct ModHideCommunityView { pub mod_hide_community: ModHideCommunity, + #[cfg_attr(feature = "full", ts(optional))] pub admin: Option, pub community: Community, } @@ -101,6 +106,7 @@ pub struct ModHideCommunityView { /// When a moderator locks a post (prevents new comments being made). pub struct ModLockPostView { pub mod_lock_post: ModLockPost, + #[cfg_attr(feature = "full", ts(optional))] pub moderator: Option, pub post: Post, pub community: Community, @@ -114,6 +120,7 @@ pub struct ModLockPostView { /// When a moderator removes a comment. pub struct ModRemoveCommentView { pub mod_remove_comment: ModRemoveComment, + #[cfg_attr(feature = "full", ts(optional))] pub moderator: Option, pub comment: Comment, pub commenter: Person, @@ -129,6 +136,7 @@ pub struct ModRemoveCommentView { /// When a moderator removes a community. pub struct ModRemoveCommunityView { pub mod_remove_community: ModRemoveCommunity, + #[cfg_attr(feature = "full", ts(optional))] pub moderator: Option, pub community: Community, } @@ -141,6 +149,7 @@ pub struct ModRemoveCommunityView { /// When a moderator removes a post. pub struct ModRemovePostView { pub mod_remove_post: ModRemovePost, + #[cfg_attr(feature = "full", ts(optional))] pub moderator: Option, pub post: Post, pub community: Community, @@ -154,6 +163,7 @@ pub struct ModRemovePostView { /// When a moderator features a post on a community (pins it to the top). pub struct ModFeaturePostView { pub mod_feature_post: ModFeaturePost, + #[cfg_attr(feature = "full", ts(optional))] pub moderator: Option, pub post: Post, pub community: Community, @@ -167,6 +177,7 @@ pub struct ModFeaturePostView { /// When a moderator transfers a community to a new owner. pub struct ModTransferCommunityView { pub mod_transfer_community: ModTransferCommunity, + #[cfg_attr(feature = "full", ts(optional))] pub moderator: Option, pub community: Community, pub modded_person: Person, @@ -180,6 +191,7 @@ pub struct ModTransferCommunityView { /// When an admin purges a comment. pub struct AdminPurgeCommentView { pub admin_purge_comment: AdminPurgeComment, + #[cfg_attr(feature = "full", ts(optional))] pub admin: Option, pub post: Post, } @@ -192,6 +204,7 @@ pub struct AdminPurgeCommentView { /// When an admin purges a community. pub struct AdminPurgeCommunityView { pub admin_purge_community: AdminPurgeCommunity, + #[cfg_attr(feature = "full", ts(optional))] pub admin: Option, } @@ -203,6 +216,7 @@ pub struct AdminPurgeCommunityView { /// When an admin purges a person. pub struct AdminPurgePersonView { pub admin_purge_person: AdminPurgePerson, + #[cfg_attr(feature = "full", ts(optional))] pub admin: Option, } @@ -214,6 +228,7 @@ pub struct AdminPurgePersonView { /// When an admin purges a post. pub struct AdminPurgePostView { pub admin_purge_post: AdminPurgePost, + #[cfg_attr(feature = "full", ts(optional))] pub admin: Option, pub community: Community, } @@ -225,12 +240,19 @@ pub struct AdminPurgePostView { #[cfg_attr(feature = "full", ts(export))] /// Querying / filtering the modlog. pub struct ModlogListParams { + #[cfg_attr(feature = "full", ts(optional))] pub community_id: Option, + #[cfg_attr(feature = "full", ts(optional))] pub mod_person_id: Option, + #[cfg_attr(feature = "full", ts(optional))] pub other_person_id: Option, + #[cfg_attr(feature = "full", ts(optional))] pub post_id: Option, + #[cfg_attr(feature = "full", ts(optional))] pub comment_id: Option, + #[cfg_attr(feature = "full", ts(optional))] pub page: Option, + #[cfg_attr(feature = "full", ts(optional))] pub limit: Option, pub hide_modlog_names: bool, } diff --git a/crates/federate/src/worker.rs b/crates/federate/src/worker.rs index 0d4ad04d5..b0254ba0b 100644 --- a/crates/federate/src/worker.rs +++ b/crates/federate/src/worker.rs @@ -449,7 +449,7 @@ mod test { protocol::context::WithContext, }; use actix_web::{dev::ServerHandle, web, App, HttpResponse, HttpServer}; - use lemmy_api_common::utils::{generate_inbox_url, generate_shared_inbox_url}; + use lemmy_api_common::utils::generate_inbox_url; use lemmy_db_schema::{ newtypes::DbUrl, source::{ @@ -491,8 +491,7 @@ mod test { let person_form = PersonInsertForm { actor_id: Some(actor_id.clone()), private_key: (Some(actor_keypair.private_key)), - inbox_url: Some(generate_inbox_url(&actor_id)?), - shared_inbox_url: Some(generate_shared_inbox_url(context.settings())?), + inbox_url: Some(generate_inbox_url()?), ..PersonInsertForm::new("alice".to_string(), actor_keypair.public_key, instance.id) }; let person = Person::create(&mut context.pool(), &person_form).await?; diff --git a/crates/utils/Cargo.toml b/crates/utils/Cargo.toml index f0369f7e9..c22f863c1 100644 --- a/crates/utils/Cargo.toml +++ b/crates/utils/Cargo.toml @@ -49,6 +49,9 @@ full = [ "dep:markdown-it", ] +[package.metadata.cargo-shear] +ignored = ["http"] + [dependencies] regex = { workspace = true, optional = true } tracing = { workspace = true, optional = true } @@ -82,9 +85,12 @@ ts-rs = { workspace = true, optional = true } enum-map = { workspace = true, optional = true } cfg-if = "1" clearurls = { version = "0.0.4", features = ["linkify"] } +markdown-it-block-spoiler = "1.0.0" +markdown-it-sub = "1.0.0" +markdown-it-sup = "1.0.0" +markdown-it-ruby = "1.0.0" [dev-dependencies] -reqwest = { workspace = true } pretty_assertions = { workspace = true } [build-dependencies] diff --git a/crates/utils/src/error.rs b/crates/utils/src/error.rs index b0add40e7..d52df2f72 100644 --- a/crates/utils/src/error.rs +++ b/crates/utils/src/error.rs @@ -23,30 +23,24 @@ pub enum LemmyErrorType { CouldntUpdateComment, CouldntUpdatePrivateMessage, CannotLeaveAdmin, - NoLinesInHtml, - SiteMetadataPageIsNotDoctypeHtml, + // TODO: also remove the translations of unused errors PictrsResponseError(String), PictrsPurgeResponseError(String), - PictrsCachingDisabled, ImageUrlMissingPathSegments, ImageUrlMissingLastPathSegment, PictrsApiKeyNotProvided, NoContentTypeHeader, NotAnImageType, NotAModOrAdmin, - NoAdmins, - NotTopAdmin, NotTopMod, NotLoggedIn, NotHigherMod, NotHigherAdmin, SiteBan, Deleted, - BannedFromCommunity, PersonIsBlocked, CommunityIsBlocked, InstanceIsBlocked, - VoteNotAllowed, InstanceIsPrivate, /// Password must be between 10 and 60 characters InvalidPassword, @@ -61,7 +55,6 @@ pub enum LemmyErrorType { OnlyAdminsCanCreateCommunities, CommunityAlreadyExists, LanguageNotAllowed, - OnlyModsCanPostInCommunity, CouldntUpdatePost, NoPostEditAllowed, EditPrivateMessageNotAllowed, @@ -73,23 +66,10 @@ pub enum LemmyErrorType { RegistrationUsernameRequired, EmailAlreadyExists, UsernameAlreadyExists, - FederationForbiddenByStrictAllowList, PersonIsBannedFromCommunity, - ObjectIsNotPublic, - InvalidCommunity, - CannotCreatePostOrCommentInDeletedOrRemovedCommunity, - CannotReceivePage, - NewPostCannotBeLocked, - OnlyLocalAdminCanRemoveCommunity, - OnlyLocalAdminCanRestoreCommunity, NoIdGiven, IncorrectLogin, - InvalidQuery, ObjectNotLocal, - PostIsLocked, - PersonIsBannedFromSite(String), - InvalidVoteValue, - PageDoesNotSpecifyCreator, NoEmailSetup, LocalSiteNotSetup, EmailSmtpServerNeedsAPort, @@ -126,7 +106,6 @@ pub enum LemmyErrorType { CouldntUpdateCommunity, CouldntUpdateReplies, CouldntUpdatePersonMentions, - PostTitleTooLong, CouldntCreatePost, CouldntCreatePrivateMessage, CouldntUpdatePrivate, @@ -140,11 +119,10 @@ pub enum LemmyErrorType { InvalidUrl, EmailSendFailed, Slurs, - RegistrationDenied(Option), - FederationDisabled, - DomainBlocked(String), - DomainNotInAllowList(String), - FederationDisabledByStrictAllowList, + RegistrationDenied { + #[cfg_attr(feature = "full", ts(optional))] + reason: Option, + }, SiteNameRequired, SiteNameLengthOverflow, PermissiveRegex, @@ -158,23 +136,55 @@ pub enum LemmyErrorType { /// Thrown when an API call is submitted with more than 1000 array elements, see /// [[MAX_API_PARAM_ELEMENTS]] TooManyItems, - CommunityHasNoFollowers, BanExpirationInPast, InvalidUnixTime, InvalidBotAction, CantBlockLocalInstance, - UrlWithoutDomain, - InboxTimeout, + Unknown(String), + UrlLengthOverflow, OauthAuthorizationInvalid, OauthLoginFailed, OauthRegistrationClosed, CouldntDeleteOauthProvider, - Unknown(String), - CantDeleteSite, - UrlLengthOverflow, + NotFound, + CommunityHasNoFollowers, PostScheduleTimeMustBeInFuture, TooManyScheduledPosts, - NotFound, + FederationError { + #[cfg_attr(feature = "full", ts(optional))] + error: Option, + }, +} + +/// Federation related errors, these dont need to be translated. +#[derive(Display, Debug, Serialize, Deserialize, Clone, PartialEq, Eq, EnumIter, Hash)] +#[cfg_attr(feature = "full", derive(ts_rs::TS))] +#[cfg_attr(feature = "full", ts(export))] +#[non_exhaustive] +pub enum FederationError { + // TODO: merge into a single NotFound error + CouldntFindActivity, + InvalidCommunity, + CannotCreatePostOrCommentInDeletedOrRemovedCommunity, + CannotReceivePage, + OnlyLocalAdminCanRemoveCommunity, + OnlyLocalAdminCanRestoreCommunity, + PostIsLocked, + PersonIsBannedFromSite(String), + InvalidVoteValue, + PageDoesNotSpecifyCreator, + CouldntGetComments, + CouldntGetPosts, + FederationDisabled, + DomainBlocked(String), + DomainNotInAllowList(String), + FederationDisabledByStrictAllowList, + ContradictingFilters, + UrlWithoutDomain, + InboxTimeout, + CantDeleteSite, + ObjectIsNotPublic, + ObjectIsNotPrivate, } cfg_if! { @@ -255,6 +265,17 @@ cfg_if! { } } + impl From for LemmyError { + fn from(error_type: FederationError) -> Self { + let inner = anyhow::anyhow!("{}", error_type); + LemmyError { + error_type: LemmyErrorType::FederationError { error: Some(error_type) }, + inner, + context: Backtrace::capture(), + } + } + } + pub trait LemmyErrorExt> { fn with_lemmy_type(self, error_type: LemmyErrorType) -> LemmyResult; } @@ -306,12 +327,12 @@ cfg_if! { #[test] fn deserializes_with_message() -> LemmyResult<()> { - let reg_banned = LemmyErrorType::PersonIsBannedFromSite(String::from("reason")); + let reg_banned = LemmyErrorType::PictrsResponseError(String::from("reason")); let err = LemmyError::from(reg_banned).error_response(); let json = String::from_utf8(err.into_body().try_into_bytes().unwrap_or_default().to_vec())?; assert_eq!( &json, - "{\"error\":\"person_is_banned_from_site\",\"message\":\"reason\"}" + "{\"error\":\"pictrs_response_error\",\"message\":\"reason\"}" ); Ok(()) diff --git a/crates/utils/src/lib.rs b/crates/utils/src/lib.rs index 7f0691496..1e0cbefbf 100644 --- a/crates/utils/src/lib.rs +++ b/crates/utils/src/lib.rs @@ -13,7 +13,6 @@ cfg_if! { } pub mod error; -pub use error::LemmyErrorType; use std::time::Duration; pub type ConnectionId = usize; diff --git a/crates/utils/src/settings/structs.rs b/crates/utils/src/settings/structs.rs index 8c28d908a..c95f66644 100644 --- a/crates/utils/src/settings/structs.rs +++ b/crates/utils/src/settings/structs.rs @@ -92,7 +92,7 @@ pub struct PictrsConfig { pub upload_timeout: u64, /// Resize post thumbnails to this maximum width/height. - #[default(256)] + #[default(512)] pub max_thumbnail_size: u32, } diff --git a/crates/utils/src/utils/markdown/image_links.rs b/crates/utils/src/utils/markdown/image_links.rs index a21bb6f41..7456190e4 100644 --- a/crates/utils/src/utils/markdown/image_links.rs +++ b/crates/utils/src/utils/markdown/image_links.rs @@ -42,13 +42,10 @@ pub fn markdown_rewrite_image_links(mut src: String) -> (String, Vec) { pub fn markdown_handle_title(src: &str, start: usize, end: usize) -> (&str, Option<&str>) { let content = src.get(start..end).unwrap_or_default(); // necessary for custom emojis which look like `![name](url "title")` - let (url, extra) = if content.contains(' ') { - let split = content.split_once(' ').expect("split is valid"); - (split.0, Some(split.1)) - } else { - (content, None) - }; - (url, extra) + match content.split_once(' ') { + Some((a, b)) => (a, Some(b)), + _ => (content, None), + } } pub fn markdown_find_links(src: &str) -> Vec<(usize, usize)> { @@ -61,9 +58,9 @@ fn find_urls(src: &str) -> Vec<(usize, usize)> { let mut links_offsets = vec![]; ast.walk(|node, _depth| { if let Some(image) = node.cast::() { - let node_offsets = node.srcmap.expect("srcmap is none").get_byte_offsets(); - let start_offset = node_offsets.1 - image.url_len() - 1 - image.title_len(); - let end_offset = node_offsets.1 - 1; + let (_, node_offset) = node.srcmap.expect("srcmap is none").get_byte_offsets(); + let start_offset = node_offset - image.url_len() - 1 - image.title_len(); + let end_offset = node_offset - 1; links_offsets.push((start_offset, end_offset)); } diff --git a/crates/utils/src/utils/markdown/mod.rs b/crates/utils/src/utils/markdown/mod.rs index a6de096e3..9d34e8a69 100644 --- a/crates/utils/src/utils/markdown/mod.rs +++ b/crates/utils/src/utils/markdown/mod.rs @@ -1,17 +1,19 @@ -use crate::{error::LemmyResult, LemmyErrorType}; +use crate::error::{LemmyErrorType, LemmyResult}; use markdown_it::MarkdownIt; use regex::RegexSet; use std::sync::LazyLock; pub mod image_links; mod link_rule; -mod spoiler_rule; static MARKDOWN_PARSER: LazyLock = LazyLock::new(|| { let mut parser = MarkdownIt::new(); markdown_it::plugins::cmark::add(&mut parser); markdown_it::plugins::extra::add(&mut parser); - spoiler_rule::add(&mut parser); + markdown_it_block_spoiler::add(&mut parser); + markdown_it_sub::add(&mut parser); + markdown_it_sup::add(&mut parser); + markdown_it_ruby::add(&mut parser); link_rule::add(&mut parser); parser @@ -102,12 +104,22 @@ mod tests { ( "basic spoiler", "::: spoiler click to see more\nhow spicy!\n:::\n", - "
click to see more

how spicy!\n

\n" + "
click to see morehow spicy!\n
\n" ), ( "escape html special chars", " hello &\"", "

<script>alert(‘xss’);</script> hello &"

\n" + ),("subscript","log~2~(a)","

log2(a)

\n"), + ( + "superscript", + "Markdown^TM^", + "

MarkdownTM

\n" + ), + ( + "ruby text", + "{漢|Kan}{字|ji}", + "

(Kan)(ji)

\n" ) ]; diff --git a/crates/utils/src/utils/markdown/spoiler_rule.rs b/crates/utils/src/utils/markdown/spoiler_rule.rs deleted file mode 100644 index fd6450b31..000000000 --- a/crates/utils/src/utils/markdown/spoiler_rule.rs +++ /dev/null @@ -1,202 +0,0 @@ -// Custom Markdown plugin to manage spoilers. -// -// Matches the capability described in Lemmy UI: -// https://github.com/LemmyNet/lemmy-ui/blob/main/src/shared/utils.ts#L159 -// that is based off of: -// https://github.com/markdown-it/markdown-it-container/tree/master#example -// -// FORMAT: -// Input Markdown: ::: spoiler VISIBLE_TEXT\nHIDDEN_SPOILER\n:::\n -// Output HTML:
VISIBLE_TEXT

nHIDDEN_SPOILER

-// -// Anatomy of a spoiler: -// keyword -// ^ -// ::: spoiler VISIBLE_HINT -// ^ ^ -// begin fence visible text -// -// HIDDEN_SPOILER -// ^ -// hidden text -// -// ::: -// ^ -// end fence - -use markdown_it::{ - parser::{ - block::{BlockRule, BlockState}, - inline::InlineRoot, - }, - MarkdownIt, - Node, - NodeValue, - Renderer, -}; -use regex::Regex; -use std::sync::LazyLock; - -#[derive(Debug)] -struct SpoilerBlock { - visible_text: String, -} - -const SPOILER_PREFIX: &str = "::: spoiler "; -const SPOILER_SUFFIX: &str = ":::"; -const SPOILER_SUFFIX_NEWLINE: &str = ":::\n"; - -static SPOILER_REGEX: LazyLock = - LazyLock::new(|| Regex::new(r"^::: spoiler .*$").expect("compile spoiler markdown regex.")); - -impl NodeValue for SpoilerBlock { - // Formats any node marked as a 'SpoilerBlock' into HTML. - // See the SpoilerBlockScanner#run implementation to see how these nodes get added to the tree. - fn render(&self, node: &Node, fmt: &mut dyn Renderer) { - fmt.cr(); - fmt.open("details", &node.attrs); - fmt.open("summary", &[]); - // Not allowing special styling to the visible text to keep it simple. - // If allowed, would need to parse the child nodes to assign to visible vs hidden text sections. - fmt.text(&self.visible_text); - fmt.close("summary"); - fmt.open("p", &[]); - fmt.contents(&node.children); - fmt.close("p"); - fmt.close("details"); - fmt.cr(); - } -} - -struct SpoilerBlockScanner; - -impl BlockRule for SpoilerBlockScanner { - // Invoked on every line in the provided Markdown text to check if the BlockRule applies. - // - // NOTE: This does NOT support nested spoilers at this time. - fn run(state: &mut BlockState) -> Option<(Node, usize)> { - let first_line: &str = state.get_line(state.line).trim(); - - // 1. Check if the first line contains the spoiler syntax... - if !SPOILER_REGEX.is_match(first_line) { - return None; - } - - let begin_spoiler_line_idx: usize = state.line + 1; - let mut end_fence_line_idx: usize = begin_spoiler_line_idx; - let mut has_end_fence: bool = false; - - // 2. Search for the end of the spoiler and find the index of the last line of the spoiler. - // There could potentially be multiple lines between the beginning and end of the block. - // - // Block ends with a line with ':::' or ':::\n'; it must be isolated from other markdown. - while end_fence_line_idx < state.line_max && !has_end_fence { - let next_line: &str = state.get_line(end_fence_line_idx).trim(); - - if next_line.eq(SPOILER_SUFFIX) || next_line.eq(SPOILER_SUFFIX_NEWLINE) { - has_end_fence = true; - break; - } - - end_fence_line_idx += 1; - } - - // 3. If available, construct and return the spoiler node to add to the tree. - if has_end_fence { - let (spoiler_content, mapping) = state.get_lines( - begin_spoiler_line_idx, - end_fence_line_idx, - state.blk_indent, - true, - ); - - let mut node = Node::new(SpoilerBlock { - visible_text: String::from(first_line.replace(SPOILER_PREFIX, "").trim()), - }); - - // Add the spoiler content as children; marking as a child tells the tree to process the - // node again, which means other Markdown syntax (ex: emphasis, links) can be rendered. - node - .children - .push(Node::new(InlineRoot::new(spoiler_content, mapping))); - - // NOTE: Not using begin_spoiler_line_idx here because of incorrect results when - // state.line == 0 (subtracts an idx) vs the expected correct result (adds an idx). - Some((node, end_fence_line_idx - state.line + 1)) - } else { - None - } - } -} - -pub fn add(markdown_parser: &mut MarkdownIt) { - markdown_parser.block.add_rule::(); -} - -#[cfg(test)] -mod tests { - - use crate::utils::markdown::spoiler_rule::add; - use markdown_it::MarkdownIt; - use pretty_assertions::assert_eq; - - #[test] - fn test_spoiler_markdown() { - let tests: Vec<_> = vec![ - ( - "invalid spoiler", - "::: spoiler click to see more\nbut I never finished", - "

::: spoiler click to see more\nbut I never finished

\n", - ), - ( - "another invalid spoiler", - "::: spoiler\nnever added the lead in\n:::", - "

::: spoiler\nnever added the lead in\n:::

\n", - ), - ( - "basic spoiler, but no newline at the end", - "::: spoiler click to see more\nhow spicy!\n:::", - "
click to see more

how spicy!\n

\n" - ), - ( - "basic spoiler with a newline at the end", - "::: spoiler click to see more\nhow spicy!\n:::\n", - "
click to see more

how spicy!\n

\n" - ), - ( - "spoiler with extra markdown on the call to action (no extra parsing)", - "::: spoiler _click to see more_\nhow spicy!\n:::\n", - "
_click to see more_

how spicy!\n

\n" - ), - ( - "spoiler with extra markdown in the fenced spoiler block", - "::: spoiler click to see more\n**how spicy!**\n*i have many lines*\n:::\n", - "
click to see more

how spicy!\ni have many lines\n

\n" - ), - ( - "spoiler mixed with other content", - "hey you\npsst, wanna hear a secret?\n::: spoiler lean in and i'll tell you\n**you are breathtaking!**\n:::\nwhatcha think about that?", - "

hey you\npsst, wanna hear a secret?

\n
lean in and i'll tell you

you are breathtaking!\n

\n

whatcha think about that?

\n" - ), - ( - "spoiler mixed with indented content", - "- did you know that\n::: spoiler the call was\n***coming from inside the house!***\n:::\n - crazy, right?", - "
    \n
  • did you know that
  • \n
\n
the call was

coming from inside the house!\n

\n
    \n
  • crazy, right?
  • \n
\n" - ) - ]; - - tests.iter().for_each(|&(msg, input, expected)| { - let md = &mut MarkdownIt::new(); - markdown_it::plugins::cmark::add(md); - add(md); - - assert_eq!( - md.parse(input).xrender(), - expected, - "Testing {}, with original input '{}'", - msg, - input - ); - }); - } -} diff --git a/crates/utils/src/utils/validation.rs b/crates/utils/src/utils/validation.rs index 98b9a2a25..f8da6f609 100644 --- a/crates/utils/src/utils/validation.rs +++ b/crates/utils/src/utils/validation.rs @@ -191,8 +191,8 @@ pub fn site_name_length_check(name: &str) -> LemmyResult<()> { ) } -/// Checks the site description length, the limit as defined in the DB. -pub fn site_description_length_check(description: &str) -> LemmyResult<()> { +/// Checks the site / community description length, the limit as defined in the DB. +pub fn site_or_community_description_length_check(description: &str) -> LemmyResult<()> { max_length_check( description, SITE_DESCRIPTION_MAX_LENGTH, @@ -368,8 +368,8 @@ mod tests { is_valid_matrix_id, is_valid_post_title, is_valid_url, - site_description_length_check, site_name_length_check, + site_or_community_description_length_check, BIO_MAX_LENGTH, SITE_DESCRIPTION_MAX_LENGTH, SITE_NAME_MAX_LENGTH, @@ -537,14 +537,14 @@ mod tests { #[test] fn test_valid_site_description() { - assert!(site_description_length_check( + assert!(site_or_community_description_length_check( &(0..SITE_DESCRIPTION_MAX_LENGTH) .map(|_| 'A') .collect::() ) .is_ok()); - let invalid_result = site_description_length_check( + let invalid_result = site_or_community_description_length_check( &(0..SITE_DESCRIPTION_MAX_LENGTH + 1) .map(|_| 'A') .collect::(), diff --git a/crates/utils/tests/test_errors_used.rs b/crates/utils/tests/test_errors_used.rs new file mode 100644 index 000000000..bfd70bdad --- /dev/null +++ b/crates/utils/tests/test_errors_used.rs @@ -0,0 +1,45 @@ +use lemmy_utils::error::LemmyErrorType; +use std::{env::current_dir, process::Command}; +use strum::IntoEnumIterator; + +#[test] +#[allow(clippy::unwrap_used)] +fn test_errors_used() { + let mut unused_error_found = false; + let mut current_dir = current_dir().unwrap(); + current_dir.pop(); + current_dir.pop(); + for error in LemmyErrorType::iter() { + let search = format!("LemmyErrorType::{error}"); + let mut grep_all = Command::new("grep"); + let grep_all = grep_all + .current_dir(current_dir.clone()) + .arg("-R") + .arg("--exclude=error.rs") + .arg(&search) + .arg("crates/") + .arg("src/"); + let output = grep_all.output().unwrap(); + let grep_all_out = std::str::from_utf8(&output.stdout).unwrap(); + + let mut grep_apub = Command::new("grep"); + let grep_apub = grep_apub + .current_dir(current_dir.clone()) + .arg("-R") + .arg("--exclude-dir=api") + .arg(&search) + .arg("crates/apub/"); + let output = grep_apub.output().unwrap(); + let grep_apub_out = std::str::from_utf8(&output.stdout).unwrap(); + + if grep_all_out.is_empty() { + println!("LemmyErrorType::{} is unused", error); + unused_error_found = true; + } + if search != "LemmyErrorType::FederationError" && grep_all_out == grep_apub_out { + println!("LemmyErrorType::{} is only used for federation", error); + unused_error_found = true; + } + } + assert!(!unused_error_found); +} diff --git a/crates/utils/translations b/crates/utils/translations index 7adddded5..dbb09b078 160000 --- a/crates/utils/translations +++ b/crates/utils/translations @@ -1 +1 @@ -Subproject commit 7adddded581fcd965ab33b91c5fe10e0d7247208 +Subproject commit dbb09b0784982827d5d9b7dcf39f1703c1212b83 diff --git a/diesel.toml b/diesel.toml index ce23d470e..0da2434d4 100644 --- a/diesel.toml +++ b/diesel.toml @@ -1,3 +1,5 @@ [print_schema] file = "crates/db_schema/src/schema.rs" patch_file = "crates/db_schema/src/diesel_ltree.patch" +# Required for https://github.com/adwhit/diesel-derive-enum +custom_type_derives = ["diesel::query_builder::QueryId"] diff --git a/docker/Dockerfile b/docker/Dockerfile index 4503dd402..9701b4ad6 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -38,6 +38,12 @@ RUN --mount=type=cache,target=/lemmy/target set -ex; \ cargo clean --release; \ cargo build --features "${CARGO_BUILD_FEATURES}" --release; \ mv target/"${RUST_RELEASE_MODE}"/lemmy_server ./lemmy_server; \ + # + # Compress the binary with upx + wget https://github.com/upx/upx/releases/download/v4.2.4/upx-4.2.4-amd64_linux.tar.xz; \ + tar -xvf upx-4.2.4-amd64_linux.tar.xz; \ + cp upx-4.2.4-amd64_linux/upx /usr/bin; \ + upx --best --lzma lemmy_server; \ fi # ARM64 builder @@ -71,9 +77,14 @@ RUN --mount=type=cache,target=./target,uid=10001,gid=10001 set -ex; \ cargo clean --release; \ cargo build --features "${CARGO_BUILD_FEATURES}" --release; \ mv "./target/$CARGO_BUILD_TARGET/$RUST_RELEASE_MODE/lemmy_server" /home/lemmy/lemmy_server; \ + # + # Compress the binary with upx + wget https://github.com/upx/upx/releases/download/v4.2.4/upx-4.2.4-arm64_linux.tar.xz; \ + tar -xvf upx-4.2.4-arm64_linux.tar.xz; \ + cp upx-4.2.4-arm64_linux/upx /usr/bin; \ + upx --best --lzma /home/lemmy/lemmy_server; \ fi - # amd64 base runner FROM ${AMD_RUNNER_IMAGE} AS runner-linux-amd64 diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index c1d8d359b..cb438af3a 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -23,7 +23,7 @@ services: lemmy: # use "image" to pull down an already compiled lemmy. make sure to comment out "build". - # image: dessalines/lemmy:0.19.5 + # image: dessalines/lemmy:0.19.6 # platform: linux/x86_64 # no arm64 support. uncomment platform if using m1. # use "build" to build your local lemmy server image for development. make sure to comment out "image". # run: docker compose up --build @@ -31,9 +31,9 @@ services: build: context: ../ dockerfile: docker/Dockerfile - # args: - # RUST_RELEASE_MODE: release - # CARGO_BUILD_FEATURES: default + # args: + # RUST_RELEASE_MODE: release + # CARGO_BUILD_FEATURES: default # this hostname is used in nginx reverse proxy and also for lemmy ui to connect to the backend, do not change hostname: lemmy restart: unless-stopped @@ -53,7 +53,7 @@ services: lemmy-ui: # use "image" to pull down an already compiled lemmy-ui. make sure to comment out "build". - image: dessalines/lemmy-ui:0.19.5 + image: dessalines/lemmy-ui:0.19.6 # platform: linux/x86_64 # no arm64 support. uncomment platform if using m1. # use "build" to build your local lemmy ui image for development. make sure to comment out "image". # run: docker compose up --build diff --git a/docker/federation/docker-compose.yml b/docker/federation/docker-compose.yml index 090ad71a8..bc4b5ea7f 100644 --- a/docker/federation/docker-compose.yml +++ b/docker/federation/docker-compose.yml @@ -2,7 +2,7 @@ version: "3.7" x-ui-default: &ui-default init: true - image: dessalines/lemmy-ui:0.19.5 + image: dessalines/lemmy-ui:0.19.6 # assuming lemmy-ui is cloned besides lemmy directory # build: # context: ../../../lemmy-ui diff --git a/migrations/2023-10-24-140438_enable_private_messages/down.sql b/migrations/2023-10-24-140438_enable_private_messages/down.sql new file mode 100644 index 000000000..0b4846679 --- /dev/null +++ b/migrations/2023-10-24-140438_enable_private_messages/down.sql @@ -0,0 +1,3 @@ +ALTER TABLE local_user + DROP COLUMN enable_private_messages; + diff --git a/migrations/2023-10-24-140438_enable_private_messages/up.sql b/migrations/2023-10-24-140438_enable_private_messages/up.sql new file mode 100644 index 000000000..a0c2919de --- /dev/null +++ b/migrations/2023-10-24-140438_enable_private_messages/up.sql @@ -0,0 +1,3 @@ +ALTER TABLE local_user + ADD COLUMN enable_private_messages boolean DEFAULT TRUE NOT NULL; + diff --git a/migrations/2024-08-03-155932_increase_post_url_max_length/down.sql b/migrations/2024-08-03-155932_increase_post_url_max_length/down.sql index d25918578..7e4feef3d 100644 --- a/migrations/2024-08-03-155932_increase_post_url_max_length/down.sql +++ b/migrations/2024-08-03-155932_increase_post_url_max_length/down.sql @@ -1,3 +1,5 @@ ALTER TABLE post ALTER COLUMN url TYPE varchar(512); +ANALYZE post (url); + diff --git a/migrations/2024-08-03-155932_increase_post_url_max_length/up.sql b/migrations/2024-08-03-155932_increase_post_url_max_length/up.sql index 7c6818d22..8e5a3a9ce 100644 --- a/migrations/2024-08-03-155932_increase_post_url_max_length/up.sql +++ b/migrations/2024-08-03-155932_increase_post_url_max_length/up.sql @@ -3,3 +3,5 @@ ALTER TABLE post ALTER COLUMN url TYPE varchar(2000); +ANALYZE post (url); + diff --git a/migrations/2024-10-16-141718_add_short_community_description/down.sql b/migrations/2024-10-16-141718_add_short_community_description/down.sql new file mode 100644 index 000000000..fa8b39af6 --- /dev/null +++ b/migrations/2024-10-16-141718_add_short_community_description/down.sql @@ -0,0 +1,5 @@ +ALTER TABLE community + DROP COLUMN description; + +ALTER TABLE community RENAME COLUMN sidebar TO description; + diff --git a/migrations/2024-10-16-141718_add_short_community_description/up.sql b/migrations/2024-10-16-141718_add_short_community_description/up.sql new file mode 100644 index 000000000..591542ca4 --- /dev/null +++ b/migrations/2024-10-16-141718_add_short_community_description/up.sql @@ -0,0 +1,7 @@ +-- Renaming description to sidebar +ALTER TABLE community RENAME COLUMN description TO sidebar; + +-- Adding a short description column +ALTER TABLE community + ADD COLUMN description varchar(150); + diff --git a/migrations/2024-10-18-074533_no-individual-inboxes/down.sql b/migrations/2024-10-18-074533_no-individual-inboxes/down.sql new file mode 100644 index 000000000..6da025919 --- /dev/null +++ b/migrations/2024-10-18-074533_no-individual-inboxes/down.sql @@ -0,0 +1,6 @@ +ALTER TABLE person + ADD COLUMN shared_inbox_url varchar(255); + +ALTER TABLE community + ADD COLUMN shared_inbox_url varchar(255); + diff --git a/migrations/2024-10-18-074533_no-individual-inboxes/up.sql b/migrations/2024-10-18-074533_no-individual-inboxes/up.sql new file mode 100644 index 000000000..98cefcee0 --- /dev/null +++ b/migrations/2024-10-18-074533_no-individual-inboxes/up.sql @@ -0,0 +1,33 @@ +-- replace value of inbox_url with shared_inbox_url and the drop shared inbox +UPDATE + person +SET + inbox_url = subquery.inbox_url +FROM ( + SELECT + id, + coalesce(shared_inbox_url, inbox_url) AS inbox_url + FROM + person) AS subquery +WHERE + person.id = subquery.id; + +ALTER TABLE person + DROP COLUMN shared_inbox_url; + +UPDATE + community +SET + inbox_url = subquery.inbox_url +FROM ( + SELECT + id, + coalesce(shared_inbox_url, inbox_url) AS inbox_url + FROM + community) AS subquery +WHERE + community.id = subquery.id; + +ALTER TABLE community + DROP COLUMN shared_inbox_url; + diff --git a/migrations/2024-10-23-091053_comment-vote-remote-postid/down.sql b/migrations/2024-10-23-091053_comment-vote-remote-postid/down.sql new file mode 100644 index 000000000..21c730af3 --- /dev/null +++ b/migrations/2024-10-23-091053_comment-vote-remote-postid/down.sql @@ -0,0 +1,15 @@ +ALTER TABLE comment_like + ADD COLUMN post_id int; + +UPDATE + comment_like +SET + post_id = comment.post_id +FROM + comment +WHERE + comment_id = comment.id; + +ALTER TABLE comment_like + ALTER COLUMN post_id SET NOT NULL; + diff --git a/migrations/2024-10-23-091053_comment-vote-remote-postid/up.sql b/migrations/2024-10-23-091053_comment-vote-remote-postid/up.sql new file mode 100644 index 000000000..f6cbf3639 --- /dev/null +++ b/migrations/2024-10-23-091053_comment-vote-remote-postid/up.sql @@ -0,0 +1,3 @@ +ALTER TABLE comment_like + DROP post_id; + diff --git a/migrations/2024-10-29-090055_private-community/down.sql b/migrations/2024-10-29-090055_private-community/down.sql new file mode 100644 index 000000000..3a5c516c2 --- /dev/null +++ b/migrations/2024-10-29-090055_private-community/down.sql @@ -0,0 +1,52 @@ +-- Remove private visibility +ALTER TYPE community_visibility RENAME TO community_visibility__; + +CREATE TYPE community_visibility AS enum ( + 'Public', + 'LocalOnly' +); + +ALTER TABLE community + ALTER COLUMN visibility DROP DEFAULT; + +ALTER TABLE community + ALTER COLUMN visibility TYPE community_visibility + USING visibility::text::community_visibility; + +ALTER TABLE community + ALTER COLUMN visibility SET DEFAULT 'Public'; + +DROP TYPE community_visibility__; + +-- Revert community follower changes +CREATE OR REPLACE FUNCTION convert_follower_state (s community_follower_state) + RETURNS bool + LANGUAGE sql + AS $$ + SELECT + CASE WHEN s = 'Pending' THEN + TRUE + ELSE + FALSE + END +$$; + +ALTER TABLE community_follower + ALTER COLUMN state TYPE bool + USING convert_follower_state (state); + +DROP FUNCTION convert_follower_state; + +ALTER TABLE community_follower + ALTER COLUMN state SET DEFAULT FALSE; + +ALTER TABLE community_follower RENAME COLUMN state TO pending; + +DROP TYPE community_follower_state; + +ALTER TABLE community_follower + DROP COLUMN approver_id; + +ALTER TABLE ONLY local_site + ALTER COLUMN federation_signed_fetch SET DEFAULT FALSE; + diff --git a/migrations/2024-10-29-090055_private-community/up.sql b/migrations/2024-10-29-090055_private-community/up.sql new file mode 100644 index 000000000..d1c0585ae --- /dev/null +++ b/migrations/2024-10-29-090055_private-community/up.sql @@ -0,0 +1,47 @@ +ALTER TYPE community_visibility + ADD value 'Private'; + +-- Change `community_follower.pending` to `state` enum +CREATE TYPE community_follower_state AS enum ( + 'Accepted', + 'Pending', + 'ApprovalRequired' +); + +ALTER TABLE community_follower + ALTER COLUMN pending DROP DEFAULT; + +CREATE OR REPLACE FUNCTION convert_follower_state (b bool) + RETURNS community_follower_state + LANGUAGE sql + AS $$ + SELECT + CASE WHEN b = TRUE THEN + 'Pending'::community_follower_state + ELSE + 'Accepted'::community_follower_state + END +$$; + +ALTER TABLE community_follower + ALTER COLUMN pending TYPE community_follower_state + USING convert_follower_state (pending); + +DROP FUNCTION convert_follower_state; + +ALTER TABLE community_follower RENAME COLUMN pending TO state; + +-- Add column for mod who approved the private community follower +-- Dont use foreign key here, otherwise joining to person table doesnt work easily +ALTER TABLE community_follower + ADD COLUMN approver_id int REFERENCES person ON UPDATE CASCADE ON DELETE CASCADE; + +-- Enable signed fetch, necessary to fetch content in private communities +ALTER TABLE ONLY local_site + ALTER COLUMN federation_signed_fetch SET DEFAULT TRUE; + +UPDATE + local_site +SET + federation_signed_fetch = TRUE; + diff --git a/migrations/2024-11-10-134311_smoosh-tables-together/down.sql b/migrations/2024-11-10-134311_smoosh-tables-together/down.sql new file mode 100644 index 000000000..29b95d2cd --- /dev/null +++ b/migrations/2024-11-10-134311_smoosh-tables-together/down.sql @@ -0,0 +1,320 @@ +-- For each new actions table, create tables that are dropped in up.sql, and insert into them +CREATE TABLE comment_saved ( + person_id int REFERENCES person ON UPDATE CASCADE ON DELETE CASCADE NOT NULL, + comment_id int REFERENCES COMMENT ON UPDATE CASCADE ON DELETE CASCADE NOT NULL, + published timestamptz DEFAULT now() NOT NULL, + PRIMARY KEY (person_id, comment_id) +); + +INSERT INTO comment_saved (person_id, comment_id, published) +SELECT + person_id, + comment_id, + saved +FROM + comment_actions +WHERE + saved IS NOT NULL; + +CREATE TABLE community_block ( + person_id int REFERENCES person ON UPDATE CASCADE ON DELETE CASCADE NOT NULL, + community_id int REFERENCES community ON UPDATE CASCADE ON DELETE CASCADE NOT NULL, + published timestamptz DEFAULT now() NOT NULL, + PRIMARY KEY (person_id, community_id) +); + +INSERT INTO community_block (person_id, community_id, published) +SELECT + person_id, + community_id, + blocked +FROM + community_actions +WHERE + blocked IS NOT NULL; + +CREATE TABLE community_person_ban ( + community_id int REFERENCES community ON UPDATE CASCADE ON DELETE CASCADE NOT NULL, + person_id int REFERENCES person ON UPDATE CASCADE ON DELETE CASCADE NOT NULL, + published timestamptz DEFAULT now() NOT NULL, + expires timestamptz, + PRIMARY KEY (person_id, community_id) +); + +INSERT INTO community_person_ban (community_id, person_id, published, expires) +SELECT + community_id, + person_id, + received_ban, + ban_expires +FROM + community_actions +WHERE + received_ban IS NOT NULL; + +CREATE TABLE community_moderator ( + community_id int REFERENCES community ON UPDATE CASCADE ON DELETE CASCADE NOT NULL, + person_id int REFERENCES person ON UPDATE CASCADE ON DELETE CASCADE NOT NULL, + published timestamptz DEFAULT now() NOT NULL, + PRIMARY KEY (person_id, community_id) +); + +INSERT INTO community_moderator (community_id, person_id, published) +SELECT + community_id, + person_id, + became_moderator +FROM + community_actions +WHERE + became_moderator IS NOT NULL; + +CREATE TABLE person_block ( + person_id int REFERENCES person ON UPDATE CASCADE ON DELETE CASCADE NOT NULL, + target_id int REFERENCES person ON UPDATE CASCADE ON DELETE CASCADE NOT NULL, + published timestamptz DEFAULT now() NOT NULL, + PRIMARY KEY (person_id, target_id) +); + +INSERT INTO person_block (person_id, target_id, published) +SELECT + person_id, + target_id, + blocked +FROM + person_actions +WHERE + blocked IS NOT NULL; + +CREATE TABLE person_post_aggregates ( + person_id int REFERENCES person ON UPDATE CASCADE ON DELETE CASCADE NOT NULL, + post_id int REFERENCES post ON UPDATE CASCADE ON DELETE CASCADE NOT NULL, + read_comments bigint DEFAULT 0 NOT NULL, + published timestamptz NOT NULL, + PRIMARY KEY (person_id, post_id) +); + +INSERT INTO person_post_aggregates (person_id, post_id, read_comments, published) +SELECT + person_id, + post_id, + read_comments_amount, + read_comments +FROM + post_actions +WHERE + read_comments IS NOT NULL; + +CREATE TABLE post_hide ( + post_id int REFERENCES post ON UPDATE CASCADE ON DELETE CASCADE NOT NULL, + person_id int REFERENCES person ON UPDATE CASCADE ON DELETE CASCADE NOT NULL, + published timestamptz DEFAULT now() NOT NULL, + PRIMARY KEY (person_id, post_id) +); + +INSERT INTO post_hide (post_id, person_id, published) +SELECT + post_id, + person_id, + hidden +FROM + post_actions +WHERE + hidden IS NOT NULL; + +CREATE TABLE post_like ( + post_id int REFERENCES post ON UPDATE CASCADE ON DELETE CASCADE NOT NULL, + person_id int REFERENCES person ON UPDATE CASCADE ON DELETE CASCADE NOT NULL, + score smallint NOT NULL, + published timestamptz DEFAULT now() NOT NULL, + PRIMARY KEY (person_id, post_id) +); + +INSERT INTO post_like (post_id, person_id, score, published) +SELECT + post_id, + person_id, + like_score, + liked +FROM + post_actions +WHERE + liked IS NOT NULL; + +CREATE TABLE post_saved ( + post_id int REFERENCES post ON UPDATE CASCADE ON DELETE CASCADE NOT NULL, + person_id int REFERENCES person ON UPDATE CASCADE ON DELETE CASCADE NOT NULL, + published timestamptz DEFAULT now() NOT NULL, + PRIMARY KEY (person_id, post_id) +); + +INSERT INTO post_saved (post_id, person_id, published) +SELECT + post_id, + person_id, + saved +FROM + post_actions +WHERE + saved IS NOT NULL; + +-- Do the opposite of the `ALTER TABLE` commands in up.sql +DELETE FROM comment_actions +WHERE liked IS NULL; + +DELETE FROM community_actions +WHERE followed IS NULL; + +DELETE FROM instance_actions +WHERE blocked IS NULL; + +DELETE FROM person_actions +WHERE followed IS NULL; + +DELETE FROM post_actions +WHERE read IS NULL; + +ALTER TABLE comment_actions RENAME TO comment_like; + +ALTER TABLE community_actions RENAME TO community_follower; + +ALTER TABLE instance_actions RENAME TO instance_block; + +ALTER TABLE person_actions RENAME TO person_follower; + +ALTER TABLE post_actions RENAME TO post_read; + +ALTER TABLE comment_like RENAME COLUMN liked TO published; + +ALTER TABLE comment_like RENAME COLUMN like_score TO score; + +ALTER TABLE community_follower RENAME COLUMN followed TO published; + +ALTER TABLE community_follower RENAME COLUMN follow_state TO state; + +ALTER TABLE community_follower RENAME COLUMN follow_approver_id TO approver_id; + +ALTER TABLE instance_block RENAME COLUMN blocked TO published; + +ALTER TABLE person_follower RENAME COLUMN person_id TO follower_id; + +ALTER TABLE person_follower RENAME COLUMN target_id TO person_id; + +ALTER TABLE person_follower RENAME COLUMN followed TO published; + +ALTER TABLE person_follower RENAME COLUMN follow_pending TO pending; + +ALTER TABLE post_read RENAME COLUMN read TO published; + +ALTER TABLE comment_like + DROP CONSTRAINT comment_actions_check_liked, + ALTER COLUMN published SET NOT NULL, + ALTER COLUMN published SET DEFAULT now(), + ALTER COLUMN score SET NOT NULL, + DROP COLUMN saved; + +ALTER TABLE community_follower + DROP CONSTRAINT community_actions_check_followed, + DROP CONSTRAINT community_actions_check_received_ban, + ALTER COLUMN published SET NOT NULL, + ALTER COLUMN published SET DEFAULT now(), + ALTER COLUMN state SET NOT NULL, + DROP COLUMN blocked, + DROP COLUMN became_moderator, + DROP COLUMN received_ban, + DROP COLUMN ban_expires; + +ALTER TABLE instance_block + ALTER COLUMN published SET NOT NULL, + ALTER COLUMN published SET DEFAULT now(); + +ALTER TABLE person_follower + DROP CONSTRAINT person_actions_check_followed, + ALTER COLUMN published SET NOT NULL, + ALTER COLUMN published SET DEFAULT now(), + ALTER COLUMN pending SET NOT NULL, + DROP COLUMN blocked; + +ALTER TABLE post_read + DROP CONSTRAINT post_actions_check_read_comments, + DROP CONSTRAINT post_actions_check_liked, + ALTER COLUMN published SET NOT NULL, + ALTER COLUMN published SET DEFAULT now(), + DROP COLUMN read_comments, + DROP COLUMN read_comments_amount, + DROP COLUMN saved, + DROP COLUMN liked, + DROP COLUMN like_score, + DROP COLUMN hidden; + +-- Rename associated stuff +ALTER INDEX comment_actions_pkey RENAME TO comment_like_pkey; + +ALTER INDEX idx_comment_actions_comment RENAME TO idx_comment_like_comment; + +ALTER TABLE comment_like RENAME CONSTRAINT comment_actions_comment_id_fkey TO comment_like_comment_id_fkey; + +ALTER TABLE comment_like RENAME CONSTRAINT comment_actions_person_id_fkey TO comment_like_person_id_fkey; + +ALTER INDEX community_actions_pkey RENAME TO community_follower_pkey; + +ALTER INDEX idx_community_actions_community RENAME TO idx_community_follower_community; + +ALTER TABLE community_follower RENAME CONSTRAINT community_actions_community_id_fkey TO community_follower_community_id_fkey; + +ALTER TABLE community_follower RENAME CONSTRAINT community_actions_person_id_fkey TO community_follower_person_id_fkey; + +ALTER TABLE community_follower RENAME CONSTRAINT community_actions_follow_approver_id_fkey TO community_follower_approver_id_fkey; + +ALTER INDEX instance_actions_pkey RENAME TO instance_block_pkey; + +ALTER TABLE instance_block RENAME CONSTRAINT instance_actions_instance_id_fkey TO instance_block_instance_id_fkey; + +ALTER TABLE instance_block RENAME CONSTRAINT instance_actions_person_id_fkey TO instance_block_person_id_fkey; + +ALTER INDEX person_actions_pkey RENAME TO person_follower_pkey; + +ALTER TABLE person_follower RENAME CONSTRAINT person_actions_target_id_fkey TO person_follower_person_id_fkey; + +ALTER TABLE person_follower RENAME CONSTRAINT person_actions_person_id_fkey TO person_follower_follower_id_fkey; + +ALTER INDEX post_actions_pkey RENAME TO post_read_pkey; + +ALTER TABLE post_read RENAME CONSTRAINT post_actions_person_id_fkey TO post_read_person_id_fkey; + +ALTER TABLE post_read RENAME CONSTRAINT post_actions_post_id_fkey TO post_read_post_id_fkey; + +-- Rename idx_community_actions_followed and remove filter +CREATE INDEX idx_community_follower_published ON community_follower (published); + +DROP INDEX idx_community_actions_followed; + +-- Move indexes back to their original tables +CREATE INDEX idx_comment_saved_comment ON comment_saved (comment_id); + +CREATE INDEX idx_comment_saved_person ON comment_saved (person_id); + +CREATE INDEX idx_community_block_community ON community_block (community_id); + +CREATE INDEX idx_community_moderator_community ON community_moderator (community_id); + +CREATE INDEX idx_community_moderator_published ON community_moderator (published); + +CREATE INDEX idx_person_block_person ON person_block (person_id); + +CREATE INDEX idx_person_block_target ON person_block (target_id); + +CREATE INDEX idx_person_post_aggregates_person ON person_post_aggregates (person_id); + +CREATE INDEX idx_person_post_aggregates_post ON person_post_aggregates (post_id); + +CREATE INDEX idx_post_like_post ON post_like (post_id); + +DROP INDEX idx_person_actions_person, idx_person_actions_target, idx_post_actions_person, idx_post_actions_post; + +-- Drop `NOT NULL` indexes of columns that still exist +DROP INDEX idx_comment_actions_liked_not_null, idx_community_actions_followed_not_null, idx_person_actions_followed_not_null, idx_post_actions_read_not_null, idx_instance_actions_blocked_not_null; + +-- Drop statistics of columns that still exist +DROP statistics comment_actions_liked_stat, community_actions_followed_stat, person_actions_followed_stat; + diff --git a/migrations/2024-11-10-134311_smoosh-tables-together/up.sql b/migrations/2024-11-10-134311_smoosh-tables-together/up.sql new file mode 100644 index 000000000..aadf95692 --- /dev/null +++ b/migrations/2024-11-10-134311_smoosh-tables-together/up.sql @@ -0,0 +1,338 @@ +-- For each new actions table, transform the table previously used for the most common action type +-- into the new actions table, which should only change the table's metadata instead of rewriting the +-- rows +ALTER TABLE comment_like RENAME TO comment_actions; + +ALTER TABLE community_follower RENAME TO community_actions; + +ALTER TABLE instance_block RENAME TO instance_actions; + +ALTER TABLE person_follower RENAME TO person_actions; + +ALTER TABLE post_read RENAME TO post_actions; + +ALTER TABLE comment_actions RENAME COLUMN published TO liked; + +ALTER TABLE comment_actions RENAME COLUMN score TO like_score; + +ALTER TABLE community_actions RENAME COLUMN published TO followed; + +ALTER TABLE community_actions RENAME COLUMN state TO follow_state; + +ALTER TABLE community_actions RENAME COLUMN approver_id TO follow_approver_id; + +ALTER TABLE instance_actions RENAME COLUMN published TO blocked; + +ALTER TABLE person_actions RENAME COLUMN person_id TO target_id; + +ALTER TABLE person_actions RENAME COLUMN follower_id TO person_id; + +ALTER TABLE person_actions RENAME COLUMN published TO followed; + +ALTER TABLE person_actions RENAME COLUMN pending TO follow_pending; + +ALTER TABLE post_actions RENAME COLUMN published TO read; + +ALTER TABLE comment_actions + ALTER COLUMN liked DROP NOT NULL, + ALTER COLUMN liked DROP DEFAULT, + ALTER COLUMN like_score DROP NOT NULL, + ADD COLUMN saved timestamptz, + ADD CONSTRAINT comment_actions_check_liked CHECK ((liked IS NULL) = (like_score IS NULL)); + +ALTER TABLE community_actions + ALTER COLUMN followed DROP NOT NULL, + ALTER COLUMN followed DROP DEFAULT, + ALTER COLUMN follow_state DROP NOT NULL, + ADD COLUMN blocked timestamptz, + ADD COLUMN became_moderator timestamptz, + ADD COLUMN received_ban timestamptz, + ADD COLUMN ban_expires timestamptz, + ADD CONSTRAINT community_actions_check_followed CHECK ((followed IS NULL) = (follow_state IS NULL) AND NOT (followed IS NULL AND follow_approver_id IS NOT NULL)), + ADD CONSTRAINT community_actions_check_received_ban CHECK (NOT (received_ban IS NULL AND ban_expires IS NOT NULL)); + +ALTER TABLE instance_actions + ALTER COLUMN blocked DROP NOT NULL, + ALTER COLUMN blocked DROP DEFAULT; + +ALTER TABLE person_actions + ALTER COLUMN followed DROP NOT NULL, + ALTER COLUMN followed DROP DEFAULT, + ALTER COLUMN follow_pending DROP NOT NULL, + ADD COLUMN blocked timestamptz, + ADD CONSTRAINT person_actions_check_followed CHECK ((followed IS NULL) = (follow_pending IS NULL)); + +ALTER TABLE post_actions + ALTER COLUMN read DROP NOT NULL, + ALTER COLUMN read DROP DEFAULT, + ADD COLUMN read_comments timestamptz, + ADD COLUMN read_comments_amount bigint, + ADD COLUMN saved timestamptz, + ADD COLUMN liked timestamptz, + ADD COLUMN like_score smallint, + ADD COLUMN hidden timestamptz, + ADD CONSTRAINT post_actions_check_read_comments CHECK ((read_comments IS NULL) = (read_comments_amount IS NULL)), + ADD CONSTRAINT post_actions_check_liked CHECK ((liked IS NULL) = (like_score IS NULL)); + +-- Add actions from other old tables to the new tables +INSERT INTO comment_actions (person_id, comment_id, saved) +SELECT + person_id, + comment_id, + published +FROM + comment_saved +ON CONFLICT (person_id, + comment_id) + DO UPDATE SET + saved = excluded.saved; + +INSERT INTO community_actions (person_id, community_id, blocked) +SELECT + person_id, + community_id, + published +FROM + community_block +ON CONFLICT (person_id, + community_id) + DO UPDATE SET + person_id = excluded.person_id, + community_id = excluded.community_id, + blocked = excluded.blocked; + +INSERT INTO community_actions (person_id, community_id, became_moderator) +SELECT + person_id, + community_id, + published +FROM + community_moderator +ON CONFLICT (person_id, + community_id) + DO UPDATE SET + person_id = excluded.person_id, + community_id = excluded.community_id, + became_moderator = excluded.became_moderator; + +INSERT INTO community_actions (person_id, community_id, received_ban, ban_expires) +SELECT + person_id, + community_id, + published, + expires +FROM + community_person_ban +ON CONFLICT (person_id, + community_id) + DO UPDATE SET + person_id = excluded.person_id, + community_id = excluded.community_id, + received_ban = excluded.received_ban, + ban_expires = excluded.ban_expires; + +INSERT INTO person_actions (person_id, target_id, blocked) +SELECT + person_id, + target_id, + published +FROM + person_block +ON CONFLICT (person_id, + target_id) + DO UPDATE SET + person_id = excluded.person_id, + target_id = excluded.target_id, + blocked = excluded.blocked; + +INSERT INTO post_actions (person_id, post_id, read_comments, read_comments_amount) +SELECT + person_id, + post_id, + published, + read_comments +FROM + person_post_aggregates +ON CONFLICT (person_id, + post_id) + DO UPDATE SET + read_comments = excluded.read_comments, + read_comments_amount = excluded.read_comments_amount; + +INSERT INTO post_actions (person_id, post_id, hidden) +SELECT + person_id, + post_id, + published +FROM + post_hide +ON CONFLICT (person_id, + post_id) + DO UPDATE SET + hidden = excluded.hidden; + +INSERT INTO post_actions (person_id, post_id, liked, like_score) +SELECT + person_id, + post_id, + published, + score +FROM + post_like +ON CONFLICT (person_id, + post_id) + DO UPDATE SET + liked = excluded.liked, + like_score = excluded.like_score; + +INSERT INTO post_actions (person_id, post_id, saved) +SELECT + person_id, + post_id, + published +FROM + post_saved +ON CONFLICT (person_id, + post_id) + DO UPDATE SET + saved = excluded.saved; + +-- Drop old tables +DROP TABLE comment_saved, community_block, community_moderator, community_person_ban, person_block, person_post_aggregates, post_hide, post_like, post_saved; + +-- Rename associated stuff +ALTER INDEX comment_like_pkey RENAME TO comment_actions_pkey; + +ALTER INDEX idx_comment_like_comment RENAME TO idx_comment_actions_comment; + +ALTER TABLE comment_actions RENAME CONSTRAINT comment_like_comment_id_fkey TO comment_actions_comment_id_fkey; + +ALTER TABLE comment_actions RENAME CONSTRAINT comment_like_person_id_fkey TO comment_actions_person_id_fkey; + +ALTER INDEX community_follower_pkey RENAME TO community_actions_pkey; + +ALTER INDEX idx_community_follower_community RENAME TO idx_community_actions_community; + +ALTER TABLE community_actions RENAME CONSTRAINT community_follower_community_id_fkey TO community_actions_community_id_fkey; + +ALTER TABLE community_actions RENAME CONSTRAINT community_follower_person_id_fkey TO community_actions_person_id_fkey; + +ALTER TABLE community_actions RENAME CONSTRAINT community_follower_approver_id_fkey TO community_actions_follow_approver_id_fkey; + +ALTER INDEX instance_block_pkey RENAME TO instance_actions_pkey; + +ALTER TABLE instance_actions RENAME CONSTRAINT instance_block_instance_id_fkey TO instance_actions_instance_id_fkey; + +ALTER TABLE instance_actions RENAME CONSTRAINT instance_block_person_id_fkey TO instance_actions_person_id_fkey; + +ALTER INDEX person_follower_pkey RENAME TO person_actions_pkey; + +ALTER TABLE person_actions RENAME CONSTRAINT person_follower_person_id_fkey TO person_actions_target_id_fkey; + +ALTER TABLE person_actions RENAME CONSTRAINT person_follower_follower_id_fkey TO person_actions_person_id_fkey; + +ALTER INDEX post_read_pkey RENAME TO post_actions_pkey; + +ALTER TABLE post_actions RENAME CONSTRAINT post_read_person_id_fkey TO post_actions_person_id_fkey; + +ALTER TABLE post_actions RENAME CONSTRAINT post_read_post_id_fkey TO post_actions_post_id_fkey; + +-- Rename idx_community_follower_published and add filter +CREATE INDEX idx_community_actions_followed ON community_actions (followed) +WHERE + followed IS NOT NULL; + +DROP INDEX idx_community_follower_published; + +-- Restore indexes of dropped tables +CREATE INDEX idx_community_actions_became_moderator ON community_actions (became_moderator) +WHERE + became_moderator IS NOT NULL; + +CREATE INDEX idx_person_actions_person ON person_actions (person_id); + +CREATE INDEX idx_person_actions_target ON person_actions (target_id); + +CREATE INDEX idx_post_actions_person ON post_actions (person_id); + +CREATE INDEX idx_post_actions_post ON post_actions (post_id); + +-- Create new indexes, with `OR` being used to allow `IS NOT NULL` filters in queries to use either column in +-- a group (e.g. `liked IS NOT NULL` and `like_score IS NOT NULL` both work) +CREATE INDEX idx_comment_actions_liked_not_null ON comment_actions (person_id, comment_id) +WHERE + liked IS NOT NULL OR like_score IS NOT NULL; + +CREATE INDEX idx_comment_actions_saved_not_null ON comment_actions (person_id, comment_id) +WHERE + saved IS NOT NULL; + +CREATE INDEX idx_community_actions_followed_not_null ON community_actions (person_id, community_id) +WHERE + followed IS NOT NULL OR follow_state IS NOT NULL; + +CREATE INDEX idx_community_actions_blocked_not_null ON community_actions (person_id, community_id) +WHERE + blocked IS NOT NULL; + +CREATE INDEX idx_community_actions_became_moderator_not_null ON community_actions (person_id, community_id) +WHERE + became_moderator IS NOT NULL; + +CREATE INDEX idx_community_actions_received_ban_not_null ON community_actions (person_id, community_id) +WHERE + received_ban IS NOT NULL; + +CREATE INDEX idx_person_actions_followed_not_null ON person_actions (person_id, target_id) +WHERE + followed IS NOT NULL OR follow_pending IS NOT NULL; + +CREATE INDEX idx_person_actions_blocked_not_null ON person_actions (person_id, target_id) +WHERE + blocked IS NOT NULL; + +CREATE INDEX idx_post_actions_read_not_null ON post_actions (person_id, post_id) +WHERE + read IS NOT NULL; + +CREATE INDEX idx_post_actions_read_comments_not_null ON post_actions (person_id, post_id) +WHERE + read_comments IS NOT NULL OR read_comments_amount IS NOT NULL; + +CREATE INDEX idx_post_actions_saved_not_null ON post_actions (person_id, post_id) +WHERE + saved IS NOT NULL; + +CREATE INDEX idx_post_actions_liked_not_null ON post_actions (person_id, post_id) +WHERE + liked IS NOT NULL OR like_score IS NOT NULL; + +CREATE INDEX idx_post_actions_hidden_not_null ON post_actions (person_id, post_id) +WHERE + hidden IS NOT NULL; + +-- This index is currently redundant because instance_actions only has 1 action type, but inconsistency +-- with other tables would make it harder to do everything correctly when adding another action type +CREATE INDEX idx_instance_actions_blocked_not_null ON instance_actions (person_id, instance_id) +WHERE + blocked IS NOT NULL; + +-- Create new statistics for more accurate estimations of how much of an index will be read (e.g. for +-- `(liked, like_score)`, the query planner might othewise assume that `(TRUE, FALSE)` and `(TRUE, TRUE)` +-- are equally likely when only `(TRUE, TRUE)` is possible, which would make it severely underestimate +-- the efficiency of using the index) +CREATE statistics comment_actions_liked_stat ON (liked IS NULL), (like_score IS NULL) +FROM comment_actions; + +CREATE statistics community_actions_followed_stat ON (followed IS NULL), (follow_state IS NULL) +FROM community_actions; + +CREATE statistics person_actions_followed_stat ON (followed IS NULL), (follow_pending IS NULL) +FROM person_actions; + +CREATE statistics post_actions_read_comments_stat ON (read_comments IS NULL), (read_comments_amount IS NULL) +FROM post_actions; + +CREATE statistics post_actions_liked_stat ON (liked IS NULL), (like_score IS NULL), (post_id IS NULL) +FROM post_actions; + diff --git a/scripts/start_dev_db.sh b/scripts/start_dev_db.sh index 5965316ba..1cbe9e16a 100644 --- a/scripts/start_dev_db.sh +++ b/scripts/start_dev_db.sh @@ -2,8 +2,12 @@ export PGDATA="$PWD/dev_pgdata" export PGHOST=$PWD + +# Necessary to encode the dev db path into proper URL params +export ENCODED_HOST=$(printf $PWD | jq -sRr @uri) + export PGUSER=postgres -export DATABASE_URL="postgresql://lemmy:password@/lemmy?host=$PWD" +export DATABASE_URL="postgresql://lemmy:password@$ENCODED_HOST/lemmy" export LEMMY_DATABASE_URL=$DATABASE_URL export PGDATABASE=lemmy diff --git a/scripts/test.sh b/scripts/test.sh index e08148db0..d3e95886f 100755 --- a/scripts/test.sh +++ b/scripts/test.sh @@ -20,9 +20,7 @@ if [ -n "$PACKAGE" ]; then cargo test -p $PACKAGE --all-features --no-fail-fast $TEST else - cargo test --workspace --no-fail-fast - # Testing lemmy utils all features in particular (for ts-rs bindings) - cargo test -p lemmy_utils --all-features --no-fail-fast + cargo test --workspace --all-features --no-fail-fast fi # Add this to do printlns: -- --nocapture diff --git a/scripts/ts_bindings_check.sh b/scripts/ts_bindings_check.sh new file mode 100755 index 000000000..a925081c8 --- /dev/null +++ b/scripts/ts_bindings_check.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash +set -e + +# This check is only used for CI. + +CWD="$(cd -P -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)" + +cd "$CWD/../" + +# Export the ts-rs bindings +cargo test --workspace export_bindings + +# Make sure no rows are returned +! grep -nr --include=\*.ts ' | null' ./crates/ diff --git a/src/api_routes_http.rs b/src/api_routes_http.rs index df1aebf84..fd65e0671 100644 --- a/src/api_routes_http.rs +++ b/src/api_routes_http.rs @@ -17,6 +17,11 @@ use lemmy_api::{ block::block_community, follow::follow_community, hide::hide_community, + pending_follows::{ + approve::post_pending_follows_approve, + count::get_pending_follows_count, + list::get_pending_follows_list, + }, random::get_random_community, transfer::transfer_community, }, @@ -204,7 +209,14 @@ pub fn config(cfg: &mut web::ServiceConfig, rate_limit: &RateLimitCell) { .route("/remove", web::post().to(remove_community)) .route("/transfer", web::post().to(transfer_community)) .route("/ban_user", web::post().to(ban_from_community)) - .route("/mod", web::post().to(add_mod_to_community)), + .route("/mod", web::post().to(add_mod_to_community)) + .service( + web::scope("/pending_follows") + .wrap(rate_limit.message()) + .route("/count", web::get().to(get_pending_follows_count)) + .route("/list", web::get().to(get_pending_follows_list)) + .route("/approve", web::post().to(post_pending_follows_approve)), + ), ) .service( web::scope("/federated_instances") diff --git a/src/code_migrations.rs b/src/code_migrations.rs index e78f8bf0b..84af43ea7 100644 --- a/src/code_migrations.rs +++ b/src/code_migrations.rs @@ -10,13 +10,7 @@ use diesel::{ use diesel_async::RunQueryDsl; use lemmy_api_common::{ lemmy_db_views::structs::SiteView, - utils::{ - generate_followers_url, - generate_inbox_url, - generate_local_apub_endpoint, - generate_shared_inbox_url, - EndpointType, - }, + utils::{generate_followers_url, generate_inbox_url, generate_local_apub_endpoint, EndpointType}, }; use lemmy_db_schema::{ source::{ @@ -49,8 +43,8 @@ pub async fn run_advanced_migrations( comment_updates_2020_04_03(pool, protocol_and_hostname).await?; private_message_updates_2020_05_05(pool, protocol_and_hostname).await?; post_thumbnail_url_updates_2020_07_27(pool, protocol_and_hostname).await?; - apub_columns_2021_02_02(pool, settings).await?; - instance_actor_2022_01_28(pool, protocol_and_hostname, settings).await?; + apub_columns_2021_02_02(pool).await?; + instance_actor_2022_01_28(pool, protocol_and_hostname).await?; regenerate_public_keys_2022_07_05(pool).await?; initialize_local_site_2022_10_10(pool, settings).await?; @@ -282,36 +276,27 @@ async fn post_thumbnail_url_updates_2020_07_27( /// We are setting inbox and follower URLs for local and remote actors alike, because for now /// all federated instances are also Lemmy and use the same URL scheme. -async fn apub_columns_2021_02_02(pool: &mut DbPool<'_>, settings: &Settings) -> LemmyResult<()> { +async fn apub_columns_2021_02_02(pool: &mut DbPool<'_>) -> LemmyResult<()> { let conn = &mut get_conn(pool).await?; info!("Running apub_columns_2021_02_02"); { - use lemmy_db_schema::schema::person::dsl::{inbox_url, person, shared_inbox_url}; + use lemmy_db_schema::schema::person::dsl::{inbox_url, person}; let persons = person .filter(inbox_url.like("http://changeme%")) .load::(conn) .await?; for p in &persons { - let inbox_url_ = generate_inbox_url(&p.actor_id)?; - let shared_inbox_url_ = generate_shared_inbox_url(settings)?; + let inbox_url_ = generate_inbox_url()?; diesel::update(person.find(p.id)) - .set(( - inbox_url.eq(inbox_url_), - shared_inbox_url.eq(shared_inbox_url_), - )) + .set((inbox_url.eq(inbox_url_),)) .get_result::(conn) .await?; } } { - use lemmy_db_schema::schema::community::dsl::{ - community, - followers_url, - inbox_url, - shared_inbox_url, - }; + use lemmy_db_schema::schema::community::dsl::{community, followers_url, inbox_url}; let communities = community .filter(inbox_url.like("http://changeme%")) .load::(conn) @@ -319,14 +304,9 @@ async fn apub_columns_2021_02_02(pool: &mut DbPool<'_>, settings: &Settings) -> for c in &communities { let followers_url_ = generate_followers_url(&c.actor_id)?; - let inbox_url_ = generate_inbox_url(&c.actor_id)?; - let shared_inbox_url_ = generate_shared_inbox_url(settings)?; + let inbox_url_ = generate_inbox_url()?; diesel::update(community.find(c.id)) - .set(( - followers_url.eq(followers_url_), - inbox_url.eq(inbox_url_), - shared_inbox_url.eq(shared_inbox_url_), - )) + .set((followers_url.eq(followers_url_), inbox_url.eq(inbox_url_))) .get_result::(conn) .await?; } @@ -342,7 +322,6 @@ async fn apub_columns_2021_02_02(pool: &mut DbPool<'_>, settings: &Settings) -> async fn instance_actor_2022_01_28( pool: &mut DbPool<'_>, protocol_and_hostname: &str, - settings: &Settings, ) -> LemmyResult<()> { info!("Running instance_actor_2021_09_29"); if let Ok(site_view) = SiteView::read_local(pool).await { @@ -356,7 +335,7 @@ async fn instance_actor_2022_01_28( let site_form = SiteUpdateForm { actor_id: Some(actor_id.clone().into()), last_refreshed_at: Some(naive_now()), - inbox_url: Some(generate_shared_inbox_url(settings)?), + inbox_url: Some(generate_inbox_url()?), private_key: Some(Some(key_pair.private_key)), public_key: Some(key_pair.public_key), ..Default::default() @@ -457,8 +436,7 @@ async fn initialize_local_site_2022_10_10( // Register the user if there's a site setup let person_form = PersonInsertForm { actor_id: Some(person_actor_id.clone()), - inbox_url: Some(generate_inbox_url(&person_actor_id)?), - shared_inbox_url: Some(generate_shared_inbox_url(settings)?), + inbox_url: Some(generate_inbox_url()?), private_key: Some(person_keypair.private_key), ..PersonInsertForm::new( setup.admin_username.clone(), @@ -488,7 +466,7 @@ async fn initialize_local_site_2022_10_10( let site_form = SiteInsertForm { actor_id: Some(site_actor_id.clone().into()), last_refreshed_at: Some(naive_now()), - inbox_url: Some(generate_shared_inbox_url(settings)?), + inbox_url: Some(generate_inbox_url()?), private_key: Some(site_key_pair.private_key), public_key: Some(site_key_pair.public_key), diff --git a/src/lib.rs b/src/lib.rs index 4aee4be69..9da09f65b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -116,7 +116,7 @@ pub async fn start_lemmy_server(args: CmdArgs) -> LemmyResult<()> { } // Set up the connection pool - let pool = build_db_pool().await?; + let pool = build_db_pool()?; // Run the Code-required migrations run_advanced_migrations(&mut (&pool).into(), &SETTINGS).await?; diff --git a/src/prometheus_metrics.rs b/src/prometheus_metrics.rs index c4ab204e7..512d63f38 100644 --- a/src/prometheus_metrics.rs +++ b/src/prometheus_metrics.rs @@ -47,7 +47,7 @@ pub fn serve_prometheus(config: PrometheusConfig, lemmy_context: LemmyContext) - // handler for the /metrics path async fn metrics(context: web::Data>) -> LemmyResult { // collect metrics - collect_db_pool_metrics(&context).await; + collect_db_pool_metrics(&context); let mut buffer = Vec::new(); let encoder = TextEncoder::new(); @@ -84,7 +84,7 @@ fn create_db_pool_metrics() -> LemmyResult { Ok(metrics) } -async fn collect_db_pool_metrics(context: &PromContext) { +fn collect_db_pool_metrics(context: &PromContext) { let pool_status = context.lemmy.inner_pool().status(); context .db_pool_metrics diff --git a/src/scheduled_tasks.rs b/src/scheduled_tasks.rs index 2f99fe8a1..148df00fe 100644 --- a/src/scheduled_tasks.rs +++ b/src/scheduled_tasks.rs @@ -22,7 +22,7 @@ use lemmy_db_schema::{ captcha_answer, comment, community, - community_person_ban, + community_actions, instance, person, post, @@ -36,7 +36,15 @@ use lemmy_db_schema::{ post::{Post, PostUpdateForm}, }, traits::Crud, - utils::{functions::coalesce, get_conn, naive_now, now, DbPool, DELETED_REPLACEMENT_TEXT}, + utils::{ + find_action, + functions::coalesce, + get_conn, + naive_now, + now, + DbPool, + DELETED_REPLACEMENT_TEXT, + }, }; use lemmy_routes::nodeinfo::{NodeInfo, NodeInfoWellKnown}; use lemmy_utils::error::LemmyResult; @@ -393,10 +401,10 @@ async fn active_counts(pool: &mut DbPool<'_>) { ("6 months", "half_year"), ]; - for i in &intervals { + for (full_form, abbr) in &intervals { let update_site_stmt = format!( "update site_aggregates set users_active_{} = (select * from site_aggregates_activity('{}')) where site_id = 1", - i.1, i.0 + abbr, full_form ); sql_query(update_site_stmt) .execute(&mut conn) @@ -404,7 +412,7 @@ async fn active_counts(pool: &mut DbPool<'_>) { .inspect_err(|e| error!("Failed to update site stats: {e}")) .ok(); - let update_community_stmt = format!("update community_aggregates ca set users_active_{} = mv.count_ from community_aggregates_activity('{}') mv where ca.community_id = mv.community_id_", i.1, i.0); + let update_community_stmt = format!("update community_aggregates ca set users_active_{} = mv.count_ from community_aggregates_activity('{}') mv where ca.community_id = mv.community_id_", abbr, full_form); sql_query(update_community_stmt) .execute(&mut conn) .await @@ -439,7 +447,7 @@ async fn update_banned_when_expired(pool: &mut DbPool<'_>) { .ok(); diesel::delete( - community_person_ban::table.filter(community_person_ban::expires.lt(now().nullable())), + community_actions::table.filter(community_actions::ban_expires.lt(now().nullable())), ) .execute(&mut conn) .await @@ -470,11 +478,10 @@ async fn publish_scheduled_posts(context: &Data) { .filter(not(person::banned.or(person::deleted))) .filter(not(community::removed.or(community::deleted))) // ensure that user isnt banned from community - .filter(not(exists( - community_person_ban::table - .filter(community_person_ban::community_id.eq(community::id)) - .filter(community_person_ban::person_id.eq(person::id)), - ))) + .filter(not(exists(find_action( + community_actions::received_ban, + (person::id, community::id), + )))) .select((post::all_columns, community::all_columns)) .get_results::<(Post, Community)>(&mut conn) .await @@ -496,7 +503,6 @@ async fn publish_scheduled_posts(context: &Data) { // send out post via federation and webmention let send_activity = SendActivityData::CreatePost(post.clone()); ActivityChannel::submit_activity(send_activity, context) - .await .inspect_err(|e| error!("Failed federate scheduled post: {e}")) .ok(); send_webmention(post, community); @@ -609,7 +615,10 @@ mod tests { use crate::scheduled_tasks::build_update_instance_form; use lemmy_api_common::request::client_builder; - use lemmy_utils::{error::LemmyResult, settings::structs::Settings, LemmyErrorType}; + use lemmy_utils::{ + error::{LemmyErrorType, LemmyResult}, + settings::structs::Settings, + }; use pretty_assertions::assert_eq; use reqwest_middleware::ClientBuilder; use serial_test::serial; diff --git a/src/session_middleware.rs b/src/session_middleware.rs index ec8f4399c..b495bdbb9 100644 --- a/src/session_middleware.rs +++ b/src/session_middleware.rs @@ -125,7 +125,7 @@ mod tests { // hack, necessary so that config file can be loaded from hardcoded, relative path set_current_dir("crates/utils")?; - let pool_ = build_db_pool_for_tests().await; + let pool_ = build_db_pool_for_tests(); let pool = &mut (&pool_).into(); let secret = Secret::init(pool).await?;